# Envía los envíos de formularios HTML a tu email

> 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.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/send-form-to-email

Un formulario HTML no puede enviar emails por sí solo, porque el navegador no tiene forma de hablar con un servidor de correo. Apunta el `action` del formulario a un backend de formularios, que recibe el envío, filtra el spam, lo guarda y te manda un email a ti y a tu equipo. Llama `email` al campo del email y, al pulsar **Reply** en tu bandeja, responderás directamente a quien rellenó el formulario.

La mayoría de los sitios necesitan una sola cosa de un formulario de contacto: que el mensaje llegue a una bandeja que alguien lea. Resulta más difícil de lo que parece. HTML tiene un elemento `<form>`, pero no una forma de enviar correo, y los viejos atajos le devuelven el trabajo al visitante o acaban en spam. Esta guía explica por qué, compara las opciones con honestidad y monta un formulario que te envía un email en cualquier sitio y con cualquier stack.

## Por qué un formulario no puede enviar emails por sí solo

Cuando un visitante pulsa **Send**, el navegador hace una petición HTTP a la URL del `action` del formulario. Es todo lo que puede hacer un formulario. Enviar un email requiere una conversación SMTP con un servidor de correo, desde una máquina en la que confíe el servidor receptor. Un navegador no puede abrir esa conexión y, aunque pudiera, el correo desde el portátil de un visitante cualquiera se rechazaría. Así que siempre hay algo en un servidor entre el formulario y tu bandeja: tu propio código, un script en tu hosting o un servicio.

Con un backend de formularios, ese algo es una URL. El formulario envía a `https://ikiform.com/f/<id>` e Ikiform hace el resto:

Diagrama: Cómo un envío de formulario se convierte en un email en tu bandeja

1. El visitante envía tu formulario, y el navegador o tu servidor lo manda a tu endpoint de Ikiform.
2. Ikiform comprueba los campos con la definición del formulario, puntúa el envío como posible spam y lo guarda.
3. Un envío que alcanza la puntuación de spam se guarda como spam. Recibe la misma respuesta de éxito, pero no envía ningún email ni webhook.
4. Cualquier otro envío manda un email a cada destinatario, con la dirección del visitante como reply-to.
5. El email llega a tu bandeja segundos después. Al pulsar Reply respondes al visitante.

El visitante nunca ve una dirección de email ni abre una app de correo. Lo único que hay en tu página es un formulario.

## Seis formas de llevar un formulario a tu bandeja

Cada una le sirve a alguien. La tabla trata de lo que te cuesta cada una cuando visitantes reales, y bots reales, empiezan a usar el formulario.

