Contactos del CRM
API externa /external/v1/contacts para herramientas de envío de correo: listado, sincronización por cambios, bajas, reactivaciones y avisos CONTACT_*.
Actualizado el . 6 min de lectura
Incluido en todos los planes. Ver precios
Con /external/v1/contacts tu herramienta de envío de correo
(Instantly, Smartlead, Apollo, Mailerlite y similares) lee los contactos
del CRM de UTILIA OS y registra bajas y reactivaciones, de forma
controlada y auditada.
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. UTILIA OS no
guarda la clave en claro (solo su huella) y puedes rotarla desde la misma ficha sin reescribir tu
aplicación. Además del permiso de la API, el plan de tu organización
tiene que incluir el aplicativo CRM; si no, la respuesta es
403 MODULE_NOT_ENABLED.
Permisos de la API (scopes)
| Scope | Qué permite |
|---|---|
crm:contacts:read | Lectura básica: listar, buscar, ver el detalle y sincronizar por cambios. |
crm:contacts:write | Registrar bajas y reactivaciones. |
crm:contacts:read:opted-out | Listar contactos con canReceiveEmails=false o con estado INACTIVE o ARCHIVED, y ver el detalle de cualquiera de ellos. |
crm:contacts:read:secondary-email | Ver secondaryEmail cuando pides ?include=secondaryEmail. |
crm:contacts:read:notes | Ver notes cuando pides ?include=notes. |
crm:contacts:read:birthday | Ver birthDay y birthMonth cuando pides ?include=birthday. |
crm:clients:read | Ver primaryClient y la lista completa clientIds. primaryClientId viaja siempre. |
Sin el permiso necesario, la respuesta es 403 con
errorCode: INSUFFICIENT_SCOPE, salvo al leer el detalle de un contacto
dado de baja o inactivo, que responde 404 (ver Consultar un contacto).
No hay periodo de gracia: el permiso
se exige desde la primera llamada en cualquier aplicación. Los permisos
se conceden en la ficha de la aplicación; cómo hacerlo, y el catálogo
de los 60, está en Permisos de la API.
Endpoints
La clave de API trabaja a nivel de organización: lee cualquier contacto de tu organización, sin el alcance de filas que tenga en el CRM la persona responsable de la aplicación. Cada lectura y cada cambio queda en el registro de auditoría a nombre de la aplicación.
Forma de las respuestas:
- Listados:
{ data, pagination: { page, limit, total, totalPages, hasNextPage, hasPreviousPage } }. - Bajas y reactivaciones:
{ contact, changed, recordedAt }. - Sincronización:
{ data, nextCursor, hasMore }.
Un contacto tiene esta forma (secondaryEmail, notes, birthDay y
birthMonth solo aparecen si los pides con include y tienes su
permiso; primaryClient y clientIds completos, con crm:clients:read):
{
"id": "con-7",
"organizationId": "org-1",
"firstName": "Ana",
"lastName": "Pérez",
"fullName": "Ana Pérez",
"email": "ana@acme.example",
"phone": "+34 928 000 000",
"position": "Directora de compras",
"isPrimary": true,
"canReceiveEmails": true,
"canReceiveSMS": false,
"status": "ACTIVE",
"clientIds": ["cli-42"],
"primaryClientId": "cli-42",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-20T11:30:00.000Z",
"lastContactedAt": null
}Listar y buscar
/api/external/v1/contactsListado paginado con los filtros de campaña por defecto: status=ACTIVE y canReceiveEmails=true. Permiso crm:contacts:read; pedir otros estados o canReceiveEmails=false exige crm:contacts:read:opted-out.
- pagenumber
- Página, desde 1.
- limitnumber
- Contactos por página, de 1 a 100. Por defecto, 25.
- clientIdstring
- Solo los contactos vinculados a ese cliente.
- statusenum[]
- ACTIVE, INACTIVE o ARCHIVED, separados por comas. Por defecto, ACTIVE.
- canReceiveEmailsboolean
- Por defecto, true.
- updatedSincestring
- Solo los cambiados desde esa fecha (ISO 8601).
- sortByenum
- updatedAt (por defecto), createdAt, fullName o email.
- sortOrderenum
- desc (por defecto) o asc.
- includestring
- secondaryEmail, notes o birthday, separados por comas. Cada uno exige su permiso.
/api/external/v1/contacts/searchBúsqueda libre (q, hasta 200 caracteres) por nombre, apellidos y correo, hasta 100 resultados (25 por defecto). Permiso crm:contacts:read. Admite include.
/api/external/v1/contacts/by-emailBúsqueda exacta por correo (email, hasta 320 caracteres), con tiempo de respuesta constante. Permiso crm:contacts:read. 404 EMAIL_NOT_INDEXED si ningún contacto activo lo tiene.
Consultar un contacto
/api/external/v1/contacts/:idDetalle de un contacto. Permiso crm:contacts:read; ?include=secondaryEmail, notes o birthday exige el permiso de cada campo (si no, 403 INSUFFICIENT_SCOPE). 404 CONTACT_NOT_FOUND si no existe, es de otra organización o está dado de baja o inactivo y la aplicación no tiene crm:contacts:read:opted-out.
Un contacto dado de baja o inactivo que tu aplicación no puede leer responde
exactamente igual que uno que no existe, con el mismo cuerpo: un 403 que
nombrara crm:contacts:read:opted-out confirmaría que esa persona existe y se
ha dado de baja de los correos. Si esperabas encontrarlo, pide ese permiso a un
administrador.
Sincronizar por cambios
/api/external/v1/contacts/syncLo que ha cambiado, de lo más antiguo a lo más reciente, con cursor compuesto (updatedAt, id). Permiso crm:contacts:read. Parámetros: updatedAfter (ISO 8601), cursor y limit (de 1 a 200, 100 por defecto).
Funciona igual que la de clientes: lo explica Sincronizar por cambios.
Bajas y reactivaciones
Dar de baja y volver a dar de alta no piden lo mismo: reactivar exige probar que la persona dio su consentimiento.
/api/external/v1/contacts/:id/unsubscribeRegistra la baja del envío de correos: canReceiveEmails pasa a false, se anota en la ficha del contacto y se emite CONTACT_OPTED_OUT. Permiso crm:contacts:write. 409 OPT_OUT_ALREADY_RECORDED si ya estaba dado de baja.
/api/external/v1/contacts/:id/resubscribeRegistra la reactivación con prueba del consentimiento: canReceiveEmails vuelve a true y se emite CONTACT_OPTED_IN. Permiso crm:contacts:write. 422 RESUBSCRIBE_REQUIRES_PROOF sin proof; 409 CONTACT_ALREADY_OPTED_IN si ya podía recibir correos.
Baja y reactivación de un contacto
POST /contacts/:id/unsubscribe
Dar de baja
reason (hasta 500 caracteres) y source, que admite external_app, bounce, complaint o manual.POST /contacts/:id/resubscribe
Reactivar
reason (hasta 500 caracteres) y proof, obligatorio, con type (web_form, double_opt_in, imported o verbal), capturedAt y, si la tienes, sourceUrl (hasta 500 caracteres).Pedir el estado que el contacto ya tiene no registra nada nuevo: responde
409 con OPT_OUT_ALREADY_RECORDED o CONTACT_ALREADY_OPTED_IN, para
que tu herramienta pueda decir «ya estaba dado de baja». La baja y la
reactivación quedan anotadas a nombre de la persona responsable de la
aplicación: sin una responsable activa, la respuesta es
403 APP_RESPONSIBLE_INACTIVE.
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.
Avisos a tu servidor de contacto
Cinco eventos a los que puedes suscribir la aplicación desde su ficha:
| Evento | Cuándo se envía |
|---|---|
CONTACT_CREATED | Se ha creado un contacto. |
CONTACT_UPDATED | Han cambiado campos relevantes. Incluye changes[]. |
CONTACT_DELETED | Baja lógica del contacto (status=INACTIVE). |
CONTACT_OPTED_OUT | canReceiveEmails pasa a false. |
CONTACT_OPTED_IN | canReceiveEmails pasa a true con prueba del consentimiento. |
Si la baja la registró tu propia aplicación, UTILIA OS no le envía el aviso resultante; el resto de aplicaciones suscritas sí lo reciben.
La firma se calcula sobre timestamp.cuerpo, no sobre el cuerpo solo
Cada aviso lleva X-Utilia-Signature y X-Utilia-Timestamp. La firma
es un HMAC-SHA256 de la cadena `${timestamp}.${rawBody}`, con
timestamp igual al valor de la cabecera (milisegundos Unix) y
rawBody el cuerpo crudo tal como llega. Si calculas el HMAC solo sobre
el cuerpo, no coincidirá nunca. El código de verificación completo está
en Firma HMAC.
Errores propios de estos endpoints
| Código | HTTP | Significado |
|---|---|---|
INSUFFICIENT_SCOPE | 403 | A la aplicación le falta el permiso de la API necesario. |
CONTACT_NOT_FOUND | 404 | El contacto no existe, es de otra organización o está dado de baja o inactivo y falta crm:contacts:read:opted-out. |
EMAIL_NOT_INDEXED | 404 | No hay ningún contacto activo con ese correo. |
OPT_OUT_ALREADY_RECORDED | 409 | El contacto ya estaba dado de baja. |
CONTACT_ALREADY_OPTED_IN | 409 | El contacto ya podía recibir correos. |
RESUBSCRIBE_REQUIRES_PROOF | 422 | Falta proof en /resubscribe. |
Límites de uso
Se combinan dos límites: el general de la clave de la aplicación (1.000 peticiones por hora) y uno por endpoint:
| Endpoint | Límite por defecto |
|---|---|
| Lectura general | 600 peticiones por hora |
GET /external/v1/contacts/sync | 60 peticiones por hora |
GET /external/v1/contacts/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.externalContactscon siete métodos:list,get,search,byEmail,sync,unsubscribeyresubscribe. - Python (
utilia-sdk):sdk.external_contacts, en versión asíncrona y síncrona.
Lo detalla la sección SDK.
Datos personales
Cada baja y reactivación queda en el registro de auditoría con la dirección IP y el navegador o cliente que la hizo; los campos sensibles solo salen con su permiso propio; las reactivaciones guardan la prueba del consentimiento; y cada organización solo ve sus propios contactos.
Siguiente paso
Tras la sincronización inicial, suscribe la aplicación a los eventos
CONTACT_* desde su ficha para mantener la lista al día sin consultar
la API cada poco. Para identificar a los usuarios de tu aplicación,
consulta Usuarios externos; la
lista completa de eventos está en
Contenido de los avisos.