Referencia
Generada del documento OpenAPI, el mismo que puedes abrir en Postman o en un generador de clientes. Servidor: https://app.koonni.com; las rutas ya llevan /api/v1.
Operaciones
GET /api/v1/openapi.json
Esta misma referencia
Público, sin clave.
Pública y sin credencial: es un contrato, no datos.
Respuestas
200El documento OpenAPI.
GET /api/v1/conversations
Conversaciones
Permiso: conversations:read
De la más reciente a la más vieja. No aplica el recorte por equipo: mira la nota de arriba.
Parámetros
cursor | en la consulta | string — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano. |
limit | en la consulta | integer — Filas por página. Se acota a 100 en vez de rechazarse. |
status | en la consulta | «open» · «pending» · «resolved» — Filtra por estado. Otro valor responde 400. |
Respuestas
200Una página de resultados.
401Falta la credencial, o ya no vale.
403La clave no tiene el alcance que este recurso pide.
429Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».
500Un fallo nuestro. El identificador va en «X-Request-Id».
GET /api/v1/messages
Mensajes
Permiso: messages:read
Con su texto. Sin «conversationId» devuelve los de toda la empresa, paginados.
Parámetros
cursor | en la consulta | string — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano. |
limit | en la consulta | integer — Filas por página. Se acota a 100 en vez de rechazarse. |
conversationId | en la consulta | string (uuid) — Solo los de esa conversación. |
Respuestas
200Una página de resultados.
401Falta la credencial, o ya no vale.
403La clave no tiene el alcance que este recurso pide.
429Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».
500Un fallo nuestro. El identificador va en «X-Request-Id».
POST /api/v1/messages
Mandar un mensaje de WhatsApp
Permiso: messages:write
A quién (contactId o phone) y qué (text o template), uno de cada.
- text solo si la persona te escribió por esa línea en las últimas 24 horas; si no, responde
window_closedy hay que mandar una plantilla. - template es una plantilla aprobada de la línea, por su nombre y su idioma como en Meta; variables van en orden: la primera es
{{1}}. Si la persona no tiene conversación en esa línea, se le abre una. - Con phone y sin ficha, se crea la ficha (con name, si lo das).
- lineId solo hace falta si tu empresa tiene varias líneas de WhatsApp.
Sale firmado con el nombre de la clave («API · nombre») y no le quita la conversación a nadie. Respeta la baja de marketing (opted_out), el tope diario de tu empresa (daily_limit_reached), el tramo de Meta de la línea (meta_limit_reached) y su calidad (quality_paused).
«idempotencyKey» es obligatoria: repetir la misma devuelve el mismo mensaje, con 200, en vez de mandar otro. Que salió de verdad lo cuenta el webhook message.sent.
Cuerpo
idempotencyKey obligatorio | string |
contactId | string (uuid) |
phone | string — Con indicativo, o el del país de tu empresa. |
name | string — El nombre para la ficha, si hay que crearla. |
lineId | string (uuid) |
text | string |
template | object |
template.name obligatorio | string |
template.language obligatorio | string |
template.variables | lista de string |
Respuestas
200Ya se había mandado con esa clave de idempotencia: es el mismo mensaje.
202En cola: sale en unos segundos.
400Faltan campos, o hay varias líneas y no se dijo cuál.
401Falta la credencial, o ya no vale.
403La clave no tiene «messages:write».
404El contacto o la línea no existen.
409No puede salir:
window_closed,opted_out,quality_pausedoline_unavailable.422La plantilla no está (
template_unavailable) o le faltan valores (missing_variables).429El cupo por minuto, el tope diario (
daily_limit_reached) o el tramo de Meta (meta_limit_reached).500Un fallo nuestro.
GET /api/v1/cases
Casos
Permiso: cases:read
De más reciente a más viejo.
Parámetros
cursor | en la consulta | string — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano. |
limit | en la consulta | integer — Filas por página. Se acota a 100 en vez de rechazarse. |
status | en la consulta | «open» · «in_progress» · «pending» · «resolved» · «cancelled» — Filtra por estado. |
reference | en la consulta | string — Busca un caso por su número, el actual («CAS-0001») o el de antes del consecutivo. |
Respuestas
200Una página de resultados.
401Falta la credencial, o ya no vale.
403La clave no tiene el alcance que este recurso pide.
429Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».
500Un fallo nuestro. El identificador va en «X-Request-Id».
POST /api/v1/cases
Abrir un caso
Permiso: cases:write
«idempotencyKey» es obligatoria. Una API se reintenta, y un tiempo de espera agotado no dice si el caso se creó: sin clave, el reintento abre un segundo caso idéntico que alguien tendrá que cerrar a mano. Repetir la misma clave devuelve el caso que ya existe.
Cuerpo
idempotencyKey obligatorio | string |
title obligatorio | string |
summary obligatorio | string |
description | string |
category obligatorio | string |
priority | «low» · «normal» · «high» · «urgent» |
conversationId | string (uuid) |
assignedMembershipId | string (uuid) — La persona que responde por el caso. Una persona o un equipo, no los dos. |
assignedTeamId | string (uuid) — El equipo que responde por el caso; los avisos van a su líder. Sin ninguno de los dos, se reparte solo: quien lleva la conversación, la persona del equipo con menos casos o el propietario. |
Respuestas
200El caso, nuevo o el que ya existía con esa clave.
400Faltan campos, o la conversación no existe.
401Falta la credencial, o ya no vale.
403La clave no tiene «cases:write».
429Se agotó el cupo por minuto de la empresa.
500Un fallo nuestro.
GET /api/v1/contacts
Contactos
Permiso: contacts:read
Las fichas vivas. Las fusionadas y las suprimidas no salen, y eso no se puede cambiar por parámetro.
Con phone, email o externalSystem + externalId busca a una persona: devuelve las fichas vivas con ese dato, sin paginar. Es lo que conviene preguntar antes de crear.
Parámetros
cursor | en la consulta | string — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano. |
limit | en la consulta | integer — Filas por página. Se acota a 100 en vez de rechazarse. |
phone | en la consulta | string — Con o sin indicativo. |
email | en la consulta | string |
externalSystem | en la consulta | string — Con `externalId`. |
externalId | en la consulta | string — Con `externalSystem`. |
Respuestas
200Una página de resultados.
401Falta la credencial, o ya no vale.
403La clave no tiene el alcance que este recurso pide.
429Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».
500Un fallo nuestro. El identificador va en «X-Request-Id».
POST /api/v1/contacts
Dar de alta un contacto, o reconocerlo
Permiso: contacts:write
Busca primero por externalRef, después por el teléfono y después por el correo. Si la persona ya está, completa lo que le falte sin pisar nada y contesta 200; si no, la crea y contesta 201.
Así una sincronización que manda a la misma persona dos veces no la duplica. Con solo un nombre no hay con qué reconocerla, y cada envío crea una ficha. Nunca une dos fichas: eso lo decide una persona en Clientes.
Respuestas
200La ficha que ya existía, completada si le faltaba algo.
201La ficha nueva.
400Faltan datos: hace falta al menos displayName, phone, email o externalRef.
401Falta la credencial, o ya no vale.
403La clave no tiene «contacts:write».
409El externalRef ya es de otra ficha.
429Se agotó el cupo por minuto de la empresa.
500Un fallo nuestro.
GET /api/v1/contacts/{id}
Un contacto
Permiso: contacts:read
Sigue las uniones: si esa ficha se unió a otra, devuelve la superviviente, con su id, que es el que conviene guardar.
Parámetros
id obligatorio | en la ruta | string (uuid) |
Respuestas
200La ficha.
401Falta la credencial, o ya no vale.
403La clave no tiene «contacts:read».
404No existe, o sus datos se suprimieron.
429Se agotó el cupo por minuto de la empresa.
500Un fallo nuestro.
PATCH /api/v1/contacts/{id}
Corregir un contacto
Permiso: contacts:write
Cambia lo que se nombra y solo eso: un campo en null se vacía y uno que no viene no se toca. El nombre queda protegido del canal, como cuando lo corrige una persona.
externalRefs pone o cambia el identificador de cada sistema. No une ni borra fichas.
Parámetros
id obligatorio | en la ruta | string (uuid) |
Respuestas
200La ficha corregida.
400Algún campo no es válido, o no hay nada que cambiar.
401Falta la credencial, o ya no vale.
403La clave no tiene «contacts:write».
404No existe, o sus datos se suprimieron.
409Un externalRef ya es de otra ficha.
429Se agotó el cupo por minuto de la empresa.
500Un fallo nuestro.
POST /api/v1/leads
Recibir un lead
Permiso: leads:write
Crea la ficha, o la reconoce por el teléfono, el correo o el externalRef, y guarda de dónde llegó (source) y qué aceptó (consent).
- consent es obligatorio: al menos un canal y el texto exacto que aceptó la persona. Es la prueba de la autorización (Ley 1581). Para una ficha sin autorización, usa
POST /contacts. - El origen se guarda una vez, el primero. Un lead repetido no lo pisa, pero suma su autorización y queda en el historial de la ficha.
- Las autorizaciones son un historial: cada lead añade una fila por canal, que no se borra.
El webhook lead.created sale la primera vez de cada persona.
Cuerpo
name | string |
phone | string — phone o email: al menos uno. |
email | string |
company | string |
documentId | string |
externalRef | object |
externalRef.system obligatorio | string |
externalRef.externalId obligatorio | string |
source obligatorio | object |
source.channel obligatorio | «formulario_web» · «anuncio» · «referido» · «evento» · «chat» · «otro» |
source.campaign | string |
source.platform | string |
source.url | string (uri) |
source.utm | object |
source.utm.source | string |
source.utm.medium | string |
source.utm.campaign | string |
source.utm.term | string |
source.utm.content | string |
consent obligatorio | object |
consent.channels obligatorio | lista de «whatsapp» · «llamada» · «correo» |
consent.text obligatorio | string — El texto que aceptó, tal cual. |
consent.grantedAt | string (date-time) — Cuándo la dio. Si no se dice, ahora. |
consent.url | string (uri) — Dónde la dio: la página del formulario. |
Respuestas
200La ficha ya existía y se reconoció; la autorización nueva quedó guardada.
201La ficha nació con este lead.
400Faltan campos: source, consent, o phone y email.
401Falta la credencial, o ya no vale.
403La clave no tiene «leads:write».
409Un externalRef ya es de otra ficha.
429Se agotó el cupo por minuto de la empresa.
500Un fallo nuestro.
Códigos de error
Todos los errores llevan {"error": {"code", "message"}}. Ramifica por code; un código que no conozcas, trátalo como un fallo genérico.
unauthorizedforbiddensubscription_restrictednot_foundconflictinvalid_requestrate_limitedwindow_closedopted_outtemplate_unavailablemissing_variablesdaily_limit_reachedmeta_limit_reachedquality_pausedline_unavailableinternal_error
Eventos de webhook
conversation.created | Conversación nueva. Alguien escribió por primera vez y se abrió una conversación. Trae: conversationId, contactId, channel, connectionId, startedAt |
conversation.resolved | Conversación resuelta. Se cerró una conversación. Dice quién la cerró: alguien del equipo, el asistente o el silencio del cliente. Trae: conversationId, contactId, channel, resolvedBy, resolvedAt |
message.received | Mensaje recibido. Entró un mensaje de un cliente. El contenido se consulta en la API. Trae: messageId, conversationId, contactId, channel, messageType, receivedAt |
message.sent | Mensaje enviado. Salió una respuesta, del asistente o de una persona del equipo. Trae: messageId, conversationId, contactId, channel, messageType, authoredBy, sentAt |
case.created | Caso abierto. Se abrió un caso, a mano o por una regla. Trae: caseId, reference, conversationId, category, priority, status, createdAt |
case.status_changed | Caso cambia de estado. Un caso pasó de un estado a otro. Lleva el anterior y el nuevo. Trae: caseId, reference, previousStatus, status, updatedAt |
contact.created | Contacto nuevo. Nació una ficha: alguien escribió por primera vez, o se creó a mano, por importación o por la API. Trae: contactId, channel, source, createdAt |
contact.updated | Contacto actualizado. Cambiaron datos de una ficha. Dice qué campos y quién, no los valores: la ficha se consulta en la API. Trae: contactId, fields, source, updatedAt |
contact.merged | Contactos unidos. Dos fichas eran la misma persona y se unieron. Lleva la que queda y la que se unió a ella. Trae: contactId, mergedContactId, mergedAt |
lead.created | Lead nuevo. Llegó un lead por la API, con su origen y los canales por los que aceptó que lo contacten. La primera vez de cada persona. Trae: contactId, newContact, origin, authorizedChannels, createdAt |
webhook.test | Evento de prueba. Llega solo cuando alguien pulsa «Enviar un evento de prueba» en Para desarrolladores. Con la misma firma que los demás: si la comprueba y contesta 2xx, el destino está listo. No es un hecho de tu empresa: no lo guardes como tal. Trae: message, endpointId |