# Cómo añadir un campo honeypot que rellenan los bots y no las personas

> Añade a cualquier formulario un honeypot que atrape bots sin molestar a lectores de pantalla, teclado ni autocompletado, con la comprobación en el servidor para cada stack.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/spam-protection/honeypot

Añade un input de texto extra que ocultas fuera de pantalla, sacas del orden de tabulación y envuelves en `aria-hidden`, con un nombre que ningún navegador autocompleta. Las personas nunca lo rellenan; los bots que rellenan todos los inputs, sí. En el servidor, trata como spam cualquier envío que lo traiga con un valor y responde exactamente igual que a un mensaje real.

## Qué tiene de especial un honeypot

Un honeypot tiene que ser dos cosas a la vez. Para un bot que lee el HTML, debe parecer un campo normal que vale la pena rellenar. Para cualquier persona, no debe existir, y eso incluye a quienes no usan el formulario como tú: alguien con lector de pantalla, alguien que navega con el teclado, alguien cuyo gestor de contraseñas rellena los formularios por él. La mayoría de los fallos de honeypot vienen de olvidar a alguna de esas personas, y el síntoma es un mensaje real archivado en silencio como spam.

Diagrama: Cómo una persona y un bot se encuentran con el mismo campo honeypot

1. Una persona rellena el formulario con lo que use: una pantalla, un teclado o un lector de pantalla.
2. La trampa está fuera de pantalla, fuera del orden de tabulación y oculta a la tecnología de asistencia, así que nunca llega a ella.
3. Su nombre es uno que los navegadores y los gestores de contraseñas no autocompletan, así que se queda vacía.
4. El envío llega con la trampa vacía y se recibe.
5. Un bot de script lee el HTML y encuentra todos los inputs, incluida la trampa.
6. Los rellena todos y envía al instante.
7. La trampa tiene un valor, así que el envío es spam. El bot recibe igualmente una respuesta de éxito normal.

Cada protección de la fila de arriba cierra una vía por la que una persona real podría llegar al campo. Quita cualquiera y la fila de arriba también puede acabar en spam.

## El markup

Pon la trampa dentro del formulario, junto a los campos reales, como un input de texto normal. Un input `type="hidden"` no sirve: los bots saben que las personas no pueden editarlos y los dejan en 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"` en el contenedor quita la etiqueta y el input del árbol de accesibilidad, así que los lectores de pantalla no los anuncian.
- `tabindex="-1"` evita que la tecla Tab caiga en un input que nadie ve.
- La etiqueta dice **Leave this empty** (déjalo vacío). Si alguna herramienta llega a mostrar el campo, quien lo lea sabe qué hacer.
- No añadas `required`. Un campo obligatorio que las personas no pueden ver bloquea todos los envíos honestos.

## Cómo ocultarlo

Los bots han aprendido a saltarse los campos que están ocultos de forma evidente. Los scrapers sencillos siguen rellenándolo todo, pero algunos miran el atributo `hidden` o un `display: none` en línea antes de escribir. Cuanto más lejos del propio input esté la ocultación, más bots caen.

| Técnica | Oculto para las personas | Bots que aún atrapa |
| --- | --- | --- |
| Atributo `hidden` o `display: none` en línea | Sí | Solo los más simples. Es lo primero que comprueba un bot más listo |
| Clase fuera de pantalla en tu hoja de estilos | Sí | La mayoría. El bot tendría que cargar y aplicar tu CSS para darse cuenta |
| `opacity: 0` o tamaño cero | Casi siempre. Algunas lupas y contornos de foco aún lo revelan | La mayoría, pero es más arriesgado para las personas |
| Añadido con JavaScript después de cargar | Sí | Bots que ejecutan scripts. Los scrapers de HTML no lo ven nunca, así que se le escapan |

Usa la clase fuera de pantalla de tu hoja de estilos. Atrapa más bots y es la más segura para las personas. Si un honeypot deja de funcionar, añade un segundo con otro nombre en lugar de un truco de ocultación más ingenioso.

## Elige un nombre que el autocompletado ignore

