> ## Documentation Index
> Fetch the complete documentation index at: https://support.relevantleads.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 39 - Referencia de la API de Relevant

> Documentación técnica para integrar tu sistema con Relevant: autenticación con llave, registro de leads (POST /api/v1/leads), eventos de lead (POST /api/v1/events), errores, idempotencia y límites.

Esta guía es para la persona que va a conectar un sistema externo (portal, tienda, app, ERP) con Relevant. La API v1 tiene dos endpoints: uno para registrar leads y otro para avisar que pasó algo con un lead ya registrado. El orden natural de integración es el de este documento: obtener la llave, registrar un lead, enviar un evento.

Si buscas la explicación para quien administra la cuenta (qué es el origen, cómo armar la automatización que escucha el evento), está en [Conecta tu sistema a Relevant](/conecta-tu-sistema-a-relevant).

## Antes de empezar

* **Base URL:** `https://app.relevantleads.io`
* **Formato:** JSON en el cuerpo y en la respuesta. Envía `Content-Type: application/json`. El cuerpo debe ser un objeto JSON (un arreglo o un valor suelto se rechaza).
* **Autenticación:** encabezado `Authorization: Bearer <llave>`.
* **Métodos:** solo `POST`. No hay CORS ni respuesta a `OPTIONS`: llama desde tu servidor, no desde el navegador del usuario final.
* **Entorno:** no existe sandbox ni llaves de prueba. Todas las llaves son `rk_live_` y operan sobre la cuenta real. Para probar sin arriesgar datos, usa un embudo de pruebas y leads de prueba.

## 1. La llave

Una persona con rol Owner o Admin la crea en **Ajustes, Integraciones, API de leads, Nueva llave**. Al crearla define tres cosas que viven en la llave, no en cada envío:

* El **embudo y etapa destino** por defecto de los leads que entren con ella (tu envío puede sobrescribirlos, ver abajo).
* El **origen** con el que se etiquetan esos leads en Contactos, Atribución y reportes (por ejemplo "Registro web"; si no se configura, "Registro externo").
* Su **nombre**, que aparece en el chat de cada lead que entra con ella.

Formato: `rk_live_` seguido de 43 caracteres, 51 en total. Se muestra completa una sola vez al crearla; Relevant solo guarda un hash. Guárdala en variables de entorno o en tu gestor de secretos. Si se pierde, el administrador la revoca y crea otra.

Cada petición autenticada actualiza el **Último uso** de la llave (aunque después falle por validación), así que quien administra puede ver desde Relevant que tu integración ya está llamando. Una llave revocada responde `401` de inmediato.

### Errores de autenticación

| HTTP | `code`         | `message`                                                                                 | Causa                                                 |
| ---- | -------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| 401  | `unauthorized` | `Falta el encabezado Authorization. Envía "Authorization: Bearer <tu llave>".`            | No mandaste el encabezado o no usa el esquema Bearer. |
| 401  | `unauthorized` | `La llave no es válida. Verifica que la copiaste completa y que pertenece a esta cuenta.` | La llave no empieza con `rk_live_` o no existe.       |
| 401  | `unauthorized` | `La llave fue revocada. Genera una nueva en Ajustes > Integraciones > API.`               | La llave existió y fue revocada.                      |

## 2. Registrar un lead

`POST /api/v1/leads`

Crea el contacto y su lead en el embudo destino. Si la persona ya existe, la reutiliza (ver deduplicación). Es seguro reintentar el mismo envío.

```bash theme={null}
curl -X POST https://app.relevantleads.io/api/v1/leads \
  -H "Authorization: Bearer rk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mariana López",
    "phone": "5512345678",
    "country_code": "52",
    "email": "mariana@ejemplo.com",
    "external_id": "usr_8841",
    "amount": 1500,
    "custom_data": { "plan": "Pro", "empresa": "Acme" },
    "utm_source": "google",
    "utm_campaign": "primavera"
  }'
```

### Campos del cuerpo

