KoonniPara desarrolladores

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

  • 200

    El 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

cursoren la consultastring — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano.
limiten la consultainteger — Filas por página. Se acota a 100 en vez de rechazarse.
statusen la consulta«open» · «pending» · «resolved» — Filtra por estado. Otro valor responde 400.

Respuestas

  • 200

    Una página de resultados.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene el alcance que este recurso pide.

  • 429

    Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».

  • 500

    Un 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

cursoren la consultastring — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano.
limiten la consultainteger — Filas por página. Se acota a 100 en vez de rechazarse.
conversationIden la consultastring (uuid) — Solo los de esa conversación.

Respuestas

  • 200

    Una página de resultados.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene el alcance que este recurso pide.

  • 429

    Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».

  • 500

    Un 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_closed y 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 obligatoriostring
contactIdstring (uuid)
phonestring — Con indicativo, o el del país de tu empresa.
namestring — El nombre para la ficha, si hay que crearla.
lineIdstring (uuid)
textstring
templateobject
template.name obligatoriostring
template.language obligatoriostring
template.variableslista de string

Respuestas

  • 200

    Ya se había mandado con esa clave de idempotencia: es el mismo mensaje.

  • 202

    En cola: sale en unos segundos.

  • 400

    Faltan campos, o hay varias líneas y no se dijo cuál.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «messages:write».

  • 404

    El contacto o la línea no existen.

  • 409

    No puede salir: window_closed, opted_out, quality_paused o line_unavailable.

  • 422

    La plantilla no está (template_unavailable) o le faltan valores (missing_variables).

  • 429

    El cupo por minuto, el tope diario (daily_limit_reached) o el tramo de Meta (meta_limit_reached).

  • 500

    Un fallo nuestro.

GET /api/v1/cases

Casos

Permiso: cases:read

De más reciente a más viejo.

Parámetros

cursoren la consultastring — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano.
limiten la consultainteger — Filas por página. Se acota a 100 en vez de rechazarse.
statusen la consulta«open» · «in_progress» · «pending» · «resolved» · «cancelled» — Filtra por estado.
referenceen la consultastring — Busca un caso por su número, el actual («CAS-0001») o el de antes del consecutivo.

Respuestas

  • 200

    Una página de resultados.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene el alcance que este recurso pide.

  • 429

    Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».

  • 500

    Un 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 obligatoriostring
title obligatoriostring
summary obligatoriostring
descriptionstring
category obligatoriostring
priority«low» · «normal» · «high» · «urgent»
conversationIdstring (uuid)
assignedMembershipIdstring (uuid) — La persona que responde por el caso. Una persona o un equipo, no los dos.
assignedTeamIdstring (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

  • 200

    El caso, nuevo o el que ya existía con esa clave.

  • 400

    Faltan campos, o la conversación no existe.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «cases:write».

  • 429

    Se agotó el cupo por minuto de la empresa.

  • 500

    Un 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

cursoren la consultastring — El «nextCursor» de la respuesta anterior. Es opaco: se manda tal cual y no se construye a mano.
limiten la consultainteger — Filas por página. Se acota a 100 en vez de rechazarse.
phoneen la consultastring — Con o sin indicativo.
emailen la consultastring
externalSystemen la consultastring — Con `externalId`.
externalIden la consultastring — Con `externalSystem`.

Respuestas

  • 200

    Una página de resultados.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene el alcance que este recurso pide.

  • 429

    Se agotó el cupo por minuto de la empresa. Mira la cabecera «Retry-After».

  • 500

    Un 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

  • 200

    La ficha que ya existía, completada si le faltaba algo.

  • 201

    La ficha nueva.

  • 400

    Faltan datos: hace falta al menos displayName, phone, email o externalRef.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «contacts:write».

  • 409

    El externalRef ya es de otra ficha.

  • 429

    Se agotó el cupo por minuto de la empresa.

  • 500

    Un 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 obligatorioen la rutastring (uuid)

Respuestas

  • 200

    La ficha.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «contacts:read».

  • 404

    No existe, o sus datos se suprimieron.

  • 429

    Se agotó el cupo por minuto de la empresa.

  • 500

    Un 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 obligatorioen la rutastring (uuid)

Respuestas

  • 200

    La ficha corregida.

  • 400

    Algún campo no es válido, o no hay nada que cambiar.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «contacts:write».

  • 404

    No existe, o sus datos se suprimieron.

  • 409

    Un externalRef ya es de otra ficha.

  • 429

    Se agotó el cupo por minuto de la empresa.

  • 500

    Un 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

namestring
phonestring — phone o email: al menos uno.
emailstring
companystring
documentIdstring
externalRefobject
externalRef.system obligatoriostring
externalRef.externalId obligatoriostring
source obligatorioobject
source.channel obligatorio«formulario_web» · «anuncio» · «referido» · «evento» · «chat» · «otro»
source.campaignstring
source.platformstring
source.urlstring (uri)
source.utmobject
source.utm.sourcestring
source.utm.mediumstring
source.utm.campaignstring
source.utm.termstring
source.utm.contentstring
consent obligatorioobject
consent.channels obligatoriolista de «whatsapp» · «llamada» · «correo»
consent.text obligatoriostring — El texto que aceptó, tal cual.
consent.grantedAtstring (date-time) — Cuándo la dio. Si no se dice, ahora.
consent.urlstring (uri) — Dónde la dio: la página del formulario.

Respuestas

  • 200

    La ficha ya existía y se reconoció; la autorización nueva quedó guardada.

  • 201

    La ficha nació con este lead.

  • 400

    Faltan campos: source, consent, o phone y email.

  • 401

    Falta la credencial, o ya no vale.

  • 403

    La clave no tiene «leads:write».

  • 409

    Un externalRef ya es de otra ficha.

  • 429

    Se agotó el cupo por minuto de la empresa.

  • 500

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

  • unauthorized
  • forbidden
  • subscription_restricted
  • not_found
  • conflict
  • invalid_request
  • rate_limited
  • window_closed
  • opted_out
  • template_unavailable
  • missing_variables
  • daily_limit_reached
  • meta_limit_reached
  • quality_paused
  • line_unavailable
  • internal_error

Eventos de webhook

conversation.createdConversación nueva. Alguien escribió por primera vez y se abrió una conversación. Trae: conversationId, contactId, channel, connectionId, startedAt
conversation.resolvedConversació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.receivedMensaje recibido. Entró un mensaje de un cliente. El contenido se consulta en la API. Trae: messageId, conversationId, contactId, channel, messageType, receivedAt
message.sentMensaje enviado. Salió una respuesta, del asistente o de una persona del equipo. Trae: messageId, conversationId, contactId, channel, messageType, authoredBy, sentAt
case.createdCaso abierto. Se abrió un caso, a mano o por una regla. Trae: caseId, reference, conversationId, category, priority, status, createdAt
case.status_changedCaso cambia de estado. Un caso pasó de un estado a otro. Lleva el anterior y el nuevo. Trae: caseId, reference, previousStatus, status, updatedAt
contact.createdContacto 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.updatedContacto 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.mergedContactos 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.createdLead 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.testEvento 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