# Referencia de la API v1

> Referencia de integración REST + Webhooks (versión 2026-06-15)

Versión legible por máquinas de la referencia de la API de Heilo, generada a partir de los mismos datos que la página. Versión para personas: https://www.heilo.io/es/docs/api

- **Versión actual**: v1 · 2026-06-15
- **Status**: Beta
- **URL base**: `https://www.heilo.io/api/v1`
- **OpenAPI 3.1**: https://www.heilo.io/openapi.json

---

## 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.

1. Exponga un endpoint de webhook en su CRM o middleware (sin código: use Zapier/Make; vea la guía más abajo).
2. Añada una suscripción en Heilo (Ajustes → Integraciones) para call.completed (opcionalmente también call.outbound.attempted).
3. Reciba call.completed y verifique la firma (cabecera Heilo-Signature, HMAC; vea más abajo).
4. Deduplique por event_id (cabecera heilo-event-id); data.call_id agrupa los eventos de la misma llamada.
5. 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).

## Autenticación

La API pública usa tokens Bearer. Genere una clave API desde la [tarjeta «Claves API»](https://www.heilo.io/settings/integrations#api-keys) de la página de Integraciones y envíela en la cabecera:

```http
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 |
| `read.recordings` | Emite un enlace al audio de una llamada mediante `GET /calls/{id}/recording-url`. Separado de `read.calls` porque el audio es un consentimiento distinto del de los metadatos: una clave existente no lo adquiere, se crea una nueva con acceso explícito a grabaciones. |
| `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 y versión

Todos los endpoints públicos están bajo /api/v1/. Producción:

```
https://www.heilo.io/api/v1
```

### 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.

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.

## 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
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.

```json
{
  "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` | El recurso no existe o está fuera de la organización de la clave API. |
| 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».

## 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.

### `GET /api/v1/me`

Introspecció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**

```bash
curl https://www.heilo.io/api/v1/me \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."
```

**Respuesta**

```json
{
  "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.

### `GET /api/v1/calls`

Lista 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 |
| `phone` | `string` | el número del cliente de la llamada; los espacios, guiones y el + inicial son opcionales, p. ej. 600 100 200 |

**Solicitud**

```bash
curl "https://www.heilo.io/api/v1/calls?limit=20&direction=inbound" \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."
```

**Respuesta**

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "direction": "inbound",
        "caller_phone": "+48600100200",
        "customer_phone_e164": "+48600100200",
        "customer_phone_national": "600 100 200",
        "company_phone_e164": "+48222630000",
        "app_url": "https://www.heilo.io/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "caller_name": "Jan Kowalski",
        "duration": 87,
        "crm_status": "new",
        "review_status": null,
        "outbound_lifecycle": null,
        "transcript_processed": {
          "caller_name": "Jan Kowalski",
          "summary": "...",
          "service_needed": "...",
          "followup_email": null
        },
        "created_at": "2026-06-03T12:34:56Z"
      }
    ],
    "has_more": false,
    "page": 1,
    "limit": 20
  },
  "meta": {
    "timestamp": "2026-06-03T12:34:56Z"
  }
}
```

### `GET /api/v1/calls/{id}`

Obtiene una sola llamada por id. 404 si no pertenece a la organización de la clave o si se eliminó.

**Solicitud**

```bash
curl https://www.heilo.io/api/v1/calls/<id> \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."
```

### `GET /api/v1/calls/{id}/recording-url`

Un enlace recién firmado al audio de la llamada, válido 15 minutos. Es un permiso de descarga con fecha límite, no una referencia duradera: guarda el `call_id` y vuelve a pedirlo, porque un enlace guardado en un campo del CRM deja de servir en menos de una hora. El enlace del webhook `call.recording.ready` está firmado siete días — este endpoint es la forma de llegar al audio después.

- 200 — el enlace y el momento en que deja de funcionar.
- 409 `RECORDING_NOT_READY` — la llamada es tuya y el audio aún no ha llegado. Vuelve a preguntar en breve.
- 409 `RECORDING_WITHHELD` — la llamada está en la papelera o programada para borrarse. Ambas decisiones se pueden deshacer, así que sigue preguntando en lugar de dar la grabación por perdida.
- 410 `RECORDING_DELETED` — el barrido de retención eliminó el audio y no se puede recuperar. Deja de preguntar y deja de encolar.
- 410 `RECORDING_NOT_CAPTURED` — esta llamada saliente nunca se grabó y nunca existirá una grabación para ella: la puerta de captura la rechazó al iniciarse, se ejecutó en modo no_recording o terminó sin conversación. Deja de preguntar; no hay motivo para reintentarlo. Una llamada entrante cuya grabación se rechazó sigue respondiendo 409, porque todavía puede llegar una grabación.

La respuesta se sirve con `Cache-Control: no-store`, porque el cuerpo lleva una credencial. El permiso se vuelve a comprobar al descargar el archivo, no solo al emitir el enlace, así que una llamada borrada entretanto deja de ser legible de inmediato.

**Solicitud**

```bash
curl https://www.heilo.io/api/v1/calls/<id>/recording-url \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."
```

**Respuesta**

```json
{
  "success": true,
  "data": {
    "url": "https://www.heilo.io/api/v1/calls/<id>/recording.mp3?token=...&exp=...",
    "expires_at": "2026-09-15T12:15:00.000Z"
  }
}
```

Los endpoints de lista y detalle de llamadas no devuelven archivos de grabación. El enlace a la grabación llega en un webhook y, una vez caducado, en `GET /calls/{id}/recording-url`. Las grabaciones pueden borrarse conforme a las reglas de retención y del RGPD.

## Webhooks salientes

Heilo envía eventos JSON firmados a tu endpoint. Crea suscripciones en la tarjeta Suscripciones de webhooks. La activación usa un handshake separado sin firma, webhook.subscription.verify, que nunca debe crear registros de negocio.

```http
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": {}
}
```

Política de reintentos y pausa:

- Los errores transitorios permiten hasta 5 intentos en total: después de 2 min, 5 min, 30 min y 2 h, con variación aleatoria (jitter) en los reintentos posteriores. Tras el quinto fallo, la entrega termina como dead-letter.
- 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.

Una respuesta 2xx confirma la recepción HTTP, no el fin de una automatización ni la escritura en el CRM. Las entregas pueden repetirse o llegar desordenadas. Heilo reconcilia lagunas recientes dentro de una ventana limitada, no todo el historial. Revisa por separado las entregas y el CRM.

### Modos de verificación

El handshake sin firma contiene solo event_type = webhook.subscription.verify y challenge. Responde únicamente para confirmar el endpoint; no proceses datos de negocio. Ambos modos usan esta petición:

```json
{
  "event_type": "webhook.subscription.verify",
  "challenge": "example-verification-token"
}
```

**Modo** — `permissive` (predeterminado)

Cualquier respuesta 2xx activa la suscripción; se ignora su contenido. Es el modo predeterminado para un receptor no-code.

**Modo** — `strict` (opcional)

Responde con 2xx y JSON con exactamente el valor challenge recibido de Heilo:

```json
{"challenge":"<echo of the challenge field from Heilo's POST>"}
```

Elige el modo al crear la suscripción. Cambiarlo exige eliminarla y crearla de nuevo. Las rutas de gestión requieren una sesión autenticada y protección CSRF; no están disponibles con una clave API.

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)

