# Cómo crear una lista de espera para un nuevo SaaS

> Crea una lista de espera previa al lanzamiento en tu propia landing page: el formulario, el antispam, los duplicados, los emails de confirmación y una exportación limpia el día del lanzamiento.

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

Pon un formulario HTML sencillo en tu landing page que envíe el email del visitante a un backend de formularios, así no necesitas un servidor antes del lanzamiento. Reenvía cada registro por webhook a la herramienta de email con la que vas a lanzar y deja que esa herramienta mande la confirmación. El día del lanzamiento, exporta la lista como CSV, quita los duplicados por email y envía un único email de lanzamiento.

Una lista de espera es la prueba más barata de una idea de SaaS. Si desconocidos dejan su email por algo que todavía no existe, tienes un motivo para construirlo y una lista de personas a las que avisar cuando salga. Esta guía crea una en tu propia landing page, sin base de datos ni servidor, y la deja lista para el día del lanzamiento.

## Cómo funciona una lista de espera

Toda lista de espera tiene las mismas tres partes: un formulario en tu página, un lugar que guarda cada registro y la herramienta desde la que enviarás emails a la lista. Aquí, el formulario envía a Ikiform. Ikiform revisa el envío en busca de spam, lo guarda, te avisa por email y manda un webhook firmado. Un pequeño relay o una herramienta de automatización pasa la dirección a tu herramienta de email.

Diagrama: Cómo viaja un registro en la lista de espera desde tu landing page hasta tu herramienta de email

1. Un visitante escribe su email en tu landing page y el formulario lo envía a tu endpoint de Ikiform.
2. Ikiform comprueba el honeypot, la puntuación de spam y el límite de frecuencia, y luego guarda el registro.
3. El registro aparece en el panel, donde puedes buscarlo y exportar la lista como CSV.
4. Ikiform te envía un email a ti y a tu equipo. Nunca escribe a la persona que se registró.
5. Ikiform envía un webhook submission.created firmado.
6. Un relay o una herramienta de automatización añade la dirección a tu herramienta de email, que envía la confirmación y, más adelante, el email de lanzamiento.

La herramienta de email es opcional el primer día. Puedes recoger registros durante semanas e importar un CSV después; el webhook mantiene ambos sincronizados desde el principio.

## Cuatro formas de gestionar una lista de espera

No hay una única herramienta correcta. La tabla compara las cuatro configuraciones habituales según en qué destacan y qué te cuestan más adelante.

| Enfoque | Destaca en | Contrapartidas |
| --- | --- | --- |
| Un SaaS de listas de espera, como LaunchList o Waitlister | Enlaces de referidos, rankings y posición en la fila, listos para usar. Una página alojada si todavía no tienes sitio. | Tu lista vive en otro producto. El widget tiene su estilo salvo que pagues o lo rediseñes. El precio suele crecer con los registros. |
| Un formulario incrustado de un constructor, como [Typeform](https://ikiform.com/alternatives/typeform) o [Tally](https://ikiform.com/alternatives/tally) | Flujos pulidos de una pregunta por pantalla, sin nada de código. | Un iframe que no encaja con tu página y carga sus propios scripts. La marca, los límites de respuestas y los webhooks dependen del plan. |
| Tu propia base de datos y ruta de API | Control total sobre los datos y sobre cualquier lógica, como códigos de referido. | Tienes que construir el antispam, los límites de frecuencia, las notificaciones y la exportación, y necesitas un servidor aunque la landing page sea estática. |
| Un backend de formularios, como Ikiform | Tu propio HTML en cualquier sitio. Filtro de spam, almacenamiento, avisos por email, webhooks firmados y exportación CSV sin servidor. | Sin ranking de referidos y sin emails para la persona que se registra. Eso lo pone tu herramienta de email. |

Elige un SaaS de listas de espera si toda tu estrategia son los referidos. Elige un backend de formularios si quieres que el formulario se vea como el resto de tu sitio, que la lista sea portable y enviar los emails desde la herramienta que vas a usar después del lanzamiento de todos modos. El resto de esta guía usa el backend de formularios.

## El formulario

Para empezar, pide un email y nada más. Cada campo extra te cuesta registros, y puedes hacer preguntas más adelante, por email, a quienes ya dijeron que sí. Elige tu stack abajo; la elección se aplica a todos los ejemplos de código de estas guías.

**HTML**

Pega el formulario donde deba aparecer el registro. Envía de la forma clásica y redirige, así que funciona sin nada de JavaScript.

`index.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" placeholder="you@company.com" autocomplete="email" required />
  <input type="hidden" name="ref" value="homepage" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Join the waitlist</button>
</form>
```

Para que los visitantes se queden en la página, añade este script después del formulario. Envía con `fetch`, cambia el formulario por la línea de agradecimiento y muestra los errores de campo del servidor.

`waitlist.js`:

```js
const form = document.querySelector('form[action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx"]');
const status = document.createElement("p");
status.setAttribute("role", "alert");
form.append(status);

const labels = {
  email: "Email",
};

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  status.textContent = "";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceWith("You're on the list. We'll email you at launch.");
      return;
    }
    const fields = Object.entries(result.error.fields ?? {});
    status.textContent = fields.length
      ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`).join(" ")
      : result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

