Guías por framework
Añade un formulario de contacto a un sitio Next.js
No necesitas una ruta de API, una Server Action ni un proveedor de email para recibir formularios en Next.js. Envía el formulario a embedio y lee los envíos en un panel. Esta guía muestra dos versiones: HTML simple que funciona con JavaScript desactivado y un componente cliente que envía sin recargar.
1. Crea el formulario en embedio
Regístrate (el plan Gratis incluye 500 envíos al mes y no pide tarjeta), crea un formulario y copia su clave. Tiene la forma frm_ seguida de diez caracteres. En todo lo que sigue, cambia frm_your_form_key por la tuya.
La clave del formulario está pensada para ser pública: solo permite enviar a ese formulario, nunca leer nada. Puedes ponerla en código del cliente y en tu repositorio.
2. La versión sin JavaScript
Un <form> simple con action funciona tal cual en un Server Component. No se envía nada de JS al navegador, y el propio navegador hace el envío. Después, embedio manda a la persona a la página _redirect de tu sitio.
export default function ContactPage() {
return (
<main>
<h1>Contact</h1>
<form action="https://api.tryembedio.com/f/frm_your_form_key" method="POST">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{/* Honeypot: hidden from people, filled in by bots */}
<input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
{/* Where to send people after a successful submit (same site) */}
<input type="hidden" name="_redirect" value="/contact/thanks" />
<button type="submit">Send</button>
</form>
</main>
);
}Luego añade app/contact/thanks/page.tsx con la confirmación que quieras. _redirect solo acepta páginas del mismo sitio que el formulario; para mandar a la persona a otro lado, configura una redirección en el formulario desde el panel.
3. Enviar sin recargar con fetch
Si prefieres que la persona se quede en la página, usa un componente cliente. Enviar JSON (o la cabecera Accept: application/json) hace que embedio responda con JSON en lugar de una página: { ok, message, redirectUrl } si todo va bien.
"use client";
import { useState, type FormEvent } from "react";
const ENDPOINT = "https://api.tryembedio.com/f/frm_your_form_key";
type Status =
| { state: "idle" | "sending" | "sent" }
| { state: "error"; message: string };
export function ContactForm() {
const [status, setStatus] = useState<Status>({ state: "idle" });
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
setStatus({ state: "sending" });
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
if (response.ok) {
form.reset();
setStatus({ state: "sent" });
return;
}
if (response.status === 429) {
setStatus({ state: "error", message: "Too many attempts. Try again in a minute." });
return;
}
const body = await response.json().catch(() => ({}));
const message =
body.error === "validation_failed"
? body.fields.map((f: { message: string }) => f.message).join(" ")
: "Something went wrong. Please try again.";
setStatus({ state: "error", message });
} catch {
setStatus({ state: "error", message: "Network error. Check your connection and retry." });
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{/* Honeypot: hidden from people, filled in by bots */}
<input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
<button type="submit" disabled={status.state === "sending"}>
{status.state === "sending" ? "Sending…" : "Send"}
</button>
<p role="status" aria-live="polite">
{status.state === "sent" && "Thanks! Your message is on its way."}
{status.state === "error" && status.message}
</p>
</form>
);
}Renderiza <ContactForm /> desde cualquier página; la página puede seguir siendo un Server Component. No hace falta variable de entorno porque la clave no es secreta, aunque NEXT_PUBLIC_EMBEDIO_FORM también sirve si prefieres una por entorno.
¿Envías archivos? Usa FormData como cuerpo en lugar de convertirlo a JSON, llama file al campo del archivo y añade tú la cabecera Accept: application/json. Las subidas necesitan el módulo Uploads y tienen un límite de 10MB por archivo.
Spam y validación
- Honeypot: un campo oculto
_gotcha. Las personas nunca lo ven; los bots que lo rellenan reciben un éxito aparente y no se guarda nada. - Doble clic: un envío idéntico segundos después del primero se descarta en lugar de guardarse dos veces.
- Nivel de confianza: cada formulario tiene un ajuste relajado, equilibrado o estricto para ráfagas y contenido repetido. Los envíos marcados quedan aparte para revisarlos, no se borran.
- Reglas en el servidor: las validaciones como obligatorio, email o longitud máxima se ejecutan en embedio, así que se cumplen aunque alguien se salte tu front end.
Recibir avisos
Cada envío aparece en tu panel al instante. Para recibir además un email por cada uno, añade una conexión de Email al formulario; funciona en todos los planes, incluido Gratis. Slack, Discord, Zapier, webhooks y las demás conexiones vienen con cualquier plan de pago, desde $9/mes.
Errores que puedes ver
Con una petición JSON, cada fallo vuelve como un código de estado más { "error": "..." }. Un envío HTML normal recibe en su lugar una página de error legible.
| Estado | error | Qué significa |
|---|---|---|
| 404 | form_not_found | La clave es incorrecta o el formulario se borró. |
| 422 | validation_failed | Falló una regla de validación; fields lista { field, message } por cada una. |
| 402 | monthly_limit_reached | La cuenta agotó sus envíos del mes. |
| 413 | file_too_large | Un archivo subido supera los 10MB. |
| 400 | invalid_file_type | El tipo de archivo no se acepta (sí se aceptan imágenes, PDF, documentos de Office, TXT y CSV). |
| 429 | Más de 20 envíos por minuto desde una misma IP. Espera y reintenta. |
La lista completa, con los errores propios de cada módulo, está en la referencia de la API.