Los eventos firmados, incluido el webhook.test manual, usan Heilo-Signature con HMAC-SHA256 sobre timestamp + punto + bytes originales de la petición. Verifica la firma antes de analizar el JSON de negocio. Solo el handshake de verificación no lleva firma.

```
signed_string = "<unix_timestamp>.<raw_request_body>"
signature     = HMAC-SHA256(signing_secret, signed_string).hex()
header        = "t=<unix_timestamp>,v1=<signature>"
```

**Node.js (Express, analizador de cuerpo RAW):**

```javascript
import { createHmac, timingSafeEqual } from 'node:crypto';

// Pass the exact received Buffer (e.g. Express raw parser), never JSON.stringify(req.body).
// signingSecrets: every secret you currently trust, tried in constant time each.
// During a secret rotation pass [newSecret, oldSecret]; remove the old one once
// Heilo's grace window ends. Heilo itself always sends exactly ONE v1 signature —
// this list is what lets your receiver accept it under either secret while both
// are live, never a second signature to parse.
function verifyHeiloSignature(rawBody, header, signingSecrets) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
  if (!match) return false;
  const timestamp = Number(match[1]);
  const now = Math.floor(Date.now() / 1000);
  // Receiver policy: at most 10 minutes old or 5 minutes ahead (clock skew).
  if (!Number.isSafeInteger(timestamp) || now - timestamp > 600 || timestamp - now > 300) return false;
  // One string is accepted too. Empty entries are dropped: an HMAC key of ''
  // is valid, so a blanked env var (OLD_SECRET=) would let anyone sign events.
  const secrets = [].concat(signingSecrets).filter((s) => typeof s === 'string' && s.length > 0);
  if (secrets.length === 0) return false;
  const actual = Buffer.from(match[2], 'hex');
  return secrets.some((signingSecret) => {
    const expected = createHmac('sha256', signingSecret)
      .update(match[1] + '.')
      .update(rawBody)
      .digest();
    return actual.length === expected.length && timingSafeEqual(actual, expected);
  });
}

function receiveHeiloWebhook(rawBody, signatureHeader, signingSecrets) {
  if (!signatureHeader) {
    // The ONLY unsigned exception confirms the endpoint; never enqueue it for CRM writes.
    const probe = JSON.parse(rawBody.toString('utf8'));
    if (probe && typeof probe === 'object' && !Array.isArray(probe)
      && Object.keys(probe).length === 2
      && probe.event_type === 'webhook.subscription.verify'
      && typeof probe.challenge === 'string') {
      return { kind: 'verification', response: { challenge: probe.challenge } };
    }
    throw new Error('Missing Heilo-Signature');
  }
  if (!verifyHeiloSignature(rawBody, signatureHeader, signingSecrets)) throw new Error('Invalid Heilo-Signature');
  return { kind: 'event', event: JSON.parse(rawBody.toString('utf8')) };
}

// HTTP adapter: limit body size; map verification to 200 JSON response.
// Signature errors -> 401 (pauses delivery; fix the secret, then Reverify). JSON errors -> 400.
// For kind=event: validate the envelope and chosen event_type, ignore webhook.test
// in business flows, then durably deduplicate/enqueue by event_id BEFORE returning 2xx.
// Enqueue failure: return 503. An HTTP acknowledgement is not proof of a CRM write.
```

