# Como adicionar um campo honeypot que bots preenchem e pessoas não

> Adicione a qualquer formulário um honeypot que pega bots sem atrapalhar leitores de tela, teclados ou preenchimento automático, com a verificação no servidor para cada stack.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/spam-protection/honeypot

Adicione um campo de texto extra que você esconde fora da tela, tira da ordem de tabulação e envolve em `aria-hidden`, com um nome que nenhum navegador preenche automaticamente. Pessoas nunca o preenchem; bots que preenchem todos os campos, sim. No servidor, trate qualquer envio com valor nele como spam e responda exatamente como faria com uma mensagem real.

## O que um honeypot tem de diferente

Um honeypot precisa ser duas coisas ao mesmo tempo. Para um bot lendo o HTML, deve parecer um campo normal que vale a pena preencher. Para toda pessoa, não pode existir, e isso inclui quem não usa o formulário do mesmo jeito que você: alguém com leitor de tela, alguém navegando pelo teclado, alguém cujo gerenciador de senhas preenche formulários por ele. A maioria dos bugs de honeypot vem de esquecer uma dessas pessoas, e o sintoma é uma mensagem real arquivada como spam sem alarde.

Diagrama: Como uma pessoa e um bot encontram o mesmo campo honeypot

1. Uma pessoa preenche o formulário com o que costuma usar: tela, teclado ou leitor de tela.
2. A armadilha está fora da tela, fora da ordem de tabulação e oculta das tecnologias assistivas, então ela nunca chega até lá.
3. O nome dela é um que navegadores e gerenciadores de senha não preenchem, então ela fica vazia.
4. O envio chega com a armadilha vazia e é recebido.
5. Um bot de script lê o HTML e encontra todos os campos, inclusive a armadilha.
6. Ele preenche todos e envia na hora.
7. A armadilha tem valor, então o envio é spam. O bot ainda recebe uma resposta de sucesso normal.

Cada proteção da linha de cima fecha um caminho pelo qual uma pessoa real chegaria ao campo. Tire qualquer uma e a linha de cima também pode terminar em spam.

## O markup

Coloque a armadilha dentro do formulário, ao lado dos campos reais, como um campo de texto comum. Um campo `type="hidden"` não funciona: os bots sabem que pessoas não podem editá-lo e o deixam em paz.

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>

  <button>Send</button>
</form>
```

`estilos`:

```html
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
```

- `aria-hidden="true"` no contêiner remove o rótulo e o campo da árvore de acessibilidade, então leitores de tela não os anunciam.
- `tabindex="-1"` impede que a tecla Tab pare num campo que ninguém vê.
- O rótulo diz **Leave this empty** (deixe vazio). Se alguma ferramenta ainda expuser o campo, quem o ler sabe o que fazer.
- Não adicione `required`. Um campo obrigatório que as pessoas não veem bloqueia todo envio honesto.

## Como escondê-lo

Os bots aprenderam a pular campos obviamente ocultos. Scrapers simples ainda preenchem tudo, mas alguns conferem o atributo `hidden` ou um `display: none` inline antes de digitar. Quanto mais longe do próprio campo estiver o que o esconde, mais bots caem.

| Técnica | Oculto para pessoas | Bots que ainda pega |
| --- | --- | --- |
| Atributo `hidden` ou `display: none` inline | Sim | Só os mais simples. É a primeira coisa que um bot mais esperto confere |
| Classe fora da tela na sua folha de estilo | Sim | A maioria. O bot precisa carregar e aplicar seu CSS para perceber |
| `opacity: 0` ou tamanho zero | Quase sempre. Algumas lupas e contornos de foco ainda o revelam | A maioria, mas é mais arriscado para pessoas |
| Adicionado por JavaScript após o carregamento | Sim | Bots que rodam scripts. Scrapers de HTML simples nunca o veem, então ele os deixa passar |

Use a classe fora da tela da sua folha de estilo. Ela pega mais bots e é a mais segura para pessoas. Se um honeypot parar de funcionar, adicione um segundo com outro nome em vez de um truque de esconder mais esperto.

## Escolha um nome que o preenchimento automático ignore

Navegadores e gerenciadores de senha preenchem campos adivinhando pelo `name`, `id`, rótulo e valor de `autocomplete`. `autocomplete="off"` é só uma dica, e alguns navegadores a ignoram em campos que parecem endereços ou dados de contato. Então quem faz o trabalho de verdade é o nome.

| Evite | Por quê | Use no lugar |
| --- | --- | --- |
| `email2`, `phone`, `name`, `address` | O preenchimento automático os reconhece e os preenche para pessoas reais | `_gotcha`, `company_website` |
| `honeypot`, `trap`, `bot` | Alguns bots pulam campos com nomes óbvios | Um nome sem graça e plausível |
| `url`, `website` sozinhos | Algumas ferramentas de preenchimento automático preenchem um campo de site a partir de um perfil salvo | Um nome composto como `company_website` |

## Confira no servidor

Um honeypot só funciona se o servidor o ler. Uma verificação no navegador é inútil, já que um bot nunca roda seu script. Quando o campo tiver valor, responda exatamente como faria com uma mensagem real e não guarde nem repasse o envio. Uma resposta diferente diz ao bot qual campo o denunciou. Os exemplos abaixo chamam a sua própria `saveMessage`; troque pelo que guarda ou envia a mensagem por e-mail.

**HTML**

Um formulário HTML simples não tem servidor próprio. Aponte-o para o Ikiform e chame a armadilha de `_gotcha`, ou renomeie por formulário em **Honeypot field**. O Ikiform faz a verificação no servidor, e a armadilha nunca é guardada como resposta.

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>

  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>

  <button>Send</button>
</form>
```

