# Formulario de suscripción a newsletter sin el embed de Mailchimp

> Crea un formulario de suscripción a tu newsletter con tu propio markup, registra el consentimiento, bloquea los registros de bots y pasa cada suscriptor a Kit, MailerLite, beehiiv o cualquier herramienta.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/newsletter-signup

Escribe el formulario de suscripción en tu propio HTML y apúntalo a un backend de formularios en lugar de pegar el código embed de tu proveedor de email. El backend valida la dirección y la casilla de consentimiento, filtra los bots y envía un webhook firmado, y un pequeño relay añade el suscriptor mediante la API de tu herramienta de email. La herramienta de email envía después el email de confirmación, la newsletter y los enlaces para darse de baja, así que cambiar de herramienta más adelante significa cambiar el relay, no todos los formularios.

Todas las herramientas de email te dan un embed: un bloque de markup generado que pegas en tu página. Funciona, pero el formulario pasa a ser de la herramienta. Envía directamente a ese proveedor, lleva su markup y a menudo sus estilos y scripts, y el día que te cambias a otra herramienta tienes que buscar cada copia por todo tu sitio. Esta guía construye un formulario de suscripción que es tuyo, con el markup y el framework que ya usas, y lo conecta con la herramienta que envíe tu newsletter.

## Cómo viaja una suscripción

Aquí hay dos trabajos, y van en sitios distintos. Recoger una suscripción es trabajo de formulario: validar la dirección, comprobar la casilla de consentimiento, dejar fuera a los bots, registrar cuándo ocurrió. Enviar una newsletter es trabajo de email: confirmar la dirección, enviar los números, gestionar las bajas y los rebotes. Ikiform hace lo primero. Tu herramienta de email hace lo segundo. Un webhook firmado los conecta.

Diagrama: Cómo viaja una suscripción a la newsletter desde tu página hasta tu herramienta de email

1. Un lector rellena el formulario de suscripción de tu página, que envía a tu endpoint de Ikiform.
2. Ikiform comprueba la dirección, la casilla de consentimiento obligatoria, el honeypot, la puntuación de spam y el límite de frecuencia.
3. La suscripción aceptada se guarda en el panel con su marca de tiempo, su origen y su referrer.
4. Ikiform puede avisarte por email de cada suscripción. Nunca envía email al lector.
5. Ikiform envía un webhook submission.created firmado.
6. Un relay verifica la firma y añade al lector a tu herramienta de email, que envía la confirmación y, después, cada número.

El relay es la única parte que sabe qué herramienta de email usas. El formulario, el filtrado de spam y el registro guardado no cambian cuando cambia la herramienta.

## Cuatro formas de recoger suscripciones a una newsletter

| Enfoque | Punto fuerte | Inconvenientes |
| --- | --- | --- |
| El embed de la herramienta de email, de Mailchimp, Kit, MailerLite o beehiiv | Nada que mantener. La confirmación, las etiquetas y el email de bienvenida se configuran en los propios ajustes de la herramienta. | El markup y el comportamiento son de la herramienta. Mailchimp advierte que sus formularios embebidos heredan los estilos de tu sitio, así que pueden verse distintos en tu página, y activar su reCAPTCHA en un formulario existente significa generar y pegar código nuevo. Cada copia está atada a un proveedor. |
| Tu propia ruta de servidor que llama a la API de la herramienta | Control total del formulario y de la petición, con la API key guardada en tu servidor. | La validación, el filtrado de bots, los límites de frecuencia y el registro del consentimiento los construyes tú, y necesitas un servidor aunque el sitio sea estático. |
| Un creador de formularios alojado más una automatización, como [Typeform](https://ikiform.com/alternatives/typeform) con Zapier | Sin código, y la herramienta de automatización conoce todos los proveedores de email. | Un iframe que no encaja con tu página, más dos suscripciones. Los webhooks y la marca dependen del plan. |
| Un backend de formularios, como Ikiform, más un relay | Tu propio markup en cualquier sitio, filtrado de spam, un registro guardado del consentimiento y webhooks firmados, sin servidor para el formulario. | Ikiform no envía ningún email al lector. La confirmación y cada newsletter salen de tu herramienta de email, mediante un pequeño relay o una automatización. |

¿Te va bien con un sitio, una herramienta y su embed? Quédate con eso. Construye el tuyo cuando el formulario deba encajar con tu diseño, esté en varios sitios o pueda sobrevivir a la herramienta.

## El formulario

Un buen formulario de newsletter es pequeño: un email, un nombre opcional para el saludo y una casilla de consentimiento cuya etiqueta es la promesa que haces. Un input oculto `source` registra en qué parte del sitio se hizo la suscripción. Elige tu stack; la elección se aplica a todos los ejemplos de código de estas guías.

**HTML**

Pega el formulario en tu footer, en la barra lateral o al final de un post. Envía de la forma normal y llega a tu propia página de agradecimiento, así que funciona sin JavaScript.

`footer.html`:

```html
<form id="newsletter" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="nl-email">Email</label>
  <input id="nl-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" />

  <label for="nl-name">First name (optional)</label>
  <input id="nl-name" name="first_name" type="text" maxlength="80" autocomplete="given-name" />

  <label>
    <input name="consent" type="checkbox" value="true" required />
    Email me the newsletter. I can unsubscribe at any time.
  </label>

  <input type="hidden" name="source" value="footer" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />

  <button>Subscribe</button>
  <p id="nl-status" role="status" hidden></p>
</form>
```

Para que el lector siga en el artículo, añade este script después del formulario. Envía con `fetch`, sustituye el formulario por una línea que le pide revisar su bandeja de entrada y muestra el mensaje del servidor para el email o la casilla.

`newsletter.js`:

```js
const form = document.querySelector("#newsletter");
const status = document.querySelector("#nl-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const body = new FormData(form);
  button.disabled = true;
  status.hidden = false;
  status.textContent = "Subscribing";
  try {
    const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(status);
      status.textContent = "Almost done. Open the email we sent you and confirm your address.";
      return;
    }
    const fields = result.error.fields ?? {};
    status.textContent = fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Una server action reenvía la suscripción con tu clave secreta. Devuelve los errores por campo, así que el mensaje del email queda bajo el input del email y el del consentimiento bajo la casilla. El componente recibe una prop `source`, así que el footer y una caja dentro del artículo registran valores distintos.

`app/newsletter/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface SignupState {
  done: boolean;
  errors: { email?: string; consent?: string; form?: string };
  email: string;
}

export async function subscribe(_previous: SignupState, formData: FormData): Promise<SignupState> {
  const email = String(formData.get("email") ?? "").trim();
  const consent = formData.get("consent") === "true";
  if (!consent) return { done: false, errors: { consent: "Tick the box so we know you want the emails." }, email };

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent,
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { done: true, errors: {}, email };
  const fields = result?.error?.fields ?? {};
  return {
    done: false,
    errors: {
      email: fields.email?.[0],
      consent: fields.consent?.[0],
      form: fields.email || fields.consent ? undefined : (result?.error?.message ?? "Couldn't subscribe. Try again."),
    },
    email,
  };
}
```

`app/newsletter/newsletter-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type SignupState, subscribe } from "./actions";