**Python (Flask / FastAPI):**

```python
import hmac, hashlib, time, re, json

# raw_body must be bytes (e.g. Flask request.get_data()), before JSON parsing.
# signing_secrets: every secret you currently trust, tried in constant time each.
# During a secret rotation pass [new_secret, old_secret]; remove the old one once
# Heilo's grace window ends. Heilo itself always sends exactly ONE v1 signature —
# this list is what lets your receiver accept it under either secret while both
# are live, never a second signature to parse.
def verify_heilo_signature(raw_body: bytes, header: str, signing_secrets):
    # One string is accepted too. Empty entries are dropped: an HMAC key of b''
    # is valid, so a blanked env var (OLD_SECRET=) would let anyone sign events.
    if isinstance(signing_secrets, str):
        signing_secrets = [signing_secrets]
    secrets = [s for s in (signing_secrets or []) if isinstance(s, str) and s]
    if not secrets:
        return False
    match = re.fullmatch(r't=([0-9]+),v1=([a-f0-9]{64})', header or '')
    if not match:
        return False
    try:
        timestamp = int(match[1])
    except ValueError:
        return False
    now = int(time.time())
    # Receiver policy: at most 10 minutes old or 5 minutes ahead (clock skew).
    if now - timestamp > 600 or timestamp - now > 300:
        return False
    return any(
        hmac.compare_digest(
            hmac.new(
                signing_secret.encode('utf-8'), match[1].encode('ascii') + b'.' + raw_body,
                hashlib.sha256,
            ).hexdigest(),
            match[2],
        )
        for signing_secret in secrets
    )

def receive_heilo_webhook(raw_body: bytes, signature_header: str, signing_secrets):
    if not signature_header:
        probe = json.loads(raw_body)
        if (isinstance(probe, dict) and set(probe) == {'event_type', 'challenge'}
            and probe['event_type'] == 'webhook.subscription.verify'
            and isinstance(probe['challenge'], str)):
            return {'kind': 'verification', 'response': {'challenge': probe['challenge']}}
        raise ValueError('Missing Heilo-Signature')
    if not verify_heilo_signature(raw_body, signature_header, signing_secrets):
        raise ValueError('Invalid Heilo-Signature')
    return {'kind': 'event', 'event': json.loads(raw_body)}

# HTTP adapter: limit body size; verification -> 200 JSON; JSON error -> 400.
# Signature error -> 401 (pauses delivery; fix the secret, then Reverify).
# For events: validate envelope/type, exclude webhook.test from CRM writes,
# durably deduplicate/enqueue by event_id before 2xx; enqueue failure -> 503.
```