| Campo                                                                                 | Tipo            | Requerido           | Notas                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------- | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                                                                                | string          | sí                  | Nombre del lead. Máximo 200 caracteres; si lo excede, la petición se rechaza (no se recorta).                                                                                                                                                   |
| `phone`                                                                               | string o número | sí                  | De 10 a 15 dígitos (se cuentan solo los dígitos): 10 con lada, o de 11 a 15 si ya incluye el código de país. Con 8 o 9 dígitos la petición se rechaza (`422`, `fields.phone`). Envía solo dígitos y, si acaso, el `+`. Ver normalización abajo. |
| `country_code`                                                                        | string          | no                  | Código de país, solo dígitos, sin `+`. Por defecto `"52"` (México). Solo se usa cuando `phone` viene con exactamente 10 dígitos. Máximo 4 caracteres.                                                                                           |
| `email`                                                                               | string          | no                  | Se valida el formato (`No parece un correo válido.`). Máximo 254 caracteres.                                                                                                                                                                    |
| `external_id`                                                                         | string          | no, **recomendado** | El identificador de esa persona en tu sistema. Es la identidad estable para deduplicar y para enviar eventos después. Máximo 128 caracteres.                                                                                                    |
| `funnel_id`                                                                           | uuid            | no                  | Sobrescribe el embudo por defecto de la llave. Al hacerlo, la etapa por defecto de la llave se descarta y el lead cae en la primera etapa del embudo indicado, salvo que también mandes `stage_id`.                                             |
| `stage_id`                                                                            | uuid            | no                  | Sobrescribe la etapa. Si mandas solo `stage_id`, el embudo se deduce de la etapa. Si mandas ambos y la etapa no pertenece al embudo, `422`.                                                                                                     |
| `amount`                                                                              | número          | no                  | Valor estimado del lead en MXN, mayor o igual a 0. Se acepta también como texto con comas (`"1,500"`). En un lead reutilizado no se actualiza.                                                                                                  |
| `custom_data`                                                                         | objeto          | no                  | Datos extra del registro. No se guardan como campos: se escriben como una nota de sistema en el chat del lead, solo cuando el lead se crea en este envío (ver abajo).                                                                           |
| `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `referrer_url` | string          | no                  | Atribución. En un contacto que ya existía solo se rellenan los que estaban vacíos. Máximo 500 caracteres cada uno.                                                                                                                              |

Los campos de texto opcionales (`external_id`, `email`, `country_code`, UTMs) que exceden su máximo se **recortan en silencio**, no se rechazan. Solo `name` rechaza por longitud. Envía valores dentro de los límites para que lo que guardes coincida con lo que mandaste.

### Normalización del teléfono

Relevant quita espacios, guiones, paréntesis, puntos y el `+`, y aplica estas reglas:

* 10 dígitos: se antepone `country_code` (o `52`). `"5512345678"` se guarda como `525512345678`.
* Empieza con `521` y tiene 13 dígitos: se quita el `1` (formato viejo de celulares mexicanos). `"5215512345678"` se guarda como `525512345678`.
* Empieza con `00`: se quitan los dos ceros.
* De 11 a 15 dígitos (por ejemplo 12 con lada): se guarda tal cual; se asume que ya trae código de país.
* 8 o 9 dígitos: no se guarda. La petición responde `422` con `fields.phone` explicando el formato ("Debe tener 10 dígitos con lada (por ejemplo "8112345678") o incluir el código de país (por ejemplo "+528112345678", máximo 15 dígitos).").

El teléfono guardado se devuelve en `phone`. En un contacto que ya existía, el teléfono guardado no se reemplaza.

### Destino del lead

1. Por defecto: el embudo y etapa de la llave. Si la llave tiene embudo pero no etapa, la primera etapa del embudo. Si la llave no tiene embudo y el cuerpo tampoco, se crea solo el contacto (`deal_id: null`).
2. `funnel_id` en el cuerpo: se valida que sea de tu cuenta (`404` si no). La etapa por defecto de la llave se descarta.
3. `stage_id` en el cuerpo: se valida que sea de tu cuenta (`404` si no). Si viene sin `funnel_id`, el embudo es el de esa etapa.

### Respuesta

`201 Created` cuando se creó el contacto o el lead. `200 OK` cuando ambos ya existían y se reutilizaron.

```json theme={null}
{
  "contact_id": "2f1800a8-1ef7-4ca1-96b4-4b74a5bedbae",
  "deal_id": "9237e11f-cae3-4e7e-b8cb-8d011baa92ea",
  "created_contact": true,
  "created_deal": true,
  "phone": "525512345678",
  "funnel_id": "bf6929af-2d63-4d18-973f-232549b4e35f",
  "stage_id": "41ace7c2-5064-498b-8280-d11007e8c040"
}
```

* `deal_id` es `null` cuando no hubo embudo destino: se creó solo el contacto.
* `funnel_id` y `stage_id` son el embudo y la etapa donde quedó el lead al terminar la petición (Relevant los relee después de crear o reutilizar el lead). Si una automatización con disparador "Nuevo lead en etapa" lo movió al crearse, aquí ves la etapa final, no la de entrada.
* Guarda `contact_id` y `deal_id` junto a tu usuario: `deal_id` sirve para enviar eventos con precisión.

### Deduplicación (idempotencia de registros)

1. Si envías `external_id` y ya existe un contacto con ese valor en tu cuenta, se reutiliza.
2. Si no envías `external_id`, o no coincide con ningún contacto, se busca por los **últimos 10 dígitos del teléfono** dentro de tu cuenta. Si más de un contacto termina en esos dígitos, se reutiliza el más antiguo (el que tiene el historial).
3. En un contacto reutilizado se actualizan `name` y `email` si los mandas, y se rellenan los UTMs vacíos. Si el contacto no tenía `external_id` y ahora lo mandas, se le guarda; si ya tenía otro, conserva el que tenía (y un evento con el nuevo `external_id` no lo encontrará: usa `deal_id` o el teléfono).
4. Si el contacto ya tiene un lead **abierto** en el embudo destino, se reutiliza ese lead (`created_deal: false`); no se tocan su título, monto ni etapa. Un segundo registro de la misma persona no es una oportunidad nueva.
5. Si su lead anterior ya se cerró (ganado o perdido), se crea uno nuevo.

Reintentar el mismo envío no genera duplicados.

### `custom_data`

Se escribe en el chat del lead como una nota de sistema con el formato `Datos del registro (nombre de la llave): clave: valor · clave: valor`, visible para el equipo. Reglas:

* Se escribe **solo cuando el lead se crea en este envío** (`created_deal: true`). Si el lead ya existía o no hay embudo destino, `custom_data` se descarta sin error.
* Máximo 20 claves (las primeras, en orden), 40 caracteres por clave, 120 por valor, 600 en total. Los valores vacíos o nulos se omiten. Un valor anidado se serializa como texto JSON y se recorta a 120.
* Un arreglo en `custom_data` se rechaza con `422`.

### Qué pasa en Relevant al registrar

* El contacto y el lead se crean con el origen de la llave. El lead nace con prioridad Media, estado abierto y título igual a `name`.
* Sobre un lead nuevo corren las reglas de auto-asignación de la cuenta (reglas, responsable de etapa, responsable de embudo). Si asigna a alguien, esa persona recibe la notificación de lead asignado.
* **Automatizaciones al crear**: un lead nuevo dispara, dentro de la misma petición, las automatizaciones cuyo disparador "Nuevo lead en etapa" vigile la etapa donde entra; por eso `stage_id` en la respuesta puede ser distinto del destino. Un lead reutilizado no dispara nada al registrarse de nuevo, porque no cambia de etapa. Para actuar cuando pase algo después en tu sistema, usa eventos.
* No se envía ningún mensaje de WhatsApp al lead.

## 3. Avisar de un evento del lead

`POST /api/v1/events`

Úsalo cuando en tu sistema pase algo con un lead ya registrado: un pago, una activación, una cancelación. Relevant registra el evento, lo deja como nota en el chat del lead, notifica a la persona responsable y ejecuta las automatizaciones que estén escuchando ese evento en ese embudo.

```bash theme={null}
curl -X POST https://app.relevantleads.io/api/v1/events \
  -H "Authorization: Bearer rk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "pago_recibido",
    "external_id": "usr_8841",
    "payload": { "plan": "Pro", "monto": 1500 },
    "idempotency_key": "pago-usr_8841-2026-09-12"
  }'