| Opción | Bueno para | Contras |
| --- | --- | --- |
| `action="mailto:you@example.com"` | Sin servidor, sin cuenta, un solo atributo. | No se envía nada. El navegador abre la app de correo del visitante con un borrador, o no hace nada si no tiene ninguna configurada. Tu dirección queda en la página al alcance de los scrapers. Consulta [la guía de Gmail](https://ikiform.com/es/guides/send-form-to-email/gmail.md). |
| Un script PHP con `mail()` | Funciona en casi cualquier hosting PHP compartido, sin terceros. | Depende de la configuración de correo del hosting, y el correo que dice venir de tu dominio suele fallar SPF o DMARC y acaba en spam. Sin filtro antispam, sin almacenamiento y sin PHP en absoluto en los hostings estáticos. Consulta [sin PHP](https://ikiform.com/es/guides/send-form-to-email/without-php.md). |
| Un SDK de email en el navegador, como EmailJS | Sin servidor. Plantillas de email que diseñas en su panel. | Las claves y los ids de plantilla están en el JavaScript de tu página, así que cualquiera puede enviar tus plantillas con su propio texto. Consulta [la alternativa a EmailJS](https://ikiform.com/es/guides/send-form-to-email/emailjs-alternative.md). |
| Tu propia ruta más una API de email, como Resend, Postmark, Amazon SES o Nodemailer por SMTP | Control total: tu asunto, tu HTML, una confirmación al visitante. | Tienes que construir la validación, el filtro antispam, los límites de frecuencia, el almacenamiento y los reintentos, verificar un dominio de envío y necesitas un servidor incluso para un sitio estático. Consulta [la guía de Next.js](https://ikiform.com/es/guides/send-form-to-email/nextjs.md). |
| Google Apps Script como app web | Funciona con una cuenta de Google que ya tienes. | Las cuotas de scripts limitan a cuántos destinatarios puede enviar al día, la URL no tiene protección antispam y el script lo mantienes tú. |
| Un backend de formularios, como Ikiform, [Formspree](https://ikiform.com/alternatives/formspree) o [Web3Forms](https://ikiform.com/alternatives/web3forms) | Una URL en `action`. Filtro antispam, almacenamiento y email sin servidor. | El diseño y el asunto del email los pone el servicio. Con Ikiform, nunca se envía nada por email al visitante. |

Si necesitas un email totalmente con tu marca, o una confirmación al visitante, escribe tu propia ruta con una API de email. Si quieres el mensaje en tu bandeja y poder responder, sin spam, un backend de formularios es lo que menos código requiere. El resto de esta guía usa Ikiform.

## El formulario

Tres campos cubren la mayoría de los formularios de contacto: quién es, a dónde responder y qué quiere. Elige tu stack abajo; la elección se mantiene en todos los ejemplos de código de estas guías.

**HTML**

Este formulario funciona sin nada de JavaScript: el navegador lo envía, Ikiform te manda un email y el visitante llega a la página de `_redirect`. Cada campo tiene debajo un hueco vacío para errores, que usa el script de abajo.

`contact.html`:

```html
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required maxlength="120" />
  <p data-error="name"></p>

  <label for="email">Your email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p data-error="email"></p>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
  <p data-error="message"></p>

  <input type="hidden" name="page" value="/contact" />
  <input type="hidden" name="_redirect" value="https://example.com/contact/sent" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send message</button>
  <p id="contact-status" role="status"></p>
</form>
```

El script mantiene al visitante en la página. Envía con `fetch`, escribe cada error del servidor debajo de su campo y cambia el formulario por una línea que repite la dirección a la que responderás, para que una errata sea fácil de detectar.

`contact.js`:

```js
const form = document.getElementById("contact");
const status = document.getElementById("contact-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  for (const slot of form.querySelectorAll("[data-error]")) slot.textContent = "";
  button.disabled = true;
  status.textContent = "Sending";

  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) {
      const email = form.elements.namedItem("email").value;
      const done = document.createElement("p");
      done.setAttribute("role", "status");
      done.textContent = `Sent. We'll reply to ${email}.`;
      form.replaceWith(done);
      return;
    }
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      const slot = form.querySelector(`[data-error="${field}"]`);
      if (slot) slot.textContent = messages[0];
    }
    status.textContent = result.error.fields ? "Check the fields above." : result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Una server action envía el mensaje con tu clave secreta. Convierte el `error.fields` de Ikiform en un mensaje por campo y devuelve los valores escritos, para que no se pierda nada de lo que escribió el visitante si hay un error.

`app/contact/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface ContactState {
  sent: boolean;
  email?: string;
  errors: Record<string, string>;
  values?: { name: string; email: string; message: string };
}

export async function sendMessage(_previous: ContactState, formData: FormData): Promise<ContactState> {
  const values = {
    name: String(formData.get("name") ?? "").trim(),
    email: String(formData.get("email") ?? "").trim(),
    message: String(formData.get("message") ?? "").trim(),
  };
  const origin = (await 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 ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({ ...values, page: "/contact", _gotcha: String(formData.get("_gotcha") ?? "") }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, email: values.email, errors: {} };

  const errors: Record<string, string> = {};
  for (const [field, messages] of Object.entries(result?.error?.fields ?? {})) {
    errors[field] = (messages as string[])[0] ?? "Check this field.";
  }
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Couldn't send your message. Try again.";
  return { sent: false, errors, values };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type ContactState, sendMessage } from "./actions";

const initial: ContactState = { sent: false, errors: {} };

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

  if (state.sent) return <p role="status">Sent. We'll reply to {state.email}.</p>;

  return (
    <form action={action}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required defaultValue={state.values?.name} />
      {state.errors.name ? <p role="alert">{state.errors.name}</p> : null}

      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required defaultValue={state.values?.email} />
      {state.errors.email ? <p role="alert">{state.errors.email}</p> : null}

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} defaultValue={state.values?.message} />
      {state.errors.message ? <p role="alert">{state.errors.message}</p> : null}

      <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 ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

¿Quieres profundizar con Next.js? Lee [Un formulario de contacto en Next.js que envía emails](https://ikiform.com/es/guides/send-form-to-email/nextjs.md).

**Astro**

Zod comprueba cada campo en la action antes de enviar nada, e `isInputError` te da sus mensajes por campo. La página debe renderizarse bajo demanda, así que mantén `prerender = false` y añade un adaptador.

`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 = {
  sendMessage: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us your name.").max(120),
      email: z.email("Enter an address we can reply to."),
      message: z.string().trim().min(10, "Write at least a sentence."),
      _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, page: "/contact" }),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { email: input.email };
    },
  }),
};
```

`src/pages/contact.astro`:

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

export const prerender = false;

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

{result?.data ? (
  <p role="status">Sent. We'll reply to {result.data.email}.</p>
) : (
  <form method="POST" action={actions.sendMessage}>
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {fields.name && <p role="alert">{fields.name[0]}</p>}

    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {fields.message && <p role="alert">{fields.message[0]}</p>}

    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Send message</button>
  </form>
)}
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

**TanStack Start**

La función de servidor lee el `FormData` en `.validator()`, lo envía desde el servidor y devuelve la dirección de respuesta o un error por campo.

`src/routes/contact.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";