¿Quieres profundizar con HTML? Lee [Cómo añadir un formulario de lista de espera en HTML puro a cualquier landing page](https://ikiform.com/es/guides/waitlist/html.md).

**Next.js**

Una server action reenvía el registro con tu clave secreta, y `useActionState` muestra el resultado sin recargar la página. El formulario sigue enviando aunque JavaScript aún no haya cargado.

`app/waitlist/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface JoinState {
  ok: boolean;
  message: string;
  email?: string;
}

export async function joinWaitlist(_previous: JoinState, formData: FormData): Promise<JoinState> {
  const email = String(formData.get("email") ?? "").trim();
  if (!email.includes("@")) return { ok: false, message: "Enter the email we should send your invite to.", 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,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return {
    ok: false,
    message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "Couldn't join the list. Try again.",
    email,
  };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type JoinState, joinWaitlist } from "./actions";

const initial: JoinState = { ok: false, message: "" };

export function WaitlistForm() {
  const [state, action, pending] = useActionState(joinWaitlist, initial);

  if (state.ok) return <p role="status">{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required defaultValue={state.email} placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

¿Quieres profundizar con Next.js? Lee [Cómo crear una lista de espera en Next.js con una server action](https://ikiform.com/es/guides/waitlist/nextjs.md).

**Astro**

Una Astro Action con `accept: "form"` valida el email con Zod y lo reenvía. El formulario envía sin JavaScript de cliente, así que la página debe renderizarse bajo demanda: añade un adaptador y deja `prerender = false`.

`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 = {
  joinWaitlist: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      ref: z.string().default("homepage"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      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),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { joined: true };
    },
  }),
};
```

`src/pages/index.astro`:

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

export const prerender = false;

const result = Astro.getActionResult(actions.joinWaitlist);
---

{result?.data?.joined ? (
  <p role="status">You're on the list. We'll email you on launch day.</p>
) : (
  <form method="POST" action={actions.joinWaitlist}>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && (
      <p role="alert">{isInputError(result.error) ? "Enter a valid email address." : result.error.message}</p>
    )}
    <button>Join the waitlist</button>
  </form>
)}
```

¿Quieres profundizar con Astro? Lee [Crea una lista de espera en Astro](https://ikiform.com/es/guides/waitlist/astro.md).

**TanStack Start**

Una función POST de `createServerFn` recibe el `FormData`, lo comprueba en `.validator()` y lo reenvía desde el servidor. La ruta la llama mediante `useServerFn`.

`src/routes/index.tsx`:

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

const joinWaitlist = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) throw new Error("Enter the email we should send your invite to.");
    return {
      email,
      ref: String(data.get("ref") ?? "homepage"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    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, message: "You're on the list. We'll email you on launch day." };
    return { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) };
  });

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  const join = useServerFn(joinWaitlist);
  const [state, setState] = useState<{ ok: boolean; message: string } | null>(null);
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setState(await join({ data }));
    } catch (error) {
      setState({ ok: false, message: error instanceof Error ? error.message : "Couldn't join the list. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state?.ok) return <p role="status">{state.message}</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state?.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

¿Quieres profundizar con TanStack Start? Lee [Crea una lista de espera en TanStack Start](https://ikiform.com/es/guides/waitlist/tanstack-start.md).

**React Router**

La `action` de la ruta reenvía el envío, y `actionData` devuelve el resultado al componente. `data()` conserva el código de estado de Ikiform en los errores.

`app/routes/home.tsx`:

```tsx
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/home";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").trim();

  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,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return data(
    { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}

export default function Home({ actionData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const pending = navigation.state === "submitting";

  if (actionData?.ok) return <p role="status">{actionData.message}</p>;

  return (
    <Form method="post">
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData?.message ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </Form>
  );
}
```

¿Quieres profundizar con React Router? Lee [Crea una lista de espera en React Router](https://ikiform.com/es/guides/waitlist/react-router.md).

**SvelteKit**

Una form action por defecto reenvía el envío y devuelve `fail()` con el mensaje cuando hay errores. `use:enhance` envía sin recargar y conserva el email escrito en el input. En SvelteKit 3, declara `IKIFORM_SECRET_KEY` en `src/env.ts` con `defineEnvVars` para que `$app/env/private` pueda leerla.

`src/routes/+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 = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) return fail(400, { email, message: "Enter the email we should send your invite to." });

    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,
        ref: String(data.get("ref") ?? "homepage"),
        _gotcha: String(data.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      return fail(response.status, {
        email,
        message: String(result.error.fields?.email?.[0] ?? result.error.message),
      });
    }
    return { joined: true };
  },
} satisfies Actions;
```

`src/routes/+page.svelte`:

```svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { PageProps } from "./$types";

  let { form }: PageProps = $props();
  let pending = $state(false);
</script>

{#if form?.joined}
  <p role="status">You're on the list. We'll email you on launch day.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update();
        pending = false;
      };
    }}
  >
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required value={form?.email ?? ""} placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message}
      <p role="alert">{form.message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
  </form>
{/if}
```

¿Quieres profundizar con SvelteKit? Lee [Crea una lista de espera en SvelteKit](https://ikiform.com/es/guides/waitlist/sveltekit.md).

**Nuxt**

Una ruta de servidor mantiene la clave secreta fuera del navegador. Define `NUXT_IKIFORM_SECRET_KEY` en el entorno y Nuxt rellena `runtimeConfig.ikiformSecretKey` con ella.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ email?: string; ref?: string; _gotcha?: string }>(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() ?? "",
      ref: body.ref ?? "homepage",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.fields?.email?.[0] ?? result.error.message),
    });
  }
  return { joined: true };
});
```

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    ikiformSecretKey: "",
  },
});
```

