Skip to content

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.

MétodoRutaScopeDescripción
GET/platform/v1/messagesread:messagesLista paginada de mensajes con filtros.
GET/platform/v1/messages/{id}read:messagesDetalle de un mensaje propio.
GET/platform/v1/conversationsread:conversationsLista paginada de conversaciones con filtros.
GET/platform/v1/conversations/{id}read:conversationsDetalle de una conversación.
PATCH/platform/v1/conversations/{id}manage:dataCerrar una conversación (status: ended) o reabrirla (active).
GET/platform/v1/contactsread:contactsLista paginada de contactos con búsqueda.
GET/platform/v1/contacts/{id}read:contactsDetalle de un contacto.
PATCH/platform/v1/contacts/{id}manage:dataEditar display_name o contact_metadata.
DELETE/platform/v1/contacts/{id}manage:dataBorrar 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.

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.

ScopeHabilita
read:messagesGET /messages, GET /messages/{id}
read:conversationsGET /conversations, GET /conversations/{id}
read:contactsGET /contacts, GET /contacts/{id}
manage:dataPATCH /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ás manage:data. Una key filtrada con solo read:* 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.

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ámetroTipoDefaultDescripción
limitint50Tamaño de página. Se clampea a [1, 200].
afterstring(none)Cursor opaco devuelto como next_cursor. Paginá hacia filas más viejas.
beforestring(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).

Cada GET de lista devuelve:

{
"data": [ /* hasta limit filas, orden newest-first */ ],
"next_cursor": "<opaco>",
"prev_cursor": "<opaco>"
}
  • next_cursor apunta a la última (más vieja) fila de la página. Pasalo como after para traer la siguiente página de filas más viejas. Es null cuando llegaste al final.
  • prev_cursor apunta a la primera (más nueva) fila de la página. Pasalo como before para 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 para before posterior.

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):

Terminal window
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):

Terminal window
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.

Para traer los mensajes que entraron después de tu última lectura, guardá el prev_cursor de la primera consulta y pasalo como before:

Terminal window
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.

Lista mensajes de tu cuenta con filtros y paginación.

ParámetroTipoDescripción
channel_idintFiltra por canal.
conversation_idintFiltra por conversación.
directioninbound | outboundSentido del mensaje.
statusstringEstado de entrega (sent, delivered, read, failed, etc.).
typestringTipo del mensaje (text, image, template, …).
has_mediabooltrue para mensajes con archivo adjunto, false para los puramente textuales.
sincedatetime ISO 8601Mensajes con created_at >= since.
untildatetime ISO 8601Mensajes con created_at <= until.
limit, after, beforePaginación (ver arriba).
{
"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"
}
CampoDescripción
idIdentificador interno del mensaje.
channel_idCanal de tu cuenta al que pertenece.
conversation_idConversación a la que se agrupó el mensaje.
meta_message_idwamid de WhatsApp o message_id de Instagram. null si la plataforma todavía no recibió el id (cola de envío).
directioninbound (lo recibimos) u outbound (lo enviaste).
peer_idwa_id o ig_id del peer.
message_typetext, image, template, interactive, etc.
text_bodyCuerpo textual cuando aplica.
has_media, media_idIndicador de adjunto + id de Meta para descargar.
statusÚltimo estado de entrega (solo para outbound).
status_historyArray con los estados aplicados al mensaje y su timestamp.
meta_error, pricing, referralSnapshots crudos de Meta cuando vienen.
sent_at, created_atTimestamps en ISO 8601 UTC.

Devuelve el detalle de un mensaje propio. 404 opaco si el id no pertenece a tu cuenta.

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).

ParámetroTipoDescripción
channel_idintFiltra por canal.
statusactive | endedFiltra por estado.
limit, after, beforePaginación.
{
"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=="
}
CampoDescripción
idIdentificador interno de la conversación.
channel_idCanal al que pertenece.
contact_idContacto asociado, si lo hay. null cuando el contacto fue borrado (el DELETE /contacts/{id} deja la FK en NULL).
peer_idwa_id o ig_id del peer.
statusactive o ended.
message_count, inbound_count, outbound_countContadores cumulativos.
last_message_preview, last_message_directionSnapshot del último mensaje para listados.
last_active_atTimestamp del último mensaje (entrante o saliente).
ended_atCuándo se cerró, si está ended.
created_atCuándo se abrió la conversación.

Nota sobre ordenamiento: la paginación es por created_at (estable bajo inserts) y NO por last_active_at (que muta con cada mensaje nuevo). Si querés ver “las conversaciones más activas”, traé una página y ordenala vos por last_active_at del lado cliente.

Devuelve el detalle de una conversación propia.

Cambia el status de la conversación.

{ "status": "ended" }
CampoTipoDescripción
statusactive | endedEl ú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).

El objeto de conversación completo, ya con el status actualizado.

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).

ParámetroTipoDescripción
channel_idintFiltra por canal.
searchstringBúsqueda ILIKE (case-insensitive) sobre display_name, profile_name, phone y username. Aplica % al inicio y al final del término.
limit, after, beforePaginación.
{
"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="
}
CampoDescripción
idIdentificador interno del contacto.
channel_idCanal al que pertenece.
wa_id, ig_idIdentificadores de Meta. Un contacto típico tiene uno de los dos.
phoneE.164 sin + cuando lo conocemos.
business_scoped_user_idBSUID de Instagram cuando aplica.
usernameUsername de Instagram, si Meta lo expone.
profile_nameNombre que el peer puso en su perfil de WhatsApp / Instagram. Read-only.
display_nameNombre editable por vos. Sustituye al profile_name en tu UI.
contact_metadataDiccionario libre string → any que vos manejás (tags, notas, ids de tu CRM…).
first_seen_at, last_seen_atPrimer y último mensaje conocido del contacto.

Devuelve el detalle de un contacto propio.

Edita los campos mutables del contacto.

{
"display_name": "Maria — cliente VIP",
"contact_metadata": {"crm_tag": "vip", "notas": "ventas frecuentes"}
}
CampoTipoDescripción
display_namestring (≤255) o nullSi lo omitís, el display_name no se toca.
contact_metadatadict o nullSi lo omitís, el contact_metadata no se toca. Si lo mandás, reemplaza el dict actual (no es un merge).

Solo display_name y contact_metadata son mutables. Los demás campos los maneja la plataforma (vienen de Meta o se calculan en ingest).

El objeto de contacto completo, ya con los campos actualizados.

Borra el contacto y dispara la limpieza asíncrona de su tráfico.

{
"status": "accepted",
"contact_id": 1207
}

El 202 Accepted indica que:

  1. La fila del contacto ya fue borrada (commit sincrónico).
  2. 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_id en conversaciones y mensajes es SET 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.

DELETE sobre un contacto ya borrado devuelve 404 opaco. Es idempotente desde el lado cliente: reintentar es seguro.

  • 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 = null apuntando al contacto recién borrado.

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.

PlanRetención
free30 días
lite90 días
pro365 días
businessIlimitada (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ódigoTrigger
401 UnauthorizedKey ausente, inválida, inactiva o expirada.
403 ForbiddenKey sin el scope requerido (read:* para GET, manage:data para PATCH/DELETE).
404 Not FoundEl {id} no pertenece a tu cuenta. Opaco (idéntico a “no existe”).
422 Unprocessable EntityValidación Pydantic: direction fuera de `inbound
429 Too Many RequestsRate limit por API key (ver Rate limits).