type Outcome = { sent: true; email: string } | { sent: false; errors: Record<string, string> };

const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<Outcome> => {
    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, page: "/contact" }),
    });
    const result = await response.json();
    if (result.ok) return { sent: true, email: data.email };
    const errors: Record<string, string> = { form: String(result.error.message) };
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      errors[field] = String((messages as string[])[0]);
    }
    return { sent: false, errors };
  });

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

function Contact() {
  const send = useServerFn(sendMessage);
  const [outcome, setOutcome] = useState<Outcome | null>(null);
  const [pending, setPending] = useState(false);
  const errors = outcome && !outcome.sent ? outcome.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setOutcome(await send({ data }));
    } catch {
      setOutcome({ sent: false, errors: { form: "Couldn't send your message. Try again." } });
    } finally {
      setPending(false);
    }
  }

  if (outcome?.sent) return <p role="status">Sent. We'll reply to {outcome.email}.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.name && !errors.email && !errors.message ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

**React Router**

El `action` de la ruta reenvía el mensaje y devuelve los errores de campo de Ikiform a través de `actionData`, conservando el código de estado de Ikiform con `data()`.

`app/routes/contact.tsx`:

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").trim();
  const origin = 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 ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      name: String(formData.get("name") ?? "").trim(),
      email,
      message: String(formData.get("message") ?? "").trim(),
      page: "/contact",
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, email };
  const fields: Record<string, string[]> = result.error.fields ?? {};
  return data(
    { sent: false as const, message: String(result.error.message), fields },
    { status: response.status },
  );
}

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

  if (actionData?.sent) return <p role="status">Sent. We'll reply to {actionData.email}.</p>;
  const fields = actionData?.fields ?? {};

  return (
    <Form method="post">
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData && !Object.keys(fields).length ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </Form>
  );
}
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

**SvelteKit**

La action por defecto devuelve `fail()` con los errores de campo. `update({ reset: false })` conserva lo que escribió el visitante cuando el servidor responde con un error. Declara `IKIFORM_SECRET_KEY` en `src/env.ts` con `defineEnvVars` para que `$app/env/private` pueda leerla.

`src/routes/contact/+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 values = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
    };

    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({ ...values, page: "/contact", _gotcha: String(data.get("_gotcha") ?? "") }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? {};
      return fail(response.status, { values, fields, message: String(result.error.message) });
    }
    return { sentTo: values.email };
  },
} satisfies Actions;
```

`src/routes/contact/+page.svelte`:

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

  let { form }: PageProps = $props();
  let pending = $state(false);
  const fields = $derived<Record<string, string[]>>(form?.fields ?? {});
</script>