`app/components/WaitlistForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const trap = ref("");
const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join() {
  status.value = "sending";
  error.value = "";
  try {
    await $fetch("/api/waitlist", {
      method: "POST",
      body: { email: email.value, ref: "homepage", _gotcha: trap.value },
    });
    status.value = "joined";
  } catch (failure) {
    error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't join the list. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" v-model="email" name="email" type="email" required placeholder="you@company.com" />
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

¿Quieres profundizar con Nuxt? Lee [Crea una lista de espera en Nuxt](https://ikiform.com/es/guides/waitlist/nuxt.md).

**React**

Una single-page app no tiene servidor propio, así que el formulario envía directamente a Ikiform desde el navegador. Nunca pongas una clave secreta en este código; en su lugar, **Allowed domains** (dominios permitidos) protege el formulario.

`src/WaitlistForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export function WaitlistForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setStatus("joined");
        return;
      }
      setError(result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    }
    setStatus("idle");
  }

  if (status === "joined") return <p role="status">You're on the list. We'll email you on launch day.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

**Vue**

El mismo envío desde el navegador en un componente de un solo archivo de Vue. El formulario conserva su `action`, así que sigue funcionando si el script no carga.

`src/components/WaitlistForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      status.value = "joined";
      return;
    }
    error.value = result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    error.value = "Couldn't reach the server. Try again.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

- El endpoint es la URL de tu formulario. Sustituye `frm_xxxxxxxxxxxxxxxx` por el id del panel.
- `name="email"` es el id del campo. Déjalo como `email`: Ikiform lo usa como dirección de respuesta de los emails de notificación y lo compara con los dominios de email desechables.
- El input oculto `_gotcha` es el honeypot. Las personas nunca lo ven. Los bots que rellenan todos los inputs se marcan solos como spam, y las versiones de servidor lo reenvían sin tocarlo.
- El input oculto `ref` registra de dónde vino el registro. Cambia su valor por página o por campaña.
- Con una definición de campos en la pestaña **Fields**, un input que no aparece en ella se rechaza con `422` y un error `unknown_field`, así que incluye `ref` además de `email`, como abajo.
- Un envío HTML normal redirige el navegador tras el éxito. Configura **Redirect after submit** en los ajustes del formulario con tu propia página de agradecimiento, o Ikiform mostrará la suya alojada.

`Campos`:

```json
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true, "placeholder": "you@company.com" },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
```

> Las versiones de servidor envían con una clave secreta, así que Ikiform se salta **Allowed domains** y su verificación humana. Todos los envíos llegan entonces desde 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.

Las versiones de servidor reenvían la cabecera `Origin` del navegador, porque Ikiform detecta con ella las pruebas gratuitas desde `localhost` y el `fetch` propio de un servidor no envía ninguna.

Las guías por framework de la documentación profundizan más: [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](https://ikiform.com/es/docs/html.md). El [SDK](https://ikiform.com/es/docs/sdk.md) envuelve la petición del navegador como `submit()`, y la [plantilla de formulario de lista de espera](https://ikiform.com/forms/waitlist) genera la versión de cuatro campos.

## Campos que vale la pena pedir

El email es el único campo que necesitas. Si quieres ordenar la lista antes del lanzamiento, añade como mucho dos más. Prueba abajo la versión de cuatro campos: ejecuta el validador de Ikiform en tu navegador y muestra lo que se guardaría, o el `422` que recibirías.

Formulario: Waitlist

- **Email** (`email`, email, obligatorio)
- **Nombre** (`name`, text)
- **¿A qué te dedicas?** (`role`, select)
- **¿Cómo nos conociste?** (`source`, text)
- **Origen** (`ref`, text)

Los campos de la [plantilla de lista de espera](https://ikiform.com/forms/waitlist) más `ref`, que tu página rellena con un input oculto en vez de una casilla visible. Pégalo en la pestaña **Fields** del formulario e Ikiform lo aplicará en el servidor.

| Campo | Por qué lo pedirías | Ten en cuenta |
| --- | --- | --- |
| `email` | La lista en sí | Obligatorio. Usa `type="email"` para que los móviles muestren el teclado adecuado |
| `name` | Un email de lanzamiento personal | Opcional. Basta con el nombre de pila |
| `role` | Invitar primero a fundadores, o escribir un email distinto a los ingenieros | Un `select` mantiene las respuestas limpias para filtrar |
| `source` | Saber qué canal funciona antes de invertir en él | El texto libre es sincero pero desordenado. Un input oculto `ref` es más limpio, mira abajo |
| `ref` | Contar registros por página o campaña sin preguntar | Un input oculto. `text` opcional con `max_length` 40 |

Para saber de dónde vienen los registros sin preguntar, añade un input oculto por página o por campaña. Los nombres que empiezan con `_` son instrucciones para Ikiform y nunca se guardan, así que usa un nombre normal como `ref`:

`Inputs ocultos`:

```html
<input type="hidden" name="ref" value="homepage" />
<input type="hidden" name="_redirect" value="https://example.com/thanks" />
```

Cada envío también registra el origen y el referrer de la página, así que puedes ver de qué página vino un registro incluso sin `ref`. El input `_redirect` lleva el navegador a tu propia página de agradecimiento; solo funciona con una URL `https:` en uno de los **Allowed domains** del formulario.

> Si vas a enviar algo más que noticias del lanzamiento, como un newsletter, añade una casilla de consentimiento obligatoria con la promesa exacta como etiqueta. Ikiform guarda `true` con la fecha y hora del envío, lo que registra cuándo aceptó la persona. La [plantilla de newsletter](https://ikiform.com/forms/newsletter-signup) incluye una.

## Mantén los bots fuera de la lista

Un formulario público con un campo de email atrae bots en cuestión de días. Los registros falsos inflan tus números y, peor aún, tu email de lanzamiento cae en trampas de spam que dañan tu reputación como remitente. Ikiform aplica cuatro capas de defensa; las dos primeras están activas sin configurar nada.

| Defensa | Qué frena | Configuración |
| --- | --- | --- |
| Honeypot | Bots simples que rellenan todos los inputs | El input oculto `_gotcha` del formulario de arriba |
| Puntuación de spam | Direcciones desechables (+2), repeticiones idénticas (+3), enlaces y frases de spam conocidas. Con 5, el registro se guarda como spam | Ninguna |
| Límites de frecuencia | Scripts que envían en bucle | Activos por defecto: Standard, 5 por minuto por visitante y 300 por minuto para todo el formulario |
| Verificación humana | Bots que usan un navegador real | Pon **Spam check** en **Ikiform**. Usa Cloudflare Turnstile sin claves que gestionar |

El spam se guarda con una respuesta de éxito normal, así que un bot no aprende nada. No envía email ni webhook y no cuenta para tu uso. Revisa el filtro **Spam** del panel una vez por semana durante el primer mes; si un registro real acaba ahí, márcalo como no spam y su webhook saldrá en ese momento.

Añade también tu dominio en **Allowed domains**. Los envíos desde otros sitios se marcan entonces como spam, o se rechazan con `403 origin_not_allowed` si eliges **Reject**. Con la verificación humana activa, el formulario HTML no necesita cambios. La versión con `fetch` debe pedir antes un token con `challenge()` de `@ikiform/sdk`; la [documentación antispam](https://ikiform.com/es/docs/spam.md#human-check) muestra las cuatro líneas.

> Los envíos del lanzamiento llegan en ráfagas. Si publicas la página en un sitio grande, el límite de 300 por minuto de todo el formulario puede saltar antes que el de cualquier visitante. Súbelo con un límite de frecuencia Custom el día anterior. Toda una oficina o un congreso comparten una IP, así que Relaxed, 20 por minuto por visitante, encaja en esos días.

## La misma persona, dos veces

La gente se vuelve a registrar desde otra pestaña, desde un segundo dispositivo o una semana después porque se le olvidó. Ikiform guarda cada envío aceptado y no fusiona ni rechaza repeticiones por email. Esto es lo que pasa con una repetición:

| Caso | Resultado |
| --- | --- |
| El mismo email, con otros campos o más de 24 horas después | Un segundo envío normal |
| Los mismos campos otra vez dentro de 24 horas | +3 a la puntuación de spam, por debajo de 5, así que un segundo envío normal |
| Una repetición idéntica desde un dominio de email desechable | +3 y +2, que llegan a 5: se guarda como spam |

Así que cuenta con algunos duplicados y quítalos donde acabe la lista. Hay tres sitios donde hacerlo, de menos a más esfuerzo:

1. **En la herramienta de email.** La mayoría de las herramientas de email identifican los contactos por dirección, así que añadir la misma dirección dos veces actualiza un contacto en lugar de crear dos.
2. **Al exportar.** Quita los emails duplicados del CSV antes de importarlo en cualquier sitio. Abajo tienes dos formas.
3. **En tu relay.** Pasa la dirección a minúsculas y quítale los espacios, y luego insértala con una restricción de unicidad o un upsert, para que una repetición no haga nada.

¿Quieres que un doble clic no cree dos filas en absoluto? Envía una [clave de idempotencia](https://ikiform.com/es/docs/frameworks.md#idempotency) por carga de página con la versión `fetch`. Eso frena las repeticiones accidentales; no impide que una persona se registre dos veces a propósito.

## Confirmar los registros

Ikiform no envía emails a la persona que rellena el formulario. Los emails de notificación van solo a ti y a tu equipo, así que no hay respuesta automática, 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). Es intencionado: la herramienta que envía tu email de lanzamiento también debería enviar la confirmación, para que las bajas y los rebotes se registren en un solo lugar.

Dos formas de conectar Ikiform con ella:

- **Sin código.** Añade un endpoint de webhook que apunte a una herramienta de automatización y usa su acción para tu herramienta de email. 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).
- **Un pequeño relay.** Un route handler en cualquier host de Node verifica la firma y llama a la API de tu herramienta de email. El ejemplo de abajo es una ruta de Next.js; el mismo código funciona en un Cloudflare Worker.

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

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const email = String(event.data.fields.email ?? "").trim().toLowerCase();
    if (!email) return new Response(null, { status: 204 });
    const response = await fetch(process.env.LIST_API_URL!, {
      method: "POST",
      headers: {
        authorization: `Bearer ${process.env.LIST_API_KEY}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        email,
        name: event.data.fields.name ?? null,
        tags: ["waitlist", String(event.data.fields.role ?? "unknown")],
      }),
    });
    if (response.status === 409) return new Response(null, { status: 204 });
    if (!response.ok) return new Response(`List answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` comprueba la firma y la marca de tiempo, así que nadie más puede añadir direcciones a través de esta URL.
- Cambia el `fetch` por la llamada para añadir contactos de tu herramienta de email. La mayoría piden un email y una lista o etiqueta, y envían su propio email de confirmación cuando el doble opt-in está activo en esa herramienta.
- Responder `204` le dice a Ikiform que la entrega tuvo éxito. Un `502` hace que Ikiform reintente, con el mismo `webhook-id`, así que una caída breve de tu herramienta de email no hace perder registros.

Añade el endpoint en **Webhooks** en el panel y copia su secreto `whsec_` en `IKIFORM_WEBHOOK_SECRET`. Un espacio de trabajo puede tener 3 endpoints. La [documentación de webhooks](https://ikiform.com/es/docs/webhooks.md) cubre los reintentos y el payload, y [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.

## El día del lanzamiento

Diagrama: Una lista de espera desde el primer registro hasta el día del lanzamiento

1. La página se publica con el formulario enviando a Ikiform.
2. Cada semana, comparte la página y lee los nuevos registros y sus respuestas.
3. Una semana antes del lanzamiento, exporta la lista como CSV, quita los duplicados y divídela por rol.
4. El día del lanzamiento, envía un email a la lista desde tu herramienta de email.

La mayor parte del trabajo es la exportación una semana antes. El email de lanzamiento en sí es breve.

### Exporta la lista

Abre **Submissions** en el panel, elige el formulario de la lista de espera, pon el estado en **Received** y el rango de fechas en **All time**, y luego elige **Export CSV**. Las columnas son `id`, `created_at`, `form` y `status`, y después una columna por campo. El archivo es UTF-8 con BOM, así que los acentos sobreviven a Excel. Una exportación tiene como máximo 100.000 filas; por encima de eso, pagina con la [API REST](https://ikiform.com/es/docs/guides/export.md#api).

Para quitar duplicados en Google Sheets, importa el CSV y usa esta fórmula en una hoja vacía, con el email en la columna E:

`Google Sheets`:

```text
=SORTN(A2:F, 9^9, 2, LOWER(TRIM(E2:E)), TRUE)
```

Conserva una fila por dirección, sin distinguir mayúsculas ni espacios, ordenada por email. O ejecuta esto con Node en la carpeta que contiene la exportación. Conserva la primera fila de cada dirección y supone que no hay comas dentro de los valores, lo que se cumple en un formulario que solo pide el email:

`dedupe.mjs`:

```js
import { readFileSync, writeFileSync } from "node:fs";

const [header = "", ...rows] = readFileSync("submissions.csv", "utf8").replace(/^\uFEFF/, "").trim().split(/\r?\n/);
const columns = header.split(",");
const emailAt = columns.indexOf("email");
const seen = new Set();
const unique = rows.filter((row) => {
  const email = row.split(",")[emailAt]?.trim().toLowerCase();
  if (!email || seen.has(email)) return false;
  seen.add(email);
  return true;
});

writeFileSync("waitlist.csv", [header, ...unique].join("\n"));
console.log(`${rows.length} rows, ${unique.length} unique emails`);
```

### Envía el email de lanzamiento

Envíalo desde tu herramienta de email, no desde tu bandeja de entrada personal. Una bandeja personal que escribe a cientos de direcciones a la vez parece spam para los proveedores de correo, y querrás gestionar las bajas desde el momento en que envíes. Que sea breve:

`Email de lanzamiento`:

```text
Subject: Acme is live. You're in.

Hi Ada,

You joined the Acme waitlist in the spring. Today it's open.

Sign in here: https://acme.example/start

You get the first 30 days free because you waited.
Reply to this email if anything breaks. It comes to me.

Ada, founder of Acme
```

- Di qué está disponible y enlaza directamente a la página de registro o de inicio de sesión.
- Premia la espera, por ejemplo con una prueba más larga o un descuento, y dilo una sola vez.
- Envíalo desde una persona. Las respuestas son el mejor feedback temprano que vas a recibir.
- Envía primeras líneas distintas a fundadores e ingenieros si recogiste `role`. Para eso sirve recogerlo.

> Si estuviste meses recogiendo registros sin confirmar las direcciones, envía primero a una pequeña parte y vigila la tasa de rebote. Una lista llena de erratas y direcciones muertas daña el dominio desde el que enviarás los emails del producto.

## Elige tu caso

Los pasos de arriba funcionan en cualquier sitio. Estas páginas cubren lo que cambia para un lanzamiento o un creador de sitios concreto.

- [Lista para lanzar en Product Hunt](https://ikiform.com/es/guides/waitlist/product-hunt.md): Reúne una lista semanas antes de Product Hunt, etiqueta cada registro por canal y envía el email del lanzamiento en dos tandas según el reinicio en hora del Pacífico.
- [Registro a una beta con filtro](https://ikiform.com/es/guides/waitlist/beta-signup.md): Un formulario de solicitud para una beta con preguntas de filtro que aplica el servidor, y un script que puntúa a los candidatos y elige cada grupo de la lista.
- [Lista de espera para app móvil](https://ikiform.com/es/guides/waitlist/mobile-app.md): Una lista de espera previa al lanzamiento para una app de iOS o Android que pregunta qué teléfono se usa, lleva a los testers de iPhone a TestFlight y llena una prueba cerrada de Play.
- [Lista de espera en Next.js](https://ikiform.com/es/guides/waitlist/nextjs.md): Una lista de espera con el App Router de Next.js: una server action que reenvía registros con una clave secreta, useActionState, un agradecimiento optimista y un límite por visitante.
- [Lista de espera en Framer](https://ikiform.com/es/guides/waitlist/framer.md): Dos formas de recoger registros de lista de espera en Framer: el formulario de Framer con un webhook a Ikiform, o un code component que conserva los límites por visitante y el antispam.
- [Lista de espera en Webflow](https://ikiform.com/es/guides/waitlist/webflow.md): Apunta un Form Block de Webflow a Ikiform, etiqueta los registros por origen con un campo oculto y conserva el estado de éxito de Webflow con un script breve para todos los formularios.
- [Lista de espera en Carrd](https://ikiform.com/es/guides/waitlist/carrd.md): Recoge registros de lista de espera en una página de Carrd: un formulario Send to URL en Pro Plus con seguimiento UTM, o un formulario HTML incrustado con honeypot en Pro Standard.
- [Lista de espera en HTML puro](https://ikiform.com/es/guides/waitlist/html.md): Una página de lista de espera completa en HTML puro y sin build que funciona con JavaScript desactivado, muestra un agradecimiento en línea cuando carga y recurre a un plan B cuando hace falta.
- [Lista de espera en Astro](https://ikiform.com/es/guides/waitlist/astro.md): Una lista de espera en Astro sobre una landing page estática: una Action reenvía cada registro, un script la llama y una página bajo demanda recoge los envíos sin JavaScript.
- [Lista de espera en TanStack Start](https://ikiform.com/es/guides/waitlist/tanstack-start.md): Una lista de espera en TanStack Start: una ruta de servidor responde tanto a un envío de formulario normal como a fetch, y validateSearch lleva el origen y el resultado en la URL.
- [Lista de espera en React Router](https://ikiform.com/es/guides/waitlist/react-router.md): Una lista de espera de React Router en el footer de cada página: un fetcher envía a una sola action de ruta, etiqueta cada registro con su página y funciona sin JavaScript.
- [Lista de espera en SvelteKit](https://ikiform.com/es/guides/waitlist/sveltekit.md): Una lista de espera en SvelteKit 3: un hook guarda el primer ?ref= en una cookie, una form action con nombre reenvía el registro y quien vuelve ve que ya se unió.
- [Lista de espera en Nuxt](https://ikiform.com/es/guides/waitlist/nuxt.md): Una lista de espera en Nuxt sobre una landing page prerenderizada: una ruta de Nitro acepta $fetch y envíos normales, los valida con Zod y redirige sin JavaScript.

## Comprueba que funciona

Repasa esto antes de compartir la página. 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 pasar a producción, elige Pro desde 6 $/mes por 500 envíos, o paga una sola 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 sitio.
2. Rellena el formulario en tu página y envíalo. Llegarás a tu página de agradecimiento, o a la alojada por Ikiform.
3. Abre **Submissions** en el panel. El nuevo envío está ahí con todos los campos que mandaste.
4. Revisa la bandeja de entrada de cada destinatario de las notificaciones. Cada uno recibe un email con los campos, y **Reply** responde a quien envió el formulario cuando este tiene un campo `email`.
5. Si añadiste un webhook, abre el endpoint en **Webhooks**. La entrega muestra `succeeded` con la respuesta de tu servidor.
6. Envía con el honeypot relleno, `_gotcha=bot`. Sigue recibiendo una respuesta de éxito, pero el envío se guarda como spam y no manda email ni webhook.
7. ¿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.
8. Regístrate dos veces con la misma dirección y confirma que tu herramienta de email sigue mostrando un solo contacto.

## Solución de problemas

| Problema | Qué hacer |
| --- | --- |
| `403 origin_not_allowed`, o todos los envíos acaban en spam | El dominio de la página no está en los **Allowed domains** del formulario. Añádelo, y también `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. |
| `403 challenge_required` | La verificación antispam del formulario está en **Ikiform** y un envío con `fetch` no llevaba token. Consigue uno con `challenge(formId)` de `@ikiform/sdk` antes de cada envío, o desactiva la verificación. |
| 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 con el filtro **Spam** para ver los motivos y márcalo como no spam para que se envíen su email y sus webhooks. |
| El navegador llega a la página de agradecimiento de Ikiform en vez de a 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. |
| El mismo email aparece dos veces | Es lo esperado. Ikiform no deduplica por email. Quita las repeticiones al exportar, o haz un upsert por email en tu relay o tu herramienta de email. |
| Los registros llegan a Ikiform pero no a la herramienta de email | Abre el endpoint en **Webhooks** y lee el registro de entregas. Un `400` significa que el secreto es incorrecto; un `502` con el estado de tu herramienta significa que la herramienta rechazó el contacto. |
| `402 test_limit_reached` | El espacio de trabajo no tiene plan y ya usó sus 50 pruebas gratuitas desde `localhost` este mes. Elige un plan, o usa **Send test** en el panel. |

## Preguntas

### ¿Necesito un backend para crear una página de lista de espera?

No necesitas un servidor propio. El formulario necesita un sitio al que enviar, y eso es un backend de formularios: apunta el action del formulario a tu endpoint de Ikiform y este guarda, filtra y reenvía cada registro. Tu landing page puede seguir siendo estática.

### ¿Ikiform envía un email de confirmación a quienes se unen a la lista de espera?

No. Ikiform te envía emails a ti y a tu equipo, no a la persona que se registra, así que no hay email de confirmación ni de doble opt-in. Envía cada registro por webhook a tu herramienta de email y activa allí el doble opt-in.

### ¿Cómo evito los registros falsos en un formulario de lista de espera?

Conserva el honeypot oculto _gotcha, deja activo el límite de frecuencia por defecto, añade tu dominio en Allowed domains y pon la verificación antispam en Ikiform para tener una comprobación de Cloudflare Turnstile sin claves que gestionar. Las direcciones desechables también suman a la puntuación de spam.

### ¿Se guardará dos veces el mismo email?

Puede pasar. Ikiform conserva cada envío aceptado y no fusiona repeticiones por email. Deduplica donde viva la lista: tu herramienta de email, una columna única en tu base de datos o el CSV antes de importarlo.

### ¿Puedo añadir un programa de referidos o una posición en la fila?

No solo con Ikiform. No tiene enlaces de referidos ni rankings. Puedes pasar un código de referido en un campo oculto y contarlo tú mismo, o usar un producto de listas de espera centrado en referidos si eso es clave para tu lanzamiento.

### ¿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 10.000 envíos por 49 $ o 50.000 envíos por 149 $, con créditos que nunca caducan.

## Más guías

- [Lista para lanzar en Product Hunt](https://ikiform.com/es/guides/waitlist/product-hunt.md): Reúne una lista semanas antes de Product Hunt, etiqueta cada registro por canal y envía el email del lanzamiento en dos tandas según el reinicio en hora del Pacífico.
- [Registro a una beta con filtro](https://ikiform.com/es/guides/waitlist/beta-signup.md): Un formulario de solicitud para una beta con preguntas de filtro que aplica el servidor, y un script que puntúa a los candidatos y elige cada grupo de la lista.
- [Lista de espera para app móvil](https://ikiform.com/es/guides/waitlist/mobile-app.md): Una lista de espera previa al lanzamiento para una app de iOS o Android que pregunta qué teléfono se usa, lleva a los testers de iPhone a TestFlight y llena una prueba cerrada de Play.
- [Lista de espera en Next.js](https://ikiform.com/es/guides/waitlist/nextjs.md): Una lista de espera con el App Router de Next.js: una server action que reenvía registros con una clave secreta, useActionState, un agradecimiento optimista y un límite por visitante.
- [Lista de espera en Framer](https://ikiform.com/es/guides/waitlist/framer.md): Dos formas de recoger registros de lista de espera en Framer: el formulario de Framer con un webhook a Ikiform, o un code component que conserva los límites por visitante y el antispam.
- [Lista de espera en Webflow](https://ikiform.com/es/guides/waitlist/webflow.md): Apunta un Form Block de Webflow a Ikiform, etiqueta los registros por origen con un campo oculto y conserva el estado de éxito de Webflow con un script breve para todos los formularios.
- [Lista de espera en Carrd](https://ikiform.com/es/guides/waitlist/carrd.md): Recoge registros de lista de espera en una página de Carrd: un formulario Send to URL en Pro Plus con seguimiento UTM, o un formulario HTML incrustado con honeypot en Pro Standard.
- [Lista de espera en HTML puro](https://ikiform.com/es/guides/waitlist/html.md): Una página de lista de espera completa en HTML puro y sin build que funciona con JavaScript desactivado, muestra un agradecimiento en línea cuando carga y recurre a un plan B cuando hace falta.
- [Lista de espera en Astro](https://ikiform.com/es/guides/waitlist/astro.md): Una lista de espera en Astro sobre una landing page estática: una Action reenvía cada registro, un script la llama y una página bajo demanda recoge los envíos sin JavaScript.
- [Lista de espera en TanStack Start](https://ikiform.com/es/guides/waitlist/tanstack-start.md): Una lista de espera en TanStack Start: una ruta de servidor responde tanto a un envío de formulario normal como a fetch, y validateSearch lleva el origen y el resultado en la URL.
- [Lista de espera en React Router](https://ikiform.com/es/guides/waitlist/react-router.md): Una lista de espera de React Router en el footer de cada página: un fetcher envía a una sola action de ruta, etiqueta cada registro con su página y funciona sin JavaScript.
- [Lista de espera en SvelteKit](https://ikiform.com/es/guides/waitlist/sveltekit.md): Una lista de espera en SvelteKit 3: un hook guarda el primer ?ref= en una cookie, una form action con nombre reenvía el registro y quien vuelve ve que ya se unió.
- [Lista de espera en Nuxt](https://ikiform.com/es/guides/waitlist/nuxt.md): Una lista de espera en Nuxt sobre una landing page prerenderizada: una ruta de Nitro acepta $fetch y envíos normales, los valida con Zod y redirige sin JavaScript.
- [Formulario de contacto en un sitio estático](https://ikiform.com/es/guides/contact-form-static-site.md): Añade un formulario de contacto que funcione a un sitio estático sin servidor: a dónde va el POST, el spam, las respuestas por email y código para cada host y framework.
- [Envía un formulario a tu email](https://ikiform.com/es/guides/send-form-to-email.md): Recibe cada envío de formulario en tu bandeja sin servidor de correo: por qué fallan mailto: y mail() de PHP, y una configuración que filtra spam y te deja responder.
- [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.
