Leads del CRM
La API externa de leads para prospección y sincronización con otro CRM: listado, cambios, cualificar, descartar, convertir y avisos LEAD_*.
Actualizado el . 8 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 Ajustes › Creadas por tu empresa, 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
La clave de API trabaja a nivel de organización: lee y cambia
cualquier lead de tu organización, sin el alcance de filas que tenga en
el CRM la persona responsable de la aplicación. Lo que la API crea
(leads, contactos, clientes, oportunidades) queda a nombre de esa persona
responsable; sin una responsable activa, la respuesta es
403 APP_RESPONSIBLE_INACTIVE.
Forma de las respuestas:
- Listados:
{ data, pagination: { page, limit, total, totalPages, hasNextPage, hasPreviousPage } }. - Alta:
{ lead, recordedAt }, con201. - Cambios (actualización, puntuación, cualificar, descartar):
{ lead, changed, recordedAt }. - Conversión:
{ lead, contactId, opportunityId, clientId, changed, recordedAt }. - Sincronización:
{ data, nextCursor, hasMore }.
Un lead tiene esta forma (score y temperature solo con
?include=scoring y crm:leads:read:scoring; notes, con
?include=notes y crm:leads:read:notes):
{
"id": "ld-42",
"organizationId": "org-1",
"firstName": "Luis",
"lastName": "Santana",
"fullName": "Luis Santana",
"companyName": "Talleres Santana",
"email": "luis@talleres.example",
"phone": "+34 928 000 002",
"position": "Gerente",
"source": "Apollo",
"status": "NEW",
"stage": "INITIAL_CONTACT",
"priority": "MEDIUM",
"isConverted": false,
"convertedToContactId": null,
"createdAt": "2026-10-19T08:00:00.000Z",
"updatedAt": "2026-10-19T08:00:00.000Z",
"lastContactedAt": null,
"convertedAt": null
}Crear un lead
/api/external/v1/leadsCrea un lead en estado NEW. Permiso crm:leads:write. Hace falta al menos email, phone o mobile (si no, 400 VALIDATION_ERROR). Responde 201 con { lead, recordedAt } y emite LEAD_CREATED.
- firstName / lastNamestring
- Nombre y apellidos, hasta 100 caracteres cada uno. fullName se compone solo.
- emailstring
- Correo válido, hasta 255 caracteres.
- phone / mobilestring
- Teléfonos, hasta 50 caracteres.
- companystring
- Empresa, hasta 200 caracteres. En la respuesta sale como companyName.
- positionstring
- Cargo, hasta 150 caracteres.
- sourcestring
- Origen (campaña, canal, fuente), hasta 100 caracteres.
- notesstring
- Notas, hasta 5.000 caracteres. Solo se guardan con crm:leads:read:notes; sin él se descartan sin error.
- scorenumber
- Puntuación inicial, de 0 a 100. Solo se guarda con crm:leads:read:scoring; sin él se descarta sin error.
- assignedToUserIdstring
- Persona de tu organización a la que asignarlo. Si no existe o no está activa, 422 USER_NOT_FOUND.
Listar y buscar
/api/external/v1/leadsListado paginado. Por defecto, leads sin convertir en estado NEW, CONTACTED, QUALIFIED o NEGOTIATING, 25 por página (hasta 100). Permiso crm:leads:read.
- page / limitnumber
- Página desde 1 y leads por página, de 1 a 100 (25 por defecto).
- statusenum[]
- NEW, CONTACTED, QUALIFIED, NEGOTIATING, WON, LOST o ARCHIVED, separados por comas.
- stageenum[]
- INITIAL_CONTACT, NEEDS_ASSESSMENT, PROPOSAL_SENT, NEGOTIATION o CLOSING.
- priorityenum[]
- LOW, MEDIUM, HIGH o URGENT.
- leadTypeenum[]
- LEAD_MAGNET, DEMO_REQUEST, CONTACT_FORM, REFERRAL, EVENT, COLD_OUTREACH, INBOUND_CALL u OTHER.
- temperatureenum[]
- COLD, WARM o HOT.
- updatedSincestring
- Solo los cambiados desde esa fecha (ISO 8601).
- sortBy / sortOrderenum
- updatedAt (por defecto), createdAt, fullName o score; desc (por defecto) o asc.
- includestring
- scoring o notes, separados por comas. Cada uno exige su permiso.
/api/external/v1/leads/searchBúsqueda libre (q, hasta 200 caracteres) por nombre, apellidos, empresa y correo, hasta 100 resultados (25 por defecto). Permiso crm:leads:read. Admite include.
/api/external/v1/leads/by-emailBúsqueda exacta por correo (email, hasta 320 caracteres), con tiempo de respuesta constante. Permiso crm:leads:read. 404 EMAIL_NOT_INDEXED_LEAD si ningún lead activo lo tiene.
Consultar un lead
/api/external/v1/leads/:idDetalle de un lead. Permiso crm:leads:read; ?include=scoring o notes exige su permiso. 404 LEAD_NOT_FOUND si no existe o es de otra organización.
Sincronizar por cambios
/api/external/v1/leads/syncLo que ha cambiado, de lo más antiguo a lo más reciente, con cursor compuesto (updatedAt, id). Permiso crm:leads:read. Parámetros: updatedAfter (ISO 8601), cursor, limit (de 1 a 200, 100 por defecto) e include.
Funciona igual que la de clientes: lo explica Sincronizar por cambios.
Actualizar un lead
/api/external/v1/leads/:idActualiza los campos que mandes, con bloqueo optimista (expectedUpdatedAt: si el lead cambió después, 409 LEAD_UPDATE_CONFLICT). Permiso crm:leads:write. notes y score solo se aplican con su permiso de lectura. Responde { lead, changed, recordedAt }.
Admite los mismos campos que el alta, todos opcionales, y
expectedUpdatedAt. Un campo que omites no se toca.
/api/external/v1/leads/:id/scoreAtajo para cambiar la puntuación (score de 0 a 100, reason opcional de hasta 500 caracteres y expectedUpdatedAt). Exige 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.
/api/external/v1/leads/:id/qualifyPasa el lead de NEW a QUALIFIED. Permiso crm:leads:write. Desde otro estado, 409 (LEAD_ALREADY_QUALIFIED también si estaba en CONTACTED o NEGOTIATING). Emite LEAD_QUALIFIED.
/api/external/v1/leads/:id/disqualifyPasa el lead a LOST: es la única baja posible por la API. Permiso crm:leads:write. 409 LEAD_ALREADY_DISQUALIFIED o LEAD_ALREADY_LOST si ya lo estaba. Emite LEAD_DISQUALIFIED.
/api/external/v1/leads/:id/convertConvierte el lead: crea el contacto, lo vincula a un cliente (el de targetClientId o uno nuevo) y, si lo pides, una oportunidad. No se deshace. Exige crm:leads:convert y reason de 20 a 1.000 caracteres. Emite LEAD_CONVERTED.
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 (si no existe,404 CLIENT_NOT_FOUND). SintargetClientId, se crea un cliente nuevo con los datos del lead. ConcreateOpportunity: true, el plan tiene que incluir las oportunidades (crm.opportunities); si no,403 CAPABILITY_NOT_ENABLEDy no se convierte nada.
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. |
USER_NOT_FOUND | 422 | Al crear o actualizar: la persona de assignedToUserId no existe en tu organización o no está activa. |
CLIENT_NOT_FOUND | 404 | Al convertir: el cliente de targetClientId no existe o es de otra organización. |
APP_RESPONSIBLE_INACTIVE | 403 | Al crear, actualizar o convertir: la aplicación no tiene una persona responsable activa. |
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.