Estos ejemplos aceptan firmas de hasta 600 segundos (10 minutos) de antigüedad y 300 segundos (5 minutos) en el futuro, igual que el verificador actual de Heilo. Sincroniza los relojes. El control de tiempo limita la ventana de repetición; sigue siendo necesario deduplicar por event_id.

El secreto de firma se muestra solo una vez, al crear la suscripción. ¿Lo perdió o sospecha una filtración? Abra la suscripción en Configuración → Integraciones → Webhooks y rótelo allí — vea «Rotación del secreto» más abajo. No hace falta eliminar la suscripción y volver a crearla.

### Rotación del secreto

Heilo siempre envía exactamente una firma v1, nunca dos a la vez. La rotación funciona por orden de instalación, no enseñando a su receptor a analizar varias firmas.

1. Prepare un secreto nuevo en la configuración de la suscripción. Se muestra una sola vez y espera hasta 24 horas para activarse.
2. Instale el secreto nuevo JUNTO AL actual — su receptor debe aceptar una firma hecha con cualquiera de los dos.
3. Opcionalmente envíe una prueba firmada con el secreto nuevo. Lleva la cabecera Heilo-Secret-Rotation-Id y un cuerpo webhook.test normal; una respuesta 2xx no prueba que su receptor comprobó la firma — confírmelo en su propio registro.
4. Active la rotación.
5. Mantenga instalado el secreto anterior 1 hora después de la activación y luego elimínelo.

Heilo no cambia nada en su receptor. Durante ese periodo es su receptor el que debe aceptar una firma hecha con cualquiera de los dos secretos.

**Emergencia: el secreto se filtró** — Una rotación de emergencia pausa el envío de inmediato. Instale SOLO el secreto nuevo — trate el anterior como comprometido — y luego actívelo; no hay ventana de gracia. Reanude el envío después con «Volver a verificar». Las entregas que ya estaban en cola al pausar se omiten; reenvíelas desde el registro de entregas.

¿Perdió el secreto sin filtración? Una rotación estándar también funciona — no hace falta eliminar la suscripción.

## 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.

webhook.test se envía con el botón manual Test y tiene data._test = true. Úsalo para mapear campos y después exclúyelo del procesamiento de negocio. El handshake de activación separado es webhook.subscription.verify.