const initial: SignupState = { done: false, errors: {}, email: "" };

export function NewsletterForm({ source = "footer" }: { source?: string }) {
  const [state, action, pending] = useActionState(subscribe, initial);

  if (state.done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action={action} noValidate>
      <label htmlFor="nl-email">Email</label>
      <input
        id="nl-email"
        name="email"
        type="email"
        required
        autoComplete="email"
        defaultValue={state.email}
        aria-invalid={Boolean(state.errors.email)}
        aria-describedby="nl-email-error"
      />
      <p id="nl-email-error">{state.errors.email}</p>
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} autoComplete="given-name" />
      <label>
        <input name="consent" type="checkbox" value="true" aria-describedby="nl-consent-error" />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      <p id="nl-consent-error">{state.errors.consent}</p>
      <input type="hidden" name="source" value={source} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.errors.form ? <p role="alert">{state.errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

¿Quieres profundizar con Next.js? Lee [Crea un formulario de suscripción a newsletter en Next.js](https://ikiform.com/es/guides/newsletter-signup/nextjs.md).

**Astro**

Una Astro Action con `accept: "form"` interpreta la casilla como booleano y el email con `z.email()`. `isInputError` distingue una dirección incorrecta de un envío rechazado. Las form actions necesitan una página bajo demanda, así que añade un adaptador y pon `export const prerender = false` en cada página que renderice el componente; el export no hace nada dentro de un componente.

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  subscribe: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      first_name: z.string().max(80).nullish(),
      consent: z.boolean(),
      source: z.string().default("footer"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      if (!input.consent) {
        throw new ActionError({ code: "BAD_REQUEST", message: "Tick the box so we know you want the emails." });
      }
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify({ ...input, first_name: input.first_name ?? undefined }),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { subscribed: true };
    },
  }),
};
```

`src/components/NewsletterForm.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

const result = Astro.getActionResult(actions.subscribe);
const fields = isInputError(result?.error) ? result.error.fields : {};
---

{result?.data?.subscribed ? (
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
) : (
  <form method="POST" action={actions.subscribe}>
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    {fields.email && <p>Enter a valid email address.</p>}
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Subscribe</button>
  </form>
)}
```

¿Quieres profundizar con Astro? Lee [Crea un formulario de suscripción a newsletter en Astro](https://ikiform.com/es/guides/newsletter-signup/astro.md).

**TanStack Start**

Una función POST de `createServerFn` convierte el `FormData` en JSON en `.validator()` y devuelve los errores de campo como datos en lugar de lanzarlos, para que el componente pueda poner cada mensaje junto a su input.

`src/components/newsletter-form.tsx`:

```tsx
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

type Errors = { email?: string; consent?: string; form?: string };

const subscribe = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? "").trim(),
      first_name: String(data.get("first_name") ?? "").trim() || undefined,
      consent: data.get("consent") === "true",
      source: String(data.get("source") ?? "footer"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<{ ok: boolean; errors: Errors }> => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, errors: {} };
    const fields = result.error.fields ?? {};
    return {
      ok: false,
      errors: { email: fields.email?.[0], consent: fields.consent?.[0], form: result.error.message },
    };
  });