**Next.js**

Numa server action, redirecione um envio pego na armadilha para a mesma página de agradecimento que um real recebe, e pule o salvamento.

`app/contact/actions.ts`:

```ts
"use server";

import { redirect } from "next/navigation";

export async function sendMessage(formData: FormData) {
  if (String(formData.get("company_website") ?? "").trim() !== "") {
    console.info("honeypot", { at: new Date().toISOString() });
    redirect("/contact/thanks");
  }
  await saveMessage({
    email: String(formData.get("email") ?? ""),
    message: String(formData.get("message") ?? ""),
  });
  redirect("/contact/thanks");
}
```

**Astro**

Numa Astro Action, tire a armadilha do input e retorne o resultado normal quando ela tiver valor.

`src/actions/index.ts`:

```ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";

export const server = {
  sendMessage: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      message: z.string().min(1),
      company_website: z.string().nullish(),
    }),
    handler: async ({ company_website, ...fields }) => {
      if (company_website?.trim()) return { sent: true };
      await saveMessage(fields);
      return { sent: true };
    },
  }),
};
```

**TanStack Start**

Leia a armadilha em `.validator()` e pule o salvamento no handler. Os dois caminhos retornam a mesma resposta.

`src/server/send-message.ts`:

```ts
import { createServerFn } from "@tanstack/react-start";

export const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? ""),
      message: String(data.get("message") ?? ""),
      trapped: String(data.get("company_website") ?? "").trim() !== "",
    };
  })
  .handler(async ({ data: { trapped, ...fields } }) => {
    if (!trapped) await saveMessage(fields);
    return { ok: true };
  });
```

**React Router**

A action da rota redireciona para a página de agradecimento nos dois casos e só salva um envio que não caiu na armadilha.

`app/routes/contact.tsx`:

```ts
import { redirect } from "react-router";
import type { Route } from "./+types/contact";

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  if (String(form.get("company_website") ?? "").trim()) return redirect("/contact/thanks");
  await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
  return redirect("/contact/thanks");
}
```

**SvelteKit**

A form action só salva quando a armadilha está vazia e depois redireciona com `303` nos dois casos.

`src/routes/contact/+page.server.ts`:

```ts
import { redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    if (!String(form.get("company_website") ?? "").trim()) {
      await saveMessage({ email: String(form.get("email")), message: String(form.get("message")) });
    }
    redirect(303, "/contact/thanks");
  },
} satisfies Actions;
```

**Nuxt**

A server route responde `201` com o mesmo corpo para um envio pego na armadilha e para um real.

`server/api/contact.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ email?: string; message?: string; company_website?: string }>(event);
  if (body.company_website?.trim()) {
    setResponseStatus(event, 201);
    return { sent: true };
  }
  await saveMessage({ email: body.email ?? "", message: body.message ?? "" });
  setResponseStatus(event, 201);
  return { sent: true };
});
```

**React**

Um app de página única não consegue conferir a armadilha sozinho. Envie-a com os outros campos e deixe o servidor para onde você envia decidir. `FormData` a inclui sem código extra.

`submit.ts`:

```ts
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
```

**Vue**

Igual no Vue: mantenha a armadilha dentro do `<form>` para que `new FormData(form)` a inclua, e deixe o servidor conferi-la.

`submit.ts`:

```ts
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
```

> Registre em log os envios pegos na armadilha, mesmo que não os guarde. Uma contagem diária mostra se o honeypot ainda faz por merecer, e uma queda repentina a zero significa que os bots aprenderam a pulá-lo.

## Como o Ikiform lida com o honeypot

Todo formulário do Ikiform tem por padrão um honeypot chamado `_gotcha`. Mude-o por formulário em **Honeypot field** nas configurações do formulário; o nome que você escolher fica reservado e nunca é guardado como resposta. Um honeypot preenchido soma 5 à pontuação de spam, o que já atinge sozinho o limite de 5.