| event_type | Descripción |
| --- | --- |
| `call.completed` | Llamada completada, transcripción lista |
| `call.outbound.attempted` | Intento de llamada saliente en estado final (conectada o fallida) |
| `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) |
| `call.followup_email.drafted` | Borrador de correo de seguimiento listo |
| `call.tracker.matched` | Tracker coincidió |
| `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).

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `direction` | `'inbound' \| 'outbound'` | Dirección de la llamada. |
| `caller_phone` | `string \| null` | Número guardado del llamante; null si no está disponible en call.completed. |
| `customer_phone_e164` | `string \| null` | Número del cliente para buscar el contacto en el CRM; null si no está disponible en call.completed. |
| `customer_phone_national` | `string \| null` | El mismo número del cliente en formato nacional, listo para pegar en un campo de teléfono del CRM; null cuando el valor guardado no es un número utilizable, por ejemplo con un identificador oculto. |
| `company_phone_e164` | `string \| null` | El número por el que entró o salió esta llamada. Normalmente tu número de Heilo; en una llamada desviada es tu propio número, el que la desvió. Sirve para encaminar notas cuando tienes más de un número; null si no se registró. |
| `call_created_at` | `string (ISO 8601) \| null` | Cuándo se registró la llamada en Heilo (ISO 8601). No es lo mismo que el created_at del sobre, que es el momento del envío y puede ser días posterior en un evento reparado. |
| `app_url` | `string \| null` | Enlace permanente a la llamada en Heilo. A diferencia de recording_url no caduca, así que puede guardarse en un registro del CRM. Abrirlo requiere iniciar sesión en Heilo. |
| `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.completed",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "direction": "inbound",
    "caller_phone": "+447700900123",
    "customer_phone_e164": "+447700900123",
    "customer_phone_national": "07700 900123",
    "company_phone_e164": "+48222630000",
    "call_created_at": "2026-09-08T10:00:00Z",
    "app_url": "https://www.heilo.io/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "duration": 87,
    "recording_url": "https://www.heilo.io/api/v1/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6/recording.mp3?token=EXAMPLE&exp=1789466400",
    "transcript_processed": {
      "caller_name": "Alicja Testowa",
      "counterparty_name": null,
      "caller_location": "Warszawa, Mokotów",
      "client_address": "ul. Przykładowa 10",
      "client_city": "Warszawa",
      "client_country": "Poland",
      "service_needed": "Tynki gipsowe w mieszkaniu 65m²",
      "services_match": true,
      "preferred_date": "w przyszłym tygodniu",
      "additional_details": "Klient wspomniał o terminie do końca czerwca. Druga rozmowa po wycenie.",
      "lead_score": 7,
      "summary": "Prośba o wycenę tynków gipsowych w mieszkaniu 65 m² na Mokotowie.",
      "subject": "Wycena tynków — Warszawa, 65 m²",
      "detected_language": "pl"
    },
    "transcript_original": "Proszę o ofertę w przyszłym tygodniu."
  }
}
```

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 en llamadas salientes y en llamadas en vivo que Heilo escucha en silencio (modo oyente). |
| `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)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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` | Número del cliente para buscar el contacto en el CRM; null si no está disponible en call.completed. |
| `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 y la llamada no estaba en modo no_recording; la grabación llega entonces como call.recording.ready. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.outbound.attempted",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "agent_user_id": "2ce127a0-c621-483d-b576-e68f69d95e84",
    "customer_phone": "+447700900123",
    "customer_phone_e164": "+447700900123",
    "outbound_lifecycle": "completed",
    "duration": 95,
    "attempted_at": "2026-09-08T10:00:00Z",
    "has_recording": true
  }
}
```