{#if form?.sentTo}
  <p role="status">Sent. We'll reply to {form.sentTo}.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update({ reset: false });
        pending = false;
      };
    }}
  >
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {#if fields.name}<p role="alert">{fields.name[0]}</p>{/if}
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {#if fields.email}<p role="alert">{fields.email[0]}</p>{/if}
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {#if fields.message}<p role="alert">{fields.message[0]}</p>{/if}
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message && !Object.keys(fields).length}<p role="alert">{form.message}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
  </form>
{/if}
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

**Nuxt**

Una ruta de servidor guarda la clave secreta y pasa el mensaje de Ikiform como `message` de `createError` y los errores de campo en su `data`; `$fetch` devuelve ambos en el cliente como `data.message` y `data.data.fields`. Nuxt rellena `runtimeConfig.ikiformSecretKey` a partir de `NUXT_IKIFORM_SECRET_KEY`.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ name?: string; email?: string; message?: 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({
      name: body.name?.trim() ?? "",
      email: body.email?.trim() ?? "",
      message: body.message?.trim() ?? "",
      page: "/contact",
      _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 { sentTo: body.email };
});
```

`nuxt.config.ts`:

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

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
const values = reactive({ name: "", email: "", message: "", _gotcha: "" });
const fields = ref<Record<string, string[]>>({});
const error = ref("");
const sentTo = ref("");
const sending = ref(false);

async function send() {
  sending.value = true;
  fields.value = {};
  error.value = "";
  try {
    const result = await $fetch("/api/contact", { method: "POST", body: values });
    sentTo.value = result.sentTo ?? values.email;
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    fields.value = data?.data?.fields ?? {};
    error.value = data?.message ?? "Couldn't send your message. Try again.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="values.name" name="name" autocomplete="name" required />
    <p v-if="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" v-model="values.email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" v-model="values.message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input v-model="values._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

Las versiones del lado del 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 del servidor. Cada ejemplo de servidor reenvía la cabecera `Origin` del navegador cuando la hay, porque el `fetch` del propio servidor no envía ninguna e Ikiform cuenta con ella los envíos de prueba gratuitos desde `localhost`.

**React**

Sin servidor propio, el navegador envía directamente a Ikiform. En este código no va ninguna clave; **Allowed domains** decide qué sitios pueden enviar.

`src/ContactForm.tsx`:

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

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

export function ContactForm() {
  const [sentTo, setSentTo] = useState("");
  const [sending, setSending] = useState(false);
  const [fields, setFields] = useState<Record<string, string[]>>({});
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setSending(true);
    setFields({});
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setSentTo(String(body.get("email")));
        return;
      }
      setFields(result.error.fields ?? {});
      setError(result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    } finally {
      setSending(false);
    }
  }

  if (sentTo) return <p role="status">Sent. We'll reply to {sentTo}.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input type="hidden" name="page" value="/contact" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error && !Object.keys(fields).length ? <p role="alert">{error}</p> : null}
      <button disabled={sending}>{sending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

**Vue**

El mismo envío desde el navegador como componente de Vue. El formulario conserva su `action`, así que sigue enviándose aunque el script no cargue.

`src/components/ContactForm.vue`:

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

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

const sentTo = ref("");
const sending = ref(false);
const fields = ref<Record<string, string[]>>({});
const error = ref("");

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

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    <p v-if="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input type="hidden" name="page" value="/contact" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

- Sustituye `frm_xxxxxxxxxxxxxxxx` por el id de tu formulario, que está en el panel.
- `name="email"` convierte ese campo en la dirección reply-to del email de notificación. Conserva el nombre aunque la etiqueta diga "Tu email".
- `page` es un campo oculto normal. Aparece en el email, así sabes desde qué página llegó el mensaje.
- `_gotcha` es el honeypot. Las personas nunca lo ven; un bot que lo rellena se guarda como spam y no se envía ningún email.
- `_redirect` solo funciona con una URL `https:` en uno de los **Allowed domains** del formulario. Omítelo y configura **Redirect after submit** en los ajustes del formulario, o Ikiform muestra su propia página de agradecimiento.

Las guías de frameworks van más allá: [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 simple](https://ikiform.com/es/docs/html.md). Los creadores de sitios como [Webflow](https://ikiform.com/es/docs/guides/webflow.md) y [Carrd](https://ikiform.com/es/docs/guides/carrd.md) tienen sus propias páginas, y la [plantilla de formulario de contacto](https://ikiform.com/forms/contact-form) añade un campo de tema.

Define también los campos en el panel, e Ikiform rechaza un envío que no coincida con `422 validation_failed` y un mensaje por campo, que el código de arriba muestra junto a cada input. Pruébalo aquí; esto ejecuta el mismo validador en tu navegador:

Formulario: Contact

- **Nombre** (`name`, text, obligatorio)
- **Tu email** (`email`, email, obligatorio)
- **Mensaje** (`message`, textarea, obligatorio)

Pega esta definición en la pestaña **Fields** del formulario. Sin definición, Ikiform acepta cualquier nombre de campo y te envía por email lo que llegue.

## Cómo es el email

Cada envío aceptado manda un email sencillo y legible. Esta es la versión de texto de uno:

`Email de notificación`:

```text
From: Ikiform <notifications@ikiform.com>
To: you@yourcompany.com
Reply-To: ada@example.com
Subject: New submission: Contact

Contact got a new submission on Oct 25, 2026, 9:14 AM UTC.

name: Ada Lovelace
email: ada@example.com
message: Do you ship to Canada? I'd like to order 40 units before December.
page: /contact

View it in Ikiform:
https://ikiform.com/dashboard/submissions?form=frm_xxxxxxxxxxxxxxxx&submission=sub_...
```

- Llega desde `notifications@ikiform.com`. No configuras registros DNS ni un dominio de envío, porque el correo sale del dominio de Ikiform.
- El asunto es siempre `New submission: <form name>`. `_subject` no cambia el asunto de los emails de notificación. Se pasa a los webhooks y a la API como `meta.subject`. Para distinguir los formularios, dale a cada uno un nombre claro en el panel, como "Contacto, página de precios".
- Los campos aparecen en el orden en que se enviaron, etiquetados por su `name`. Los campos que empiezan con `_` se omiten.
- Un formulario o una respuesta muy largos se acortan, y el email indica cuántos campos más hay en el panel. La [página de límites](https://ikiform.com/es/docs/limits.md#notifications) tiene las cifras.
- Los campos que parecen contraseñas o números de tarjeta se omiten, y el email de un envío que los contiene se retiene para revisión. No recojas credenciales con un backend de formularios.
- Los miembros del espacio de trabajo reciben un botón **View submission**. Las personas de fuera del espacio de trabajo reciben los campos y un enlace para darse de baja.

## Quién recibe el email

Los destinatarios se configuran por formulario en el panel, nunca desde el propio formulario. Un campo que indicara el destinatario permitiría a cualquiera usar tu formulario para enviar emails a quien quisiera, así que Ikiform no tiene ninguno.

| Quién | Cómo se añaden | Notas |
| --- | --- | --- |
| Por defecto | No hay que hacer nada | El miembro elegido como **Notification email** del espacio de trabajo, o todos los propietarios del espacio de trabajo si no hay ninguno elegido |
| Miembros elegidos | Márcalos en los ajustes de **Notifications** del formulario | Solo miembros con una dirección de email verificada |
| Personas de fuera del espacio de trabajo | **Add someone outside the workspace** y luego **Send link** | Reciben emails solo después de confirmar. Hasta 5 por formulario, y cada una puede darse de baja |

Cada formulario envía emails a 10 direcciones como máximo. Cada destinatario recibe su propia copia en lugar de un CC compartido, así nadie ve las direcciones de los demás. Si el buzón de un destinatario rebota, Ikiform pausa los emails a esa dirección durante un tiempo y el panel muestra **Emails bounced** a su lado. [Varios destinatarios](https://ikiform.com/es/guides/send-form-to-email/multiple-recipients.md) explica cómo repartir por tema, algo que los destinatarios por sí solos no pueden hacer.

## Responde a la persona, no a Ikiform

Ikiform toma la dirección reply-to primero de un campo `_replyto` y, si no, de un campo llamado `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. El valor debe ser una única dirección de email válida o se ignora. Con un reply-to configurado, al pulsar **Reply** en Gmail, Outlook o Apple Mail se abre una respuesta al visitante, y tu respuesta sale desde tu propia dirección.

Sin él, una respuesta va a `notifications@ikiform.com`, que nadie lee. Es la sorpresa más habitual, y casi siempre se debe a un campo llamado `user_email` o `Email Address` con un espacio. Renómbralo o añade `_replyto`. [La guía de reply-to](https://ikiform.com/es/guides/send-form-to-email/reply-to.md) cubre formularios con dos campos de email y creadores de sitios que renombran los campos por ti.

## Cuando llega mucho a la vez

Cada formulario envía hasta 50 emails de notificación por hora, o 10 mientras el espacio de trabajo tenga menos de 7 días. A partir de ahí, el resto de los envíos de esa hora se guardan como siempre y llegan en un único email de resumen cuando termina la hora. El límite protege tu bandeja, y la opinión que tu proveedor de correo tiene del correo de Ikiform, cuando un formulario se publica en algún sitio grande o un bot se cuela entre los filtros.

Diagrama: Qué pasa con los emails de notificación en una hora con mucho tráfico

1. Los primeros 50 envíos a un formulario dentro de una hora de reloj envían cada uno un email como siempre.
2. Los envíos posteriores de esa misma hora se guardan y aparecen en el panel al instante, sin un email por cada uno.
3. Cuando termina la hora, cada destinatario recibe un resumen que los cuenta y enlaza al panel.

El límite se cuenta por formulario, así que una ráfaga en un formulario no frena a otro. Los webhooks no tienen límite y se disparan con cada envío.

El asunto del resumen es el número de envíos y el nombre del formulario; no enumera los campos ni configura un reply-to, así que responde a esos mensajes desde el panel. Si superas el límite con frecuencia, el email no es el canal adecuado para ese formulario: envíalo a [Slack](https://ikiform.com/blog/form-submissions-to-slack.md) o a una bandeja compartida mediante un [webhook](https://ikiform.com/es/docs/webhooks.md).

## Mantén el spam fuera de tu bandeja

El email encarece el spam: cada envío de un bot es una notificación que alguien abre. Ikiform guarda el spam con una respuesta de éxito normal, así el bot no aprende nada, y no envía ningún email por él. Cuatro cosas deciden qué cuenta como spam:

| Defensa | Qué detecta | Configuración |
| --- | --- | --- |
| Honeypot | Bots que rellenan todos los inputs | El input oculto `_gotcha` |
| Puntuación de spam | Enlaces en el campo del nombre, tres o más enlaces, frases de spam conocidas, direcciones desechables y repeticiones idénticas. Con 5, el envío es spam | Ninguna |
| Dominios permitidos | Envíos desde otros sitios | Añade tu dominio en los ajustes del formulario |
| Verificación humana | Bots que usan un navegador real | Pon **Spam check** en **Ikiform** |

Revisa de vez en cuando el filtro **Spam** del panel. Si un mensaje real acaba ahí, márcalo como no spam y su email se envía en ese momento. La [documentación de spam](https://ikiform.com/es/docs/spam.md) explica cada señal, incluido cuándo activar la verificación humana.

## Lo que el email no puede hacer

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, activada por un [webhook](https://ikiform.com/es/docs/webhooks.md). Si tu sitio promete "hemos recibido tu mensaje", muéstralo en la página tras el envío, donde ya lo hace el código de arriba.

- **Una bandeja compartida del equipo.** Añade la dirección del equipo como destinataria, o reenvía desde tu proveedor de correo.
- **Un canal de chat.** Envía cada envío a [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) o [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md) por webhook. El chat va bien para formularios que reciben más de unos pocos mensajes al día.
- **Una hoja de cálculo o un CRM.** [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md), o cualquier herramienta a través de [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) o [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Archivos adjuntos.** La subida de archivos aún no está disponible. Un envío con un archivo recibe `422 files_not_supported`. Pide en su lugar un enlace a un archivo compartido.

### Desactivar el email

Algunos formularios funcionan mejor sin email, como un registro a una newsletter con mucho tráfico. Desactiva **Email new submissions** en los ajustes de **Notifications** del formulario. Los envíos se siguen guardando y los webhooks se siguen disparando.

> El límite empieza más bajo en los espacios de trabajo nuevos: 10 emails por formulario por hora durante los primeros 7 días. Si estás probando con un script, cuenta con recibir un resumen antes de lo que pasaría más adelante.

## Elige tu caso

La configuración de arriba es la misma en todas partes. Estas páginas cubren una bandeja concreta, un stack concreto o la herramienta que estás dejando.

- [Sin PHP](https://ikiform.com/es/guides/send-form-to-email/without-php.md): Sustituye un script contact.php que llama a mail() por un formulario que envía a un endpoint alojado, conservando tus nombres de campo, tu página de agradecimiento y las respuestas.
- [A Gmail](https://ikiform.com/es/guides/send-form-to-email/gmail.md): Recibe los envíos de formularios en Gmail sin enlaces mailto:, contraseñas de aplicación ni Apps Script; luego etiquétalos, mantenlos fuera del spam y responde desde Gmail.
- [Varios destinatarios](https://ikiform.com/es/guides/send-form-to-email/multiple-recipients.md): Envía un formulario por email a todo tu equipo, o dirige cada mensaje a la persona adecuada según el tema, sin dejar que el propio formulario elija quién recibe el correo.
- [Responde desde tu bandeja](https://ikiform.com/es/guides/send-form-to-email/reply-to.md): Haz que Reply en una notificación de formulario responda a quien lo rellenó: qué nombres de campo funcionan, cuándo usar _replyto y por qué se pierden las respuestas.
- [Alternativa a EmailJS](https://ikiform.com/es/guides/send-form-to-email/emailjs-alternative.md): Saca un formulario de contacto de EmailJS: por qué las claves de email en el navegador son un riesgo, qué cambia en tu código y qué pierdes, como la respuesta automática.
- [Next.js](https://ikiform.com/es/guides/send-form-to-email/nextjs.md): Dos formas de enviar por email un formulario de Next.js con una Server Action: enviarlo tú con Resend o Nodemailer, o reenviarlo a un backend de formularios, comparadas con honestidad.

## Comprueba que funciona

Repasa esto antes de publicar el formulario. 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 no caducan. Consulta los [precios](https://ikiform.com/es/pricing).

1. En **Allowed domains** del formulario, añade el dominio de tu sitio, más `localhost` mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier lugar.
2. En los ajustes de **Notifications** del formulario, comprueba que **Email new submissions** está activado y que están marcadas las personas correctas.
3. Rellena el formulario en tu página y envíalo. Llegas a tu página de agradecimiento, o a la alojada de Ikiform.
4. Abre **Submissions** en el panel. El nuevo envío está ahí con todos los campos que mandaste.
5. Abre el email de notificación. El asunto es `New submission:` y el nombre del formulario, y aparecen todos los campos que enviaste.
6. Pulsa **Reply**. La línea **To** muestra la dirección que escribiste en el formulario, no la de Ikiform.
7. 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 ningún 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 |
| --- | --- |
| El envío está en el panel, pero no llegó ningún email | Mira primero en tu carpeta de spam o de promociones, y añade `notifications@ikiform.com` a tus contactos. Después revisa los ajustes de **Notifications** del formulario: puede que el email esté desactivado, que una dirección externa siga en **Waiting for confirmation** o que muestre **Emails bounced**. |
| 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. |
| **Reply** va a notifications@ikiform.com | No se encontró ningún reply-to. Llama al campo del email `email`, `email_address`, `your_email`, `reply_to` o `contact_email`, o añade un campo `_replyto` con la dirección del visitante. |
| Los emails se detuvieron y llegó un resumen en su lugar | El formulario superó los 50 emails en una hora (10 en un espacio de trabajo nuevo). Todo está en el panel. Busca una ráfaga de bots en **Spam**, o usa un webhook para los formularios con mucho tráfico. |
| `_subject` no cambia el asunto | `_subject` no cambia el asunto de los emails de notificación. Se pasa a los webhooks y a la API como `meta.subject`. |
| `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, más `localhost` para las pruebas locales. Los subdominios de un dominio de la lista están permitidos. |
| `422 validation_failed` | El envío no coincide con la definición de campos del formulario. `error.fields` indica cada campo y el motivo. Haz que el `name` del input coincida con el id del campo. |
| `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

### ¿Puede un formulario HTML enviar emails sin servidor?

Por sí solo, no. Un formulario solo hace una petición HTTP, y enviar correo requiere un servidor que hable con un servidor de correo. Un backend de formularios es ese servidor: apunta el action del formulario a él y te enviará los emails, así tu sitio puede seguir siendo estático.

### ¿Por qué no usar mailto: en el action del formulario?

Un action mailto: no envía nada. Le pide al ordenador del visitante que abra una app de correo con un borrador, lo que falla para quien no tenga una configurada, y además el visitante tiene que pulsar Enviar. Mucha gente abandona ahí, y tú nunca sabrás que lo intentó.

### ¿Puedo cambiar el asunto del email de notificación?

No. El asunto es siempre "New submission:" seguido del nombre del formulario, así que ponle a cada formulario un nombre claro. Un campo _subject se guarda y se pasa a los webhooks y a la API, pero no cambia el email.

### ¿Cuántas personas pueden recibir el email?

Hasta 10 por formulario: los miembros del espacio de trabajo que marques en los ajustes del formulario, más hasta 5 direcciones externas que confirmen mediante un enlace. Cada una recibe su propia copia.

### ¿Puede Ikiform enviar un email a la persona que envió el formulario?

No. Ikiform te envía emails a ti y a tu equipo, no a quien envía el formulario, así que no hay respuesta automática ni email de confirmación. Envía un webhook a tu herramienta de email y manda la confirmación desde ahí.

### ¿Los emails del formulario acabarán en spam?

Se envían desde el propio dominio de Ikiform, así que no necesitas registros SPF ni DKIM para tu dominio. El primero aún puede acabar en spam o en promociones; añade notifications@ikiform.com a tus contactos o crea un filtro para que no pase nunca.

### ¿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 no caducan.

## Más guías

- [Sin PHP](https://ikiform.com/es/guides/send-form-to-email/without-php.md): Sustituye un script contact.php que llama a mail() por un formulario que envía a un endpoint alojado, conservando tus nombres de campo, tu página de agradecimiento y las respuestas.
- [A Gmail](https://ikiform.com/es/guides/send-form-to-email/gmail.md): Recibe los envíos de formularios en Gmail sin enlaces mailto:, contraseñas de aplicación ni Apps Script; luego etiquétalos, mantenlos fuera del spam y responde desde Gmail.
- [Varios destinatarios](https://ikiform.com/es/guides/send-form-to-email/multiple-recipients.md): Envía un formulario por email a todo tu equipo, o dirige cada mensaje a la persona adecuada según el tema, sin dejar que el propio formulario elija quién recibe el correo.
- [Responde desde tu bandeja](https://ikiform.com/es/guides/send-form-to-email/reply-to.md): Haz que Reply en una notificación de formulario responda a quien lo rellenó: qué nombres de campo funcionan, cuándo usar _replyto y por qué se pierden las respuestas.
- [Alternativa a EmailJS](https://ikiform.com/es/guides/send-form-to-email/emailjs-alternative.md): Saca un formulario de contacto de EmailJS: por qué las claves de email en el navegador son un riesgo, qué cambia en tu código y qué pierdes, como la respuesta automática.
- [Next.js](https://ikiform.com/es/guides/send-form-to-email/nextjs.md): Dos formas de enviar por email un formulario de Next.js con una Server Action: enviarlo tú con Resend o Nodemailer, o reenviarlo a un backend de formularios, comparadas con honestidad.
- [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.
- [Más allá de los formularios del creador](https://ikiform.com/es/guides/website-builder-forms.md): Cuando los formularios de Webflow, Netlify, Carrd o Framer chocan con un tope o les falta algo, conserva el formulario que diseñaste y apúntalo a un backend de formularios.
- [Formulario de feedback para un sitio web o una app](https://ikiform.com/es/guides/feedback-form.md): Pon un formulario de feedback en cualquier página o dentro de tu app: una valoración, un comentario, la página de origen, filtro antispam y avisos cuando la nota es baja.
