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 . 5 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
Aplicaciones externas, 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. |
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. 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 56, está en Permisos de la API.
Endpoints
| Método | Ruta | Para qué sirve |
|---|---|---|
| GET | /external/v1/contacts | Listado paginado con los filtros de campaña por defecto (status=ACTIVE, canReceiveEmails=true). |
| GET | /external/v1/contacts/:id | Detalle de un contacto. 404 CONTACT_NOT_FOUND si no existe o es de otra organización. |
| GET | /external/v1/contacts/search?q= | Búsqueda libre por nombre, apellidos y correo. |
| GET | /external/v1/contacts/by-email?email= | Búsqueda exacta por correo. 404 EMAIL_NOT_INDEXED si no hay coincidencia. |
| GET | /external/v1/contacts/sync | Sincronización por cambios con cursor compuesto (updatedAt, id). |
| POST | /external/v1/contacts/:id/unsubscribe | Registra una baja. Devuelve { contact, changed, recordedAt }. |
| POST | /external/v1/contacts/:id/resubscribe | Registra una reactivación con prueba del consentimiento obligatoria. |
Forma de las respuestas:
- Listados:
{ data, pagination: { page, limit, total, totalPages, hasNextPage, hasPreviousPage } }. - Cambios:
{ contact, changed, recordedAt }.changed=falsecuando el contacto ya estaba en el estado pedido. - Sincronización:
{ data, nextCursor, hasMore }.
La sincronización 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.
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).Si el contacto ya estaba en el estado pedido, la respuesta llega con
changed: false y no se registra nada nuevo.
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 o es de otra organización. |
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.