# Una alternativa a EmailJS que mantiene las claves fuera del cliente

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

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

EmailJS envía emails desde el navegador, así que tu service id, tu template id y tu clave pública van en el JavaScript de tu página para que cualquiera los copie. Un backend de formularios no necesita ninguna clave en el navegador: el formulario envía a una URL y el backend filtra el spam, guarda el mensaje y te manda un email. Pierdes la respuesta automática de EmailJS al visitante, que enviarías desde tu propia herramienta de email.

## Qué cambia aquí

EmailJS es popular por una buena razón: envía emails reales desde un sitio estático sin servidor. Para ello, el navegador llama directamente a EmailJS con tres valores: un service id, un template id y una clave pública. Están en tu bundle, legibles en el código fuente de la página o en la pestaña de red.

Las propias preguntas frecuentes de EmailJS dicen que quien los copie solo puede enviar tus plantillas con tu contenido, no emails arbitrarios. Eso vale para el texto fijo de la plantilla. Pero quien llama rellena las variables de la plantilla, así que las claves copiadas pueden enviar tu plantilla con cualquier nombre, mensaje y enlaces, tantas veces como permita tu cuota. Y si el **To** de una plantilla es una variable, como en una respuesta automática a la dirección del visitante, quien llama también elige el destinatario.

Diagrama: Quién puede enviar con una clave de email del navegador, y con un endpoint de formulario

1. Con un SDK de email en el navegador, la página contiene todo lo necesario para enviar. Un script en otro sitio puede copiar los ids y la clave y enviar tu plantilla con su propio texto, hasta agotar tu cuota.
2. Si el destinatario de la plantilla sale de una variable, las claves copiadas también eligen quién la recibe.
3. Con un endpoint de formulario, la página solo contiene un id de formulario. Los envíos se filtran contra el spam y tienen límite de frecuencia, y los emails solo van a los destinatarios que configures en el panel.

Un id de formulario tampoco es un secreto. La diferencia es lo que un desconocido puede hacer con él: enviar tu formulario, no enviar tus emails.

EmailJS tiene ajustes que reducen esto, como limitar qué dominios pueden usar tu clave y añadir un captcha a una plantilla. Comprueba cuáles incluye tu plan y actívalos si te quedas. La comparación de abajo trata del diseño, no es un veredicto sobre el servicio:

|   | EmailJS | Ikiform |
| --- | --- | --- |
| Qué contiene el navegador | Service id, template id, clave pública | Un id de formulario |
| Quién decide el destinatario | La plantilla, que puede leer una variable | Solo los ajustes de tu panel |
| Filtro antispam | Captcha por plantilla, si lo añades | Honeypot, puntuación de spam y límites de frecuencia en todos los formularios, más una verificación humana opcional |
| Tus mensajes después | En tu bandeja | En el panel, la exportación CSV y la API, además de en tu bandeja |
| Email al visitante | Sí, con una plantilla de respuesta automática | No |
| Tu propio diseño de email | Sí, plantillas en su editor | No, un diseño fijo con el asunto `New submission: <form name>` |

Si lo importante es la respuesta automática o un email con tu marca, encaja mejor EmailJS, o tu propio servidor con una API de email. Si quieres el mensaje en tu bandeja, de forma segura y guardado en algún sitio, encaja un backend de formularios.

## Migra el formulario

Un formulario típico de EmailJS en React, basado en su tutorial de formulario de contacto:

`ContactForm.tsx, antes`:

```tsx
import emailjs from "@emailjs/browser";
import { type FormEvent, useRef, useState } from "react";

export function ContactForm() {
  const form = useRef<HTMLFormElement>(null);
  const [status, setStatus] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!form.current) return;
    try {
      await emailjs.sendForm("service_abc123", "template_xyz789", form.current, {
        publicKey: "aBcDeFgHiJkLmNoPq",
      });
      setStatus("Message sent.");
    } catch {
      setStatus("Couldn't send your message.");
    }
  }

  return (
    <form ref={form} onSubmit={onSubmit}>
      <input name="user_name" required />
      <input name="user_email" type="email" required />
      <textarea name="message" required />
      <button>Send</button>
      <p role="status">{status}</p>
    </form>
  );
}
```