Los navegadores y los gestores de contraseñas rellenan campos adivinando a partir de su `name`, su `id`, su etiqueta y su valor de `autocomplete`. `autocomplete="off"` es una sugerencia, y algunos navegadores la ignoran en campos que parecen direcciones o datos de contacto. Así que el nombre es lo que hace el trabajo de verdad.

| Evita | Por qué | Usa en su lugar |
| --- | --- | --- |
| `email2`, `phone`, `name`, `address` | El autocompletado los reconoce y los rellena para personas reales | `_gotcha`, `company_website` |
| `honeypot`, `trap`, `bot` | Algunos bots se saltan los campos con nombres obvios | Un nombre soso y creíble |
| `url` o `website` a secas | Algunas herramientas de autocompletado rellenan un campo de sitio web desde un perfil guardado | Un nombre compuesto, como `company_website` |

## Compruébalo en el servidor

Un honeypot solo funciona si el servidor lo lee. Una comprobación en el navegador no sirve de nada, porque un bot nunca ejecuta tu script. Cuando el campo tenga un valor, responde exactamente igual que a un mensaje real, y no guardes ni reenvíes el envío. Una respuesta distinta le dice al bot qué campo lo delató. Los ejemplos de abajo llaman a tu propio `saveMessage`; cámbialo por lo que guarde o envíe por email el mensaje.

**HTML**

Un formulario HTML básico no tiene servidor propio. Apúntalo a Ikiform y llama a la trampa `_gotcha`, o renómbrala en cada formulario en **Honeypot field**. Ikiform hace la comprobación en el servidor, y la trampa nunca se guarda como respuesta.

`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**

En una server action, redirige un envío atrapado a la misma página de agradecimiento que recibe uno real, y sáltate el guardado.

`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**

En una Astro Action, saca la trampa del input y devuelve el resultado normal cuando tenga un 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**

Lee la trampa en `.validator()` y sáltate el guardado en el handler. Ambos caminos devuelven la misma respuesta.

`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**

La action de la ruta redirige a la página de agradecimiento en ambos casos y solo guarda un envío sin la trampa rellena.

`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**

La form action solo guarda cuando la trampa está vacía, y luego redirige con `303` en ambos 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**

La ruta de servidor responde `201` con el mismo cuerpo para un envío atrapado y para uno 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**

Una single-page app no puede comprobar la trampa por sí misma. Envíala con los demás campos y deja que decida el servidor al que envías. `FormData` la incluye sin 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 en Vue: deja la trampa dentro del `<form>` para que `new FormData(form)` la incluya, y deja que el servidor la compruebe.

`submit.ts`:

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

> Registra los envíos atrapados aunque no los guardes. Un conteo diario te dice si el honeypot sigue mereciendo su lugar, y una caída repentina a cero significa que los bots han aprendido a saltárselo.

## Cómo gestiona Ikiform el honeypot

Cada formulario de Ikiform tiene por defecto un honeypot llamado `_gotcha`. Cámbialo en cada formulario en **Honeypot field**, en los ajustes del formulario; el nombre que elijas queda reservado y nunca se guarda como respuesta. Un honeypot relleno suma 5 a la puntuación de spam, lo que alcanza por sí solo el umbral de 5.

