Leads del CRM
API externa /external/v1/leads para prospección y sincronización con otro CRM: listado, sincronización por cambios, cualificar, descartar, convertir y avisos LEAD_*.
Actualizado el . 6 min de lectura
Incluido en los planes Profesional y Avanzado. Ver precios
Con /external/v1/leads tu aplicación lee y trabaja los leads del CRM
de UTILIA OS de forma controlada y auditada. Sirve a herramientas de
prospección (Apollo, Cognism, Lusha), a plataformas que sincronizan
con otro CRM (HubSpot, Pipedrive, Salesforce) y a herramientas de
soporte que necesitan ver la captación.
Autenticación
Todas las peticiones llevan la clave de API de la aplicación:
X-Api-Key: <api_key_de_la_app>
La clave se obtiene al dar de alta la aplicación en Aplicaciones externas, necesitas entrar con tu cuenta y puedes rotarla desde su ficha sin reescribir tu aplicación.
Permisos de la API (scopes)
| Scope | Qué permite |
|---|---|
crm:leads:read | Lectura base: listar, buscar, ver el detalle y sincronizar por cambios. |
crm:leads:write | Crear (POST /external/v1/leads), actualizar (PATCH /external/v1/leads/:id), cualificar, descartar y ajustar la puntuación (/score). |
crm:leads:convert | Convertir el lead: crea un contacto y, si lo pides, una oportunidad. No se puede deshacer. |
crm:leads:read:scoring | Ver score y temperature cuando pides ?include=scoring. |
crm:leads:read:notes | Ver notes cuando pides ?include=notes. Dato personal crítico. |
Sin el permiso necesario, la respuesta es 403 con
errorCode: INSUFFICIENT_SCOPE.
crm:leads:convert y crm:leads:read:notes son permisos críticos:
concederlos a una aplicación exige el rol de Super Administrador y un
motivo de al menos 20 caracteres. Además, cada llamada a /convert
exige su propio reason con esa misma longitud mínima.
Endpoints
| Método | Ruta | Para qué sirve |
|---|---|---|
| POST | /external/v1/leads | Crea un lead. Requiere crm:leads:write y al menos email o phone. |
| GET | /external/v1/leads | Listado paginado. Por defecto, leads sin convertir y en estados activos (NEW, CONTACTED, QUALIFIED, NEGOTIATING). |
| GET | /external/v1/leads/:id | Detalle. 404 LEAD_NOT_FOUND si no existe o es de otra organización. |
| GET | /external/v1/leads/search?q= | Búsqueda libre por nombre, apellidos, empresa y correo. |
| GET | /external/v1/leads/by-email?email= | Búsqueda exacta, con tiempo de respuesta constante. 404 EMAIL_NOT_INDEXED_LEAD si no hay coincidencia. |
| GET | /external/v1/leads/sync | Sincronización por cambios con cursor compuesto (updatedAt, id). |
| POST | /external/v1/leads/:id/qualify | Pasa el lead de NEW a QUALIFIED. Desde cualquier otro estado responde 409 (con LEAD_ALREADY_QUALIFIED también si estaba en CONTACTED o NEGOTIATING). |
| POST | /external/v1/leads/:id/disqualify | Pasa el lead a LOST. Es la única baja posible. |
| POST | /external/v1/leads/:id/convert | Convierte el lead en contacto (y en oportunidad, si lo pides). Requiere crm:leads:convert y reason de al menos 20 caracteres. |
| PATCH | /external/v1/leads/:id | Actualiza el lead con bloqueo optimista (expectedUpdatedAt). notes y score solo se aplican con su permiso. |
| POST | /external/v1/leads/:id/score | Atajo para la puntuación. Requiere crm:leads:write y crm:leads:read:scoring. Emite LEAD_SCORE_UPDATED. |
Qué puede cambiar tu aplicación
Desde la API, un lead se cualifica, se descarta o se convierte. Los estados Contactado y Negociando los pone tu equipo desde el CRM.
Estados de un lead desde la API
Nuevo
NEW
Recién creado.Se llega desde Nuevo cuando: disqualify, desde cualquier estado sin convertir
Perdido
LOST
Es la única baja posible.
Se llega desde Nuevo cuando: qualify, solo desde Nuevo
Calificado
QUALIFIED
Desde otro estado, qualify responde 409.Se llega desde Calificado cuando: convert crea el contacto y, si lo pides, la oportunidad
Ganado
WON
Estado final
isConverted pasa a true. No se deshace.
Otras transiciones
- Calificado pasa a Perdido: disqualify también lo descarta.
- Nuevo pasa a Ganado: convert funciona desde cualquier estado que no sea Ganado.
- Perdido pasa a Ganado: convert también acepta un lead perdido.
La sincronización funciona igual que la de clientes: lo explica Sincronizar por cambios.
Cuerpo de las acciones
qualify:reason(hasta 500 caracteres) ysource, que admiteexternal_app,crm_sync,manualoautomation.disqualify:reason(hasta 500 caracteres) ysource, con los mismos valores másbounce.convert:reason(de 20 a 1.000 caracteres),createOpportunity(opcional,falsepor defecto) ytargetClientId(opcional): la ficha de cliente existente a la que vincular el contacto. SintargetClientId, se crea un cliente nuevo con los datos del lead.
Forma de las respuestas:
- Listados:
{ data, pagination: { page, limit, total, totalPages, hasNextPage, hasPreviousPage } }. - Cambios:
{ lead, changed, recordedAt }.changed=falsecuando el lead ya estaba en el estado pedido. - Sincronización:
{ data, nextCursor, hasMore }. - Conversión:
{ lead, contactId, opportunityId?, clientId?, createdAt }.
Los campos personalizados del CRM no salen por esta API. Puedes leerlos y escribirlos por MCP: lo explica el apartado «Campos personalizados del CRM» de la referencia MCP.
Ejemplos
# Listar leads activos
curl -H "X-Api-Key: $KEY" \
"https://os.utilia.ai/api/external/v1/leads?limit=25"
# Cualificar un lead
curl -X POST \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{"reason":"Pidió una demostración y encaja en el perfil","source":"external_app"}' \
"https://os.utilia.ai/api/external/v1/leads/ld-42/qualify"
# Convertir (no se puede deshacer)
curl -X POST \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{"reason":"Aceptó la propuesta económica y empezamos la implantación","createOpportunity":true}' \
"https://os.utilia.ai/api/external/v1/leads/ld-42/convert"// SDK JavaScript
import { UtiliaSDK } from '@utilia-os/sdk-js';
const sdk = new UtiliaSDK({ baseURL: 'https://os.utilia.ai/api', apiKey: KEY });
const page = await sdk.externalLeads.list({ limit: 25 });
await sdk.externalLeads.qualify('ld-42', {
reason: 'Pidió una demostración y encaja en el perfil',
source: 'external_app',
});
const conv = await sdk.externalLeads.convert('ld-42', {
reason: 'Aceptó la propuesta económica y empezamos la implantación',
createOpportunity: true,
});# SDK Python
from utilia_sdk import (
ConvertLeadInput,
ExternalLeadListFilters,
QualifyLeadInput,
UtiliaSDK,
)
async with UtiliaSDK(base_url="https://os.utilia.ai/api", api_key=KEY) as sdk:
page = await sdk.external_leads.list(ExternalLeadListFilters(limit=25))
await sdk.external_leads.qualify(
"ld-42",
QualifyLeadInput(
reason="Pidió una demostración y encaja en el perfil",
source="external_app",
),
)
conv = await sdk.external_leads.convert(
"ld-42",
ConvertLeadInput(
reason="Aceptó la propuesta económica y empezamos la implantación",
create_opportunity=True,
),
)Avisos a tu servidor de lead
Seis eventos a los que puedes suscribir la aplicación desde su ficha. Si el cambio lo hizo tu propia aplicación, no le llega el aviso; al resto de aplicaciones suscritas, sí.
| Evento | Cuándo se envía |
|---|---|
LEAD_CREATED | Se ha creado un lead en el CRM. |
LEAD_UPDATED | Cambian el correo, el estado, la etapa, la puntuación o la persona asignada. Incluye changes[]. |
LEAD_QUALIFIED | El lead pasa a QUALIFIED, con el motivo si la aplicación lo aporta. |
LEAD_DISQUALIFIED | El lead pasa a LOST, con el motivo registrado. |
LEAD_CONVERTED | El lead queda convertido (isConverted=true, status=WON). Incluye contactId y opportunityId. |
LEAD_SCORE_UPDATED | Cambian score o temperature. Solo llega a aplicaciones con crm:leads:read:scoring. |
Errores propios de estos endpoints
| Código | HTTP | Significado |
|---|---|---|
INSUFFICIENT_SCOPE | 403 | A la aplicación le falta el permiso de la API necesario. |
MODULE_NOT_ENABLED | 403 | Tu organización no tiene contratado el aplicativo CRM. Ninguna operación de esta API funcionará. |
CAPABILITY_NOT_ENABLED | 403 | El CRM está contratado, pero la capacidad de leads (crm.leads) no entra en el plan. |
LEAD_NOT_FOUND | 404 | El lead no existe o es de otra organización. |
EMAIL_NOT_INDEXED_LEAD | 404 | No hay ningún lead activo con ese correo. |
LEAD_ALREADY_QUALIFIED | 409 | El lead ya estaba cualificado. |
LEAD_ALREADY_DISQUALIFIED | 409 | El lead ya estaba descartado. |
LEAD_ALREADY_LOST | 409 | Variante del anterior cuando el estado es LOST. |
LEAD_ALREADY_CONVERTED | 409 | El lead ya se había convertido. |
LEAD_UPDATE_CONFLICT | 409 | Bloqueo optimista: el lead cambió después de la lectura que indica expectedUpdatedAt. |
LEAD_CONVERSION_REQUIRES_REASON | 422 | Falta reason o tiene menos de 20 caracteres. |
Tres 403 distintos, y solo uno se arregla con permisos
Delante de /external/v1/leads hay dos puertas comerciales además de
la de permisos: el aplicativo CRM tiene que estar contratado y la
capacidad crm.leads tiene que entrar en el plan de tu
organización. Las tres responden 403; solo las distingue el campo
errorCode del cuerpo.
Conceder permisos a la aplicación no desbloquea un MODULE_NOT_ENABLED
ni un CAPABILITY_NOT_ENABLED: eso solo cambia cuando tu organización
amplía su plan, y reintentar tampoco sirve. El cuerpo exacto de cada
uno y cómo detectarlos desde los SDK están en
Errores de plan.
Límites de uso
| Endpoint | Límite por defecto |
|---|---|
| Lectura general | 600 peticiones por hora |
GET /external/v1/leads/sync | 60 peticiones por hora |
GET /external/v1/leads/by-email | 300 peticiones por hora |
Al superar un límite, la API responde 429 con la cabecera
Retry-After, que indica los segundos que faltan para que se renueve.
Con los SDK oficiales
- JavaScript (
@utilia-os/sdk-js):sdk.externalLeadscon once métodos:list,get,search,byEmail,sync,qualify,disqualify,convert,create,updateyupdateScore. - Python (
utilia-sdk):sdk.external_leadscon los mismos once métodos, en versión asíncrona y síncrona.
Las versiones de los dos SDK están en un único sitio: Versiones y compatibilidad. Consúltalo antes de fijar la dependencia de tu proyecto.
Decisiones de producto
- No hay
DELETEsobre leads desde fuera: la única baja es/disqualify, que pasa el lead aLOST. /convertno se puede deshacer: crea un contacto y, si lo pides, una oportunidad.- Las notas internas del lead solo salen con su permiso propio (
crm:leads:read:notes).
Cada cambio queda en el registro de auditoría con la dirección IP y el cliente que lo hizo, y cada organización solo ve sus propios leads.
Siguiente paso
Tras integrar la lectura, suscribe la aplicación a los eventos LEAD_*
desde su ficha para mantener la captación sincronizada sin consultar la
API cada poco. Para trabajar también con la ficha de cliente vinculada,
consulta Clientes del CRM; el
catálogo completo de eventos está en
Contenido de los avisos.