Envía un formulario a tu email6 min de lectura
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.
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.
- 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.
- Si el destinatario de la plantilla sale de una variable, las claves copiadas también eligen quién la recibe.
- 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.
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:
| Item | 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:
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:
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/browsery 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_emailpasa a seremail. A EmailJS no le importan los nombres, pero Ikiform usa el campo llamadoemailcomo 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.
challenge(FORM_ID) antes de submit y pasa el token como turnstileToken. La guía de React 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. 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 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
- En Allowed domains del formulario, añade el dominio de tu sitio, más
localhostmientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier lugar. - Busca
emailjsy tu antiguo service id en tu sitio compilado. No debería aparecer nada. - Rellena el formulario en tu página y envíalo. Llegas a tu página de agradecimiento, o a la alojada de Ikiform.
- 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. - 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
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.
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.
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.
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í.
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.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.