```

### Campos del cuerpo

| Campo             | Tipo            | Requerido           | Notas                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`           | string          | sí                  | Nombre del evento: minúsculas, dígitos, `_`, `-` o `.`; el primer carácter debe ser letra o dígito; máximo 64. Se convierte a minúsculas antes de validar (`"PAGO_RECIBIDO"` se acepta como `pago_recibido`). Debe coincidir exactamente con el nombre configurado en el disparador **Evento externo** de la automatización; ese nombre se ve y se copia desde el panel del disparador. |
| `deal_id`         | uuid            | uno de los tres     | El `deal_id` que devolvió el registro. Es la forma más precisa. Si lo mandas, `external_id` y `phone` se ignoran.                                                                                                                                                                                                                                                                       |
| `external_id`     | string          | uno de los tres     | El identificador de tu sistema, el mismo que mandaste al registrar. Forma recomendada si no guardaste `deal_id`. Máximo 128.                                                                                                                                                                                                                                                            |
| `phone`           | string o número | uno de los tres     | Teléfono del lead, con la misma regla que en el registro: de 10 a 15 dígitos (con 8 o 9, `422`). Se usa si no mandas `deal_id` ni `external_id`, o si `external_id` no coincide con ningún contacto.                                                                                                                                                                                    |
| `funnel_id`       | uuid            | no                  | Limita el evento a los leads abiertos de ese embudo. Se valida contra tu cuenta (`404` si no existe).                                                                                                                                                                                                                                                                                   |
| `payload`         | objeto          | no                  | Datos del evento. Se muestran en el chat del lead y en la notificación (mismos límites de texto que `custom_data`). Las claves que empiezan con `_` se ocultan del resumen. El `payload` no llega a las acciones de la automatización; se guarda íntegro en el registro del evento.                                                                                                     |
| `idempotency_key` | string          | no, **recomendado** | Clave única por evento (máximo 128). Un reintento con la misma clave devuelve el resultado original y no vuelve a ejecutar nada.                                                                                                                                                                                                                                                        |