El mismo formulario enviando a Ikiform con `submit()` de `@ikiform/sdk`:

`ContactForm.tsx, después`:

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

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [fields, setFields] = useState<Record<string, string[]>>({});
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
    setFields({});
    setError("");
    const result = await submit(FORM_ID, new FormData(event.currentTarget));
    if (result.ok) {
      setStatus("sent");
      return;
    }
    setFields(result.error.fields ?? {});
    setError(result.error.code === "network_error" ? "You're offline. Try again." : result.error.message);
    setStatus("idle");
  }

  if (status === "sent") return <p role="status">Message sent. We'll reply by email.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="user_name">Name</label>
      <input id="user_name" name="user_name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error && !Object.keys(fields).length ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
    </form>
  );
}
```

- `@emailjs/browser` y los tres ids desaparecen. Desinstala el paquete y borra la plantilla o el servicio de EmailJS cuando el nuevo formulario funcione, para que las copias en caché de tu página dejen de enviar.
- `user_email` pasa a ser `email`. A EmailJS no le importan los nombres, pero Ikiform usa el campo llamado `email` como reply-to. Los demás nombres pueden quedarse, porque las notificaciones enumeran los campos tal como llegan.
- `submit()` nunca lanza una excepción. Devuelve el objeto de error, así los errores de campo de la definición del formulario aparecen debajo de su input.
- El honeypot es nuevo. EmailJS no tenía; este input oculto atrapa a los bots más simples sin configurar nada.
- Añade tu dominio en **Allowed domains**, para que los envíos desde otros sitios se marquen como spam.

> ¿Usas la verificación humana? Pon el **Spam check** del formulario en **Ikiform**, llama a `challenge(FORM_ID)` antes de `submit` y pasa el token como `turnstileToken`. La [guía de React](https://ikiform.com/es/docs/guides/react.md) lo muestra.

## Si usabas la respuesta automática

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). Un servicio de envío que controles desde un servidor, como Resend o Postmark, puede enviarlo con cada webhook `submission.created`, tomando la dirección del visitante del payload de confianza en lugar del navegador. La [guía de varios destinatarios](https://ikiform.com/es/guides/send-form-to-email/multiple-recipients.md#route) tiene un handler que puedes adaptar.

Pregúntate si lo necesitas. El mensaje de agradecimiento en la página de un formulario de contacto le dice al visitante que funcionó, y una respuesta real tuya una hora después vale más que una plantilla.

## Comprueba que funciona

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. Busca `emailjs` y tu antiguo service id en tu sitio compilado. No debería aparecer nada.
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. Revisa la bandeja de cada destinatario de las notificaciones. Cada uno recibe un email con los campos, y **Reply** va a quien envió el formulario cuando este tiene un campo `email`.
5. 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.

## Si algo sale mal

| Problema | Qué hacer |
| --- | --- |
| Reply sigue yendo a Ikiform | El campo sigue siendo `user_email`. Renómbralo a `email`, o a uno de `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. |
| `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. |
| `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. |
| `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. |

## Preguntas

### ¿Ikiform es más barato que EmailJS?

Depende del volumen y del plan, y ambos cambian con el tiempo, así que compara las páginas de precios actuales. Ikiform no tiene plan gratuito; cobra por envío, con Pro o con paquetes de pago único.

### ¿Es seguro exponer la clave pública de EmailJS?

EmailJS dice que una clave copiada solo puede enviar tus plantillas, no emails personalizados. Aun así, quien llama rellena las variables de la plantilla y, si el destinatario es una variable, también lo elige. Limita la clave a tus dominios y añade un captcha si sigues usándola.

### ¿Ikiform necesita una clave de API en el navegador?

No. Un formulario en el navegador envía a la URL de tu formulario sin ninguna clave. Las claves secretas solo existen para enviar desde tu propio servidor, y van en variables de entorno del lado del servidor.

### ¿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í.

### ¿Puedo conservar mis nombres de campo de EmailJS?

Sí, salvo el del email: llámalo email para que las respuestas a las notificaciones vayan al visitante. Ikiform guarda cualquier nombre de campo que envíes.

## Más guías

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