Data plane (consultas)
La plataforma persiste todo el tráfico de tus canales gestionados (entrante y saliente) en tres recursos del data plane: mensajes, conversaciones y contactos. Cada fila queda atada a tu cuenta de desarrollador, así que vos solo ves tu propia data.
El data plane expone una superficie de consulta read-only sobre esos
tres recursos, más mutaciones livianas (cerrar una conversación, editar
el display_name o el metadata de un contacto, borrar un contacto y su
historial). Auth: X-API-Key. Los scopes están separados en dos grupos:
los tres read:* (read:messages, read:conversations,
read:contacts) son solo lectura, y el scope manage:data habilita
las tres rutas de mutación (PATCH conversación, PATCH contacto, DELETE
contacto). Una key con solo read:contacts puede consultar la lista de
contactos pero NO puede borrar ninguno.
Resumen de endpoints
Section titled “Resumen de endpoints”| Método | Ruta | Scope | Descripción |
|---|---|---|---|
GET | /platform/v1/messages | read:messages | Lista paginada de mensajes con filtros. |
GET | /platform/v1/messages/{id} | read:messages | Detalle de un mensaje propio. |
GET | /platform/v1/conversations | read:conversations | Lista paginada de conversaciones con filtros. |
GET | /platform/v1/conversations/{id} | read:conversations | Detalle de una conversación. |
PATCH | /platform/v1/conversations/{id} | manage:data | Cerrar una conversación (status: ended) o reabrirla (active). |
GET | /platform/v1/contacts | read:contacts | Lista paginada de contactos con búsqueda. |
GET | /platform/v1/contacts/{id} | read:contacts | Detalle de un contacto. |
PATCH | /platform/v1/contacts/{id} | manage:data | Editar display_name o contact_metadata. |
DELETE | /platform/v1/contacts/{id} | manage:data | Borrar el contacto y disparar la cascada asíncrona que limpia sus mensajes y conversaciones. |
Las rutas
{id}que no pertenecen a tu cuenta devuelven 404 opaco (idéntico a “no existe”) para evitar enumeración cross-tenant. Igual patrón que el resto de la plataforma.
Scopes
Section titled “Scopes”Los cuatro scopes del data plane (3 de lectura + 1 de mutación) se eligen
al crear una API key desde el dashboard (Cuenta → API keys). Son
opt-in: una key solo de envío (send:messages) no puede leer mensajes
hasta que le agregues read:messages; una key con read:contacts puede
consultar pero no borrar, etc.
| Scope | Habilita |
|---|---|
read:messages | GET /messages, GET /messages/{id} |
read:conversations | GET /conversations, GET /conversations/{id} |
read:contacts | GET /contacts, GET /contacts/{id} |
manage:data | PATCH /conversations/{id}, PATCH /contacts/{id}, DELETE /contacts/{id} |
Separación lectura ↔ mutación: los scopes
read:*son exclusivamente de lectura. Para cualquier mutación del data plane (cerrar conversación, editar contacto, borrar contacto) necesitásmanage:data. Una key filtrada con soloread:*no es un vector de destrucción de datos.
Un endpoint sin el scope adecuado responde 403. La key sigue siendo válida; lo que falta es el scope.
Paginación por cursor
Section titled “Paginación por cursor”Las tres listas (/messages, /conversations, /contacts) usan
paginación por cursor sobre (created_at, id). Sin OFFSET,
estable bajo inserts concurrentes y consistente en O(log n) por página.
Parámetros
Section titled “Parámetros”| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
limit | int | 50 | Tamaño de página. Se clampea a [1, 200]. |
after | string | (none) | Cursor opaco devuelto como next_cursor. Paginá hacia filas más viejas. |
before | string | (none) | Cursor opaco devuelto como prev_cursor. Paginá hacia filas más nuevas. |
Solo uno de after o before se honra por petición; si mandás los dos,
gana after. Un cursor mal formado se trata como primera página
(decode_cursor nunca rompe).
Response envelope
Section titled “Response envelope”Cada GET de lista devuelve:
{ "data": [ /* hasta limit filas, orden newest-first */ ], "next_cursor": "<opaco>", "prev_cursor": "<opaco>"}next_cursorapunta a la última (más vieja) fila de la página. Pasalo comoafterpara traer la siguiente página de filas más viejas. Esnullcuando llegaste al final.prev_cursorapunta a la primera (más nueva) fila de la página. Pasalo comobeforepara traer las filas nuevas que entraron desde tu última consulta. En la primera página apunta a la fila más nueva conocida, lo que sirve como ancla parabeforeposterior.
Ejemplo: caminar hacia atrás en el tiempo
Section titled “Ejemplo: caminar hacia atrás en el tiempo”Primera página (50 mensajes, newest-first):
curl -H "X-API-Key: $KEY" \ "https://api.kalipto.app/platform/v1/messages?channel_id=42&limit=50"{ "data": [ /* 50 mensajes */ ], "next_cursor": "eyJpZCI6MTIzLCJ0cyI6IjIwMjYtMDYtMTBUMTI6MDA6MDAifQ==", "prev_cursor": "eyJpZCI6MTcyLCJ0cyI6IjIwMjYtMDYtMTBUMTM6MDA6MDAifQ=="}Segunda página (50 mensajes más viejos):
curl -H "X-API-Key: $KEY" \ "https://api.kalipto.app/platform/v1/messages?channel_id=42&limit=50\&after=eyJpZCI6MTIzLCJ0cyI6IjIwMjYtMDYtMTBUMTI6MDA6MDAifQ=="Cuando next_cursor viene null, ya estás en la página más vieja.
Ejemplo: refrescar los mensajes nuevos
Section titled “Ejemplo: refrescar los mensajes nuevos”Para traer los mensajes que entraron después de tu última lectura,
guardá el prev_cursor de la primera consulta y pasalo como before:
curl -H "X-API-Key: $KEY" \ "https://api.kalipto.app/platform/v1/messages?channel_id=42\&before=eyJpZCI6MTcyLCJ0cyI6IjIwMjYtMDYtMTBUMTM6MDA6MDAifQ=="Esto devuelve solo las filas nuevas, también en orden newest-first.
GET /messages
Section titled “GET /messages”Lista mensajes de tu cuenta con filtros y paginación.
Query params
Section titled “Query params”| Parámetro | Tipo | Descripción |
|---|---|---|
channel_id | int | Filtra por canal. |
conversation_id | int | Filtra por conversación. |
direction | inbound | outbound | Sentido del mensaje. |
status | string | Estado de entrega (sent, delivered, read, failed, etc.). |
type | string | Tipo del mensaje (text, image, template, …). |
has_media | bool | true para mensajes con archivo adjunto, false para los puramente textuales. |
since | datetime ISO 8601 | Mensajes con created_at >= since. |
until | datetime ISO 8601 | Mensajes con created_at <= until. |
limit, after, before | Paginación (ver arriba). |
Response 200
Section titled “Response 200”{ "data": [ { "id": 18432, "channel_id": 42, "conversation_id": 901, "meta_message_id": "wamid.HBgL...AA==", "direction": "inbound", "peer_id": "59899123456", "message_type": "text", "text_body": "Hola, ¿siguen abiertos?", "has_media": false, "media_id": null, "status": null, "status_history": [], "meta_error": null, "pricing": null, "referral": null, "sent_at": null, "created_at": "2026-06-10T18:42:13Z" } ], "next_cursor": "eyJpZCI6MTg0MzIsInRzIjoiMjAyNi0wNi0xMFQxODo0MjoxMyJ9", "prev_cursor": "eyJpZCI6MTg0MzIsInRzIjoiMjAyNi0wNi0xMFQxODo0MjoxMyJ9"}| Campo | Descripción |
|---|---|
id | Identificador interno del mensaje. |
channel_id | Canal de tu cuenta al que pertenece. |
conversation_id | Conversación a la que se agrupó el mensaje. |
meta_message_id | wamid de WhatsApp o message_id de Instagram. null si la plataforma todavía no recibió el id (cola de envío). |
direction | inbound (lo recibimos) u outbound (lo enviaste). |
peer_id | wa_id o ig_id del peer. |
message_type | text, image, template, interactive, etc. |
text_body | Cuerpo textual cuando aplica. |
has_media, media_id | Indicador de adjunto + id de Meta para descargar. |
status | Último estado de entrega (solo para outbound). |
status_history | Array con los estados aplicados al mensaje y su timestamp. |
meta_error, pricing, referral | Snapshots crudos de Meta cuando vienen. |
sent_at, created_at | Timestamps en ISO 8601 UTC. |
GET /messages/{id}
Section titled “GET /messages/{id}”Devuelve el detalle de un mensaje propio. 404 opaco si el id no pertenece a tu cuenta.
GET /conversations
Section titled “GET /conversations”Lista las conversaciones de tu cuenta. Una conversación es un agrupador
por (channel_id, peer_id) que se mantiene active mientras hay
tráfico reciente y pasa a ended cuando vos la cerrás explícitamente o
no hay actividad por un período largo (definido por nuestra heurística
interna; el cierre automático no es sincrónico con la última lectura).
Query params
Section titled “Query params”| Parámetro | Tipo | Descripción |
|---|---|---|
channel_id | int | Filtra por canal. |
status | active | ended | Filtra por estado. |
limit, after, before | Paginación. |
Response 200
Section titled “Response 200”{ "data": [ { "id": 901, "channel_id": 42, "contact_id": 1207, "peer_id": "59899123456", "status": "active", "message_count": 12, "inbound_count": 7, "outbound_count": 5, "last_message_preview": "Te paso el pedido.", "last_message_direction": "inbound", "last_active_at": "2026-06-10T18:42:13Z", "ended_at": null, "created_at": "2026-06-09T12:00:00Z" } ], "next_cursor": null, "prev_cursor": "eyJpZCI6OTAxLCJ0cyI6IjIwMjYtMDYtMDlUMTI6MDA6MDAifQ=="}| Campo | Descripción |
|---|---|
id | Identificador interno de la conversación. |
channel_id | Canal al que pertenece. |
contact_id | Contacto asociado, si lo hay. null cuando el contacto fue borrado (el DELETE /contacts/{id} deja la FK en NULL). |
peer_id | wa_id o ig_id del peer. |
status | active o ended. |
message_count, inbound_count, outbound_count | Contadores cumulativos. |
last_message_preview, last_message_direction | Snapshot del último mensaje para listados. |
last_active_at | Timestamp del último mensaje (entrante o saliente). |
ended_at | Cuándo se cerró, si está ended. |
created_at | Cuándo se abrió la conversación. |
Nota sobre ordenamiento: la paginación es por
created_at(estable bajo inserts) y NO porlast_active_at(que muta con cada mensaje nuevo). Si querés ver “las conversaciones más activas”, traé una página y ordenala vos porlast_active_atdel lado cliente.
GET /conversations/{id}
Section titled “GET /conversations/{id}”Devuelve el detalle de una conversación propia.
PATCH /conversations/{id}
Section titled “PATCH /conversations/{id}”Cambia el status de la conversación.
Request body
Section titled “Request body”{ "status": "ended" }| Campo | Tipo | Descripción |
|---|---|---|
status | active | ended | El único campo mutable. Cualquier otro valor devuelve 422. |
Al pasar a ended, la plataforma setea ended_at con el timestamp del
PATCH. Al volver a active, ended_at vuelve a null. La conversación
se puede reabrir tantas veces como quieras: el cambio es idempotente
(PATCH status=ended dos veces deja el mismo ended_at con la última
marca temporal).
Response 200
Section titled “Response 200”El objeto de conversación completo, ya con el status actualizado.
GET /contacts
Section titled “GET /contacts”Lista los contactos de tu cuenta. Un contacto se crea automáticamente
cuando recibimos el primer mensaje de un wa_id o ig_id nuevo. Una
sola fila por (channel_id, peer_id).
Query params
Section titled “Query params”| Parámetro | Tipo | Descripción |
|---|---|---|
channel_id | int | Filtra por canal. |
search | string | Búsqueda ILIKE (case-insensitive) sobre display_name, profile_name, phone y username. Aplica % al inicio y al final del término. |
limit, after, before | Paginación. |
Response 200
Section titled “Response 200”{ "data": [ { "id": 1207, "channel_id": 42, "wa_id": "59899123456", "ig_id": null, "phone": "59899123456", "business_scoped_user_id": null, "username": null, "profile_name": "Maria Lopez", "display_name": "Maria — cliente VIP", "contact_metadata": {"crm_tag": "vip"}, "first_seen_at": "2026-06-09T12:00:00Z", "last_seen_at": "2026-06-10T18:42:13Z", "created_at": "2026-06-09T12:00:00Z" } ], "next_cursor": null, "prev_cursor": "eyJpZCI6MTIwNywidHMiOiIyMDI2LTA2LTA5VDEyOjAwOjAwIn0="}| Campo | Descripción |
|---|---|
id | Identificador interno del contacto. |
channel_id | Canal al que pertenece. |
wa_id, ig_id | Identificadores de Meta. Un contacto típico tiene uno de los dos. |
phone | E.164 sin + cuando lo conocemos. |
business_scoped_user_id | BSUID de Instagram cuando aplica. |
username | Username de Instagram, si Meta lo expone. |
profile_name | Nombre que el peer puso en su perfil de WhatsApp / Instagram. Read-only. |
display_name | Nombre editable por vos. Sustituye al profile_name en tu UI. |
contact_metadata | Diccionario libre string → any que vos manejás (tags, notas, ids de tu CRM…). |
first_seen_at, last_seen_at | Primer y último mensaje conocido del contacto. |
GET /contacts/{id}
Section titled “GET /contacts/{id}”Devuelve el detalle de un contacto propio.
PATCH /contacts/{id}
Section titled “PATCH /contacts/{id}”Edita los campos mutables del contacto.
Request body
Section titled “Request body”{ "display_name": "Maria — cliente VIP", "contact_metadata": {"crm_tag": "vip", "notas": "ventas frecuentes"}}| Campo | Tipo | Descripción |
|---|---|---|
display_name | string (≤255) o null | Si lo omitís, el display_name no se toca. |
contact_metadata | dict o null | Si lo omitís, el contact_metadata no se toca. Si lo mandás, reemplaza el dict actual (no es un merge). |
Solo
display_nameycontact_metadatason mutables. Los demás campos los maneja la plataforma (vienen de Meta o se calculan en ingest).
Response 200
Section titled “Response 200”El objeto de contacto completo, ya con los campos actualizados.
DELETE /contacts/{id}
Section titled “DELETE /contacts/{id}”Borra el contacto y dispara la limpieza asíncrona de su tráfico.
Response 202
Section titled “Response 202”{ "status": "accepted", "contact_id": 1207}El 202 Accepted indica que:
- La fila del contacto ya fue borrada (commit sincrónico).
- La cascada que limpia sus mensajes y conversaciones se ejecuta
asíncronamente, identificando los registros por
(channel_id, peer_id). La respuesta no espera al barrido.
Cascada: la FK
platform_contact_iden conversaciones y mensajes esSET NULL, así que el borrado del contacto no arrastra a esas filas automáticamente. El barrido asincrónico es el que las elimina de forma explícita.
Idempotencia
Section titled “Idempotencia”DELETE sobre un contacto ya borrado devuelve 404 opaco. Es
idempotente desde el lado cliente: reintentar es seguro.
Notas operativas
Section titled “Notas operativas”- Si entra un mensaje del mismo peer después del borrado, la
plataforma crea un contacto nuevo (mismo
(channel_id, peer_id)que el viejo). El borrado es histórico, no un bloqueo del peer. - La ventana entre el commit del borrado y el fin del barrido es
típicamente corta (segundos). Durante esa ventana podés ver
conversaciones con
contact_id = nullapuntando al contacto recién borrado.
Retención por plan
Section titled “Retención por plan”Cada plan tiene un cap de retención sobre las filas del data plane. Una
vez que un mensaje cumple retention_days de antigüedad, queda elegible
para borrado por el barrido de retención (job interno). El borrado es
sin marcha atrás.
| Plan | Retención |
|---|---|
free | 30 días |
lite | 90 días |
pro | 365 días |
business | Ilimitada (null) |
La retención aplica a los tres recursos: mensajes, conversaciones y contactos. Si necesitás un horizonte mayor, exportá periódicamente a tu propio almacenamiento. La plataforma no garantiza retención más allá del cap del plan.
Códigos de error
Section titled “Códigos de error”| Código | Trigger |
|---|---|
401 Unauthorized | Key ausente, inválida, inactiva o expirada. |
403 Forbidden | Key sin el scope requerido (read:* para GET, manage:data para PATCH/DELETE). |
404 Not Found | El {id} no pertenece a tu cuenta. Opaco (idéntico a “no existe”). |
422 Unprocessable Entity | Validación Pydantic: direction fuera de `inbound |
429 Too Many Requests | Rate limit por API key (ver Rate limits). |
Próximo paso
Section titled “Próximo paso”- Webhook entrante (forward): cómo recibís en tu URL los mensajes que después podés consultar acá.
- Uso y cuotas: cuántos mensajes y templates consumiste en el período actual.