O Ikiform não descarta o envio. Ele o mantém como spam, então se uma pessoa real cair na armadilha, você pode achar a mensagem e restaurá-la. Um envio julgado como spam recebe a mesma resposta de sucesso que um real, então o bot não aprende nada. Ele é guardado com o status `spam`, não dispara e-mail de notificação nem webhook, não conta no seu uso e é apagado após 30 dias. Toda segunda-feira, um workspace que bloqueou spam recebe um único e-mail de resumo com o total, os formulários mais visados e os principais motivos. A [documentação de spam](https://ikiform.com/pt-br/docs/spam.md#honeypot) e a de [formulários HTML](https://ikiform.com/pt-br/docs/html.md#honeypot) têm a referência.

## Confira se funciona

1. Carregue o formulário com um leitor de tela ligado e percorra-o. A armadilha nunca é anunciada.
2. Navegue pelo formulário com Tab. O foco vai do último campo real direto para o botão.
3. Deixe seu navegador e seu gerenciador de senhas preencherem o formulário automaticamente. A armadilha continua vazia.
4. Envie com o honeypot preenchido, `_gotcha=bot`. Ele ainda recebe uma resposta de sucesso, mas o envio é guardado como spam e não dispara e-mail nem webhook.
5. Envie uma mensagem normal. Ela chega como recebida, sem nenhum vestígio do campo armadilha.

## Pegadinhas

- **Envios em JSON.** Se você monta o corpo à mão em vez de usar `FormData`, inclua a armadilha, ou seu servidor também a lê como vazia para os bots.
- **Construtores de formulário que renomeiam campos.** Alguns construtores colocam prefixo ou renomeiam os campos. Confira o corpo enviado, não o editor, para saber o nome real da armadilha.
- **Cache.** Se uma página em cache ainda tiver o nome antigo da armadilha depois que você o renomear, os envios chegam com o nome antigo e passam. Renomeie dos dois lados ao mesmo tempo.
- **Honeypots de checkbox.** Uma checkbox oculta também funciona, mas leitores de tela lidam pior com checkboxes soltas do que com campos de texto. Prefira um campo de texto.
- **Uma camada só.** Bots que rodam um navegador real e pulam campos invisíveis passam por qualquer honeypot. Combine-o com um [limite de taxa](https://ikiform.com/pt-br/guides/spam-protection/rate-limiting.md) e, se precisar, uma [verificação humana](https://ikiform.com/pt-br/guides/spam-protection/cloudflare-turnstile.md).

## Perguntas

### Campos honeypot ainda funcionam contra bots de spam?

Sim, contra os mais comuns: scripts que preenchem todos os campos que encontram. Bots que rodam um navegador completo e pulam campos invisíveis passam, então use o honeypot como primeira camada, não a única.

### Um campo honeypot faz mal à acessibilidade?

Só se for escondido sem cuidado. Envolva-o num elemento com aria-hidden, dê tabindex -1 ao campo, use o rótulo Leave this empty e nunca o marque como obrigatório. Assim leitores de tela e teclados o pulam.

### Por que uma mensagem real caiu no meu honeypot?

Geralmente é o preenchimento automático. Um navegador ou gerenciador de senhas preencheu a armadilha porque o nome dela parecia de um campo real, como email2 ou phone. Renomeie-a para algo que nenhum preenchimento automático reconheça.

### Devo avisar o remetente que a mensagem foi rejeitada?

Não. Responda a um envio pego na armadilha exatamente como a um real. Um erro diz ao bot qual campo o pegou, e a próxima versão do bot pula esse campo.

## Mais guias

- [Acabe com o spam no formulário de contato](https://ikiform.com/pt-br/guides/spam-protection.md): Acabe com o spam no formulário de contato com camadas que não custam nada a pessoas reais: honeypot, tempo, pontuação de conteúdo, limites de taxa e verificação humana só quando precisar.
- [Sem captcha](https://ikiform.com/pt-br/guides/spam-protection/without-captcha.md): Barre a maior parte do spam em formulários sem captcha nenhum: honeypot, horário de renderização assinado, pontuação de conteúdo e limite de taxa, somados em vez de usados sozinhos.
- [Limite de taxa](https://ikiform.com/pt-br/guides/spam-protection/rate-limiting.md): Limite a taxa de envios por visitante com um armazenamento compartilhado, responda 429 com Retry-After e evite bloquear escritórios que compartilham um IP.
- [Cloudflare Turnstile](https://ikiform.com/pt-br/guides/spam-protection/cloudflare-turnstile.md): Adicione o Cloudflare Turnstile a um formulário HTML simples: o widget, o siteverify no servidor, chaves de teste e um jeito de rodá-lo sem chaves nem servidor próprio.
- [Alternativas ao reCAPTCHA](https://ikiform.com/pt-br/guides/spam-protection/recaptcha-alternatives.md): Compare alternativas ao reCAPTCHA para formulários, do Turnstile e hCaptcha a prova de trabalho e nenhum captcha, e migre com uma mudança pequena e segura.
- [Sites estáticos](https://ikiform.com/pt-br/guides/spam-protection/static-site.md): Acabe com o spam num formulário de site estático mesmo sem guardar segredos nem contar envios, com um partial de honeypot, um script seguro e verificações no servidor.
