API de Formularios de Captura
Los formularios de captura se crean desde el panel (CRM → Formularios) y sirven para juntar leads desde tu sitio: cada envío crea o actualiza un contacto en tu CRM y, si el formulario lo tiene configurado, abre una oportunidad en el pipeline y etapa que definiste.
Un formulario se identifica por su slug (lo ves en el panel al crearlo, por ejemplo contacto-landing-verano-a1b2c3). Hay tres formas de usarlo:
- Página hosteada: un link directo (
/f/{slug}) que abre el formulario con el estilo de tu marca. - iframe: insertás un
<iframe>que carga esa misma página — cero configuración. - Script nativo: un
<script>que renderiza el formulario dentro de tu propio HTML.
Esta página documenta la API pública que hay detrás de las tres (para consumirla directamente si estás construyendo tu propia integración) y los dos snippets de embed. Si solo querés crear y compartir un formulario desde el panel, sin tocar código, mirá primero Formularios de captura.
Autenticación y seguridad
Los dos endpoints de abajo son públicos: no requieren API key ni cookies de sesión. La organización dueña del formulario se resuelve en el servidor a partir del slug — nunca se envía ni se expone un orgId.
- CORS abierto: podés llamarlos desde JavaScript en el navegador, desde cualquier origen (
Access-Control-Allow-Origin: *), sin credenciales. - Honeypot: el envío incluye un campo señuelo (
_hp) pensado para bots — ver detalle abajo. - Rate limit por IP +
slugen ambos endpoints (ver tabla). - Consentimiento obligatorio: todo envío exige un
consentVersionvigente.
Endpoints
GET /api/public/forms/{slug}
Devuelve la configuración pública del formulario: qué campos mostrar, textos, y la marca asociada. Es lo que consume la página hosteada y el script de embed para dibujar el formulario.
curl https://emmia.io/api/public/forms/contacto-landing-verano-a1b2c3Respuesta 200:
{
"success": true,
"data": {
"slug": "contacto-landing-verano-a1b2c3",
"title": "Contacto — Landing Verano",
"fields": {
"name": { "enabled": true, "required": true },
"email": { "enabled": true, "required": true },
"phone": { "enabled": true, "required": false },
"company": { "enabled": false, "required": false },
"message": { "enabled": true, "required": false }
},
"customFields": [
{ "key": "rubro", "entity": "contact", "required": false, "label": "Rubro", "type": "select", "options": ["Retail", "Servicios"] }
],
"consentText": "Acepto ser contactado y el tratamiento de mis datos.",
"consentVersion": "form-consent-v1",
"successMessage": "¡Gracias! Te contactaremos pronto.",
"redirectUrl": null,
"branding": {
"name": "Mi Marca",
"primaryColor": "#6C6BFF",
"backgroundColor": "#ffffff",
"textColor": "#111827",
"fontFamily": null,
"logoUrl": "https://emmia.io/api/brands/.../logo?slot=light"
}
}
}fields: los cinco campos built-in (name,email,phone,company,message), cada uno conenabled(si se muestra) yrequired(si es obligatorio).customFields: campos personalizados del CRM que el operador sumó al formulario, ya resueltos con sulabel/type/options— un campo cuya definición fue archivada o borrada simplemente no aparece acá.branding:nullsi el formulario no tiene un agente/marca asociada o no se pudo resolver.- Nunca incluye
orgIdni ningún otro identificador interno de tu organización. 404(FORM_NOT_FOUND) si elslugno existe o el formulario está desactivado.- Rate limit: 60 peticiones/minuto por IP +
slug.
POST /api/public/forms/{slug}/submit
Envía un lead. Body en JSON:
{
"consentVersion": "form-consent-v1",
"name": "Ana Pérez",
"email": "ana@example.com",
"phone": "+56912345678",
"company": "Acme",
"message": "Quiero cotizar",
"custom": { "rubro": "Retail" },
"metadata": { "utm_source": "google", "utm_medium": "cpc", "utm_campaign": "verano" },
"_hp": ""
}consentVersiones obligatorio y debe coincidir exactamente con elconsentVersionque devolvió elGET— si cambiaste el texto de consentimiento del formulario y eso hizo cambiar la versión, un cliente con la versión vieja en caché recibeCONSENT_REQUIRED.- Necesitás al menos
emailophone— es la llave con la que el CRM identifica al contacto (evita duplicados). - Cualquier campo built-in o personalizado marcado como
requireden la config debe venir con valor, o el envío falla conFIELD_REQUIRED. custom: un objeto{ key: valor }con los campos personalizados que el formulario tenga configurados.metadata.utm_*: solo se guardan si el envío traeconsentVersion(evita registrar tracking sin consentimiento, por Ley 21.719). Claves soportadas:utm_source,utm_medium,utm_campaign,utm_term,utm_content._hp(honeypot): un campo oculto que un visitante humano deja vacío. Dejalo siempre vacío o no lo envíes. Si llega con algún valor, el servidor asume que es un bot: responde200con un mensaje genérico y no procesa nada (no crea contacto, no guarda el envío) — no vas a recibir ningún error que te avise de esto, es intencional.
Respuesta 200:
{ "success": true, "data": { "ok": true, "successMessage": "¡Gracias! Te contactaremos pronto.", "redirectUrl": null } }Si redirectUrl no es null, es la URL (siempre http:// o https://, ya validada por el panel) a la que redirigir al visitante tras el envío exitoso.
Efecto del envío: crea o actualiza el contacto (por email o teléfono) con source: "form", agrega el message como nota del contacto si el campo está habilitado, y —si el formulario tiene "Crear oportunidad al enviar" activo— crea una oportunidad en el pipeline/etapa configurados (o el pipeline default/primera etapa si no se especificó ninguno).
Errores
code | HTTP | Cuándo pasa |
|---|---|---|
FORM_NOT_FOUND | 404 | El slug no existe o el formulario está inactivo. |
CONSENT_REQUIRED | 400 | Falta consentVersion o no coincide con la del formulario. |
IDENTITY_REQUIRED | 400 | No vino ni email ni phone. |
FIELD_REQUIRED | 400 | Falta un campo (built-in o personalizado) marcado como obligatorio. |
RATE_LIMIT_EXCEEDED | 429 | Superaste el límite de envíos. |
Nota: si el formulario apunta a un pipeline o etapa que ya no existe (por ejemplo, alguien los borró después de crear el formulario), el envío puede fallar con otro código 4xx propio del CRM — es un error de configuración del formulario, no algo que puedas corregir desde tu integración.
- Rate limit: 10 envíos/minuto por IP +
slug.
Ejemplo: fetch en JavaScript
async function submitLead(slug, consentVersion) {
const res = await fetch(`https://emmia.io/api/public/forms/${slug}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
consentVersion,
name: 'Ana Pérez',
email: 'ana@example.com',
custom: { rubro: 'Retail' },
_hp: '', // honeypot: siempre vacío
}),
});
const json = await res.json();
if (!res.ok || !json.success) throw new Error(json.error || 'No se pudo enviar.');
return json.data; // { ok, successMessage, redirectUrl }
}Opciones de embed
El botón Embed de cada formulario (en el panel) te da los tres.
iframe (recomendado)
Cero configuración: es una página completa dentro de un marco, aislada del CSS y del CSP de tu sitio.
<iframe
src="https://emmia.io/f/contacto-landing-verano-a1b2c3?embed=1"
style="width:100%;max-width:520px;border:0;min-height:520px"
loading="lazy"
title="Contacto — Landing Verano">
</iframe>Script nativo
Se integra al diseño de tu sitio: renderiza el formulario dentro de un Shadow DOM (opens in a new tab) (aislado del CSS de tu página, pero sin el marco visible de un iframe). Requiere que tu sitio permita cargar un script externo — si tu CSP lo bloquea, usá el iframe.
<div data-emmia-form="contacto-landing-verano-a1b2c3"></div>
<script src="https://emmia.io/form-embed.js" async></script>Podés repetir el <div data-emmia-form="..."> varias veces en la misma página (con el mismo slug o con slugs distintos) — un único <script> alcanza para todos.
Link directo
https://emmia.io/f/{slug} — la misma página hosteada del iframe, para compartir como un link suelto (por ejemplo, en un email o un mensaje) sin insertarlo en ningún sitio.
Gestionar formularios
Crear, editar, activar/desactivar, borrar formularios y ver los leads recibidos se hace desde el panel — CRM → Formularios (ver Formularios de captura). Esta API pública es solo para consumir un formulario ya creado (leer su configuración o enviar un lead); no hay un endpoint público para administrarlos.