Ikiform no descarta el envío. Lo guarda como spam, así que si alguna vez una persona real cae en la trampa, puedes encontrar el mensaje y restaurarlo. Un envío considerado spam recibe la misma respuesta de éxito que uno real, así que el bot no aprende nada. Se guarda con el estado `spam`, no envía email de notificación ni webhook, no cuenta para tu uso y se elimina a los 30 días. Cada lunes, un espacio de trabajo que haya bloqueado spam recibe un email de resumen con el total, los formularios con más actividad y los motivos principales. La [documentación de spam](https://ikiform.com/es/docs/spam.md#honeypot) y [Formularios HTML](https://ikiform.com/es/docs/html.md#honeypot) tienen la referencia.

## Comprueba que funciona

1. Carga el formulario con un lector de pantalla activado y recórrelo. La trampa nunca se anuncia.
2. Recorre el formulario con Tab. El foco pasa del último campo real directamente al botón.
3. Deja que tu navegador y tu gestor de contraseñas autocompleten el formulario. La trampa sigue vacía.
4. Envía con el honeypot relleno, `_gotcha=bot`. Sigue recibiendo una respuesta de éxito, pero el envío se guarda como spam y no envía email ni webhook.
5. Envía un mensaje normal. Llega como recibido, sin rastro del campo trampa.

## Trampas habituales

- **Envíos JSON.** Si construyes el cuerpo a mano en lugar de con `FormData`, incluye la trampa, o tu servidor la leerá vacía también para los bots.
- **Creadores de formularios que renombran campos.** Algunos creadores añaden prefijos o renombran los inputs. Mira el cuerpo enviado, no el editor, para saber el nombre real de la trampa.
- **Caché.** Si una página en caché conserva el nombre antiguo de la trampa después de renombrarla, los envíos llegan con el nombre viejo y pasan. Renómbrala en ambos lados a la vez.
- **Honeypots de checkbox.** Una checkbox oculta también funciona, pero los lectores de pantalla gestionan peor las checkboxes sueltas que los inputs de texto. Mejor un input de texto.
- **Una sola capa.** Los bots que usan un navegador real y se saltan los campos invisibles pasan por delante de cualquier honeypot. Combínalo con un [límite de frecuencia](https://ikiform.com/es/guides/spam-protection/rate-limiting.md) y, si hace falta, una [verificación humana](https://ikiform.com/es/guides/spam-protection/cloudflare-turnstile.md).

## Preguntas

### ¿Los campos honeypot siguen funcionando contra los bots de spam?

Sí, contra los más comunes: scripts que rellenan todos los inputs que encuentran. Los bots que usan un navegador completo y se saltan los campos invisibles pasan, así que usa el honeypot como primera capa, no como la única.

### ¿Un campo honeypot es malo para la accesibilidad?

Solo si se oculta sin cuidado. Envuélvelo en un elemento con aria-hidden, dale al input tabindex -1, ponle la etiqueta Leave this empty y nunca lo marques como obligatorio. Así los lectores de pantalla y el teclado se lo saltan.

### ¿Por qué mi honeypot atrapó un mensaje real?

Normalmente por el autocompletado. Un navegador o un gestor de contraseñas rellenó la trampa porque su nombre parecía un campo real, como email2 o phone. Renómbrala con algo que ningún autocompletado reconozca.

### ¿Debo decirle al remitente que su mensaje fue rechazado?

No. Responde a un envío atrapado exactamente igual que a uno real. Un error le dice al bot qué campo lo atrapó, y la siguiente versión del bot se salta ese campo.

## Más guías

- [Frena el spam en formularios de contacto](https://ikiform.com/es/guides/spam-protection.md): Frena el spam en tu formulario de contacto con capas que no cuestan nada a las personas reales: honeypot, tiempo, puntuación del contenido, límites de frecuencia y verificación humana solo donde haga falta.
- [Sin captcha](https://ikiform.com/es/guides/spam-protection/without-captcha.md): Frena casi todo el spam de tus formularios sin ningún captcha: honeypot, hora de renderizado firmada, puntuación del contenido y límite de frecuencia, sumados en lugar de usarlos por separado.
- [Límites de frecuencia](https://ikiform.com/es/guides/spam-protection/rate-limiting.md): Limita los envíos de un formulario por visitante con un almacén compartido, responde 429 con Retry-After y no bloquees oficinas que comparten una IP.
- [Cloudflare Turnstile](https://ikiform.com/es/guides/spam-protection/cloudflare-turnstile.md): Añade Cloudflare Turnstile a un formulario HTML básico: el widget, siteverify en el servidor, claves de prueba y una forma de usarlo sin claves ni servidor propios.
- [Alternativas a reCAPTCHA](https://ikiform.com/es/guides/spam-protection/recaptcha-alternatives.md): Compara alternativas a reCAPTCHA para formularios, de Turnstile y hCaptcha a proof-of-work o ningún captcha, y cambia con un ajuste pequeño y seguro.
- [Sitios estáticos](https://ikiform.com/es/guides/spam-protection/static-site.md): Frena el spam en el formulario de un sitio estático aunque no pueda guardar secretos ni contar envíos, con un partial de honeypot, un script seguro y comprobaciones en el servidor.
