Referencia de la API v1
Referencia de integración REST + Webhooks (versión 2026-06-15)
Guía rápida de integración de CRM
La vía más rápida para conectar un CRM a Heilo. La referencia completa de la API está más abajo.
Integre con un enfoque webhook-first: el evento call.completed es la fuente de datos principal (lleva el enlace de la grabación y la transcripción procesada). REST /calls es un complemento: introspección y lectura de metadatos seleccionados.
- Exponga un endpoint de webhook en su CRM o middleware (sin código: use Zapier/Make; vea la guía más abajo).
- Añada una suscripción en Heilo (Ajustes → Integraciones) para call.completed (opcionalmente también call.outbound.attempted).
- Reciba call.completed y verifique la firma (cabecera Heilo-Signature, HMAC; vea más abajo).
- Deduplique por event_id (cabecera heilo-event-id); data.call_id agrupa los eventos de la misma llamada.
- Mapee los campos a su CRM: busque/cree un contacto por teléfono, cree un lead/negocio y adjunte una actividad/nota (resumen + enlace de la grabación).
¿Sin código? La guía de conexión sin código (Zapier/Make) lo explica paso a paso.
Autenticación
La API pública usa tokens Bearer. Genere una clave API desde la tarjeta «Claves API» de la página de Integraciones y envíela en la cabecera:
Authorization: Bearer hk_live_AbC1MnPq...
Heilo tiene tres modos de autenticación:
- Bearer (claves API hk_live_…): para la API pública. Sin CSRF, sin cookies.
- Cookies de sesión: para la aplicación web (heilo.io). NO las use para la API pública.
- HMAC-SHA256: para los Webhooks que Heilo envía a SU endpoint (usted verifica la cabecera de firma).
| Permiso (scope) | Significado |
|---|---|
| read.calls | Lectura de llamadas: GET /api/v1/calls, GET /api/v1/calls/:id |
| write.calls, read.contacts, write.contacts, manage.webhooks, manage.api_keys | Reservados para los endpoints de API planificados; no los marque por adelantado. |
El campo environment en la respuesta de /me tiene hoy siempre el valor live. Las claves de prueba están previstas.
URL base
Todos los endpoints públicos están bajo /api/v1/. Producción:
https://www.heilo.io/api/v1
Endpoints
Cada endpoint de /api/v1 requiere Bearer. El /me siguiente sirve para la introspección de la clave; la lectura de llamadas está en la sección «Llamadas». Al integrar un CRM, trate los webhooks como la fuente de datos principal: REST sirve para la introspección y para leer metadatos seleccionados.
/api/v1/meIntrospección de la clave API: devuelve el id de la clave, user_id, scopes y el límite de tasa. Útil para la «prueba de conexión» de Zapier/Make.
Solicitud
curl https://www.heilo.io/api/v1/me \ -H "Authorization: Bearer hk_live_AbC1MnPq..."
Respuesta
{
"success": true,
"data": {
"api_key_id": "a1b2c3d4-5e6f-7081-92a3-b4c5d6e7f809",
"user_id": "5f4e3d2c-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
"organization_id": "7a8b9c0d-1e2f-4a3b-9c8d-7e6f5a4b3c2d",
"scopes": ["read.calls", "manage.webhooks"],
"rate_limit_per_hour": 1000,
"environment": "live"
},
"meta": { "timestamp": "2026-06-03T12:34:56Z" }
}Caso de uso: prueba de conexión de Zapier durante la configuración de una integración personalizada. Una respuesta 200 demuestra que la clave y la red funcionan.
Llamadas
Endpoints de lectura para llamadas. Requieren el scope read.calls.
/api/v1/callsLista las llamadas de la organización. Paginación (page/limit≤100), filtros: direction, status, rango de fechas (dateFrom/dateTo). Devuelve has_more.
Parámetros de consulta
| Parámetro | Tipo | Valores |
|---|---|---|
| page | int | desde 1 (por defecto 1) |
| limit | int | 1–100 (por defecto 20) |
| direction | enum | inbound | outbound |
| status | enum | new | to_call | contacted | qualified |
| dateFrom / dateTo | string | fecha YYYY-MM-DD o ISO 8601, p. ej. 2026-06-03T12:34:56Z |
Solicitud
curl "https://www.heilo.io/api/v1/calls?limit=20&direction=inbound" \ -H "Authorization: Bearer hk_live_AbC1MnPq..."
Respuesta
{
"success": true,
"data": {
"items": [
{
"call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"direction": "inbound",
"caller_phone": "+48600100200",
"customer_phone_e164": "+48600100200",
"caller_name": "Jan Kowalski",
"duration": 87,
"crm_status": "new",
"review_status": null,
"outbound_lifecycle": null,
"transcript_processed": { "caller_name": "Jan Kowalski", "summary": "...", "service_needed": "..." },
"created_at": "2026-06-03T12:34:56Z"
}
],
"has_more": false,
"page": 1,
"limit": 20
},
"meta": { "timestamp": "2026-06-03T12:34:56Z" }
}/api/v1/calls/:idObtiene una sola llamada por id. 404 si no pertenece a la organización de la clave o si se eliminó.
Solicitud
curl https://www.heilo.io/api/v1/calls/<id> \ -H "Authorization: Bearer hk_live_AbC1MnPq..."
Los archivos de grabación NO se exponen a través de la API: se borran según la retención legal (RGPD). La API devuelve los metadatos de la llamada y el texto de la transcripción.
Límites de tasa
Límites por hora por clave y por usuario (suma de todas las claves). Se reinician en la hora UTC. Cada solicitud cuenta, independientemente del estado de la respuesta.
Por clave
1000 req/h
Por cuenta (suma de las claves)
5000 req/h
Cuando se supera, devolvemos 429 con la cabecera Retry-After (segundos hasta el reinicio):
HTTP/1.1 429 Too Many Requests Retry-After: 1842 X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 2026-06-03T13:00:00Z
Errores
Todos los errores devuelven una estructura JSON uniforme con error.code (estable) y error.message (legible por humanos, puede cambiar). Registre el code, no el message.
{
"success": false,
"error": { "code": "RATE_LIMITED", "message": "Per-key rate limit 1000/h exceeded" },
"meta": { "timestamp": "2026-06-03T12:34:56Z" }
}| HTTP | code | Significado |
|---|---|---|
| 400 | BAD_REQUEST | Parámetros de consulta o de cuerpo mal formados (validación genérica) |
| 401 | UNAUTHORIZED | Token Bearer ausente o no válido |
| 402 | SUBSCRIPTION_INACTIVE | Suscripción inactiva: renueve la facturación para reactivar la clave |
| 403 | FORBIDDEN | La clave no tiene el scope requerido |
| 404 | NOT_FOUND | Recurso no encontrado (o pertenece a otro usuario) |
| 422 | VALIDATION_ERROR | Una regla de negocio rechazó la solicitud (p. ej. número de teléfono no válido, cuota) |
| 429 | RATE_LIMITED | Límite por hora superado (consulte Retry-After) |
| 500 | DATABASE_ERROR | Error de servidor / base de datos: es seguro reintentar con backoff |
| 503 | MAINTENANCE | API pública desactivada temporalmente (kill switch) |
El código SUBSCRIPTION_INACTIVE aparece en dos situaciones: HTTP 402, la suscripción de Heilo ha caducado (pago), y HTTP 409, la suscripción del webhook está pausada (p. ej., con la acción de prueba); en ese caso, haga clic primero en «Volver a verificar».
Webhooks salientes
Heilo envía un POST con JSON a su endpoint tras cada evento. Las suscripciones se crean desde la tarjeta «Suscripciones de Webhook»: requiere un handshake en la activación. Cada solicitud incluye una firma HMAC que debe verificar:
POST <your URL>
content-type: application/json
heilo-signature: t=1717423396,v1=4f3a...
heilo-event-id: 1bf3a5e2-...
heilo-event-type: call.completed
{
"api_version": "2026-06-15",
"event_id": "1bf3a5e2-...",
"event_type": "call.completed",
"resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2026-06-03T12:34:56Z",
"data": { /* see the setup guide for the full schema */ }
}Política de reintentos y pausa:
- Los errores transitorios (HTTP 408/429/5xx, red) se reintentan con backoff exponencial (2 min, 5 min, 30 min, 2 h) y luego pasan a dead-letter tras 5 intentos.
- Los errores permanentes (HTTP 401/403/422) pausan la suscripción de inmediato, sin reintentos.
- 50 fallos transitorios consecutivos, o 2 HTTP 410 Gone consecutivos (p. ej. un escenario de Make eliminado), también pausan la suscripción.
- Reanude una suscripción pausada con «Volver a verificar»: un nuevo handshake reactiva la cola.
Si una suscripción se pausa automáticamente, enviamos un correo a la dirección del propietario de la cuenta. Puede reenviar los eventos en dead-letter con el botón «Reenviar» del Registro de entregas, una vez que la suscripción supere la nueva verificación.
Modos de verificación
Heilo admite dos modelos de activación para las suscripciones de webhook. El predeterminado (permisivo) encaja con Zapier / Make / los endpoints típicos de CRM. El estricto coincide con el modelo de la Slack Events API: útil para servidores personalizados que pueden devolver el challenge de forma síncrona.
Modo permissive (predeterminado)
Heilo envía un POST de webhook.subscription.verify a su URL. Cualquier respuesta 2xx activa la suscripción. El cuerpo de la respuesta se ignora. Esto coincide con el modelo de Stripe / GitHub / Twilio.
Modo strict (opcional)
Heilo envía un POST de webhook.subscription.verify con un campo challenge. Su endpoint DEBE responder con 2xx y un cuerpo JSON:
{"challenge":"<echo of the challenge field from Heilo's POST>"}Usted elige el modo al crear la suscripción (verification_mode en POST /api/v1/webhook-subscriptions, permisivo por defecto). Cambiar el modo tras la creación requiere eliminar y volver a crear: es intencionado, ya que cambia la semántica del contrato.
Límites: máximo 20 suscripciones activas por cuenta (configurable por variable de entorno). Una suscripción se pausa automáticamente tras 50 fallos transitorios consecutivos o 2 HTTP 410 consecutivos, e inmediatamente con HTTP 401/403/422.
Verificación HMAC (signing_secret)
Formato compatible con Stripe. signed_string = "<unix_ts>.<raw_body>" (separados por un punto). Verifique siempre el cuerpo RAW de la solicitud (antes de analizar el JSON): cualquier normalización cambia la firma.
signed_string = "<unix_timestamp>.<raw_request_body>" signature = HMAC-SHA256(signing_secret, signed_string).hex() header = "t=<unix_timestamp>,v1=<signature>"
import { createHmac, timingSafeEqual } from 'crypto';
// rawBody MUST be the exact received bytes — do NOT re-serialize the JSON.
function verifyHeiloSignature(rawBody, header, signingSecret, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=').map((s) => s.trim()))
);
const t = Number(parts.t);
const sig = parts.v1;
if (!Number.isFinite(t) || !sig) return false;
// Asymmetric tolerance: reject old replays; allow only small clock skew ahead.
const now = Math.floor(Date.now() / 1000);
if (now - t > toleranceSec || t - now > 60) return false;
const expected = createHmac('sha256', signingSecret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(sig, 'utf8');
// timingSafeEqual THROWS on length mismatch — length-check first.
return a.length === b.length && timingSafeEqual(a, b);
}La tolerancia predeterminada de 300 s (5 min) evita los ataques de repetición. Los relojes del servidor deben estar sincronizados por NTP.
El secreto de firma se muestra solo una vez, al crear la suscripción. Si lo pierde o sospecha una filtración: elimine la suscripción y créela de nuevo con la misma URL (la dirección en Zapier/Make no cambia). La rotación del secreto sin eliminar está prevista.
Tipos de evento
Elija los event_types al crear una suscripción. Cada evento tiene un event_id único (UUID v5) y se deduplica por suscripción.
El primer evento que recibirá es webhook.test, un mensaje de prueba con data._test = true. Úselo para mapear los campos o fíltrelo.
| event_type | Descripción |
|---|---|
| call.completed | Llamada completada, transcripción lista |
| call.outbound.attempted | Marcación saliente intentada (antes de conectar) |
| call.recording.ready | Archivo de grabación disponible para descargar |
| call.transcribed | Transcripción lista (separada de call.completed) |
| call.failed | Llamada fallida (ocupado/sin respuesta/error) |
| call.outbound.lifecycle_repaired | Corrección del ciclo de vida saliente: estado de la llamada reparado |
| call.deletion_scheduled | Llamada programada para eliminación (RGPD art. 17, retención) |
| call.recording.deleted | Grabación eliminada (RGPD) |
| contact.created | Nuevo contacto creado |
| contact.updated | Contacto actualizado |
A continuación, el objeto data de cada evento. Los nombres de campo, tipos y valores enumerados forman parte del contrato de la API y no cambian de significado dentro de v1; con el tiempo pueden añadirse nuevos campos opcionales.
call.completed
Se envía tras procesar la grabación y la transcripción de una llamada completada. La fuente de datos principal para un CRM: payload completo (incluidos recording_url y la transcripción procesada).
{
"api_version": "2026-06-15",
"event_id": "1bf3a5e2-...", // same value as heilo-event-id header
"event_type": "call.completed",
"resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created_at": "2026-06-03T12:34:56Z",
"data": {
"call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"direction": "inbound",
"caller_phone": "+48600100200",
"customer_phone_e164": "+48600100200",
"duration": 87,
"recording_url": "https://www.heilo.io/api/v1/calls/.../recording.mp3?token=...&exp=...",
"transcript_processed": { "caller_name": "Jan Kowalski", "summary": "...", "service_needed": "..." },
"transcript_original": "..."
}
}| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| direction | 'inbound' | 'outbound' | Dirección de la llamada. |
| caller_phone | string | Número de teléfono del interlocutor, tal como está guardado en la llamada. |
| customer_phone_e164 | string | El mismo número de nuevo, un alias para facilitar el mapeo de campos. |
| duration | number | Duración de la grabación en segundos. |
| recording_url | string | null | Enlace estable a la grabación; null cuando la grabación aún no estaba guardada al procesar. |
| transcript_processed | object | Análisis procesado de la llamada; consulte la referencia de campos de transcript_processed en esta sección. |
| transcript_original | string | null | Transcripción literal sin procesar; null cuando no está disponible. |
La entrega es al menos una vez y no se garantiza el orden: persista de forma idempotente. Clave de deduplicación: event_id. data.call_id vincula los eventos de la misma llamada (p. ej. call.completed después de call.recording.ready).
transcript_processed: referencia de campos
El subconjunto estable en el que puede confiar al mapear a un CRM. Todos los campos son opcionales: valen null cuando la llamada no contenía esa información.
| Campo | Tipo | Descripción |
|---|---|---|
| caller_name | string | null | Nombre del llamante, si lo dio. |
| summary | string | null | Resumen breve de la llamada. |
| subject | string | null | Título de una línea de la llamada (hasta 80 caracteres). |
| service_needed | string | null | Qué pidió el llamante. |
| services_match | boolean | null | Si la petición encaja con los servicios que usted ofrece. |
| lead_score | number | null (1–10) | Estimación de la calidad del lead, de 1 a 10. |
| preferred_date | string | null | Fecha u hora mencionada por el llamante, si la hubo. |
| client_city | string | null | Ciudad, si se mencionó. |
| client_address | string | null | Dirección, si se mencionó. |
| additional_details | string | null | Contexto adicional de la llamada. |
Campos que pueden aparecer
| Campo | Tipo | Descripción |
|---|---|---|
| caller_location | string | null | Referencia geográfica detectada en la conversación. |
| client_country | string | null | País, si se mencionó. |
| counterparty_name | string | null | Nombre de la otra parte; solo llamadas salientes y modo conversación. |
| detected_language | string | Código de idioma de la conversación (p. ej. pl); la clave puede faltar por completo. |
| proposal_items | object[] | null | Acciones y decisiones sugeridas extraídas de la llamada. |
El análisis puede incluir campos adicionales: trate los campos desconocidos como opcionales y no dé por hecha su presencia.
call.outbound.attempted
Se envía cuando un intento de llamada saliente alcanza un estado final, incluidos los fallos. completed significa que la llamada se estableció y terminó con normalidad; la grabación y la transcripción llegan como eventos separados.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| agent_user_id | string (uuid) | Identificador del usuario de Heilo que realizó la llamada. |
| customer_phone | string | Número del cliente marcado. |
| customer_phone_e164 | string | El mismo número de nuevo, un alias para facilitar el mapeo de campos. |
| outbound_lifecycle | 'completed' | 'agent_no_answer' | 'customer_no_answer' | 'failed_to_initiate' | Estado final que desencadenó el evento; completed significa que la llamada se estableció y terminó con normalidad. |
| duration | number | null | Duración de la llamada en segundos; null cuando la llamada falló al iniciarse o la duración aún no se conoce. |
| attempted_at | string (ISO 8601) | Momento de emisión del evento (ISO 8601). |
| has_recording | boolean | true solo cuando outbound_lifecycle es completed; la grabación llega entonces como call.recording.ready. |
call.recording.ready
Se envía cuando el archivo de la grabación está disponible. Úselo para obtener o archivar el audio.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| recording_url | string | null | Enlace estable a la grabación; en casos raros null, cuando no se pudo generar el enlace. |
| duration | number | null | Duración en segundos; null cuando aún no se conoce. |
call.transcribed
Se envía en el mismo procesamiento que call.completed: contiene solo la transcripción, sin metadatos de la llamada ni enlace a la grabación.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| transcript_original | string | null | Transcripción literal sin procesar; null cuando no está disponible. |
| transcript_processed | object | Análisis procesado de la llamada; consulte la referencia de campos de transcript_processed en esta sección. |
call.failed
Se envía cuando una llamada saliente no se completó (sin respuesta, ocupado, error al iniciar). Solo afecta a llamadas salientes. Normalmente no conviene crear un lead; registre un intento de contacto.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| outbound_lifecycle | 'agent_no_answer' | 'customer_no_answer' | 'failed_to_initiate' | Qué fase de la llamada saliente falló. |
| failure_reason | string | null | Código técnico del motivo (p. ej. customer_busy, agent_no_confirmation); puede ser null. |
call.outbound.lifecycle_repaired
Se envía cuando Heilo corrige retroactivamente el estado de una llamada saliente (una confirmación tardía del operador demostró que la llamada sí se estableció). Actualice el estado de la llamada en su sistema.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| previous_lifecycle | 'agent_only' | Estado antes de la corrección; actualmente siempre agent_only. |
| new_lifecycle | 'completed' | Estado tras la corrección; actualmente siempre completed. |
| repaired_at | string (ISO 8601) | Momento de la corrección (ISO 8601). |
call.deletion_scheduled
RGPD art. 17: la llamada está programada para su eliminación. Su CRM debería dejar de usar la grabación y prepararse para eliminar los datos.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| pending_deletion_at | string (ISO 8601) | Cuándo se eliminarán definitivamente los datos (ISO 8601). |
| reason | 'consent_not_asked' | 'consent_withdrawn' | 'retention_expired' | 'user_erasure' | Motivo de la eliminación programada. |
call.recording.deleted
RGPD: la grabación se eliminó; recording_url devuelve 410. Elimine o desactive el enlace de la grabación por su parte.
| Campo | Tipo | Descripción |
|---|---|---|
| call_id | string (uuid) | Identificador de la llamada en Heilo, el mismo valor que resource_id; vincula todos los eventos de una llamada. |
| reason | string | null | Motivo registrado al programar la eliminación; puede ser null. |
| recording_sid | string | null | Identificador de la grabación en Twilio; null cuando no se pudo determinar. |
| deletion_kind | 'hard_deleted' | 'twilio_404' | hard_deleted = eliminada por Heilo; twilio_404 = el archivo ya no existía en Twilio. |
| deleted_at | string (ISO 8601) | Momento de la eliminación de la grabación (ISO 8601). |
contact.created
Se envía al crear un contacto nuevo en Heilo. data.contact es una instantánea completa del contacto nuevo; no incluye notas ni etiquetas.
| Campo | Tipo | Descripción |
|---|---|---|
| contact | object | Instantánea completa del contacto nuevo (campos abajo). |
| contact.id | string (uuid) | Identificador del contacto en Heilo. |
| contact.phone | string | Número de teléfono del contacto. |
| contact.first_name | string | null | null si no se indicó. |
| contact.last_name | string | null | null si no se indicó. |
| contact.email | string | null | null si no se indicó. |
| contact.company | string | null | null si no se indicó. |
contact.updated
Se envía al editar un contacto. A diferencia de contact.created, no es una instantánea: data.diff contiene solo los campos modificados.
| Campo | Tipo | Descripción |
|---|---|---|
| contact_id | string (uuid) | Identificador del contacto actualizado. |
| diff | object (partial) | Solo los campos modificados; las claves ausentes de diff no cambiaron. |
Claves posibles en diff: first_name, last_name, email, phone, company, notes, tags
webhook.test
El primer mensaje tras crear una suscripción y en cada prueba manual. resource_id es el identificador de la suscripción (no de una llamada); el resto de campos de data refleja el ejemplo de call.completed con valores de muestra.
| Campo | Tipo | Descripción |
|---|---|---|
| _test | true | Siempre true; distingue el mensaje de prueba de los eventos reales. |
| _message | string | Aviso legible de que se trata de una prueba. |
| _sent_at | string (ISO 8601) | Momento del envío de la prueba (ISO 8601). |
Versionado y roadmap
Versionado
La API de Heilo usa una versión con fecha. Solo los cambios incompatibles incrementan la versión principal (v1 → v2). Añadir campos o endpoints no rompe la compatibilidad.
Versión actual
v1 · 2026-06-15
La fecha es el identificador de versión de la API (con fecha), no la fecha de hoy.
Status
Beta
Compatible con versiones anteriores: nuevos campos de respuesta, nuevos event_types, nuevos endpoints. Cambio incompatible = nueva versión principal (v2). La versión antigua se mantiene un mínimo de 12 meses tras el anuncio de v2.
Roadmap (v1.1+)
Endpoints previstos para próximas versiones v1.X. No es un compromiso firme: la dirección depende de sus comentarios.
- GET /contacts — listar contactos
- POST /contacts — crear contacto (sincronizar desde el CRM hacia Heilo)
¿Necesita un endpoint? Escriba a support@heilo.io con su caso de uso: priorizamos el roadmap según la demanda real.
Gestione claves y webhooks en el panel
Tras iniciar sesión podrá generar claves API, añadir suscripciones de webhooks y consultar el historial de envíos.