Guía para integradores (ES/EN). Estado: v1, disponible. Las novedades se añaden sin romper lo que ya funciona.
- API de tareas y funciones (función
api.tasks): para conectar herramientas de tareas (Jira, Asana, ClickUp, Linear, Monday, Trello, Notion…) y leer, modificar y ejecutar el trabajo de todas las funcionalidades. Tiene su propia guía para integradores: /developers/api-v1-tareas.html, con scopes y paquetes, actor y delegación (X-AIO-Act-As),dryRun, idempotencia, feed de cambios, webhooks y recetas. Su catálogo estático de tipos y acciones está enhttps://www.routerclip.com/developers/api-v1-task-kinds.json. / Task and function API (api.tasksfeature): guide for integrators in /developers/api-v1-tareas.html; static catalog of kinds and actions at/developers/api-v1-task-kinds.json. - Contrato OpenAPI (generado de las rutas reales, filtrado a
/api/v1):https://www.routerclip.com/developers/api-v1.json. En producción/openapi.jsony/docsno están disponibles. - Documentación publicada / Published documentation: esta guía, la de la API de tareas, el contrato y el catálogo están en
https://www.routerclip.com/developers/, enlazados desde Integraciones → API y eventos, desde el panel de la API de tareas y desde el pie de la web pública. / This guide, the task API guide, the contract and the catalog are published at/developers/.
Español
1. Qué es
La API v1 conecta los servidores de tu empresa (ERP, tienda, backend, CRM anterior) con RouterClip. Es de servidor a servidor: no tiene CORS y la clave nunca debe estar en un navegador, en una app móvil, en un gestor de etiquetas (GTM) ni en una URL. Para medir lo que pasa en tu web se usa el script aiocrm.js de un sitio verificado, que nunca lleva claves.
2. Activación, fuentes y claves
- El equipo de RouterClip activa la función «API y eventos» (
marketing.api) para tu empresa. - Un administrador crea una fuente por sistema en Ajustes → Integraciones → API y eventos. La fuente decide qué puede hacer la integración: tipos de evento permitidos, campos de contacto, contactos a los que llega (por defecto, solo los vinculados a ella) y recorridos.
- En la fuente emite una clave con sus permisos (scopes). Se muestra una sola vez: guárdala en el gestor de secretos de tu integración. Puede haber dos claves activas por fuente para rotarlas sin cortes; caducidad opcional de hasta 365 días (con avisos a 14 y 3 días). Revocar una clave o desactivar la fuente surte efecto en la petición siguiente.
| Scope | Permite |
|---|---|
events:write | Enviar eventos de los tipos permitidos en la fuente y leer los recibos de su propia fuente |
events:read | Leer los recibos de su propia fuente y, con la función api.tasks, el feed de eventos de la empresa (GET /api/v1/events y GET /api/v1/event-types, §6 bis) de las áreas que la clave puede leer (<área>:read) |
events:import | Cargar histórico por lotes (POST /api/v1/events/import, §5 bis) de los tipos permitidos en la fuente, sin efectos. events:write no lo incluye: lo concede un administrador al emitir la clave |
contacts:read | Leer contactos que la fuente alcanza (GET /api/v1/contacts/{id}, POST /api/v1/contacts/lookup), listarlos por cursor (GET /api/v1/contacts, filtros ids y updatedSince), buscarlos (POST /api/v1/contacts/search, q solo en el cuerpo) y leer sus notas (GET /api/v1/contacts/{id}/notes: texto y autor), solo con los campos de lectura de la fuente (§8). Listado, búsqueda y notas exigen además la función api.tasks |
contacts:write | Crear y actualizar contactos (POST /api/v1/contacts, PATCH /api/v1/contacts/{id}) con los campos, etiquetas y campos personalizados permitidos en la fuente, y añadirles notas (POST /api/v1/contacts/{id}/notes, con la función api.tasks y como un usuario delegado: requiere además act_as y X-AIO-Act-As o el usuario por defecto de la fuente) (§8) |
subscriptions:invite | Enviar la invitación de doble opt-in con la conexión SMTP elegida en la fuente (§9). Nunca concede permiso |
automations:enroll | Ver los recorridos que la fuente permite (GET /api/v1/automations) e inscribir contactos que alcanza en los que tienen disparador «Desde una integración (API)» (POST /api/v1/automations/{id}/enroll, §10) |
identity:link | Pedir tokens de un solo uso para vincular el navegador o la suscripción push de un cliente con su ficha, solo en los sitios web permitidos en la fuente (POST /api/v1/identity/link-tokens, §10 bis). Nunca viene marcado: lo concede un administrador y solo si la función marketing.identity está activa |
API de tareas y funciones (función api.tasks; detalle, paquetes y actor en /developers/api-v1-tareas.html). Los scopes son <área>:<nivel> y los niveles no se heredan: read (leer), write (crear y editar), execute (acciones operativas), admin (acciones de administrador; exigen actuar como un administrador) y content (cuerpos, notas internas y transcripciones). Además de contacts:read, contacts:write, subscriptions:invite y automations:enroll, ya descritos:
| Scope | Permite |
|---|---|
companies:read, companies:write | Empresas cliente, sus contactos, notas e historial |
deals:read, deals:write, deals:execute | Oportunidades, pipelines (lectura) y sus acciones (ganar, perder, archivar…) |
activities:read, activities:write, activities:execute | Actividades y seguimientos (crear, editar, completar, reprogramar…) |
sequences:read, sequences:execute | Secuencias comerciales, inscripciones, buzón y respuestas comerciales (función crm.sequences) |
playbooks:read, playbooks:write, playbooks:execute | Guiones comerciales y sesiones (función crm.playbooks) |
duplicates:read, duplicates:write, duplicates:admin | Duplicados, fusión y deshacer (función crm.merge) |
support:read, support:write, support:execute, support:admin, support:content | Conversaciones, tickets y SLA, menciones, recados y avisos, macros y disponibilidad |
csat:read, csat:execute, csat:admin | Encuestas e invitaciones CSAT |
voice:read, voice:execute, voice:admin, voice:content | Llamadas, trabajos de voz y capacidad (nunca el audio) |
calendar:read, calendar:execute | Calendarios, disponibilidad y reservas |
campaigns:read, campaigns:write, campaigns:admin | Campañas de email, SMS y push, plantillas, revisión de entregas y experimentos |
journeys:read, journeys:admin | Recorridos, inscripciones, versiones y puntuación |
segments:read | Segmentos y vista previa (solo recuentos) |
forms:read, forms:admin | Formularios, páginas y colocaciones |
sites:read, sites:admin | Sitios verificados y dominios propios |
conversions:read, conversions:admin | Conversiones y su conciliación (función marketing.tracking) |
knowledge:read, knowledge:admin | Documentos y fuentes de conocimiento |
channels:read, channels:admin | Estado de canales, conexiones y dominios de correo |
reports:read | Informes en JSON (cada uno exige además su función) |
team:read, team:emails | Miembros (id, nombre y rol) y, aparte, su email |
webhooks:manage | Webhooks de tareas de la propia fuente y reintento de webhooks fallidos |
automation:read, automation:enroll | Flujos de automatización (estado, disparador y versión en uso, nunca su configuración) y sus ejecuciones; automation:enroll inicia los flujos con disparador «Por la API» (función automation.flows, ver /developers/api-v1-tareas.html). automation:enroll no es automations:enroll (recorridos, §10) y no está en ningún paquete |
act_as | Actuar como un usuario de la lista «Actuar como» de la fuente (cabecera X-AIO-Act-As) |
Los scopes *:admin, *:content, team:emails y act_as son sensibles: la pantalla pide confirmarlos. La UI ofrece paquetes predefinidos: lectura, gestión, operación completa y personalizado.
Comprueba una clave sin efectos: GET /api/v1/key.
3. Autenticación, respuestas y errores
Authorization: Bearer aio_<keyId>.<secreto>
Content-Type: application/json
X-Request-ID: erp-7f3c1a2b (opcional)
- Toda respuesta de
/api/v1/llevaX-Request-ID: el tuyo si es válido (8-128 caracteresA-Z a-z 0-9 . _ : -) o uno nuevo. Cítalo al pedir soporte. - Los errores tienen la forma
{"detail": {"code": "CODIGO", "message": "…", "requestId": "…", "field": "ruta.del.campo"}}.fieldindica dónde está el problema; los valores enviados nunca se repiten. El código es estable; el mensaje puede cambiar.
| HTTP | Cuándo | Qué hacer |
|---|---|---|
200 | Lectura o actualización de un contacto; página de un listado o búsqueda de contactos o de sus notas; alta vinculada a un contacto existente (result: "linked"); inscripción no realizada con su motivo (result: "skipped"); lote de importación procesado, con un resultado por evento (§5 bis) | Guardar el ETag si vas a actualizarlo; con skipped, no reintentar igual; en la importación, revisar cada results[i] |
201 | Contacto creado (result: "created", cabeceras Location y ETag); contacto inscrito en un recorrido (result: "enrolled"); token de vínculo emitido (§10 bis); nota añadida a un contacto (§8) | Guardar el id o el enrollmentId; pasar el token a la página sin guardarlo |
202 | Evento y entregas guardados, o duplicado del mismo evento (duplicate: true); invitación encolada | Guardar el id del recibo |
400 | Cursor de un listado o búsqueda alterado, caducado o de otra consulta (CURSOR_INVALID, §8) | Empezar de nuevo sin cursor |
401 | Clave ausente, inválida, caducada o revocada, fuente desactivada, empresa inactiva | No reintentar; revisar la clave |
402 | La empresa no tiene acceso al servicio | No reintentar hasta regularizar |
403 | Falta el scope, la función está apagada, falta la base jurídica (LEGAL_BASIS_REQUIRED), o el tipo, campo, etiqueta, campo personalizado o sitio web (SITE_NOT_ALLOWED) no está permitido en la fuente | Revisar la fuente o la clave |
404 | El recibo, el contacto o el recorrido no es de la fuente de la clave (o la fuente no lo alcanza o no lo permite) | — |
409 | Mismo eventId o Idempotency-Key con otro contenido, identidades que no concuerdan, identidad ya usada o ambigua, ficha fusionada (MERGED_RECORD), capacidad alcanzada, recorrido sin disparador API (AUTOMATION_TRIGGER_NOT_API), contacto opuesto al perfilado (CONTACT_OPTED_OUT) | Corregir; no reintentar igual |
412 | If-Match no es la revisión actual del contacto (REVISION_CONFLICT, con currentRevision) | Leer de nuevo y reintentar |
413 | Cuerpo de más de 32 KiB (1 MiB en POST /api/v1/events/import) | Reducir el cuerpo o dividir el lote |
415 | Falta Content-Type: application/json | Corregir la cabecera |
422 | Cuerpo inválido (ver códigos en §5 y §8) | Corregir; no reintentar igual |
428 | PATCH sin If-Match (PRECONDITION_REQUIRED) | Enviar el ETag leído |
429 | Cuota agotada (Retry-After) | Esperar lo indicado y reintentar con el mismo eventId o Idempotency-Key |
503 | Recepción detenida temporalmente por la plataforma (Retry-After) | Esperar y reintentar con el mismo eventId |
4. Cuotas
Límites técnicos (no comerciales), aplicados antes de escribir; ninguna petición queda aceptada a medias:
- 120 peticiones por minuto por clave y 600 por empresa (una clave nueva no reinicia la de empresa);
- 50.000 eventos nuevos por empresa y día UTC; un duplicado no consume esta cuota;
- importación histórica (
events:import): 200.000 eventos importados por empresa y día UTC, aparte de los 50.000 en vivo; cada lote cuenta como una petición en las cuotas por minuto y un duplicado no consume (§5 bis); - invitaciones de doble opt-in: las mismas cuotas que el formulario público (200 por hora y empresa, 3 por día y dirección, compartidas con él);
- tokens de vínculo (
identity:link): 60 por minuto y clave; - las credenciales inválidas se frenan por dirección IP.
5. Enviar un evento: POST /api/v1/events
Un evento por petición (sin lotes). Requiere events:write.
{
"eventId": "erp-order-842-paid-v1",
"eventType": "order.paid",
"schemaVersion": 1,
"occurredAt": "2026-09-28T10:15:30Z",
"subject": { "externalId": "customer_872" },
"data": { "orderId": "order_842", "paymentId": "payment_603", "amountMinor": 12990, "currency": "EUR" }
}
| Campo | Regla |
|---|---|
eventId | Obligatorio. Tu identificador estable del hecho, 1-160 caracteres ASCII visibles, único por fuente. Los reintentos usan el mismo. |
eventType | conversion.reported, order.paid, order.refunded o custom, y solo si la fuente lo permite (si no, 403 EVENT_TYPE_FORBIDDEN). Los tipos internos (contact.*, deal.*, web.*…) dan 422 EVENT_TYPE_NOT_ALLOWED; los reservados (cart.*, mail.*…), 422 EVENT_TYPE_RESERVED. |
schemaVersion | Opcional; solo 1. |
occurredAt | Obligatorio. RFC 3339 en UTC (Z o +00:00). Hasta 5 minutos en el futuro y 30 días en el pasado (EVENT_OCCURRED_IN_FUTURE, EVENT_OCCURRED_TOO_OLD). Para hechos anteriores usa la importación histórica (§5 bis). |
subject | Obligatorio (ver abajo). |
data | Obligatorio; esquema cerrado del tipo: cualquier campo desconocido da 422 EVENT_FIELD_NOT_ALLOWED. |
context | Opcional; solo site (ver abajo). |
Campos que fija el servidor y que se rechazan si los envías: empresa, fuente, confianza (trust; financialVerified da EVENT_TRUST_RESERVED), modo, fecha de recepción, verificación.
Datos por tipo
| Tipo | data |
|---|---|
order.paid | orderId, paymentId e importe (obligatorios). Un pedido puede tener varios pagos. |
order.refunded | orderId, refundId, importe (mayor que cero, el de este reembolso) y full (true si el pedido queda reembolsado del todo); paymentId opcional. |
conversion.reported | conversionId y conversionType ([a-z0-9_.], p. ej. demo_request); orderId, opportunityId (una oportunidad de tu empresa en RouterClip) e importe opcionales. Nunca marca una oportunidad como ganada. |
custom | name ([a-z0-9_.]{1,48}) y properties opcionales: hasta 30 propiedades planas (texto ≤500, números, booleanos, null o listas de hasta 20 valores). Nombres con marcas de credencial (token, secret, password…) se rechazan (EVENT_PAYLOAD_CREDENTIAL). |
Los identificadores (orderId, paymentId, refundId, conversionId, opportunityId) tienen 1-64 caracteres ASCII visibles y no pueden ser un email (EVENT_REFERENCE_PERSONAL).
Importes. amount como cadena decimal ("129.90") o amountMinor como entero en unidades menores (12990), siempre con currency ISO 4217. Nunca números con decimales en JSON (MONEY_FLOAT_NOT_ALLOWED); sin redondeos (MONEY_PRECISION); sin negativos. Se admiten divisas de 0, 2 y 3 decimales (JPY, EUR, KWD…). Se guarda {"amount": "129.90", "currency": "EUR"} y el recibo devuelve amount, amountMinor y exponent. Son importes declarados por tu integración, nunca pagos verificados.
Sujeto.
contactId: ID de un contacto de RouterClip que la fuente puede alcanzar (por defecto, uno vinculado a la fuente). Si fue fusionado, se usa la ficha que se conserva.externalId: tu ID de cliente vinculado a un contacto en esta fuente (1-128 caracteres ASCII visibles; nunca un email ni un teléfono:EXTERNAL_ID_EMAIL_SHAPE,EXTERNAL_ID_PHONE_SHAPE).- Si envías varios, deben identificar al mismo contacto: si no,
409 SUBJECT_IDENTITY_CONFLICT. customexige un contacto resuelto (422 SUBJECT_UNRESOLVED). Una conversión o un pedido con unexternalIdaún sin vincular se guardan sin contacto y no se vinculan después.anonymousId: solo en conversiones y pedidos, y solo concontext.site. Es el identificador de visitante que tu página obtiene conaiocrm.push(['getVisitorId', cb])y pasa a tu servidor. Sirve de referencia de atribución en ese sitio; no identifica a ningún contacto. RouterClip guarda solo su HMAC. Excepción (visitantes vinculados,marketing.identity): si la página vinculó ese navegador conaiocrm.push(['identify', token])antes deoccurredAt, el evento resuelve esa ficha cuando no envías otra identidad; entoncescontext.sitedebe estar entre los sitios permitidos de tu fuente (422 ANONYMOUS_ID_SITE_NOT_ALLOWED).
Contexto. context.site es el ID público (st_…) de un sitio activo y verificado de tu empresa. Cualquier otra clave, incluido attributionToken, da 422 CONTEXT_FIELD_NOT_ALLOWED: el sobre de atribución de los enlaces nunca se envía a esta API.
Ejemplo de pedido declarado desde el servidor de tu web:
curl -sS https://www.routerclip.com/api/v1/events \
-H "Authorization: Bearer $AIOCRM_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: checkout_ORDER-42_paid" \
-d '{"eventId":"checkout_ORDER-42_paid","eventType":"order.paid","occurredAt":"2026-09-28T10:20:00Z",
"subject":{"anonymousId":"5f0c2a9e-3b7d-4c1a-9e55-0d7c1b2a6f10"},
"data":{"orderId":"ORDER-42","paymentId":"pay_42","amountMinor":12900,"currency":"EUR"},
"context":{"site":"st_AbCdEfGhIjKlMnOpQrStUvWx"}}'
5 bis. Importar histórico: POST /api/v1/events/import
Para cargar hechos pasados (pedidos, reembolsos y eventos propios de tu ERP o de tu CRM anterior) sin que provoquen nada. Requiere el scope events:import, que events:write no incluye: un administrador lo marca al emitir la clave. Sigue la función marketing.api como el resto de la API.
curl -sS https://www.routerclip.com/api/v1/events/import \
-H "Authorization: Bearer $AIOCRM_API_KEY" -H "Content-Type: application/json" \
-d '{"events":[
{"eventId":"erp-order-512-paid-v1","eventType":"order.paid","occurredAt":"2026-03-02T09:30:00Z",
"subject":{"externalId":"customer_872"},
"data":{"orderId":"order_512","paymentId":"payment_377","amountMinor":4990,"currency":"EUR"}},
{"eventId":"erp-order-512-refund-1","eventType":"order.refunded","occurredAt":"2026-03-10T16:00:00Z",
"subject":{"externalId":"customer_872"},
"data":{"orderId":"order_512","refundId":"refund_12","amount":"49.90","currency":"EUR","full":true}}]}'
- Lote.
{"events": [...]}con 1 a 500 eventos y como máximo 1 MiB (solo esta ruta; el resto de/api/v1mantiene 32 KiB). Más de 500:422 IMPORT_TOO_MANY_EVENTS; más de 1 MiB:413. Otros errores del lote completo:415,422 IMPORT_BODY_INVALID,IMPORT_FIELD_NOT_ALLOWED(solo se admiteevents),IMPORT_EVENTS_REQUIREDeIDEMPOTENCY_KEY_NOT_SUPPORTED(cada evento ya es idempotente por sueventId). - Cada evento sigue el contrato de
POST /api/v1/events(§5): sobre cerrado, tipos permitidos en la fuente (conversion.reported,order.paid,order.refunded,custom), sujeto, contexto e importes. Cambia solo la fecha:occurredAtdebe caer dentro de la retención de datos de tu empresa (12 meses por defecto) y como mucho 5 minutos en el futuro; fuera, se rechaza solo ese evento conEVENT_OCCURRED_OUT_OF_RETENTION(conretentionMonths). El límite de 30 días del envío en vivo no se aplica. - Sin efectos. Los eventos se guardan con
mode: "import"y sin entregas: no inscriben en recorridos, no despiertan esperas, no crean conversiones ni atribuciones, no puntúan y no envían nada. Su recibo (GET /api/v1/events/{id}) muestramode: "import"ydeliveries: []. Sí cuentan en los segmentos de compras declaradas por integración, cuya vista previa lo indica: «incluye histórico importado» (purchaseCoverage.includesImportedeimportedSince). - Respuesta
200con un resultado por evento, en orden:{"mode": "import", "received", "accepted", "duplicates", "rejected", "retention": {"months", "oldestOccurredAt"}, "dailyQuota": {"limit", "used"}, "results": [...]}. Cadaresults[i]llevaindex, tueventIdystatus:accepted: guardado; conid,eventType,occurredAt,mode,contactResolvedystatusUrl.duplicate: eseeventIdde la fuente ya se había recibido (en vivo o importado) con el mismo cuerpo normalizado; devuelve suidy sumode. No cuesta cuota.rejected: concode,message,httpStatus(el que respondería el envío en vivo: 403, 409, 422 o 429) yfield. Por ejemploEVENT_TYPE_FORBIDDEN,IDEMPOTENCY_CONFLICT(mismoeventIdcon otro cuerpo),SUBJECT_UNRESOLVED,MONEY_FLOAT_NOT_ALLOWED,EVENT_OCCURRED_OUT_OF_RETENTIONoRATE_LIMITED.
- Cada evento se procesa por separado: uno rechazado no deja rastro ni anula los demás. Reintentar un lote entero es seguro: lo ya aceptado vuelve como
duplicate. - Cuota diaria propia: 200.000 eventos importados nuevos por empresa y día UTC, aparte de los 50.000 en vivo. Al llenarse, los eventos nuevos restantes del lote se rechazan con
RATE_LIMITED(quota: "companyImportsPerDay") y la respuesta llevaRetry-After; los duplicados siguen respondiendo.503 INGEST_PAUSEDsi la plataforma detiene la recepción: reintenta el mismo lote más tarde. - Un evento importado enviado después por
POST /api/v1/eventscon el mismoeventIdes el mismo hecho (duplicate: true,mode: "import"): importar no vuelve a disparar nada en vivo.
6. Recibo: 202 y GET /api/v1/events/{id}
{
"id": "5f2c9e0b7d8a4c1e9b3f6a2d4e8c1b7a",
"eventId": "erp-order-842-paid-v1",
"eventType": "order.paid",
"schemaVersion": 1,
"status": "accepted",
"duplicate": false,
"occurredAt": "2026-09-28T10:15:30Z",
"receivedAt": "2026-09-28T10:15:31Z",
"trust": "clientDeclared",
"mode": "live",
"contactResolved": true,
"normalization": { "status": "normalized", "money": { "amount": "129.90", "amountMinor": 12990, "exponent": 2, "currency": "EUR" } },
"attribution": { "basis": "contact", "site": null },
"deliveries": [ { "consumer": "marketing.journeys", "status": "pending", "attempts": 0, "lastError": null, "completedAt": null } ],
"statusUrl": "/api/v1/events/5f2c9e0b7d8a4c1e9b3f6a2d4e8c1b7a"
}
202significa recepción durable: el evento y sus entregas están guardados. No significa que un recorrido haya empezado, que se haya enviado nada ni que la atribución esté hecha.deliverieses el recibo de cada consumidor activo en tu empresa (pending,processing,done,failed,skipped), sin datos del evento. Puede estar vacío si no hay ninguno activo.doneno significa mensaje entregado.attribution.basis:browser(visitante decontext.site),contact(clics de las campañas enviadas al contacto) onone. La decisión la toma después el consumidor de atribución.GET /api/v1/events/{id}(conevents:readoevents:write) devuelve el mismo recibo, solo para eventos de la fuente de la clave (404 EVENT_NOT_FOUNDen otro caso). La respuesta202incluye la cabeceraLocationcon esa URL.- El recibo nunca incluye el contenido del evento (identidades, propiedades, referencias).
6 bis. Feed de eventos: GET /api/v1/events y GET /api/v1/event-types
Lee los hechos de toda la empresa (contactos, oportunidades, conversaciones, tickets, SLA, CSAT, seguimientos, secuencias, recorridos, formularios, conversiones…) en orden, con el mismo cuerpo «despertador» que los webhooks de empresa: solo IDs, tipo y versión. Después lees el registro con tu clave.
- Requisitos: scope
events:read, la funciónapi.tasksy, por cada tipo, el scope de lectura de su área con la función del área activa (deals:readparadeal.*,support:readparaconversation.*,ticket.*ysla.*,contacts:readparacontact.*ycustom…). Con soloevents:readlas páginas salen vacías, sin error. La visibilidad la deciden los scopes de área, nocontactScope. - Catálogo:
GET /api/v1/event-typeslista cada tipo con su área, scope y función, si tu clave lo puede leer ya (available), si es un hecho de atención que solo se registra mientras alguien lo quiere (recordedWhenWanted) y con qué ruta se lee cada agregado (readWith, p. ej.opportunity→/api/v1/deals/{id},marketing_site→/api/v1/sites/{id},marketing_signup_form→/api/v1/forms/{id}). Los agregados sin ruta de detalle por ese ID salen enaggregatesWithoutDetailcon el motivo:listOnly(búscalo enlistPath:bookingen/api/v1/bookings,pipelineen/api/v1/pipelines),externalReference(conversionyorder: el ID es tu propioconversionIduorderIddel evento; la conversión atribuida está en/api/v1/conversions) onoResource(journey_enrollment,marketing_link: lee elsubject).task.*no está (usaGET /api/v1/task-changes) ni los hechos internos del motor (automation.*). - Petición:
GET /api/v1/events?types=deal.won,deal.lost&after=<cursor>&limit=100(limit1-500, 100 por defecto;typeshasta 30). Sinafterempieza por el hecho más antiguo conservado. Pedir un tipo de un área que la clave no lee responde403 API_SCOPE_REQUIRED(conrequiredScopes) oFEATURE_DISABLED; un tipo desconocido o fuera del feed,422 VALIDATION_ERROR. - Respuesta:
{"items": [...], "nextCursor": "c1.…", "hasMore": false}. Cada elemento es{id, type, schemaVersion, occurredAt, subject: {type, id} | null, aggregate: {type, id} | null, version, changedFields?}:subjectes el contacto del hecho (el que se conserva tras una fusión) o, sin contacto, el registro del que trata. Nunca nombres, emails, teléfonos, importes, cuerpos ni propiedades. - Orden y garantías: cada hecho recibe un número por empresa en el orden en que se confirma, así que leyendo con el cursor no hay huecos; la entrega es al menos una vez y dos hechos del mismo registro pueden llegar desordenados: descarta los de
versionno mayor que la que ya tienes.nextCursorestá ligado a tu fuente y atypes(otro filtro u otra fuente:400 CURSOR_INVALID), no caduca y, con la página vacía, se queda al final para que el sondeo no vuelva a recorrer nada. Si la retención de la empresa ya borró lo anterior a tu cursor:410 CURSOR_EXPIRED(empieza sinafter). - Desde cuándo: un tipo entra en el feed mientras exista una clave activa de una fuente activa que pueda leerlo; lo anterior no se rellena. Solo entran hechos en vivo (nunca los importados) de fuentes activas.
- Datos personales: al borrar un contacto sus hechos quedan sin él; los hechos siguen la retención de la empresa.
7. Idempotencia y reintentos
- La unicidad es fuente +
eventId: rotar la clave no crea otro hecho, y otra clave de la misma fuente recibeduplicate: true. Dos fuentes distintas no comparteneventId. - Mismo
eventIdy mismo contenido (normalizado:amountoamountMinorequivalentes cuentan igual) →202con el recibo original yduplicate: true, sin coste de cuota diaria. Contenido distinto →409 IDEMPOTENCY_CONFLICT. Idempotency-Keyes opcional; si la envías, debe ser igual aeventId(422 IDEMPOTENCY_KEY_MISMATCH).- Tras un timeout o un
5xx, reintenta con el mismoeventId(espera exponencial). Con429y503, respetaRetry-After. Los4xxno se reintentan sin corregir. Puedes consultar el recibo antes de reenviar.
8. Contactos
La fuente decide qué ve y qué cambia la integración (lo configura un administrador en API y eventos):
- Contactos que alcanza. Por defecto, solo los vinculados a la fuente por un
externalId(tu ID de cliente). Un administrador puede ampliarlo a todos los contactos de la empresa; el cambio queda con autor y fecha. - Campos de lectura y de escritura:
name,email,phone,company,language,tags,customValues,timezoneyprofilingOptOut. Email y teléfono son permisos aparte. Por defecto se leen nombre, empresa, idioma y etiquetas, y se escriben nombre, empresa e idioma. - Etiquetas que puede añadir o quitar y campos personalizados que puede leer y escribir (nunca los sensibles).
linkOnExactMatch(offpor defecto,emailophone): qué hacer cuando el contacto que creas ya existe.
Consentimientos, supresiones, conversaciones y oportunidades no se leen ni se escriben con contacts:*. Las notas sí: se leen con contacts:read y se añaden con contacts:write como el usuario delegado (con la función api.tasks). No hay DELETE.
Función exigida. Leer (GET /api/v1/contacts/{id}), buscar por tu ID (lookup), crear y actualizar funcionan si tu empresa tiene activa cualquiera de las funciones marketing.api («API y eventos») o api.tasks («API de tareas y funciones»); con las dos apagadas, 403 FEATURE_DISABLED. El listado, la búsqueda y las notas son rutas de la API de tareas y solo las abre api.tasks: con marketing.api sola responden 403 FEATURE_DISABLED (con feature: "api.tasks"), aunque la clave tenga contacts:read o contacts:write. La invitación de doble opt-in (§9) sigue exigiendo marketing.api.
Leer: GET /api/v1/contacts/{contactId} (contacts:read). Devuelve el contacto con la cabecera ETag: W/"<revisión>". Si el ID pertenece a una ficha fusionada, responde la ficha que se conserva: id es el canónico y mergedFrom el que pediste. Un contacto de otra empresa o que la fuente no alcanza da 404 CONTACT_NOT_FOUND.
{
"id": "0c0b6f3e-…",
"revision": 4,
"mergedFrom": null,
"mergedRecordIds": [],
"externalId": "customer_872",
"createdAt": "2026-09-28T10:15:31Z",
"name": "Contacto de ejemplo",
"company": "Acme",
"language": "es",
"tags": ["cliente_web"]
}
Solo aparecen los campos de lectura de la fuente, y solo si la clave tiene contacts:read (con contacts:write sin lectura, las respuestas llevan únicamente los identificadores). mergedRecordIds lista las fichas fusionadas en esta, por si guardabas alguno de esos IDs. Con timezone se devuelve también timezoneSource (contact, import, api o company).
Buscar por tu ID: POST /api/v1/contacts/lookup (contacts:read) con {"externalId": "customer_872"} en el cuerpo. El externalId nunca va en la ruta ni en la query, para que no quede en logs de proxies. 404 CONTACT_NOT_FOUND si no hay ningún contacto vinculado a ese valor en tu fuente.
Listar: GET /api/v1/contacts (contacts:read, función api.tasks). Devuelve por páginas los contactos que la fuente alcanza (contactScope: por defecto solo los vinculados a ella), ordenados por fecha de alta, con los mismos campos de lectura de la fuente que GET /api/v1/contacts/{id}. Las fichas absorbidas por una fusión nunca se listan.
- Parámetros de la query:
ids(hasta 100 IDs separados por comas; un ID absorbido devuelve la ficha que se conserva conmergedFrom),updatedSince(RFC 3339 con zona: contactos creados, cambiados o que absorbieron otra ficha desde ese momento),limit(1-100, 50 por defecto) ycursor. - Respuesta
{"items": [...], "nextCursor": "…", "hasMore": true}. ElnextCursorestá firmado para tu empresa, tu fuente y los mismos filtros, y caduca a las 24 h; uno alterado, caducado o de otra consulta da400 CURSOR_INVALID(empieza de nuevo sin él). - Nunca lleva un término de búsqueda ni datos personales en la URL:
q,email,phone,name,externalIdo cualquier otro parámetro fuera de la lista dan422 QUERY_PARAMETER_NOT_ALLOWED. Para buscar, usaPOST /api/v1/contacts/search.
Buscar: POST /api/v1/contacts/search (contacts:read, función api.tasks) con el término solo en el cuerpo: {"q": "Lucía", "limit": 20}. Admite q (1-200 caracteres), ids, updatedSince, limit y cursor; cualquier otra clave da 422 FIELD_NOT_ALLOWED y cualquier parámetro en la query, 422 QUERY_PARAMETER_NOT_ALLOWED.
- Respeta
contactScopey los campos de lectura de la fuente, igual que el listado, con el mismo cursor firmado (24 h). qse busca siempre en el nombre; en el email y el teléfono solo si la fuente puede leerlos. La respuesta lo dice ensearchedFields(por ejemplo["name", "email"]): una fuente sin permiso de email no puede averiguar un email probando búsquedas.
Notas: GET y POST /api/v1/contacts/{contactId}/notes (función api.tasks).
- Leer:
GET /api/v1/contacts/{contactId}/notes(contacts:read):{"contactId", "mergedFrom", "items": [{"id", "text", "author": {"id", "name"}, "createdAt"}]}, las 100 más recientes primero, con el cuerpo completo de cada nota y su autor. Un ID absorbido responde con las notas de la ficha que se conserva (mergedFrom); un contacto que la fuente no alcanza da404 CONTACT_NOT_FOUND. - Añadir:
POST /api/v1/contacts/{contactId}/notes(contacts:write):{"text": "…"}(1-5.000 caracteres; otra clave da422 FIELD_NOT_ALLOWED) y respuesta201 {"contactId", "note": {"id", "author", "createdAt"}}. La nota la escribe un usuario delegado (administrador o agente activo de la lista «Actuar como» de la fuente), así que la clave necesita también el scopeact_as: el usuario es el deX-AIO-Act-Aso, sin cabecera, el usuario por defecto de la fuente. Sinact_as,403 DELEGATED_USER_NOT_ALLOWED; sin usuario delegado,403 DELEGATED_USER_REQUIRED; con un usuario que ya no puede actuar,403 DELEGATED_USER_INACTIVE. Usa la misma función que la UI y nunca escribe en una ficha absorbida (409 MERGED_RECORDconcanonicalId).Idempotency-Keyes opcional (misma clave y cuerpo, misma respuesta conIdempotent-Replayed; otro cuerpo,409 IDEMPOTENCY_CONFLICT), cuenta en la cuota de 60 escrituras por minuto y clave y queda en la auditoría de acciones de la API (90 días). - No hay
DELETEni edición de notas con una clave. - Las notas de contacto no tienen un scope de contenido aparte. En la API de tareas, las notas internas de tickets y conversaciones exigen
<área>:content(/developers/api-v1-tareas.html); esa regla no se aplica aquí:contacts:readda el texto y el autor de las notas del contacto. Concedecontacts:readsolo a integraciones que puedan ver esas notas.
Las tareas de un contacto (GET /api/v1/contacts/{contactId}/tasks) son de la API de tareas (función api.tasks y el scope read de cada área): ver /developers/api-v1-tareas.html.
Crear: POST /api/v1/contacts (contacts:write) con Idempotency-Key obligatoria:
{
"externalId": "customer_872",
"name": "Contacto de ejemplo",
"email": "[email protected]",
"language": "es",
"customValues": { "customer_tier": "professional" },
"tagsToAdd": ["cliente_web"]
}
- Campos admitidos:
externalId,name(obligatorio),email,phone,company,language,tagsToAdd,customValues,timezoneyprofilingOptOut(solotrue). Cualquier otra clave, incluidasconsent,tagsomarketingPermission, da422 CONTACT_FIELD_UNKNOWN. Un campo, etiqueta o campo personalizado no permitido en la fuente da403 CONTACT_FIELD_FORBIDDEN,CONTACT_TAG_FORBIDDENoCONTACT_CUSTOM_FIELD_FORBIDDEN. externalIdes obligatorio si la fuente solo alcanza sus contactos vinculados (422 EXTERNAL_ID_REQUIRED): sin él no podrías volver a leer el contacto. Si ya identifica a un contacto de tu fuente:409 EXTERNAL_ID_TAKENconcontactId.- Respuesta
201conresult: "created",ETagyLocation. Crea el contacto por el mismo servicio que la UI (eventocontact.createdconorigin: "api"). No concede ningún consentimiento de marketing. - Idempotencia: la misma
Idempotency-Keycon el mismo cuerpo devuelve el mismo resultado durante 7 días; con otro cuerpo,409 IDEMPOTENCY_CONFLICT. Si el alta falla, la clave queda libre para reintentar con el cuerpo corregido. - Coincidencias. Con
linkOnExactMatch: off, un email o teléfono que ya usa otro contacto da409 IDENTITY_EXISTS(conmatchedIdsolo si la fuente alcanza todos los contactos). Conemailophone, si hay una ficha con ese dato exacto, se vincula tuexternalIda ella y se responde200conresult: "linked"sin sobrescribir nada (ni campos, ni etiquetas, ni consentimientos); si hay varias,409 AMBIGUOUS_IDENTITYsin decir cuáles; si esa ficha ya tiene otroexternalIdde tu fuente,409 EXTERNAL_ID_CONFLICT. - Capacidad. El límite de contactos del plan (o la capacidad que asigne RouterClip) se aplica por defecto: con la capacidad llena,
409 CONTACT_LIMIT_REACHEDconlimit,used,availableyplan. Libera plazas fusionando o eliminando fichas, o amplía el plan; en Empresa (el plan de mayor capacidad) o con una capacidad asignada por RouterClip, escribe a RouterClip para ampliarla.
Actualizar: PATCH /api/v1/contacts/{contactId} (contacts:write) con If-Match (el ETag que leíste):
PATCH /api/v1/contacts/0c0b6f3e-…
If-Match: W/"4"
Content-Type: application/json
{"customValues": {"customer_tier": "enterprise"}, "tagsToAdd": ["vip"], "tagsToRemove": ["cliente_web"]}
- Sin
If-Match:428 PRECONDITION_REQUIRED. Con una revisión que ya no es la actual (alguien la cambió en la UI, en una importación o por la API):412 REVISION_CONFLICTconcurrentRevisiony elETagactual; vuelve a leer y reintenta. Cualquier cambio en RouterClip cambia elETag. - Sobre una ficha fusionada:
409 MERGED_RECORDconcanonicalId(la ficha que se conserva). Nunca se escribe en una ficha oculta. - Campos: los de crear salvo
externalId, mástagsToRemove.tagsToAddytagsToRemovesolo tocan etiquetas permitidas (se guardan con la grafía de la fuente); el resto de etiquetas del contacto no cambia.customValuesvalida tipos y opciones reales y nunca crea definiciones (nullborra un valor). emailyphonepasan las mismas comprobaciones de identidad que en la UI (409 IDENTITY_EXISTS); un email nuevo no hereda los consentimientos del anterior. Los alias ocultos de WhatsApp y los visitantes del chat conservan sus restricciones.timezone(zona IANA) se guarda con procedenciaapiy nunca sustituye una zona declarada por el propio contacto.profilingOptOut: trueregistra la oposición al perfilado; una clave no puede retirarla (422 PROFILING_OPT_OUT_LOCKED).- Sin cambios efectivos,
200con la misma revisión y sin evento.
Registro de lecturas. Cada lectura de un contacto con una clave se anota en api_contact_reads por fuente, clave, contacto y día, y se conserva 90 días: GET, lookup, el 200 linked del alta, el listado y la búsqueda (una lectura por cada contacto devuelto) y la lectura de sus notas. También cuentan los datos del contacto que viajan en otras respuestas de /api/v1: los enlaces de una tarea y el feed, los tickets y conversaciones, las llamadas (teléfonos), las reservas (nombre y email del invitado), las oportunidades y los seguimientos (contactName), los destinatarios de campañas (address), las inscripciones en recorridos, las encuestas CSAT y los trabajos de voz (dirección enmascarada), las inscripciones en secuencias y las sesiones de playbooks (nombre y respuestas que escriben campos del contacto), y los duplicados y fusiones (campos, claves de identidad y nombres, también los de los blockers de un error), una vez por clave, contacto y petición y solo cuando se muestra algún valor. Las lecturas de una petición se escriben juntas y siempre en el mismo orden (clave, contacto y día), no en el de la respuesta, así que las peticiones en paralelo de una misma clave no se bloquean entre sí. Los administradores lo consultan por fuente y por contacto (Integraciones → API y eventos → Lecturas de contactos) y, mientras la empresa tenga la API de eventos o la de tareas activada, también en la ficha del cliente (Lecturas por integraciones (API)).
Actividad de la API. Las altas, vinculaciones (200 linked) y cambios de contactos, las invitaciones (§9) y las inscripciones (§10) hechas con una clave quedan en la actividad de la fuente 90 días, con su resultado y su X-Request-ID y sin cuerpos ni direcciones; también sus rechazos 403, 404, 409 y 412. Se consulta en Integraciones → API y eventos → API de tareas → Actividad de la API; si la empresa solo tiene la API de eventos, ese panel muestra únicamente Actividad de la API.
9. Invitaciones de doble opt-in: POST /api/v1/subscriptions/invite
Requiere subscriptions:invite y que el administrador haya elegido en la fuente una conexión SMTP verificada. Encola el mismo email de confirmación que el formulario público; el permiso de marketing solo existe cuando la persona confirma.
{
"externalId": "customer_872",
"purpose": "marketing",
"policyVersion": "newsletter-2026-09",
"policyText": "Quiero recibir novedades por email. Puedo darme de baja en cualquier momento.",
"evidence": "Casilla sin marcar por defecto aceptada en el checkout, pedido 842"
}
- Destinatario:
contactIdoexternalIdde un contacto que la fuente alcanza (se usa su email) y/oemail. Con contacto y email, deben coincidir (422 INVITE_EMAIL_MISMATCH). policyVersion,policyText(el aviso que verá la persona) yevidence(cómo pidió la invitación) son obligatorios;purposeesmarketingpor defecto.- Respuesta
202 {"status": "pending"}igual si se encola, si ya había una pendiente o si la dirección está suprimida: la API no revela si alguien está suscrito. 409 INVITE_CONNECTION_REQUIREDoINVITE_CONNECTION_NOT_READYsi la conexión de la fuente falta o no está verificada y activa;429con las cuotas del formulario público.409 MARKETING_SEND_DISABLEDmientras el envío de marketing del servidor (MARKETING_SEND_ENABLED) está apagado: el email de confirmación no podría salir, así que no se encola nada ni se gasta cuota. Reintenta cuando el operador lo active.
10. Recorridos: GET /api/v1/automations y POST /api/v1/automations/{id}/enroll
Requieren automations:enroll. Un administrador elige en la fuente qué recorridos permite (Integraciones → API y eventos) y el recorrido debe tener el disparador «Desde una integración (API)» y estar activo. La clave nunca ve otros recorridos.
- Lista:
GET /api/v1/automationsdevuelve solo los recorridos permitidos en la fuente, conid,name,status,triggeryenrollable(activo y con disparador API). Como la inscripción, esta lista exige la funciónmarketing.api: con soloapi.tasksresponde403 FEATURE_DISABLED(feature: "marketing.api") y los recorridos se listan conjourneys:read(/developers/api-v1-tareas.html). - Inscripción:
POST /api/v1/automations/{automationId}/enrollconIdempotency-Keyobligatoria y este cuerpo:
{
"subject": { "externalId": "customer_872" },
"reasonEventId": "5f2c9e0b7d8a4c1e9b3f6a2d4e8c1b7a"
}
subject:contactId(un contacto que la fuente alcanza; una ficha fusionada se resuelve en la que se conserva) y/oexternalId(vinculado en tu fuente; nunca un email ni un teléfono). Si envías los dos, deben ser el mismo contacto (409 SUBJECT_IDENTITY_CONFLICT).reasonEventId(opcional): eliddel recibo de un evento de tu misma fuente y del mismo contacto (no tueventId). Queda como causa de la entrada; cualquier otro es422 REASON_EVENT_INVALID.- La decisión es síncrona y la toma la política de reinscripción del recorrido:
201 {"result": "enrolled", "enrollmentId": "…", "automationId": "…"};200 {"result": "skipped", "reason": "…", "automationId": "…"}conreason:alreadyEnrolled(ya pasó por el recorrido y la política es «una sola vez»),activeEnrollmentExists(está dentro ahora),cooldown(terminó hace menos del periodo de espera),entryLimitReached(alcanzó el máximo de entradas del periodo),automationNotActive(en pausa, borrador o cancelado) osubjectUnavailable(no hay un contacto que la fuente alcance con esa identidad).
- Mismo
Idempotency-Keyy mismo cuerpo → la misma respuesta durante 7 días; otro cuerpo →409 IDEMPOTENCY_CONFLICT. 404 AUTOMATION_NOT_FOUNDsi el recorrido no está en la lista de la fuente;409 AUTOMATION_TRIGGER_NOT_APIsi su disparador no es la API. No se admiten campos adicionales (422 ENROLL_FIELD_UNKNOWN): ni anónimos, ni mensajes, ni conexiones, ni activación, ni consentimiento.enrolledno significa enviado: cada paso «Enviar» comprueba permiso, bajas, cuotas y transporte como cualquier envío. Los recorridos que empiezan por un evento no se inscriben con esta ruta: les basta con enviar el evento (POST /api/v1/events), y solo cuentan los eventos recibidos después de activarlos.
10 bis. Vincular navegadores: POST /api/v1/identity/link-tokens
Cuando un cliente inicia sesión en tu web, tu servidor (nunca el navegador) pide un token de vínculo para él y se lo pasa a la página, que lo entrega a aiocrm.js (['identify', token]). RouterClip vincula entonces ese navegador (el visitante del tracker, con consentimiento analítico) o su suscripción push con la ficha. La clave nunca llega al navegador y el navegador nunca elige el contacto. Vincular no concede ningún consentimiento (ni push, ni email, ni SMS).
Requisitos, en este orden: scope identity:link (403 API_SCOPE_REQUIRED); la función «Vincular navegadores con contactos» (marketing.identity) activa para tu empresa (403 FEATURE_DISABLED); la base jurídica de marketing.identity confirmada por un administrador en Integraciones → API y eventos (403 LEGAL_BASIS_REQUIRED); y la cuota de 60 tokens por minuto y clave (429).
{
"subject": { "externalId": "customer_872" },
"site": "st_AbCdEfGhIjKlMnOpQrStUvWx",
"purposes": ["push", "push_manage"]
}
subject: uno deexternalId(vinculado en tu fuente) ocontactId(un contacto que la fuente alcanza). Una ficha fusionada emite el token para la que se conserva. Sin contacto:404 CONTACT_NOT_FOUND.site: elpublicIdde un sitio web activo y verificado de tu empresa que el administrador haya permitido en la fuente (Sitios web en los que puede vincular navegadores). Cualquier otro caso, también un sitio de otra empresa, responde igual:403 SITE_NOT_ALLOWED.purposes: una o varias, sin repetir:push(vincular la suscripción push de ese navegador),anonymous(vincular el visitante del tracker; un contacto que se ha opuesto al perfilado da409 CONTACT_OPTED_OUT) ypush_manage(permitir que la página cambie las preferencias push del contacto).- Respuesta
201conCache-Control: no-store:{"token": "ilt_…", "tokenType": "identity_link", "expiresAt": "…", "expiresIn": 300, "site": "st_…", "purposes": […], "shownOnce": true}. El token es aleatorio (32 bytes), vale 5 minutos, solo en ese sitio, y cada finalidad se usa una vez. RouterClip guarda solo su hash (se purga al día) y registra cada emisión (fuente, clave, sitio, contacto y finalidades). - Pásalo a la página en el HTML o en una respuesta JSON de tu servidor, nunca en una URL;
aiocrm.jslo envía solo en el cuerpo. Si el token caducó, ya se usó, es de otro sitio o de otra finalidad, la vinculación no ocurre y la página recibe la misma respuesta neutra: pide otro. - Otros errores:
415sinContent-Type: application/json;422LINK_TOKEN_BODY_INVALID,LINK_TOKEN_FIELD_UNKNOWN,SUBJECT_REQUIRED,SUBJECT_INVALID,EXTERNAL_ID_*,SITE_REQUIRED,SITE_INVALIDoPURPOSES_INVALID. - Una fusión revoca los tokens pendientes de la ficha absorbida. Si se apaga la función, se revoca la base jurídica, se revoca la clave que lo pidió, se desactiva la fuente, se quita el sitio de la fuente o el contacto se opone al perfilado (finalidad
anonymous), los tokens pendientes dejan de valer.
11. Seguridad y datos personales
- Clave solo en el servidor; nunca en URLs, logs ni repositorios. Si se filtra, revócala: el operador de la plataforma también puede revocarla, una a una o todas las activas de la empresa a la vez, siempre con motivo. Con una sola de las dos funciones apagada, «API y eventos» (
marketing.api) o «API de tareas y funciones» (api.tasks), las claves siguen funcionando en las rutas de la otra (y en las de contactos, que admiten cualquiera de las dos): para cortar todas las integraciones de la empresa hay que apagar las dos. - No envíes datos personales en referencias ni en propiedades que no los necesiten. El
externalIdno puede ser un email o un teléfono. - RouterClip guarda el evento mínimo: los datos normalizados, el
externalIdsolo si no está vinculado, el HMAC del visitante y un hash del cuerpo para la idempotencia. Al borrar un contacto se desvinculan y depuran sus eventos; los eventos siguen la retención de tu empresa. - Las lecturas de contactos quedan registradas (§8). Pide solo los campos que tu integración necesita: el administrador puede reducirlos en cualquier momento.
12. Fuera de v1
Lotes de eventos en vivo (la importación histórica sí existe: §5 bis), callbacks de recibos, pagos verificados (financialVerified), carritos (cart.*) y webhooks salientes de estos tipos. En contactos: borrado (DELETE), escritura de consentimientos o supresiones y vinculación masiva por CSV.
English
1. What it is
API v1 connects your company's servers (ERP, shop, backend, previous CRM) with RouterClip. It is server to server: no CORS, and the key must never live in a browser, a mobile app, a tag manager or a URL. Website measurement uses the aiocrm.js script of a verified site, which never carries keys.
2. Activation, sources and keys
- The RouterClip team enables “API and events” (
marketing.api) for your company. - An administrator creates one source per system in Settings → Integrations → API and events. The source decides what the integration may do: allowed event types, contact fields, which contacts it reaches (by default only those linked to it) and journeys.
- Issue a key with its scopes on the source. It is shown once: keep it in your secret manager. A source can have two active keys for zero-downtime rotation; optional expiry up to 365 days (notices at 14 and 3 days). Revoking a key or disabling the source applies to the next request.
| Scope | Allows |
|---|---|
events:write | Sending events of the types allowed in the source and reading its own receipts |
events:read | Reading the receipts of its own source and, with the api.tasks feature, the company event feed (GET /api/v1/events and GET /api/v1/event-types, §6 bis) of the areas the key can read (<area>:read) |
events:import | Loading history in batches (POST /api/v1/events/import, §5 bis) of the types allowed in the source, without effects. events:write does not include it: an administrator grants it when issuing the key |
contacts:read | Reading contacts the source reaches (GET /api/v1/contacts/{id}, POST /api/v1/contacts/lookup), listing them by cursor (GET /api/v1/contacts, filters ids and updatedSince), searching them (POST /api/v1/contacts/search, q only in the body) and reading their notes (GET /api/v1/contacts/{id}/notes: text and author), only with the source's read fields (§8). Listing, search and notes also need the api.tasks feature |
contacts:write | Creating and updating contacts (POST /api/v1/contacts, PATCH /api/v1/contacts/{id}) with the fields, tags and custom fields allowed in the source, and adding notes to them (POST /api/v1/contacts/{id}/notes, with the api.tasks feature and as a delegated user: it also needs act_as and X-AIO-Act-As or the source's default user) (§8) |
subscriptions:invite | Sending the double opt-in invitation with the SMTP connection chosen in the source (§9); never grants a permission |
automations:enroll | Listing the journeys the source allows (GET /api/v1/automations) and enrolling contacts it reaches into those with the «From an integration (API)» trigger (POST /api/v1/automations/{id}/enroll, §10) |
identity:link | Requesting single-use tokens that link a customer's browser or push subscription with their record, only on the websites allowed in the source (POST /api/v1/identity/link-tokens, §10 bis). Never ticked by default: an administrator grants it, and only while the marketing.identity feature is on |
Task and function API (api.tasks feature; details, packages and actor in /developers/api-v1-tareas.html): scopes are <area>:<level> with levels that do not inherit — read, write, execute, admin (needs acting as an administrator) and content (bodies, internal notes and transcripts) — over the areas companies, deals, activities, sequences, playbooks, duplicates, support, csat, voice, calendar, campaigns, journeys, segments, forms, sites, conversions, knowledge, channels, reports, team and automation (automation:read and automation:enroll: automation flows without their configuration, their runs and starting the flows with the “Through the API” trigger; feature automation.flows; automation:enroll is not automations:enroll and is in no package), plus team:emails, webhooks:manage and act_as (act as a user of the source's “Act as” list with X-AIO-Act-As). *:admin, *:content, team:emails and act_as are sensitive.
Check a key without side effects: GET /api/v1/key.
3. Authentication, answers and errors
Send Authorization: Bearer aio_<keyId>.<secret> and Content-Type: application/json. Every /api/v1/ answer carries X-Request-ID (yours when valid, otherwise a new one). Errors are {"detail": {"code", "message", "requestId", "field"}}: field says where the problem is and values are never echoed. Status codes: 200 contact read or updated, a page of a contact listing or search or of its notes, or created as a link to an existing contact (result: "linked"), or enrollment not done with its reason (result: "skipped"), or an import batch processed with one result per event (§5 bis); 201 contact created (Location, ETag), contact enrolled into a journey (result: "enrolled") or link token issued (§10 bis), or note added to a contact (§8); 202 event accepted or duplicate, invitation queued; 400 CURSOR_INVALID (altered, expired or other-query cursor of a listing or search: start again without it); 401 key or company problem (do not retry); 402 no service access; 403 scope, feature or legal basis (LEGAL_BASIS_REQUIRED) missing, or event type, field, tag, custom field or website (SITE_NOT_ALLOWED) not allowed in the source; 404 receipt, contact or journey outside the key's source; 409 IDEMPOTENCY_CONFLICT, SUBJECT_IDENTITY_CONFLICT, IDENTITY_EXISTS, AMBIGUOUS_IDENTITY, MERGED_RECORD, CONTACT_LIMIT_REACHED, AUTOMATION_TRIGGER_NOT_API or CONTACT_OPTED_OUT; 412 REVISION_CONFLICT (stale If-Match); 413 body above 32 KiB (1 MiB on POST /api/v1/events/import); 415 wrong content type; 422 invalid body; 428 PATCH without If-Match; 429 quota (honor Retry-After); 503 intake paused by the platform (honor Retry-After).
4. Quotas
Technical limits applied before anything is written: 120 requests per minute per key and 600 per company; 50,000 new events per company per UTC day (duplicates do not count); historical import (events:import): 200,000 imported events per company per UTC day, apart from the live 50,000 (a batch is one request for the per-minute quotas; duplicates do not count, §5 bis); double opt-in invitations share the quotas of the public form (200 per hour per company, 3 per day per address); link tokens (identity:link): 60 per minute per key; invalid credentials are throttled per IP address.
5. Sending an event: POST /api/v1/events
One event per request, scope events:write.
eventId(required): your stable ID of the fact, 1-160 visible ASCII characters, unique per source; retries reuse it.eventType:conversion.reported,order.paid,order.refundedorcustom, only when allowed in the source (403 EVENT_TYPE_FORBIDDEN). Internal types give422 EVENT_TYPE_NOT_ALLOWEDand reserved ones422 EVENT_TYPE_RESERVED.schemaVersion: optional, only1.occurredAt(required): RFC 3339 in UTC (Zor+00:00), at most 5 minutes ahead and 30 days old. Older facts go through the historical import (§5 bis).data(required): closed schema per type —order.paid{orderId, paymentId, amount|amountMinor, currency};order.refunded{orderId, refundId, amount|amountMinor, currency, full, paymentId?}(amount of this refund, greater than zero);conversion.reported{conversionId, conversionType, orderId?, opportunityId?, amount?};custom{name, properties?}with up to 30 flat properties. References are 1-64 visible ASCII characters and never an email.- Money:
amountas a decimal string oramountMinoras an integer, with an ISO 4217currencyof 0-3 decimals. Never JSON numbers with decimals, no rounding, no negatives. Stored as{"amount": "129.90", "currency": "EUR"}; the receipt addsamountMinorandexponent. These are declared amounts, never verified payments. subject(required):contactId(a contact the source can reach; merged records resolve to the kept one) and/orexternalId(your customer ID linked in this source; never an email or phone). All identities must match (409 SUBJECT_IDENTITY_CONFLICT).customneeds a resolved contact (422 SUBJECT_UNRESOLVED); conversions and orders with an unlinkedexternalIdare stored without contact and never linked later.anonymousId(fromaiocrm.push(['getVisitorId', cb])) is accepted only in conversions and orders together withcontext.site; it is an attribution reference of that site, never a contact, and only its HMAC is stored. Exception (linked visitors,marketing.identity): when the page linked that browser withaiocrm.push(['identify', token])beforeoccurredAt, the event resolves that contact if you sent no other identity; thencontext.sitemust be one of your source's allowed sites (422 ANONYMOUS_ID_SITE_NOT_ALLOWED).context(optional): onlysite, the public ID (st_…) of an active verified website of your company.attributionTokenor any other key answers422 CONTEXT_FIELD_NOT_ALLOWED.- Server-owned fields (company, source,
trust, mode, reception time, verification) are rejected;financialVerifiedanswersEVENT_TRUST_RESERVED.
5 bis. Importing history: POST /api/v1/events/import
Loads past facts (orders, refunds and custom events of your ERP or previous CRM) without triggering anything. It needs the scope events:import, which events:write does not include: an administrator ticks it when issuing the key. Like the rest of the API it follows the marketing.api feature.
- Batch.
{"events": [...]}with 1 to 500 events and at most 1 MiB (this route only; the rest of/api/v1keeps 32 KiB). More than 500:422 IMPORT_TOO_MANY_EVENTS; above 1 MiB:413. Other errors of the whole batch:415,422 IMPORT_BODY_INVALID,IMPORT_FIELD_NOT_ALLOWED(onlyeventsis accepted),IMPORT_EVENTS_REQUIREDandIDEMPOTENCY_KEY_NOT_SUPPORTED(each event is already idempotent by itseventId). - Each event follows the contract of
POST /api/v1/events(§5): closed envelope, types allowed in the source (conversion.reported,order.paid,order.refunded,custom), subject, context and money. Only the date differs:occurredAtmust fall inside your company's data retention (12 months by default) and at most 5 minutes ahead; otherwise only that event is rejected withEVENT_OCCURRED_OUT_OF_RETENTION(withretentionMonths). The live 30-day limit does not apply. - No effects. Events are stored with
mode: "import"and without deliveries: they never enroll into journeys, wake waits, create conversions or attributions, score or send anything. Their receipt (GET /api/v1/events/{id}) showsmode: "import"anddeliveries: []. They do count in purchases declared by integration segments, whose preview says so: “includes imported history” (purchaseCoverage.includesImportedandimportedSince). 200answer with one result per event, in order:{"mode": "import", "received", "accepted", "duplicates", "rejected", "retention": {"months", "oldestOccurredAt"}, "dailyQuota": {"limit", "used"}, "results": [...]}. Eachresults[i]hasindex, youreventIdandstatus:accepted(stored; withid,eventType,occurredAt,mode,contactResolvedandstatusUrl),duplicate(thateventIdof the source was already received, live or imported, with the same normalized body; it returns itsidandmodeand costs no quota) orrejected(withcode,message,httpStatus—the one the live route would answer: 403, 409, 422 or 429— andfield), e.g.EVENT_TYPE_FORBIDDEN,IDEMPOTENCY_CONFLICT,SUBJECT_UNRESOLVED,MONEY_FLOAT_NOT_ALLOWED,EVENT_OCCURRED_OUT_OF_RETENTIONorRATE_LIMITED.- Each event is processed on its own: a rejected one leaves nothing behind and never cancels the others. Retrying a whole batch is safe: what was accepted comes back as
duplicate. - Own daily quota: 200,000 new imported events per company per UTC day, apart from the live 50,000. When it is full, the remaining new events of the batch are rejected with
RATE_LIMITED(quota: "companyImportsPerDay") and the answer carriesRetry-After; duplicates keep answering.503 INGEST_PAUSEDwhen the platform pauses intake: retry the same batch later. - An imported event sent later to
POST /api/v1/eventswith the sameeventIdis the same fact (duplicate: true,mode: "import"): importing never fires anything live afterwards.
6. Receipts
202 means durable reception: the event and its deliveries are stored. It does not mean a journey started, a message was sent or attribution was decided. The receipt (same body for GET /api/v1/events/{id}, scopes events:read or events:write, own source only, Location header on 202) has the normalization (money with amount, amountMinor, exponent), contactResolved, attribution.basis (browser, contact or none) and one entry per active consumer in deliveries (pending, processing, done, failed, skipped), never the event content.
6 bis. Event feed: GET /api/v1/events and GET /api/v1/event-types
Read the facts of the whole company in order, with the same «wake-up» body as the company webhooks: IDs, type and version only; then read the record with your key. It needs events:read, the api.tasks feature and, per type, the read scope of its area with the area's feature on (deals:read for deal.*, support:read for conversation.*, ticket.* and sla.*, contacts:read for contact.* and custom…); with events:read alone pages are empty, not an error. GET /api/v1/event-types lists every type with its area, scope, feature, available for your key, recordedWhenWanted (attention facts recorded only while someone wants them) and readWith (the route that reads each aggregate by its ID, e.g. marketing_site → /api/v1/sites/{id}); the aggregates without a detail route are in aggregatesWithoutDetail with their reason: listOnly (find it in listPath: booking, pipeline), externalReference (conversion, order: the ID is your own conversionId or orderId; the attributed conversion is in /api/v1/conversions) or noResource (journey_enrollment, marketing_link: read the subject); task.* (use GET /api/v1/task-changes) and the engine's own automation.* facts are not in it. GET /api/v1/events?types=&after=&limit= (1-500, 100 by default; up to 30 types) answers {items, nextCursor, hasMore}; each item is {id, type, schemaVersion, occurredAt, subject, aggregate, version, changedFields?} (subject: the contact, the kept one after a merge, else the record). Each fact gets a per-company number in commit order, so the cursor never skips one; delivery is at least once and two facts of one record may arrive out of order: compare version. nextCursor is bound to your source and types (400 CURSOR_INVALID otherwise), never expires and stays at the head on an empty page; a cursor older than the company retention is 410 CURSOR_EXPIRED. A type enters the feed while an active key of an active source can read it; the past is never back-filled and only live facts of active sources enter. Asking for a type the key cannot read is 403 API_SCOPE_REQUIRED (requiredScopes) or FEATURE_DISABLED.
7. Idempotency and retries
Uniqueness is source + eventId: rotating keys never creates another fact and another key of the same source gets duplicate: true. The same normalized body returns the original receipt with duplicate: true (no daily quota); another body returns 409 IDEMPOTENCY_CONFLICT. Idempotency-Key is optional and must equal eventId. After a timeout or a 5xx, retry with the same eventId and exponential backoff; honor Retry-After on 429 and 503; do not retry 4xx unchanged.
8. Contacts
The source decides what the integration sees and changes (set by an administrator in API and events): which contacts it reaches (by default only those linked to the source by an externalId, your customer ID; an administrator can widen it to every contact, recorded with author and date), its read and write fields (name, email, phone, company, language, tags, customValues, timezone, profilingOptOut; email and phone are separate permissions), the tags it may add or remove, the custom fields it may read and write (never sensitive ones) and linkOnExactMatch (off by default, email or phone). Consents, suppressions, conversations and deals are never read or written with contacts:*. Notes are: they are read with contacts:read and added with contacts:write as the delegated user (with the api.tasks feature). There is no DELETE.
Required feature. Read (GET /api/v1/contacts/{id}), look up, create and update work while your company has any of the features marketing.api (“API and events”) or api.tasks (“Task and function API”) on; with both off, 403 FEATURE_DISABLED. The listing, the search and the notes are routes of the task API and only api.tasks opens them: with marketing.api alone they answer 403 FEATURE_DISABLED (with feature: "api.tasks"), even when the key has contacts:read or contacts:write. The double opt-in invitation (§9) still needs marketing.api.
- Read:
GET /api/v1/contacts/{contactId}(contacts:read) withETag: W/"<revision>". A merged ID answers the kept record:idis the canonical one andmergedFromthe one you asked for;mergedRecordIdslists the records merged into it. Only the source's read fields are present, and only withcontacts:read(write-only keys get identifiers only).404 CONTACT_NOT_FOUNDfor contacts of other companies or out of the source's reach. - Look up:
POST /api/v1/contacts/lookup(contacts:read) with{"externalId": "customer_872"}in the body; the externalId never travels in the path or the query. - List:
GET /api/v1/contacts(contacts:read,api.tasksfeature): pages of the contacts the source reaches (contactScope: by default only those linked to it), by creation date, with the same source read fields asGET /api/v1/contacts/{id}; records absorbed by a merge are never listed. Query parameters:ids(up to 100 comma-separated IDs; an absorbed ID answers the kept record withmergedFrom),updatedSince(RFC 3339 with its zone: contacts created, changed or that absorbed another record since then),limit(1-100, 50 by default) andcursor. Answer{"items": [...], "nextCursor", "hasMore"};nextCursoris signed for your company, your source and the same filters and expires after 24 h (an altered, expired or other-query cursor is400 CURSOR_INVALID: start again without it). A search term or personal data in the URL (q,email,phone,name,externalIdor any parameter outside the list) is422 QUERY_PARAMETER_NOT_ALLOWED: search withPOST /api/v1/contacts/search. - Search:
POST /api/v1/contacts/search(contacts:read,api.tasksfeature) with the term only in the body:{"q": "Lucía", "limit": 20}. Keysq(1-200 characters),ids,updatedSince,limitandcursor; any other key is422 FIELD_NOT_ALLOWEDand any query parameter422 QUERY_PARAMETER_NOT_ALLOWED. It followscontactScopeand the source's read fields like the listing, with the same signed cursor (24 h).qalways searches the name, and the email and the phone only when the source may read them;searchedFields(e.g.["name", "email"]) says which, so a source without the email permission cannot guess an email by searching. - Notes:
GETandPOST /api/v1/contacts/{contactId}/notes(api.tasksfeature). Reading (GET /api/v1/contacts/{contactId}/notes,contacts:read) answers{"contactId", "mergedFrom", "items": [{"id", "text", "author": {"id", "name"}, "createdAt"}]}, the 100 newest first, with the full body of every note and its author; an absorbed ID answers the notes of the kept record (mergedFrom) and a contact the source does not reach is404 CONTACT_NOT_FOUND. Adding (POST /api/v1/contacts/{contactId}/notes,contacts:write) takes{"text": "…"}(1-5,000 characters; another key is422 FIELD_NOT_ALLOWED) and answers201 {"contactId", "note": {"id", "author", "createdAt"}}. The note is written by a delegated user (an active administrator or agent of the source's “Act as” list), so the key also needs theact_asscope: the user ofX-AIO-Act-Asor, without the header, the source's default user. Withoutact_as,403 DELEGATED_USER_NOT_ALLOWED; without a delegated user,403 DELEGATED_USER_REQUIRED; with a user who can no longer act,403 DELEGATED_USER_INACTIVE. It goes through the same function as the UI and never writes on an absorbed record (409 MERGED_RECORDwithcanonicalId).Idempotency-Keyis optional (same key and body replay the answer withIdempotent-Replayed; another body is409 IDEMPOTENCY_CONFLICT); it counts in the quota of 60 writes per minute per key and stays in the API action audit (90 days). Notes are never edited or deleted with a key. - Contact notes have no separate content scope. In the task API, the internal notes of tickets and conversations need
<area>:content(/developers/api-v1-tareas.html); that rule does not apply here:contacts:readgives the text and author of a contact's notes. Grantcontacts:readonly to integrations that may see those notes. - The tasks of a contact (
GET /api/v1/contacts/{contactId}/tasks) belong to the task API (api.tasksfeature and thereadscope of each area): see /developers/api-v1-tareas.html. - Create:
POST /api/v1/contacts(contacts:write) with a requiredIdempotency-Key. Keys:externalId,name(required),email,phone,company,language,tagsToAdd,customValues,timezone,profilingOptOut(onlytrue); anything else,consentortagsincluded, is422 CONTACT_FIELD_UNKNOWN, and fields, tags or custom fields outside the source are403.externalIdis required when the source reaches only its linked contacts (422 EXTERNAL_ID_REQUIRED); an externalId already in use is409 EXTERNAL_ID_TAKENwithcontactId.201withresult: "created",ETagandLocation; it goes through the same contact service as the UI (contact.createdwithorigin: "api") and grants no marketing consent. The same key and body replay the same answer for 7 days; another body is409 IDEMPOTENCY_CONFLICT; a failed creation frees the key. WithlinkOnExactMatch: offan email or phone of another contact is409 IDENTITY_EXISTS(withmatchedIdonly for sources that reach every contact); withemailorphone, one exact match gets your externalId linked and answers200withresult: "linked"without overwriting anything, several matches are409 AMBIGUOUS_IDENTITY(never saying which) and a match that already has another externalId of your source is409 EXTERNAL_ID_CONFLICT. The company's contact capacity (its plan's, or the one RouterClip assigned) is applied by default: when it is full,409 CONTACT_LIMIT_REACHEDwithlimit,used,availableandplan. - Update:
PATCH /api/v1/contacts/{contactId}(contacts:write) withIf-Match(theETagyou read). Missing:428 PRECONDITION_REQUIRED; stale (changed in the UI, an import or the API):412 REVISION_CONFLICTwithcurrentRevisionand the currentETag. A merged record answers409 MERGED_RECORDwithcanonicalId. Keys: those of creation exceptexternalId, plustagsToRemove; tag operations only touch allowed tags;customValueskeep their real types and options (nullclears one). Email and phone pass the same identity checks as the UI and a new email never inherits the previous consents.timezonegets theapisource and never replaces a zone the contact declared; a key can recordprofilingOptOut: truebut never withdraw it (422 PROFILING_OPT_OUT_LOCKED). No effective change:200with the same revision. - Read log. Every read of a contact with a key is recorded in
api_contact_readsper source, key, contact and day and kept 90 days:GET,lookup, the200 linkedof creation, the listing and the search (one read per contact returned) and reading its notes. The contact data carried by other/api/v1answers counts too: a task's links and the feed, tickets and conversations, calls (phone numbers), bookings (the guest's name and email), deals and follow-ups (contactName), campaign recipients (address), journey enrollments, CSAT surveys and voice jobs (masked address), sequence enrollments and playbook sessions (the name and answers that write contact fields), and duplicates and merges (fields, identity keys and names, also those of an error'sblockers), once per key, contact and request and only when a value is shown. A request's reads are written together and always in the same order (key, contact and day), never in the order of the answer, so parallel requests of one key never lock each other. Administrators see it per source and per contact (Integrations → API and events → Contact reads) and, while the company has the events or the task API on, in the customer record too (Reads by integrations (API)). - API activity. Contacts created, linked (
200 linked) and updated, invitations (§9) and enrollments (§10) made with a key appear in the source's activity for 90 days, with their result andX-Request-IDand without bodies or addresses; so do their403,404,409and412rejections. It is under Integrations → API and events → Task API → API activity; when the company only has the events API, that panel shows just API activity.
9. Double opt-in invitations: POST /api/v1/subscriptions/invite
Scope subscriptions:invite, with a verified SMTP connection chosen in the source. It queues the same confirmation email as the public form; the marketing permission exists only once the person confirms. Body: contactId or externalId of a contact the source reaches (its email is used) and/or email (they must match: 422 INVITE_EMAIL_MISMATCH), plus the required policyVersion, policyText (the notice the person sees) and evidence (how the person asked), and purpose (marketing by default). The answer is 202 {"status": "pending"} whether the email is queued, already pending or suppressed, so it never reveals who is subscribed. 409 INVITE_CONNECTION_REQUIRED or INVITE_CONNECTION_NOT_READY when the source's connection is missing or not verified and enabled; 409 MARKETING_SEND_DISABLED while the server's marketing sending (MARKETING_SEND_ENABLED) is off, because the confirmation email could not leave: nothing is queued and no quota is spent, so retry once the operator turns it on; 429 with the public form's quotas.
10. Journeys: GET /api/v1/automations and POST /api/v1/automations/{id}/enroll
Scope automations:enroll. An administrator chooses in the source which journeys it allows (Integrations → API and events); the journey must have the «From an integration (API)» trigger and be active. A key never sees other journeys.
- List:
GET /api/v1/automationsreturns only the journeys the source allows, withid,name,status,triggerandenrollable(active with the API trigger). Like enrollment, this list needs themarketing.apifeature: with onlyapi.tasksit answers403 FEATURE_DISABLED(feature: "marketing.api") and journeys are listed withjourneys:read(/developers/api-v1-tareas.html). - Enroll:
POST /api/v1/automations/{automationId}/enrollwith a requiredIdempotency-Keyand{"subject": {"externalId": "customer_872"}, "reasonEventId": "5f2c…"}.subjecttakescontactId(a contact the source reaches; a merged record resolves to the kept one) and/orexternalId(linked in your source; never an email or phone); both must be the same contact (409 SUBJECT_IDENTITY_CONFLICT).reasonEventId(optional) is the receiptidof an event of your source and the same contact (not youreventId), kept as the cause of the entry; anything else is422 REASON_EVENT_INVALID. - The decision is synchronous, by the journey's re-entry policy:
201 {"result": "enrolled", "enrollmentId", "automationId"}or200 {"result": "skipped", "reason", "automationId"}withalreadyEnrolled(already went through a once-only journey),activeEnrollmentExists(inside now),cooldown(finished less than the waiting time ago),entryLimitReached(maximum entries of the period),automationNotActive(paused, draft or cancelled) orsubjectUnavailable(no contact the source reaches with that identity). - The same
Idempotency-Keyand body replay the same answer for 7 days; another body is409 IDEMPOTENCY_CONFLICT.404 AUTOMATION_NOT_FOUNDoutside the source's list;409 AUTOMATION_TRIGGER_NOT_APIfor other triggers; extra fields are422 ENROLL_FIELD_UNKNOWN(no anonymous subjects, messages, connections, activation or consent). enrolledis not sent: every Send step checks permission, unsubscribes, quotas and transport like any send. Journeys started by an event are not enrolled with this route: send the event (POST /api/v1/events); only events received after their activation count.
10 bis. Linking browsers: POST /api/v1/identity/link-tokens
When a customer signs in on your website, your server (never the browser) requests a link token for them and hands it to the page, which gives it to aiocrm.js (['identify', token]). RouterClip then links that browser (the tracker's visitor, with analytics consent) or its push subscription with the record. The key never reaches the browser and the browser never chooses the contact. Linking grants no consent (push, email or SMS).
Requirements, in this order: scope identity:link (403 API_SCOPE_REQUIRED); the “Link browsers with contacts” feature (marketing.identity) on for your company (403 FEATURE_DISABLED); the legal basis of marketing.identity confirmed by an administrator in Integrations → API and events (403 LEGAL_BASIS_REQUIRED); and the quota of 60 tokens per minute per key (429).
- Body:
{"subject": {"externalId": "customer_872"}, "site": "st_…", "purposes": ["push", "push_manage"]}.subjectis one ofexternalId(linked in your source) orcontactId(a contact the source reaches); a merged record gives the token of the kept one; no contact is404 CONTACT_NOT_FOUND. site: thepublicIdof an active, verified website of your company that an administrator allowed in the source (Websites where it may link browsers). Any other case, another company's website included, gets the same403 SITE_NOT_ALLOWED.purposes: one or more, without repetitions:push(link that browser's push subscription),anonymous(link the tracker's visitor; a contact who objected to profiling gets409 CONTACT_OPTED_OUT) andpush_manage(let the page change the contact's push preferences).201withCache-Control: no-store:{"token": "ilt_…", "tokenType": "identity_link", "expiresAt", "expiresIn": 300, "site", "purposes", "shownOnce": true}. The token is random (32 bytes), valid 5 minutes, only on that website, and each purpose is used once. RouterClip stores only its hash (purged after a day) and records every issue (source, key, website, contact and purposes).- Hand it to the page in the HTML or a JSON answer of your server, never in a URL;
aiocrm.jssends it only in a body. An expired, used, other-website or other-purpose token links nothing and the page gets the same neutral answer: request a new one. - Other errors:
415withoutContent-Type: application/json;422LINK_TOKEN_BODY_INVALID,LINK_TOKEN_FIELD_UNKNOWN,SUBJECT_REQUIRED,SUBJECT_INVALID,EXTERNAL_ID_*,SITE_REQUIRED,SITE_INVALIDorPURPOSES_INVALID. - A merge revokes the pending tokens of the absorbed record. Turning the feature off, revoking the legal basis, revoking the key that requested it, disabling the source, removing the website from the source or a profiling objection (purpose
anonymous) makes pending tokens stop working.
11. Security and personal data
Keep the key server-side only; revoke it if leaked (the platform operator can revoke it too, one by one or every active key of the company at once, always with a reason). With only one of the two features off, “API and events” (marketing.api) or “Task and function API” (api.tasks), the keys keep working on the routes of the other (and on the contact routes, which accept either): cutting every integration of the company needs both off. Do not send personal data in references or properties. RouterClip stores a minimal event (normalized data, the externalId only while unlinked, the visitor HMAC and a body hash for idempotency); deleting a contact unlinks and purges its events, and events follow your company's retention. Contact reads are logged (§8): ask only for the fields your integration needs.
12. Not in v1
Batches of live events (the historical import does exist: §5 bis), receipt callbacks, verified payments (financialVerified), carts (cart.*) and outgoing webhooks for these types. For contacts: DELETE, writing consents or suppressions and bulk linking by CSV.