### Cómo se resuelve el evento

1. Relevant localiza al lead: por `deal_id` si viene; si no, por `external_id`; si no coincide, por los últimos 10 dígitos de `phone`.
2. Toma sus leads **abiertos** (todos, o solo los del `funnel_id` indicado). Los leads cerrados no se tocan.
3. Para cada lead abierto, ejecuta cada automatización **activa** que tenga un disparador **Evento externo** con ese nombre exacto y con el embudo del lead. En el editor, cada disparador de este tipo está ligado a un embudo; si la persona tiene leads abiertos en dos embudos y mandas el evento sin `funnel_id`, ambos leads reciben la nota y la notificación, pero solo se ejecuta la automatización del embudo que coincide.
4. Deja rastro: registro del evento, nota de sistema `Evento recibido de <nombre de la llave>: <event> (<resumen del payload>)` en el chat de cada lead abierto alcanzado, y notificación dentro de la app a la persona asignada a cada uno.

### Respuesta

`200 OK` siempre que el lead exista, aunque no tenga leads abiertos o ninguna automatización lo escuche.

```json theme={null}
{
  "event_id": "c0a1f3d2-8b4e-4d1a-9f0e-2a6b7c8d9e01",
  "event": "pago_recibido",
  "contact_id": "2f1800a8-1ef7-4ca1-96b4-4b74a5bedbae",
  "deal_ids": ["9237e11f-cae3-4e7e-b8cb-8d011baa92ea"],
  "deals": [{ "deal_id": "9237e11f-cae3-4e7e-b8cb-8d011baa92ea", "handovers_fired": 1 }],
  "handovers_fired": 1,
  "outcome": "fired",
  "duplicate": false,
  "received_at": "2026-09-12T04:00:00.000Z"
}
```

| `outcome`       | Significado                                                                                                                                                                   |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fired`         | Al menos una automatización se ejecutó. `handovers_fired` cuenta automatizaciones ejecutadas (no acciones), en total y por lead en `deals`.                                   |
| `no_automation` | El lead tiene leads abiertos, pero ninguna automatización activa escucha ese evento en ese embudo. El evento quedó registrado y anotado en el chat.                           |
| `no_open_deals` | La persona existe pero no tiene leads abiertos (ya se ganó o se perdió, o no está en el embudo indicado). Con `deal_id` de un lead cerrado también se responde así, no `404`. |

* Si el lead no existe: `404 not_found`, con `field` igual a `deal_id`, `external_id` o `phone` según lo que mandaste. Regístralo primero con `POST /api/v1/leads`.
* `event_id` puede venir `null` en el caso raro de que la ejecución ocurrió pero el registro del evento falló. La ejecución no se repite.

### Idempotencia de eventos

* La `idempotency_key` es única por cuenta y **no caduca**: la misma clave meses después sigue devolviendo el resultado original.
* Un reintento con una clave ya recibida responde `200` con `duplicate: true`, el `outcome` original (`fired`, `no_automation` o `no_open_deals`) y sin el arreglo `deals`. No se ejecuta nada.
* Si dos envíos con la misma clave llegan al mismo tiempo, el segundo recibe `409 conflict`. Reintenta una vez: obtendrás el resultado original con `duplicate: true`. Esta protección cubre reintentos secuenciales (que es como reintentan los webhooks); si tu sistema puede mandar el mismo evento en paralelo, serializa esos envíos de tu lado.

## 4. Errores

Todas las respuestas de error tienen esta forma:

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "Hay campos inválidos. Revisa \"fields\".",
    "fields": { "phone": "Requerido. Teléfono con lada, por ejemplo \"5512345678\" o \"+525512345678\"." }
  }
}
```