export function NewsletterForm() {
  const send = useServerFn(subscribe);
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<Errors>({});
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      const result = await send({ data: new FormData(event.currentTarget) });
      setDone(result.ok);
      setErrors(result.errors);
    } catch {
      setErrors({ form: "Couldn't subscribe. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.email && !errors.consent ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

¿Quieres profundizar con TanStack Start? Lee [Crea un formulario de suscripción a newsletter en TanStack Start](https://ikiform.com/es/guides/newsletter-signup/tanstack-start.md).

**React Router**

Un formulario en el footer no debería sacarte de la página que estás leyendo. Pon la action en una resource route en `/newsletter` y envía a ella con `useFetcher`, que envía sin navegar y mantiene el resultado en la página actual.

`app/routes/newsletter.tsx`:

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email: String(formData.get("email") ?? "").trim(),
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent: formData.get("consent") === "true",
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true as const };
  return data(
    { ok: false as const, fields: (result.error.fields ?? {}) as Record<string, string[]>, message: String(result.error.message) },
    { status: response.status },
  );
}
```

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [index("routes/home.tsx"), route("newsletter", "routes/newsletter.tsx")] satisfies RouteConfig;
```

`app/components/newsletter-form.tsx`:

```tsx
import { useFetcher } from "react-router";
import type { action } from "~/routes/newsletter";

export function NewsletterForm() {
  const fetcher = useFetcher<typeof action>();
  const result = fetcher.data;
  const busy = fetcher.state !== "idle";

  if (result?.ok) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <fetcher.Form method="post" action="/newsletter">
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {result?.fields.email ? <p>{result.fields.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {result?.fields.consent ? <p>{result.fields.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {result && !result.fields.email && !result.fields.consent ? <p role="alert">{result.message}</p> : null}
      <button disabled={busy}>{busy ? "Subscribing" : "Subscribe"}</button>
    </fetcher.Form>
  );
}
```

¿Quieres profundizar con React Router? Lee [Crea un formulario de suscripción a newsletter en React Router](https://ikiform.com/es/guides/newsletter-signup/react-router.md).

**SvelteKit**

Las actions viven en un `+page.server.ts`, nunca en un layout, así que la action está en una página `/newsletter` y el footer envía a `/newsletter?/subscribe`. El callback personalizado de `use:enhance` gestiona el resultado por sí mismo en lugar de llamar a `update()`, así que el lector se queda en el artículo. Declara `IKIFORM_SECRET_KEY` en `src/env.ts` con `defineEnvVars` para que `$app/env/private` pueda leerla.

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

```ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

export const actions = {
  subscribe: async ({ request, fetch, url }) => {
    const form = await request.formData();
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify({
        email: String(form.get("email") ?? "").trim(),
        first_name: String(form.get("first_name") ?? "").trim() || undefined,
        consent: form.get("consent") === "true",
        source: String(form.get("source") ?? "footer"),
        _gotcha: String(form.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields = result.error.fields ?? {};
      return fail(response.status, { message: String(fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message) });
    }
    return { subscribed: true };
  },
} satisfies Actions;
```

`src/lib/NewsletterForm.svelte`:

```svelte
<script lang="ts">
  import { applyAction, enhance } from "$app/forms";

  let done = $state(false);
  let message = $state("");
  let pending = $state(false);
</script>

{#if done}
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
{:else}
  <form
    method="POST"
    action="/newsletter?/subscribe"
    use:enhance={() => {
      pending = true;
      return async ({ result }) => {
        pending = false;
        if (result.type === "success") done = true;
        else if (result.type === "failure") message = String(result.data?.message ?? "Couldn't subscribe.");
        else await applyAction(result);
      };
    }}
  >
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if message}
      <p role="alert">{message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
  </form>
{/if}
```

¿Quieres profundizar con SvelteKit? Lee [Crea un formulario de suscripción a newsletter en SvelteKit](https://ikiform.com/es/guides/newsletter-signup/sveltekit.md).

**Nuxt**

Una server route mantiene la clave secreta fuera del navegador y devuelve los errores de campo de Ikiform en el `data` del error. Define `NUXT_IKIFORM_SECRET_KEY` y declara `ikiformSecretKey` en `runtimeConfig` dentro de `nuxt.config.ts`.

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

```ts
interface Signup {
  email?: string;
  first_name?: string;
  consent?: boolean;
  source?: string;
  _gotcha?: string;
}

export default defineEventHandler(async (event) => {
  const body = await readBody<Signup>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      email: body.email?.trim() ?? "",
      first_name: body.first_name?.trim() || undefined,
      consent: body.consent === true,
      source: body.source ?? "footer",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.message),
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { subscribed: true };
});
```

`app/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const firstName = ref("");
const consent = ref(false);
const trap = ref("");
const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe() {
  status.value = "sending";
  errors.value = {};
  message.value = "";
  try {
    await $fetch("/api/newsletter", {
      method: "POST",
      body: { email: email.value, first_name: firstName.value, consent: consent.value, source: "footer", _gotcha: trap.value },
    });
    status.value = "done";
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    message.value = data?.message ?? "Couldn't subscribe. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" v-model="email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" v-model="firstName" name="first_name" type="text" maxlength="80" />
    <label>
      <input v-model="consent" name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

¿Quieres profundizar con Nuxt? Lee [Crea un formulario de suscripción a newsletter en Nuxt](https://ikiform.com/es/guides/newsletter-signup/nuxt.md).

**React**

Una single-page app envía desde el navegador. `submit()` de `@ikiform/sdk` envía el `FormData` y siempre resuelve con `ok` y, si falla, con los errores de campo, así que no hay ningún bloque `try` que escribir. Nunca pongas aquí una clave secreta; en su lugar, **Allowed domains** protege el formulario.

`src/NewsletterForm.tsx`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

type FieldErrors = Record<string, string[]>;

export function NewsletterForm() {
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<FieldErrors>({});
  const [message, setMessage] = useState("");
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    setErrors({});
    setMessage("");
    const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.currentTarget));
    setPending(false);
    if (result.ok) {
      setDone(true);
      return;
    }
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {message && !errors.email && !errors.consent ? <p role="alert">{message}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

**Vue**

La misma llamada a `submit()` en un componente de un solo archivo de Vue. El formulario conserva su `action`, así que sigue enviando si el script no carga.

`src/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
import { submit } from "@ikiform/sdk";
import { ref } from "vue";

const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe(event: SubmitEvent) {
  status.value = "sending";
  const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.target as HTMLFormElement));
  if (result.ok) {
    status.value = "done";
    return;
  }
  errors.value = result.error.fields ?? {};
  message.value = result.error.message;
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

- Sustituye `frm_xxxxxxxxxxxxxxxx` por el id de tu formulario, que está en el panel.
- Mantén `email` como nombre del input del email. Ikiform lo valida, pasa el dominio a minúsculas y lo compara con los dominios de email desechables.
- El input oculto `_gotcha` es el honeypot. Las personas nunca lo ven, y los bots que rellenan todos los inputs se marcan solos como spam.
- La casilla envía `true` cuando está marcada. Sin marcar, no envía nada, y una casilla obligatoria se rechaza con `This box must be checked`.
- La línea de éxito pide revisar la bandeja de entrada, porque tu herramienta de email envía una confirmación. Si desactivaste el doble opt-in, cámbiala por un simple agradecimiento.
- Las versiones de servidor reenvían la cabecera `Origin` del navegador, porque Ikiform reconoce por ella las pruebas gratuitas desde `localhost` y el propio `fetch` de un servidor no envía ninguna.

> Las versiones de servidor envían con una clave secreta, así que Ikiform se salta **Allowed domains** (dominios permitidos) y su verificación humana. Todos los envíos vienen entonces de la IP de tu servidor, y el límite Standard de 5 por minuto por visitante cuenta a todos tus visitantes juntos. Elige el [límite de frecuencia](https://ikiform.com/es/docs/rate-limits.md) Relaxed o Custom para un formulario que envías a través de un servidor, y guarda `IKIFORM_SECRET_KEY` en variables de entorno solo de servidor.

La configuración de cada framework está en la documentación: [Next.js](https://ikiform.com/es/docs/guides/nextjs.md), [Astro](https://ikiform.com/es/docs/guides/astro.md), [SvelteKit](https://ikiform.com/es/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/es/docs/guides/nuxt.md), [React](https://ikiform.com/es/docs/guides/react.md), [Vue](https://ikiform.com/es/docs/guides/vue.md) y [HTML puro](https://ikiform.com/es/docs/html.md). La [plantilla de formulario de newsletter](https://ikiform.com/forms/newsletter-signup) genera los mismos campos como HTML, React y una server action de Next.js.

## Los campos y la definición

Pega la definición de abajo en la pestaña **Fields** del formulario e Ikiform la aplicará en el servidor, envíe lo que envíe la página. La vista previa ejecuta el mismo validador en tu navegador: deja la casilla sin marcar y envía para ver el error que recibiría un bot o una página rota.

Formulario: Newsletter

- **Email** (`email`, email, obligatorio)
- **Nombre** (`first_name`, text)
- **Envíame la newsletter. Puedo darme de baja cuando quiera.** (`consent`, checkbox, obligatorio)
- **Lugar de la suscripción** (`source`, text)

En tu página, `source` es un input oculto. Aquí es un campo de texto visible solo para que puedas probarlo.

| Campo | Por qué | Ten en cuenta |
| --- | --- | --- |
| `email` | El suscriptor | Obligatorio. `type="email"` muestra el teclado adecuado en el móvil |
| `first_name` | Un saludo en cada número | Opcional. La mayoría de herramientas de email tienen un campo de nombre con el que relacionarlo |
| `consent` | Un registro de que la persona pidió recibir emails | Casilla obligatoria. Se guarda como `true` |
| `source` | Qué formulario del sitio funcionó: footer, post, popup | Oculto. Pon un valor distinto en cada ubicación y envíalo como etiqueta o campo |

Con campos definidos, Ikiform rechaza cualquier input que no conozca con `unknown_field`, así que añade un campo a la definición antes de añadir un input a la página. Los nombres que empiezan por `_`, como `_redirect`, son instrucciones para Ikiform y nunca cuentan como campos.

## Registrar el consentimiento

Una newsletter es un email que alguien ha pedido. La casilla hace explícita esa petición, y el envío guardado la registra. Esto es lo que Ikiform guarda de cada suscripción aceptada:

`data.fields`:

```json
{
  "email": "ada@example.com",
  "first_name": "Ada",
  "consent": true,
  "source": "footer"
}
```

Junto a los campos, cada envío tiene su marca de tiempo `created_at`, el origen y el referrer de la página, el país y el user agent del navegador. Juntos muestran cuándo y dónde marcó la persona la casilla. Dos hábitos hacen que ese registro sea útil más adelante:

1. **Escribe la etiqueta como la promesa.** "Envíame la newsletter. Puedo darme de baja cuando quiera." dice lo que van a recibir. Si también envías ofertas, dilo en la etiqueta.
2. **Anota la fecha cuando cambie el texto.** Un `true` guardado dice que la persona aceptó, no a qué texto. Lleva un pequeño historial de la etiqueta con fechas, para poder relacionar cualquier suscripción con las palabras que vio.

Ikiform guarda el registro; no puede decirte qué exigen las normas allí donde vivís tú y tus lectores. Compruébalo con alguien que las conozca.

## Los emails de confirmación salen de tu herramienta de email

Ikiform no envía email a la persona que rellena el formulario. Los emails de notificación van solo a ti y a tu equipo, así que no hay autorespuesta, email de confirmación ni doble opt-in. Envíalos desde tu herramienta de email, activados por un [webhook](https://ikiform.com/es/docs/webhooks.md). Esa separación es intencionada. La herramienta que envía tu newsletter ya registra quién confirmó, quién se dio de baja y qué direcciones rebotan. Si un segundo sistema enviara confirmaciones, esos registros acabarían desincronizados.

Así que el relay añade al lector en un estado que hace que la herramienta de email envíe su propia confirmación, y la herramienta lo marca como activo cuando hace clic. Cada herramienta lo activa de una forma: un ajuste del formulario, una opción de la cuenta o un flag en la petición a la API. La [guía de doble opt-in](https://ikiform.com/es/guides/newsletter-signup/double-opt-in.md) cubre las tres, y qué pasa cuando un suscriptor conocido se vuelve a suscribir.

## El relay

Un relay es una función corta que recibe el webhook de Ikiform, comprueba su firma y llama a la API de tu herramienta de email. Necesita una URL HTTPS pública y un secreto por cada lado. Este es un route handler de Next.js; el mismo código funciona en cualquier runtime con `fetch`.

`app/api/newsletter/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { addSubscriber } from "@/lib/email-tool";

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const fields = event.data.fields;
  if (fields.consent !== true) return new Response("No consent, skipped", { status: 200 });

  const answer = await addSubscriber({
    email: String(fields.email),
    firstName: typeof fields.first_name === "string" ? fields.first_name : undefined,
    source: typeof fields.source === "string" ? fields.source : "unknown",
    referrer: typeof event.data.meta.referrer === "string" ? event.data.meta.referrer : undefined,
  });
  if (answer.status === 429) {
    return new Response("Email tool rate limit", {
      status: 503,
      headers: { "retry-after": answer.headers.get("retry-after") ?? "60" },
    });
  }
  if (!answer.ok) return new Response(`Email tool answered ${answer.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` comprueba la firma HMAC y la marca de tiempo. Sin él, cualquiera que encuentre la URL puede añadir direcciones a tu lista.
- Los eventos `endpoint.test`, de **Send test** en el panel, se responden con `204` y se saltan.
- La comprobación del consentimiento es un segundo cerrojo detrás de la definición del formulario.
- `addSubscriber` es la única parte que depende de la herramienta. Su cuerpo para cada herramienta está en las páginas de [Kit](https://ikiform.com/es/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/es/guides/newsletter-signup/mailerlite.md) y [beehiiv](https://ikiform.com/es/guides/newsletter-signup/beehiiv.md).

Lo que responde el relay decide lo que hace Ikiform a continuación:

Diagrama: Lo que responde el relay a Ikiform según cada respuesta de la herramienta de email

1. La herramienta de email añade o actualiza el suscriptor: el relay responde 204 y la entrega se marca como correcta.
2. La herramienta de email aplica un límite de frecuencia: el relay responde 503 con Retry-After, e Ikiform espera ese tiempo antes del siguiente intento.
3. La herramienta de email rechaza la petición o está caída: el relay responde 502, e Ikiform reintenta según su calendario con el mismo webhook-id.

Cada intento está en el registro de entregas del endpoint con la respuesta de tu relay, así que un 502 que indica el estado de la herramienta te dice qué arreglar.

Añade la URL del relay en **Webhooks** en el panel, elige el formulario de la newsletter y copia el secreto `whsec_` en `IKIFORM_WEBHOOK_SECRET`. Un espacio de trabajo tiene hasta 3 endpoints. La URL tiene que ser HTTPS pública en el puerto 443; los hostnames `workers.dev` y `pages.dev` se rechazan, así que dale a un Worker un dominio propio. Los reintentos, los payloads y el esquema de firma están en la [documentación de webhooks](https://ikiform.com/es/docs/webhooks.md), y [envíos de formulario a tu propio servidor](https://ikiform.com/blog/form-submissions-to-your-own-server.md) tiene una versión que guarda cada entrega una sola vez.

> ¿Sin código? Apunta el webhook a una herramienta de automatización y usa su acción para tu herramienta de email en lugar de un relay. Consulta [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) o [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). No hay integraciones de un clic. Cada herramienta se conecta mediante un [webhook](https://ikiform.com/es/docs/webhooks.md) firmado, directamente o a través de una herramienta de automatización como [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) o [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Deja fuera a los bots y el list bombing

Los registros de bots hacen más daño a una newsletter que a la mayoría de formularios. Una dirección real suscrita por un bot recibe emails que nunca pidió, y las quejas de esas personas perjudican al dominio desde el que envías. Algunos bots suscriben la dirección de una víctima a cientos de formularios a la vez para enterrar su bandeja de entrada, y cada formulario con email de confirmación participa.

| Defensa | Qué frena | Configuración |
| --- | --- | --- |
| Honeypot | Bots que rellenan todos los inputs | El input oculto `_gotcha` |
| Consentimiento obligatorio | Scripts que solo envían un email | La casilla `consent` con `required: true` en la definición |
| Puntuación de spam | Direcciones desechables (+2), repeticiones idénticas (+3), enlaces y frases de spam. Con 5, una suscripción se guarda como spam | Ninguna |
| Límites de frecuencia | Un visitante enviando en bucle | Activo por defecto: Standard, 5 por minuto por visitante |
| Verificación humana | Bots que usan un navegador real | Pon **Spam check** en **Ikiform** para tener Cloudflare Turnstile sin claves que gestionar |
| Dominios permitidos | El endpoint de tu formulario copiado en otros sitios | Lista todos los dominios en los que aparece el formulario |

El spam recibe la misma respuesta de éxito que una suscripción real y no envía webhook, así que nunca llega a tu herramienta de email ni activa un email de confirmación. Si un lector real acaba en spam, márcalo como no spam y su webhook saldrá en ese momento. La [documentación de spam](https://ikiform.com/es/docs/spam.md) explica cada comprobación y su puntuación.

> Un límite estricto por visitante encaja con un formulario de newsletter, porque una persona se suscribe una vez. Si tu público lee desde oficinas o campus que comparten una IP, Standard los cuenta juntos; ahí, Relaxed, 20 por minuto por visitante, es la opción más segura. Consulta los [límites de frecuencia](https://ikiform.com/es/docs/rate-limits.md).

## Un formulario en muchas páginas

Un mismo endpoint puede estar en el footer de cada página, al final de cada post y en una página `/newsletter`. Dale a cada ubicación su propio valor de `source` para saber cuál consigue suscripciones:

- `footer`: se ve en todas las páginas. Limítalo al email y la casilla.
- `post`: lectores que han terminado un artículo, a menudo los que mejor convierten.
- `newsletter-page`: adonde enlazan los perfiles sociales. Hay sitio para un número de muestra.

Cada envío registra también el referrer, que es la página exacta en la que estaba el lector, así que `source` te dice la ubicación y el referrer te dice el artículo. Añade en **Allowed domains** cada dominio que muestre el formulario, hasta 20 por formulario; los subdominios de un dominio listado se permiten automáticamente.

### El mismo lector dos veces

Ikiform no fusiona ni rechaza envíos repetidos por email. Una repetición idéntica en 24 horas solo suma a la puntuación de spam. Elimina los duplicados donde vive la lista, o al exportar. En una newsletter eso rara vez es un problema: Kit, MailerLite y beehiiv identifican a los suscriptores por email, así que una repetición actualiza un contacto en lugar de crear otro.

## Cambiar de herramienta de email más adelante

Aquí es donde tener tu propio formulario sale a cuenta. Pasar de una herramienta de email a otra normalmente significa sustituir cada embed del sitio. Aquí el formulario no sabe qué herramienta hay detrás:

Diagrama: Qué cambia cuando te pasas a otra herramienta de email

1. Con un embed, cada página lleva código que envía directamente a la herramienta antigua, así que cambiar significa sustituir cada copia.
2. Con tu propio formulario, cada página sigue enviando a Ikiform, e Ikiform sigue enviando el mismo webhook.
3. Solo cambia la función addSubscriber del relay, para llamar a la API de la nueva herramienta.

Si quieres, usa las dos herramientas durante una semana: dos endpoints de webhook, dos relays, un formulario. Después elimina el endpoint antiguo.

Mueve los suscriptores existentes con la exportación de la propia herramienta antigua, porque solo ella sabe quién confirmó y quién se fue. La exportación CSV de Ikiform, de hasta 100.000 filas por archivo, sirve para comprobar las cifras, no para importar.

## Elige tu caso

El formulario de arriba es el mismo en todas partes. Estas páginas cubren los emails de confirmación, los sitios sin servidor y las llamadas exactas a la API de tres herramientas de email.

- [Doble opt-in](https://ikiform.com/es/guides/newsletter-signup/double-opt-in.md): Ikiform no envía emails de confirmación, así que el doble opt-in se hace en tu herramienta de email: un relay añade cada suscripción como no confirmada y la herramienta envía el enlace.
- [Sitio estático](https://ikiform.com/es/guides/newsletter-signup/static-site.md): Añade un formulario de suscripción a tu newsletter en un sitio estático con Hugo, Jekyll, Eleventy o Astro, deja la API key fuera de la página y confirma en la herramienta.
- [Kit (ConvertKit)](https://ikiform.com/es/guides/newsletter-signup/kit.md): Conecta tu propio formulario de newsletter con Kit mediante un relay que crea el suscriptor, lo añade a un formulario de Kit para el incentive email y etiqueta la ubicación.
- [MailerLite](https://ikiform.com/es/guides/newsletter-signup/mailerlite.md): Conecta tu propio formulario de suscripción con MailerLite: un relay añade a los lectores nuevos como no confirmados para el doble opt-in y nunca pide a un suscriptor que confirme dos veces.
- [beehiiv](https://ikiform.com/es/guides/newsletter-signup/beehiiv.md): Conecta tu propio formulario de suscripción con beehiiv: un relay crea cada suscripción con el doble opt-in activado, la ubicación como fuente UTM y la página como referrer.
- [Formulario de newsletter en Next.js](https://ikiform.com/es/guides/newsletter-signup/nextjs.md): Un formulario de newsletter en el footer de Next.js: una server action con comprobación de consentimiento y Turnstile que envía a Ikiform, más un route handler que añade a los lectores a Kit.
- [Formulario de newsletter en Astro](https://ikiform.com/es/guides/newsletter-signup/astro.md): Un formulario de newsletter en Astro que se queda en páginas prerenderizadas: una Astro Action comprueba el consentimiento y envía a Ikiform, y un endpoint añade a los lectores a MailerLite.
- [Formulario de newsletter en TanStack Start](https://ikiform.com/es/guides/newsletter-signup/tanstack-start.md): Un formulario de newsletter en TanStack Start: una server function para suscribirse sin salir de la página, una server route como alternativa sin JavaScript y las suscripciones reales enviadas a beehiiv.
- [Formulario de newsletter en React Router](https://ikiform.com/es/guides/newsletter-signup/react-router.md): Un formulario de newsletter en el footer con React Router: useFetcher envía a una ruta /newsletter que también funciona sin JavaScript, y una resource route alimenta MailerLite.
- [Formulario de newsletter en SvelteKit](https://ikiform.com/es/guides/newsletter-signup/sveltekit.md): Un formulario de newsletter en el layout de SvelteKit 3: una form action con consentimiento y un límite por IP, use:enhance que se queda en la página y una ruta de webhook para Kit.
- [Formulario de newsletter en Nuxt](https://ikiform.com/es/guides/newsletter-signup/nuxt.md): Un formulario de newsletter en Nuxt que funciona con o sin JavaScript: una server route con comprobación de Turnstile envía a Ikiform, y una ruta de Nitro alimenta beehiiv.

## Comprueba que funciona

Repasa esto antes de añadir el formulario a todas las páginas. Ikiform no tiene plan gratuito. Probar es gratis: un espacio de trabajo sin plan acepta 50 envíos de prueba al mes desde `localhost`. Para salir a producción, elige Pro desde 6 $/mes para 500 envíos, o paga una vez: 10.000 envíos por 49 $ o 50.000 envíos por 149 $, con créditos que nunca caducan. Consulta los [precios](https://ikiform.com/es/pricing).

1. En **Allowed domains** del formulario, añade el dominio de tu sitio, y `localhost` mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier lugar.
2. Envía el formulario con la casilla sin marcar desde las herramientas de desarrollo del navegador, o quita `required` un momento. Recibes `422` con `This box must be checked` y no se guarda nada.
3. Envíalo correctamente con una dirección tuya. Ves la línea que pide revisar la bandeja de entrada, o llegas a tu página de agradecimiento.
4. Abre **Submissions** en el panel. El nuevo envío está ahí con todos los campos que enviaste.
5. Abre el endpoint en **Webhooks**. La entrega aparece como `succeeded` con la respuesta de tu relay.
6. Abre tu herramienta de email. El suscriptor está ahí, sin confirmar si el doble opt-in está activado, y el email de confirmación está en tu bandeja de entrada.
7. Envía con el honeypot relleno, `_gotcha=bot`. Igualmente recibe una respuesta de éxito, pero el envío se guarda como spam y no envía email ni webhook.
8. ¿No llegó nada? Cambia el filtro de estado del panel a **Spam**. El spam recibe la misma respuesta de éxito que un envío real.

## Solución de problemas

| Problema | Qué hacer |
| --- | --- |
| `422 validation_failed` con `consent` | La casilla no estaba marcada, o el `name` de su input no es `consent`. Una versión de servidor debe enviar `true`, `"true"` o `"on"`; una casilla sin marcar no envía nada. |
| `422 validation_failed` con `unknown_field` | La página envía un input que la definición no incluye, a menudo un `source` oculto o un input de seguimiento añadido más tarde. Añádelo a la definición o quita el input. |
| `403 origin_not_allowed`, o todos los envíos acaban en spam | El dominio de la página no está en **Allowed domains** del formulario. Añádelo, y `localhost` para las pruebas locales. Los subdominios de un dominio de la lista están permitidos. |
| `429 rate_limited` | Un visitante envió más de lo que permite el límite de frecuencia del formulario. Standard permite 5 por minuto por visitante. Espera lo que indique `Retry-After`, o elige Relaxed o Custom para formularios que envían desde una IP compartida. |
| El envío está en el panel pero no llegó ningún email ni webhook | Se guardó como spam. El spam no envía nada. Ábrelo en el filtro **Spam** para ver los motivos y márcalo como no spam para enviar su email y sus webhooks. |
| Los registros llegan a Ikiform pero no a la herramienta de email | Lee el registro de entregas en **Webhooks**. `400` significa que el secreto del relay es incorrecto. `502` lleva el estado de la herramienta de email: normalmente una API key, un id de lista o un id de formulario incorrectos. |
| El suscriptor está en la herramienta de email pero no recibió email de confirmación | El doble opt-in está desactivado en la herramienta, o el relay creó al suscriptor como ya confirmado. Consulta [doble opt-in](https://ikiform.com/es/guides/newsletter-signup/double-opt-in.md) para ver dónde se activa en cada herramienta. |
| El navegador acaba en la página de agradecimiento de Ikiform en lugar de la tuya | `_redirect` se ignora salvo que su dominio esté en **Allowed domains** y sea una URL `https:`. Añade el dominio, o configura **Redirect after submit** en los ajustes del formulario. |

## Preguntas

### ¿Puedo crear un formulario de suscripción a newsletter sin el código embed de Mailchimp?

Sí. Escribe el formulario en tu propio HTML, envíalo a un backend de formularios como Ikiform y reenvía cada suscripción por webhook a Mailchimp o a cualquier otra herramienta de email mediante su API. Así el formulario se ve como el resto de tu sitio y no cambia cuando cambia la herramienta de email.

### ¿Ikiform envía el email de confirmación o la newsletter?

No. Ikiform recoge, valida y guarda las suscripciones y las reenvía por webhook. Nunca envía email al suscriptor, así que el email de confirmación, cada número y el enlace para darse de baja salen de tu herramienta de email.

### ¿Con qué herramientas de email funciona?

Con cualquier herramienta que tenga una API para añadir suscriptores, o cualquiera que admita un servicio de automatización como Zapier, Make o n8n. No hay integraciones de un clic; el relay o la automatización hacen la conexión. Kit, MailerLite y beehiiv tienen páginas paso a paso en esta guía.

### ¿Dónde guardo la API key de mi herramienta de email?

En las variables de entorno del servidor del relay, nunca en la página. La API key de una herramienta de email normalmente puede leer y cambiar toda la lista, así que una clave en el JavaScript del navegador es una clave que cualquiera puede copiar.

### ¿Cómo evito que los bots se suscriban a mi newsletter?

Mantén el honeypot oculto _gotcha y la casilla de consentimiento obligatoria, lista tus dominios en Allowed domains y pon la comprobación antispam en Ikiform para tener una verificación de Cloudflare Turnstile. Las suscripciones guardadas como spam nunca envían webhook, así que nunca activan un email de confirmación.

### ¿Ikiform elimina los envíos duplicados?

No. Cada envío aceptado se guarda. Una repetición idéntica en menos de 24 horas suma a la puntuación de spam pero no es spam por sí sola, así que elimina duplicados por email en tu lista, tu base de datos o tu exportación CSV.

### ¿Ikiform es gratis?

No, no hay plan gratuito. Probar es gratis: puedes enviar 50 envíos de prueba al mes desde localhost antes de pagar. Para pasar a producción, Pro empieza en 6 $/mes por 500 envíos, o puedes pagar una sola vez 49 $ por 10.000 envíos o 149 $ por 50.000 envíos, con créditos que nunca caducan.

## Más guías

- [Doble opt-in](https://ikiform.com/es/guides/newsletter-signup/double-opt-in.md): Ikiform no envía emails de confirmación, así que el doble opt-in se hace en tu herramienta de email: un relay añade cada suscripción como no confirmada y la herramienta envía el enlace.
- [Sitio estático](https://ikiform.com/es/guides/newsletter-signup/static-site.md): Añade un formulario de suscripción a tu newsletter en un sitio estático con Hugo, Jekyll, Eleventy o Astro, deja la API key fuera de la página y confirma en la herramienta.
- [Kit (ConvertKit)](https://ikiform.com/es/guides/newsletter-signup/kit.md): Conecta tu propio formulario de newsletter con Kit mediante un relay que crea el suscriptor, lo añade a un formulario de Kit para el incentive email y etiqueta la ubicación.
- [MailerLite](https://ikiform.com/es/guides/newsletter-signup/mailerlite.md): Conecta tu propio formulario de suscripción con MailerLite: un relay añade a los lectores nuevos como no confirmados para el doble opt-in y nunca pide a un suscriptor que confirme dos veces.
- [beehiiv](https://ikiform.com/es/guides/newsletter-signup/beehiiv.md): Conecta tu propio formulario de suscripción con beehiiv: un relay crea cada suscripción con el doble opt-in activado, la ubicación como fuente UTM y la página como referrer.
- [Formulario de newsletter en Next.js](https://ikiform.com/es/guides/newsletter-signup/nextjs.md): Un formulario de newsletter en el footer de Next.js: una server action con comprobación de consentimiento y Turnstile que envía a Ikiform, más un route handler que añade a los lectores a Kit.
- [Formulario de newsletter en Astro](https://ikiform.com/es/guides/newsletter-signup/astro.md): Un formulario de newsletter en Astro que se queda en páginas prerenderizadas: una Astro Action comprueba el consentimiento y envía a Ikiform, y un endpoint añade a los lectores a MailerLite.
- [Formulario de newsletter en TanStack Start](https://ikiform.com/es/guides/newsletter-signup/tanstack-start.md): Un formulario de newsletter en TanStack Start: una server function para suscribirse sin salir de la página, una server route como alternativa sin JavaScript y las suscripciones reales enviadas a beehiiv.
- [Formulario de newsletter en React Router](https://ikiform.com/es/guides/newsletter-signup/react-router.md): Un formulario de newsletter en el footer con React Router: useFetcher envía a una ruta /newsletter que también funciona sin JavaScript, y una resource route alimenta MailerLite.
- [Formulario de newsletter en SvelteKit](https://ikiform.com/es/guides/newsletter-signup/sveltekit.md): Un formulario de newsletter en el layout de SvelteKit 3: una form action con consentimiento y un límite por IP, use:enhance que se queda en la página y una ruta de webhook para Kit.
- [Formulario de newsletter en Nuxt](https://ikiform.com/es/guides/newsletter-signup/nuxt.md): Un formulario de newsletter en Nuxt que funciona con o sin JavaScript: una server route con comprobación de Turnstile envía a Ikiform, y una ruta de Nitro alimenta beehiiv.
- [Envío de formularios HTML](https://ikiform.com/es/guides/html-form-submission.md): Qué hace un navegador cuando se envía un formulario: qué campos envía, cómo los codifica, qué debería responder el servidor y cómo responde Ikiform.
- [Inscripción a eventos](https://ikiform.com/es/guides/event-registration.md): Recibe RSVP en la página de tu evento: el formulario, límite de acompañantes, un recuento fiable, cierre al llenarse y lista de entrada, sin app de entradas.
- [Verifica las firmas de webhook](https://ikiform.com/es/guides/webhook-signature-verification.md): Comprueba que un webhook de formulario viene de verdad de Ikiform: el esquema HMAC-SHA256 de Standard Webhooks, un vector de prueba, cuerpos sin procesar, marcas de tiempo y rotación de secretos.