### `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)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.recording.ready",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "recording_url": "https://www.heilo.io/api/v1/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6/recording.mp3?token=EXAMPLE&exp=1789466400",
    "duration": 87
  }
}
```

### `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)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.transcribed",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "transcript_original": "Proszę o ofertę w przyszłym tygodniu.",
    "transcript_processed": {
      "caller_name": "Alicja Testowa",
      "counterparty_name": null,
      "caller_location": "Warszawa, Mokotów",
      "client_address": "ul. Przykładowa 10",
      "client_city": "Warszawa",
      "client_country": "Poland",
      "service_needed": "Tynki gipsowe w mieszkaniu 65m²",
      "services_match": true,
      "preferred_date": "w przyszłym tygodniu",
      "additional_details": "Klient wspomniał o terminie do końca czerwca. Druga rozmowa po wycenie.",
      "lead_score": 7,
      "summary": "Prośba o wycenę tynków gipsowych w mieszkaniu 65 m² na Mokotowie.",
      "subject": "Wycena tynków — Warszawa, 65 m²",
      "detected_language": "pl"
    }
  }
}
```

### `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)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.failed",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "outbound_lifecycle": "customer_no_answer",
    "failure_reason": "customer_busy"
  }
}
```

### `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)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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). |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.outbound.lifecycle_repaired",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "previous_lifecycle": "agent_only",
    "new_lifecycle": "completed",
    "repaired_at": "2026-09-08T10:00:00Z"
  }
}
```

### `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. Solo se le informa de una llamada si Heilo tiene constancia de que su contenido pudo llegar a su endpoint. Suscribirse únicamente a los eventos de borrado no le convierte en destinatario: no hay nada que borrar si nunca recibió nada.

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.deletion_scheduled",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "pending_deletion_at": "2026-09-09T10:00:00Z",
    "reason": "user_erasure"
  }
}
```

### `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. Solo se le informa de una llamada si Heilo tiene constancia de que su contenido pudo llegar a su endpoint, por lo que esto no es un registro completo de todos los borrados de su organización. Un 2xx por su parte significa que la instrucción llegó, nunca que la copia haya desaparecido.

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `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). |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.recording.deleted",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "reason": "user_erasure",
    "recording_sid": null,
    "deletion_kind": "twilio_404",
    "deleted_at": "2026-09-08T10:00:00Z"
  }
}
```

### `call.followup_email.drafted`

Se envía después de procesar una llamada bidireccional cuando Heilo ha redactado un borrador de correo de seguimiento a partir de lo acordado. No se envía para el buzón de voz ni cuando no se pudo redactar. Heilo NUNCA envía este correo a la persona que llamó — el borrador es para que tu comercial lo revise y lo envíe.

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `subject` | `string` | Asunto del correo, listo para pegar. |
| `body` | `string` | Cuerpo del correo en texto plano, SIN firma — el comercial añade la suya al enviarlo. |
| `language` | `string` | Idioma del borrador: el idioma detectado de la llamada o, en su defecto, el idioma de la cuenta configurado en Heilo (no un ajuste por número). |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.followup_email.drafted",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "subject": "Oferta po rozmowie",
    "body": "Dzień dobry, przesyłam ustalenia naszej rozmowy.",
    "language": "pl"
  }
}
```

### `call.tracker.matched`

Se envía cuando al menos un tracker de palabras clave coincide con la transcripción procesada. Incluye términos y fragmentos; si faltan fragmentos, la lista está vacía.

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `matches` | `object[]` | Trackers encontrados en esta llamada. |
| `matches[].tracker_id` | `string (uuid)` | Identificador del tracker configurado. |
| `matches[].name` | `string` | Nombre del tracker. |
| `matches[].matched_terms` | `string[]` | Términos encontrados. |
| `matches[].excerpts` | `string[]` | Fragmentos de transcripción; puede estar vacío. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "call.tracker.matched",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "matches": [
      {
        "tracker_id": "cd794362-6117-44d1-b487-c5e7e1ea9082",
        "name": "Oferta",
        "matched_terms": [
          "ofertę"
        ],
        "excerpts": [
          "Proszę o ofertę w przyszłym tygodniu."
        ]
      }
    ]
  }
}
```