| HTTP | `code`              | Cuándo                                                                                                                                                                                        | Qué hacer                                                                                                       |
| ---- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 401  | `unauthorized`      | Falta el encabezado, la llave no existe o fue revocada. El `message` distingue los tres casos.                                                                                                | Revisa la llave; si fue revocada, pide una nueva al administrador.                                              |
| 400  | `invalid_json`      | El cuerpo no es un objeto JSON válido (`El body debe ser un objeto JSON. Envía Content-Type: application/json.`).                                                                             | Envía un objeto JSON.                                                                                           |
| 413  | `payload_too_large` | El cuerpo supera 16 KB (`El body supera el máximo de 16 KB.`).                                                                                                                                | Un lead cabe de sobra en 16 KB; revisa `custom_data` o `payload`.                                               |
| 422  | `validation_error`  | Algún campo no cumple el formato. `fields` trae el mensaje por campo. En leads, también cuando `stage_id` no pertenece al `funnel_id` indicado (`stage_id no pertenece al embudo indicado.`). | Corrige los campos indicados y reintenta.                                                                       |
| 404  | `not_found`         | `funnel_id`, `stage_id` o `deal_id` no existen en tu cuenta, o el lead del evento no existe. `field` indica cuál.                                                                             | Usa identificadores de tu cuenta; para eventos, registra el lead primero.                                       |
| 409  | `conflict`          | Dos envíos simultáneos con la misma `idempotency_key`.                                                                                                                                        | Reintenta una vez; recibirás el resultado original con `duplicate: true`.                                       |
| 500  | `internal_error`    | Fallo al guardar o al ejecutar. `step` indica el paso: en leads `contact_create_failed`, `deal_create_failed` o `unknown`; en eventos `fire`.                                                 | Reintenta (en eventos, con la misma `idempotency_key`). Si persiste, escribe a soporte con el `step` y la hora. |

Orden de evaluación en ambos endpoints: autenticación (401), cuerpo (413, 400), validación (422), existencia de identificadores (404), proceso (500).

## 5. Límites y comportamiento

* **Tamaño del cuerpo:** 16 KB.
* **Límite de tasa:** no hay. La llave registra su último uso y puede revocarse al instante; ese es el mecanismo para detectar y cortar un abuso. Si vas a enviar volúmenes altos (miles por hora), avísanos antes a [soporte@relevantleads.io](mailto:soporte@relevantleads.io).
* **Recortes silenciosos:** `external_id` e `idempotency_key` a 128, `email` a 254, UTMs a 500, `country_code` a 4 caracteres.
* **Plan:** el registro de leads y los eventos funcionan en todos los planes. No hay validación de plan en estos endpoints.
* **Sin CORS, solo POST, sin sandbox.**
* **Edición de llaves:** una llave no se edita; para cambiar su embudo, etapa u origen, el administrador crea otra y revoca la anterior. La revocación no borra el historial de los leads que entraron con ella.

## 6. Buenas prácticas

* Manda siempre `external_id`: hace seguros los reintentos y es lo que identifica al lead cuando envías eventos.
* Guarda el `deal_id` que te devuelve el registro y úsalo en los eventos: es la forma más precisa y evita ambigüedades si la persona tiene varios leads.
* En eventos, manda siempre `idempotency_key`; construye la clave con algo que identifique la acción de negocio (por ejemplo `pago-<id de tu pago>`), no con la fecha y hora del envío.
* Manda `country_code` explícito si tus usuarios no son solo de México, y el teléfono con exactamente 10 dígitos o ya con lada.
* Usa una llave por sistema. Si necesitas que dos sistemas cuenten como el mismo origen en reportes, dales el mismo texto de origen al crearlas.
* Antes de conectar, pide a quien administra Relevant que pruebe la automatización con el botón **Probar con un lead** y que te pase el nombre exacto del evento.

***

¿Necesitas ayuda? Escríbenos a **[soporte@relevantleads.io](mailto:soporte@relevantleads.io)** y con gusto te apoyamos.
