Desarrolladores / API
API de Formularios

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 + slug en ambos endpoints (ver tabla).
  • Consentimiento obligatorio: todo envío exige un consentVersion vigente.

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

Respuesta 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 con enabled (si se muestra) y required (si es obligatorio).
  • customFields: campos personalizados del CRM que el operador sumó al formulario, ya resueltos con su label/type/options — un campo cuya definición fue archivada o borrada simplemente no aparece acá.
  • branding: null si el formulario no tiene un agente/marca asociada o no se pudo resolver.
  • Nunca incluye orgId ni ningún otro identificador interno de tu organización.
  • 404 (FORM_NOT_FOUND) si el slug no 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": ""
}
  • consentVersion es obligatorio y debe coincidir exactamente con el consentVersion que devolvió el GET — si cambiaste el texto de consentimiento del formulario y eso hizo cambiar la versión, un cliente con la versión vieja en caché recibe CONSENT_REQUIRED.
  • Necesitás al menos email o phone — es la llave con la que el CRM identifica al contacto (evita duplicados).
  • Cualquier campo built-in o personalizado marcado como required en la config debe venir con valor, o el envío falla con FIELD_REQUIRED.
  • custom: un objeto { key: valor } con los campos personalizados que el formulario tenga configurados.
  • metadata.utm_*: solo se guardan si el envío trae consentVersion (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: responde 200 con 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

codeHTTPCuándo pasa
FORM_NOT_FOUND404El slug no existe o el formulario está inactivo.
CONSENT_REQUIRED400Falta consentVersion o no coincide con la del formulario.
IDENTITY_REQUIRED400No vino ni email ni phone.
FIELD_REQUIRED400Falta un campo (built-in o personalizado) marcado como obligatorio.
RATE_LIMIT_EXCEEDED429Superaste 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.