### `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ó. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "contact.created",
  "resource_id": "86c68698-d54c-43e7-bbbc-328499b98d12",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "contact": {
      "id": "86c68698-d54c-43e7-bbbc-328499b98d12",
      "phone": "+447700900123",
      "first_name": "Alicja",
      "last_name": "Testowa",
      "email": "alicja@example.com",
      "company": null
    }
  }
}
```

### `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. |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "contact.updated",
  "resource_id": "86c68698-d54c-43e7-bbbc-328499b98d12",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "contact_id": "86c68698-d54c-43e7-bbbc-328499b98d12",
    "diff": {
      "company": "Example",
      "notes": "Oddzwonić",
      "tags": [
        "oferta"
      ]
    }
  }
}
```

Claves posibles en diff: `first_name, last_name, email, phone, company, notes, tags`

### `webhook.test`

Se envía solo al pulsar Test en una suscripción. resource_id identifica la suscripción, no una llamada. data contiene datos sintéticos de call.completed y los tres marcadores descritos abajo. No demuestra que exista audio real.

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `call_id` | `string (uuid)` | ID de llamada en Heilo. Coincide con resource_id en eventos de llamadas de negocio; en webhook.test es un ID sintético. |
| `direction` | `'inbound' \| 'outbound'` | Dirección de la llamada. |
| `caller_phone` | `string \| null` | Número guardado del llamante; null si no está disponible en call.completed. |
| `customer_phone_e164` | `string \| null` | Número del cliente para buscar el contacto en el CRM; null si no está disponible en call.completed. |
| `customer_phone_national` | `string \| null` | El mismo número del cliente en formato nacional, listo para pegar en un campo de teléfono del CRM; null cuando el valor guardado no es un número utilizable, por ejemplo con un identificador oculto. |
| `company_phone_e164` | `string \| null` | El número por el que entró o salió esta llamada. Normalmente tu número de Heilo; en una llamada desviada es tu propio número, el que la desvió. Sirve para encaminar notas cuando tienes más de un número; null si no se registró. |
| `call_created_at` | `string (ISO 8601) \| null` | Cuándo se registró la llamada en Heilo (ISO 8601). No es lo mismo que el created_at del sobre, que es el momento del envío y puede ser días posterior en un evento reparado. |
| `app_url` | `string \| null` | Enlace permanente a la llamada en Heilo. A diferencia de recording_url no caduca, así que puede guardarse en un registro del CRM. Abrirlo requiere iniciar sesión en Heilo. |
| `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. |
| `_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). |

```json
{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-4f82-4335-988e-4e044fa648d4",
  "event_type": "webhook.test",
  "resource_id": "c2d38cc8-bc1d-4336-91ea-51b8a549a882",
  "created_at": "2026-09-08T10:00:00Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "direction": "inbound",
    "caller_phone": "+447700900123",
    "customer_phone_e164": "+447700900123",
    "customer_phone_national": "07700 900123",
    "company_phone_e164": "+48222630000",
    "call_created_at": "2026-09-08T10:00:00Z",
    "app_url": "https://www.heilo.io/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "duration": 87,
    "recording_url": "https://www.heilo.io/api/v1/calls/3fa85f64-5717-4562-b3fc-2c963f66afa6/recording.mp3?token=EXAMPLE&exp=1789466400",
    "transcript_processed": {
      "caller_name": "Alicja Testowa",
      "counterparty_name": null,
      "caller_location": "Warszawa, Mokotów",
      "client_address": "ul. Przykładowa 10",
      "client_city": "Warszawa",
      "client_country": "Poland",
      "service_needed": "Tynki gipsowe w mieszkaniu 65m²",
      "services_match": true,
      "preferred_date": "w przyszłym tygodniu",
      "additional_details": "Klient wspomniał o terminie do końca czerwca. Druga rozmowa po wycenie.",
      "lead_score": 7,
      "summary": "Prośba o wycenę tynków gipsowych w mieszkaniu 65 m² na Mokotowie.",
      "subject": "Wycena tynków — Warszawa, 65 m²",
      "detected_language": "pl"
    },
    "transcript_original": "Proszę o ofertę w przyszłym tygodniu.",
    "_test": true,
    "_message": "Synthetic webhook test; do not create CRM records.",
    "_sent_at": "2026-09-08T10:00:00Z"
  }
}
```

## 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)
- `POST /calls` — iniciar una llamada saliente a través de su número Heilo; se graba, se transcribe y se entrega a su CRM como cualquier otra llamada

¿Necesita un endpoint? Escriba a support@heilo.io con su caso de uso: priorizamos el roadmap según la demanda real.
