API REST
La API REST de Artificialic te permite integrar tu plataforma de mensajería con sistemas externos como CRMs, bots, ERPs o cualquier aplicación que necesite enviar mensajes o gestionar contactos programáticamente.
Esta página es para desarrolladores. Asume que sabes qué es una request HTTP, cómo enviar headers de autenticación y cómo parsear JSON. Si no eres quien va a programar la integración, pásale este link a tu equipo técnico — ellos van a saber qué hacer con esto.
Alcance actual: la API v1 cubre canales, contactos, mensajes (texto, plantillas y media), conversaciones, plantillas, difusiones, etiquetas, carpetas y webhooks salientes — 45 operaciones en total. Lo que todavía se gestiona solo desde el panel: automatizaciones (más allá de dispararlas por webhook), agentes de IA y base de conocimiento.
Autenticación
Todas las solicitudes a la API deben incluir un header Authorization con una clave de API válida:
Authorization: Bearer ak_tu_clave_aqui
Obtener una clave de API
- Ve a Configuración → API en tu dashboard.
- Haz clic en Crear clave.
- Asigna un nombre descriptivo (ej. "Integración CRM") y selecciona los permisos necesarios.
- Copia la clave inmediatamente. Solo se muestra una vez. Si la pierdes, deberás crear una nueva.
Características de seguridad
- Las claves tienen el prefijo
ak_seguido de 64 caracteres hexadecimales. - Se almacenan como un hash SHA-256 — ni siquiera nosotros podemos ver tu clave.
- Puedes revocar una clave en cualquier momento desde el dashboard. La revocación es inmediata.
- Cada clave registra la fecha de último uso para que puedas auditar el acceso.
Importante: Nunca expongas tu clave de API en código del lado del cliente (frontend, apps móviles). Úsala solo desde tu servidor backend.
Scopes (Permisos)
Cada clave de API tiene uno o más scopes que determinan qué operaciones puede realizar. Asigna solo los permisos que necesites (principio de menor privilegio).
| Scope | Qué permite | Operaciones |
|---|---|---|
| channels:read | Leer canales | GET /api/v1/channels GET /api/v1/channels/{id} GET /api/v1/channels/{id}/health |
| contacts:read | Leer contactos | GET /api/v1/contacts GET /api/v1/contacts/{id} |
| contacts:write | Crear y editar contactos | POST /api/v1/contacts PATCH /api/v1/contacts/{id} |
| messages:send | Enviar mensajes y plantillas | POST /api/v1/messages POST /api/v1/messages/{id}/retry |
| messages:read | Leer mensajes y consultar estado de entrega | GET /api/v1/messages/{id}/status GET /api/v1/conversations/{id}/messages |
| automations:trigger | Disparar automatizaciones (webhooks) | POST /api/hooks/{token} — el trigger "webhook" de una automatización, cuando el nodo exige clave de API |
| contacts:delete | Eliminar contactos (borra su historial) | DELETE /api/v1/contacts/{id} |
| media:send | Enviar imágenes, audio y documentos | POST /api/v1/messages/media |
| conversations:read | Leer conversaciones | GET /api/v1/conversations GET /api/v1/conversations/{id} |
| conversations:write | Crear conversaciones, asignar, archivar y cambiar el modo | POST /api/v1/conversations PATCH /api/v1/conversations/{id} |
| templates:read | Leer plantillas | GET /api/v1/templates GET /api/v1/templates/{id} |
| templates:write | Crear, sincronizar y eliminar plantillas | POST /api/v1/templates DELETE /api/v1/templates/{id} POST /api/v1/templates/sync |
| labels:read | Leer etiquetas | GET /api/v1/labels |
| labels:write | Crear, aplicar y eliminar etiquetas | POST /api/v1/conversations/{id}/labels DELETE /api/v1/conversations/{id}/labels POST /api/v1/labels DELETE /api/v1/labels/{id} |
| folders:read | Leer carpetas | GET /api/v1/folders |
| folders:write | Crear, editar y eliminar carpetas | POST /api/v1/folders PATCH /api/v1/folders/{id} DELETE /api/v1/folders/{id} |
| broadcasts:read | Leer difusiones y sus envíos | GET /api/v1/broadcasts GET /api/v1/broadcasts/{id} GET /api/v1/broadcasts/{id}/runs POST /api/v1/broadcasts/preview-audience |
| broadcasts:write | Crear y editar difusiones | POST /api/v1/broadcasts |
| broadcasts:execute | Lanzar, pausar y cancelar difusiones | POST /api/v1/broadcasts/{id}/{action} |
| webhooks:manage | Gestionar webhooks salientes | GET /api/v1/webhooks POST /api/v1/webhooks PATCH /api/v1/webhooks/{id} DELETE /api/v1/webhooks/{id} POST /api/v1/webhooks/{id}/test GET /api/v1/webhooks/{id}/deliveries POST /api/v1/webhooks/{id}/reset-failures POST /api/v1/webhooks/deliveries/{deliveryId}/retry |
Nota: eliminar contactos exige su propio scope contacts:delete, separado de contacts:write a propósito: el borrado arrastra en cascada las conversaciones del contacto y todo su historial de mensajes. Lo mismo con las difusiones: crear un borrador (broadcasts:write) es inofensivo, lanzarlo (broadcasts:execute) envía miles de mensajes reales.
Modo de envío
Cuando un mensaje sale por la API hay que decidir a quién se le atribuye. No es cosmético: define si la conversación queda marcada como atendida por un humano, lo que detiene los bots y las automatizaciones de ese hilo.
Cada clave tiene su modo, configurable en Configuración → API:
- API REST (por defecto): el mensaje se atribuye a la clave. No toma la conversación, no frena las automatizaciones y no se asigna a nadie. En el panel se ve como
API REST · nombre de la clave. Es lo que quieres para avisos, confirmaciones y recordatorios que manda tu sistema. - Como agente humano: sale a nombre de un miembro de tu equipo, toma el control de la conversación y pausa los bots. Úsalo solo cuando tu sistema es la interfaz por la que responde una persona.
Se puede des-escalar por solicitud mandando sendMode: "via_api" en el cuerpo. Lo contrario no: una clave en modo API REST que pida as_user recibe 403 SEND_MODE_NOT_ALLOWED. Escalar es decisión del dueño de la clave, no de quien llama.
Canales de la clave
Por defecto una clave accede a todos los canales de tu organización, incluidos los que conectes después. Puedes restringirla desde el panel: a partir de ahí, usar un canal fuera de la lista devuelve 403 CHANNEL_NOT_ALLOWED, y las lecturas también se acotan.
Al enviar, el canal se resuelve así: si mandas channelId se usa ese; si mandas channelType gana el canal por defecto de la clave, y si hay varios candidatos se elige el más antiguo de forma determinista avisándolo en el header X-Channel-Resolution. Si no mandas ninguno, se usa el canal por defecto. Si prefieres que la ambigüedad sea un error, configura la clave para exigir canal explícito.
Límites e idempotencia
120 solicitudes por minuto por clave (ajustable según tu plan), con sub-límites más estrictos para envíos, borrados y difusiones. El cuerpo máximo es 256 KB. Al superar un límite respondemos 429 con Retry-After y los headers X-RateLimit-*.
Si se te corta la conexión antes de recibir la respuesta, no sabes si el mensaje salió. Manda un header Idempotency-Key único por operación: si reintentas con la misma clave y el mismo cuerpo, te devolvemos la respuesta original con Idempotency-Replayed: true en vez de enviar de nuevo.
curl -X POST "$BASE/api/v1/messages" \
-H "Authorization: Bearer ak_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"channelId":"...","phoneNumber":"5215512345678","content":"Hola"}'Webhooks salientes
En vez de preguntarnos cada X segundos si pasó algo, registra una URL y te avisamos nosotros. Se configuran en Configuración → API → Webhooks. Los eventos se suscriben uno por uno: mensaje entrante, cambio de estado de entrega, conversación nueva, cambio de asignación, cambio de modo de atención y difusión finalizada.
Tu URL es pública, así que cualquiera podría mandarle eventos falsos: por eso cada entrega va firmada con el secreto que te damos al crear el webhook (se muestra una sola vez). El header X-Artificialic-Signature tiene el formato t=<unix>,v1=<hmac>, y lo firmado es timestamp.cuerpo — verifica sobre el cuerpo crudo, antes de parsear el JSON.
Cada entrega trae además estos headers:
X-Artificialic-Event— tipo de evento, para rutear sin parsear el cuerpo.X-Artificialic-Delivery— id único de la entrega: úsalo para deduplicar reintentos.X-Artificialic-Attempt— número de intento (1 = primero).
Verificar la firma
// Node.js — verifica sobre el cuerpo CRUDO, no el JSON re-serializado
import { createHmac, timingSafeEqual } from "crypto";
function verifySignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("="))
);
// Anti-replay: rechaza firmas de hace más de 5 minutos
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}Responde 2xx para confirmar. Ante 5xx o 429 reintentamos hasta 5 veces con espera creciente; otros 4xx no se reintentan. Si tu endpoint falla 20 eventos seguidos lo desactivamos y te lo mostramos en el panel.
Referencia completa (OpenAPI)
Esta página cubre los conceptos y los endpoints más usados. La referencia exhaustiva está en la especificación OpenAPI 3.1, que puedes importar en Postman, Insomnia o un generador de SDK:
GET /api/v1/openapi.json
Es pública y no requiere autenticación: describe la forma de la API, no datos de nadie. Ahí están también conversaciones, media, plantillas, difusiones, etiquetas, carpetas y webhooks.
Formato de respuestas
Todas las respuestas son JSON. Las respuestas exitosas envuelven los datos en un campo data:
// Respuesta exitosa
{
"data": {
"id": "clxyz...",
"name": "Juan Pérez",
...
}
}
// Lista con paginación
{
"data": [ ... ],
"nextCursor": "clxyz..." // null si no hay más resultados
}Las respuestas de error usan un campo error con un código máquina y un mensaje legible:
// Respuesta de error
{
"error": {
"code": "UNAUTHORIZED",
"message": "API key inválida o revocada"
}
}Códigos HTTP
| Código | Significado |
|---|---|
| 200 | Operación exitosa |
| 201 | Recurso creado exitosamente |
| 400 | Error de validación en los datos enviados |
| 401 | Clave de API faltante, inválida o revocada |
| 403 | Scope insuficiente o plan sin acceso a API |
| 404 | Recurso no encontrado |
| 409 | Conflicto (ej. contacto duplicado) |
| 500 | Error interno del servidor |
Todas las operaciones
La lista completa de endpoints, con el permiso que exige cada uno. Las secciones siguientes profundizan en los más usados — parámetros, respuestas y ejemplos. Para el detalle exhaustivo de cualquiera de los demás, la especificación OpenAPI los describe todos y se puede importar en Postman o en un generador de SDK.
Canales
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/channels | Listar canales | channels:read |
GET /api/v1/channels/{id} | Detalle de un canal | channels:read |
GET /api/v1/channels/{id}/health | Salud del canal (calidad, tier, problemas de cuenta) | channels:read |
Contactos
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/contacts | Listar contactos | contacts:read |
POST /api/v1/contacts | Crear contacto | contacts:write |
GET /api/v1/contacts/{id} | Detalle de contacto con sus conversaciones | contacts:read |
PATCH /api/v1/contacts/{id} | Editar contacto | contacts:write |
DELETE /api/v1/contacts/{id} | Eliminar contacto | contacts:delete |
Mensajes
| Operación | Qué hace | Permiso |
|---|---|---|
POST /api/v1/messages | Enviar un mensaje o una plantilla | messages:send |
POST /api/v1/messages/media | Enviar imagen, audio, video o documento | media:send |
GET /api/v1/messages/{id}/status | Estado de entrega de un mensaje | messages:read |
POST /api/v1/messages/{id}/retry | Reintentar un mensaje fallido | messages:send |
Conversaciones
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/conversations | Listar conversaciones | conversations:read |
POST /api/v1/conversations | Abrir una conversación con un contacto | conversations:write |
GET /api/v1/conversations/{id} | Detalle de conversación | conversations:read |
PATCH /api/v1/conversations/{id} | Archivar, asignar, mover de carpeta, marcar leída o cambiar el modo | conversations:write |
GET /api/v1/conversations/{id}/messages | Mensajes de una conversación | messages:read |
POST /api/v1/conversations/{id}/labels | Aplicar una etiqueta a la conversación | labels:write |
DELETE /api/v1/conversations/{id}/labels | Quitar una etiqueta de la conversación | labels:write |
Plantillas
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/templates | Listar plantillas de un canal | templates:read |
POST /api/v1/templates | Crear plantilla en Meta | templates:write |
GET /api/v1/templates/{id} | Detalle de plantilla | templates:read |
DELETE /api/v1/templates/{id} | Eliminar plantilla | templates:write |
POST /api/v1/templates/sync | Sincronizar el estado de las plantillas con Meta | templates:write |
Difusiones
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/broadcasts | Listar difusiones | broadcasts:read |
POST /api/v1/broadcasts | Crear difusión | broadcasts:write |
GET /api/v1/broadcasts/{id} | Detalle de difusión con su última corrida | broadcasts:read |
GET /api/v1/broadcasts/{id}/runs | Corridas de una difusión | broadcasts:read |
POST /api/v1/broadcasts/{id}/{action} | Lanzar, pausar, reanudar o cancelar una difusión | broadcasts:execute |
POST /api/v1/broadcasts/preview-audience | Cuántos destinatarios tendría una difusión, sin crearla | broadcasts:read |
Etiquetas
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/labels | Listar etiquetas | labels:read |
POST /api/v1/labels | Crear etiqueta | labels:write |
DELETE /api/v1/labels/{id} | Eliminar etiqueta | labels:write |
Carpetas
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/folders | Listar carpetas | folders:read |
POST /api/v1/folders | Crear carpeta | folders:write |
PATCH /api/v1/folders/{id} | Editar carpeta | folders:write |
DELETE /api/v1/folders/{id} | Eliminar carpeta | folders:write |
Webhooks salientes
| Operación | Qué hace | Permiso |
|---|---|---|
GET /api/v1/webhooks | Listar webhooks salientes | webhooks:manage |
POST /api/v1/webhooks | Registrar un webhook saliente | webhooks:manage |
PATCH /api/v1/webhooks/{id} | Editar o pausar un webhook | webhooks:manage |
DELETE /api/v1/webhooks/{id} | Eliminar un webhook | webhooks:manage |
POST /api/v1/webhooks/{id}/test | Enviar un evento de prueba y ver la respuesta real | webhooks:manage |
GET /api/v1/webhooks/{id}/deliveries | Historial de entregas: qué se mandó y qué respondió tu servidor | webhooks:manage |
POST /api/v1/webhooks/{id}/reset-failures | Reiniciar el contador de fallos consecutivos | webhooks:manage |
POST /api/v1/webhooks/deliveries/{deliveryId}/retry | Reintentar una entrega fallida | webhooks:manage |
Endpoints — Canales
GET/api/v1/channels
Retorna la lista de canales conectados a tu organización. No incluye credenciales de acceso.
Scope requerido: channels:read
Query params
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| type | string | No | Filtrar por tipo: WHATSAPP, MESSENGER, INSTAGRAM, WEBCHAT |
Respuesta
{
"data": [
{
"id": "cm1abc...",
"name": "WhatsApp Business",
"type": "WHATSAPP",
"status": "CONNECTED",
"profilePictureUrl": "https://...",
"createdAt": "2026-01-15T10:30:00.000Z"
}
]
}Ejemplo cURL
curl -H "Authorization: Bearer ak_tu_clave" \ https://tu-dominio.com/api/v1/channels
Endpoints — Contactos
GET/api/v1/contacts
Lista los contactos de tu organización con paginación basada en cursor.
Scope requerido: contacts:read
Query params
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| search | string | No | Buscar por nombre, teléfono, email o ID externo (case-insensitive) |
| cursor | string | No | ID del último contacto de la página anterior (para paginación) |
| limit | number | No | Contactos por página (default: 20, máximo: 100) |
Respuesta
{
"data": [
{
"id": "cm1def...",
"name": "Juan Pérez",
"phoneNumber": "584121234567",
"externalId": null,
"email": "[email protected]",
"avatarUrl": null,
"createdAt": "2026-02-10T08:00:00.000Z"
}
],
"nextCursor": "cm1ghi..." // null si no hay más páginas
}Paginación
La API usa paginación por cursor. Para obtener la siguiente página, pasa el valor de nextCursor como parámetro cursor:
# Primera página GET /api/v1/contacts?limit=20 # Siguiente página GET /api/v1/contacts?limit=20&cursor=cm1ghi...
Ejemplo cURL
curl -H "Authorization: Bearer ak_tu_clave" \ "https://tu-dominio.com/api/v1/contacts?search=juan&limit=10"
GET/api/v1/contacts/:id
Obtiene un contacto con todas sus conversaciones. Esto es clave para saber qué channelId usar al enviar un mensaje.
Scope requerido: contacts:read
Path params
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | string | Sí | ID del contacto |
Respuesta
{
"data": {
"id": "cm1def...",
"name": "Juan Pérez",
"phoneNumber": "584121234567",
"externalId": null,
"email": "[email protected]",
"avatarUrl": null,
"metadata": null,
"createdAt": "2026-02-10T08:00:00.000Z",
"conversations": [
{
"id": "cm1conv...",
"status": "OPEN",
"channelId": "cm1ch...",
"lastMessageAt": "2026-03-20T14:00:00.000Z",
"channel": {
"name": "WhatsApp Business",
"type": "WHATSAPP"
}
}
]
}
}El array conversations te permite identificar en qué canales tiene conversaciones activas este contacto. Usa el channelId junto con el id del contacto para enviar mensajes.
Ejemplo cURL
curl -H "Authorization: Bearer ak_tu_clave" \ https://tu-dominio.com/api/v1/contacts/cm1def...
POST/api/v1/contacts
Crea un nuevo contacto en tu organización.
Scope requerido: contacts:write
Body (JSON)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | No | Nombre del contacto |
| phoneNumber | string | No | Teléfono en formato internacional (ej. +584121234567) |
| string | No | Correo electrónico |
Nota: Se requiere al menos name o phoneNumber. El número de teléfono se valida como formato E.164 internacional y se almacena sin el signo + (ej. 584121234567).
Respuesta (201 Created)
{
"data": {
"id": "cm1new...",
"name": "María García",
"phoneNumber": "584121234567",
"externalId": null,
"email": null,
"createdAt": "2026-03-23T10:00:00.000Z"
}
}Errores específicos
| HTTP | Código | Causa |
|---|---|---|
| 400 | BAD_REQUEST | Falta name y phoneNumber, o teléfono inválido |
| 409 | CONFLICT | Ya existe un contacto con ese número de teléfono |
Ejemplo cURL
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{"name":"María García","phoneNumber":"+584121234567"}' \
https://tu-dominio.com/api/v1/contactsPATCH/api/v1/contacts/:id
Actualiza uno o más campos de un contacto existente. Solo envía los campos que deseas modificar.
Scope requerido: contacts:write
Path params
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | string | Sí | ID del contacto |
Body (JSON, todos opcionales)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | No | Nuevo nombre |
| phoneNumber | string | No | Nuevo teléfono |
| string | No | Nuevo email |
Respuesta
{
"data": {
"id": "cm1def...",
"name": "Juan A. Pérez",
"phoneNumber": "584121234567",
"externalId": null,
"email": "[email protected]",
"createdAt": "2026-02-10T08:00:00.000Z"
}
}Ejemplo cURL
curl -X PATCH \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}' \
https://tu-dominio.com/api/v1/contacts/cm1def...DELETE/api/v1/contacts/:id
Elimina un contacto y todas sus conversaciones asociadas. Esta acción es irreversible.
Scope requerido: contacts:delete — scope aparte de contacts:write, porque el borrado arrastra en cascada las conversaciones del contacto y todo su historial de mensajes. Una clave con solo contacts:write recibe 403.
Path params
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | string | Sí | ID del contacto a eliminar |
Respuesta
{
"success": true
}Ejemplo cURL
curl -X DELETE \ -H "Authorization: Bearer ak_tu_clave" \ https://tu-dominio.com/api/v1/contacts/cm1def...
Endpoints — Mensajes
POST/api/v1/messages
Envía un mensaje de texto o una plantilla a un contacto. Acepta múltiples formas de identificar al contacto y al canal. Si el contacto o la conversación no existen, se crean automáticamente — salvo que lo apagues con createContactIfMissing o en la configuración de la clave.
Scope requerido: messages:send
Identificación del contacto (al menos uno requerido)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| contactId | string | No | ID de un contacto existente |
| phoneNumber | string | No | Número de teléfono con código de país (ej. +584121234567). Solo para canales WHATSAPP. Auto-crea contacto si no existe. |
| facebookId | string | No | ID de Facebook/Messenger del usuario. Solo para canales MESSENGER. Auto-crea contacto si no existe. |
| instagramId | string | No | ID de Instagram del usuario. Solo para canales INSTAGRAM. Auto-crea contacto si no existe. |
Identificación del canal (al menos uno requerido)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| channelId | string | No | ID de un canal específico |
| channelType | string | No | Tipo de canal: WHATSAPP, MESSENGER, INSTAGRAM, WEBCHAT. Si hay varios conectados de ese tipo gana el canal por defecto de la clave; si no hay, el más antiguo de forma determinista, y la respuesta lo avisa en meta.warnings y en el header X-Channel-Resolution. |
Si no mandas ninguno de los dos, se usa el canal por defecto de la clave. Si tampoco hay, la respuesta es 400 CHANNEL_REQUIRED. Ver Canales de la clave.
Contenido del mensaje (exactamente uno requerido)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| content | string | No | Texto plano del mensaje |
| template | object | No | Plantilla a enviar (ver estructura abajo) |
| template.id | string | Sí | ID de la plantilla. Para message_template puedes usar el cuid interno (cm...) o el ID numérico que te da Meta. |
| template.type | string | Sí | "message_template" (Meta/WhatsApp) o "generic_template" (plantilla genérica de la organización) |
| template.variables | object | No | Variables de sustitución: {"1": "valor", "2": "valor"} |
Tipos de plantilla:
message_template— Plantilla de WhatsApp (Meta). Debe estar aprobada. Solo funciona en canales WHATSAPP. Se envía como mensaje de plantilla nativo.generic_template— Plantilla genérica de tu organización. Funciona en cualquier canal. Las variables se sustituyen y se envía como texto plano.
Comportamiento del envío (opcionales)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| sendMode | string | No | "via_api" o "as_user". Solo permite DES-escalar: una clave en modo humano puede mandar "via_api", pero una clave en modo API que pida "as_user" recibe 403 SEND_MODE_NOT_ALLOWED. |
| sendAsUserId | string | No | Solo con sendMode "as_user": miembro a nombre del que sale el mensaje. |
| createContactIfMissing | boolean | No | Por defecto true. Con false, si el destinatario no existe la respuesta es 404 CONTACT_NOT_FOUND y no se crea nada. Mandar true contra una clave que lo tiene apagado devuelve 403 CONTACT_CREATION_NOT_ALLOWED. |
Flujo interno
- Se resuelve el canal (por ID, por tipo, o el canal por defecto de la clave) y se valida contra su lista de canales permitidos.
- Se resuelve o crea el contacto (por ID, teléfono, facebookId o instagramId).
- Se crea la conversación si no existe (upsert).
- Se verifica la ventana de conversación (ej. 24h para WhatsApp — las plantillas no requieren ventana abierta).
- Se crea el mensaje con estado
PENDING. - Se envía al proveedor del canal.
- Se actualiza el estado a
SENToDELIVERED. - Se emite un evento SSE para actualización en tiempo real.
Respuesta
Con el modo API REST (el de fábrica) el mensaje se persiste como SYSTEM sin usuario asociado: no toma la conversación ni frena las automatizaciones. En el panel se ve como API REST · <nombre de la clave>. Con as_user el senderType es AGENT y sender trae al miembro.
{
"data": {
"id": "cm1msg...",
"conversationId": "cm1conv...",
"senderType": "SYSTEM",
"senderId": null,
"contentType": "TEXT",
"content": "Hola, tu pedido está listo",
"status": "SENT",
"externalId": "wamid.abc...",
"createdAt": "2026-03-23T10:30:00.000Z",
"updatedAt": "2026-03-23T10:30:01.000Z",
"sender": null
},
"meta": {
"channel": { "id": "cm1ch...", "name": "Ventas", "type": "WHATSAPP" }
}
}Errores específicos
| HTTP | Código | Causa |
|---|---|---|
| 400 | BAD_REQUEST | Falta identificador de contacto o canal |
| 400 | BAD_REQUEST | phoneNumber solo válido para WHATSAPP / facebookId para MESSENGER / instagramId para INSTAGRAM |
| 400 | BAD_REQUEST | message_template solo funciona en canales WHATSAPP |
| 400 | BAD_REQUEST | Plantilla no aprobada / Ventana de conversación expirada |
| 400 | CHANNEL_REQUIRED | No mandaste canal y la clave no tiene uno por defecto |
| 404 | CHANNEL_NOT_FOUND | El canal no existe (o es de otra organización) |
| 403 | CHANNEL_NOT_ALLOWED | La clave está limitada a otros canales. error.details trae los permitidos. |
| 409 | CHANNEL_DISCONNECTED | El canal existe pero está desconectado |
| 409 | AMBIGUOUS_CHANNEL | Hay varios canales de ese tipo y la clave exige canal explícito. error.details lista los candidatos. |
| 404 | CONTACT_NOT_FOUND | El destinatario no existe y no se puede dar de alta |
| 403 | CONTACT_CREATION_NOT_ALLOWED | Pediste createContactIfMissing: true con una clave que lo tiene apagado |
| 403 | SEND_MODE_NOT_ALLOWED | Pediste as_user con una clave en modo API REST |
| 404 | NOT_FOUND | Contacto o plantilla no encontrada |
| 424 | PROVIDER_ERROR | El proveedor (Meta/WhatsApp) rechazó el envío. El campo error.message contiene la razón (token expirado, sin permisos, número inválido, etc.). El mensaje se persiste en DB con status FAILED. |
Ventana de conversación: En WhatsApp, solo puedes enviar mensajes de texto libre dentro de las 24 horas posteriores al último mensaje del contacto. Después de ese período, usa una message_template aprobada para reabrir la conversación. Las plantillas genéricas (generic_template) se envían como texto y están sujetas a la misma restricción de ventana.
Ejemplo 1: Texto plano por número de teléfono
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+584121234567",
"channelType": "WHATSAPP",
"content": "Hola, tu pedido está listo para retiro."
}' \
https://tu-dominio.com/api/v1/messagesEjemplo 2: Template de WhatsApp
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+584121234567",
"channelType": "WHATSAPP",
"template": {
"id": "cm1tmpl...",
"type": "message_template",
"variables": { "1": "Juan", "2": "#12345" }
}
}' \
https://tu-dominio.com/api/v1/messagesEjemplo 3: Template genérica
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"contactId": "cm1def...",
"channelId": "cm1ch...",
"template": {
"id": "cm1gen...",
"type": "generic_template",
"variables": { "1": "Juan", "2": "#12345" }
}
}' \
https://tu-dominio.com/api/v1/messagesEjemplo 4: Retrocompatible (contactId + channelId + content)
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"contactId": "cm1def...",
"channelId": "cm1ch...",
"content": "Hola, tu pedido está listo."
}' \
https://tu-dominio.com/api/v1/messagesGET/api/v1/messages/:id/status
Consulta el estado de entrega actual de un mensaje. La respuesta inicial del POST /api/v1/messages indica solo que el mensaje fue encolado en el proveedor (status SENT); los estados posteriores (DELIVERED, READ, FAILED) llegan de manera asíncrona a través de webhooks. Este endpoint te permite consultar el estado más reciente sin tener que escuchar webhooks.
Scope requerido: messages:read
Parámetros de URL
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | string | Sí | ID del mensaje. Acepta el cuid interno (cm...) o el externalId del proveedor (en WhatsApp es el wamid devuelto al enviar). |
Respuesta
{
"data": {
"id": "cm1msg...",
"externalId": "wamid.HBg...",
"status": "DELIVERED",
"errorMessage": null,
"contentType": "TEMPLATE",
"conversationId": "cm1conv...",
"createdAt": "2026-03-23T10:30:00.000Z",
"updatedAt": "2026-03-23T10:30:05.123Z"
}
}Estados posibles
| Status | Significado |
|---|---|
| PENDING | El mensaje se creó pero aún no se envió al proveedor. |
| SENT | El proveedor (Meta, etc.) aceptó el mensaje y lo encoló para entrega. |
| DELIVERED | El mensaje llegó al dispositivo del destinatario. |
| READ | El destinatario abrió el chat y vio el mensaje (si tiene acuses de lectura activados). |
| FAILED | El proveedor rechazó la entrega. El campo errorMessage contiene la razón. |
Errores específicos
| HTTP | Código | Causa |
|---|---|---|
| 403 | FORBIDDEN | La API key no tiene scope messages:read |
| 404 | NOT_FOUND | El mensaje no existe o pertenece a otra organización |
Ejemplo: consultar por wamid
curl -H "Authorization: Bearer ak_tu_clave" \ https://tu-dominio.com/api/v1/messages/wamid.HBgMNTg.../status
Ejemplo: consultar por cuid interno
curl -H "Authorization: Bearer ak_tu_clave" \ https://tu-dominio.com/api/v1/messages/cm1msg.../status
POST/api/v1/messages/media
Envía una imagen, audio, video o documento a una conversación existente. El archivo viaja en base64 dentro del JSON (no multipart): mantiene el mismo contrato que el resto de la API — JSON + Bearer — y las mismas defensas de tamaño.
Scope requerido: media:send
Body (JSON)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| conversationId | string | Sí | Conversación destino. Lo devuelve el POST /api/v1/messages (campo conversationId), GET /api/v1/conversations, o POST /api/v1/conversations para abrir una nueva. |
| mediaType | string | Sí | "image", "audio", "video" o "document" |
| data | string | Sí | El archivo codificado en base64 |
| mimeType | string | Sí | Content-type real del archivo (ej. image/jpeg, application/pdf) |
| filename | string | No | Nombre del archivo (relevante para documentos) |
| caption | string | No | Texto que acompaña al medio |
Tope de tamaño: el cuerpo completo de la solicitud se corta en 256 KB, y el base64 infla el archivo ~33%, así que el archivo efectivo máximo ronda los 190 KB. Para archivos más grandes, envía un enlace como mensaje de texto con POST /api/v1/messages.
Acepta Idempotency-Key igual que el envío de texto. El mensaje se atribuye según el modo de la clave (API REST o agente humano), con las mismas reglas.
Respuesta
{
"data": {
"id": "cm1msg...",
"conversationId": "cm1conv...",
"senderType": "SYSTEM",
"contentType": "IMAGE",
"mediaUrl": "https://...",
"status": "SENT",
"externalId": "wamid.abc...",
"createdAt": "2026-03-23T10:30:00.000Z"
}
}Ejemplo cURL
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d "{
\"conversationId\": \"cm1conv...\",
\"mediaType\": \"image\",
\"mimeType\": \"image/jpeg\",
\"filename\": \"recibo.jpg\",
\"caption\": \"Tu comprobante de pago\",
\"data\": \"$(base64 -w0 recibo.jpg)\"
}" \
https://tu-dominio.com/api/v1/messages/mediaErrores comunes
| HTTP | Código | Causa probable | Solución |
|---|---|---|---|
| 401 | UNAUTHORIZED | Falta el header Authorization | Agrega Authorization: Bearer ak_... |
| 401 | UNAUTHORIZED | Clave inválida o revocada | Verifica la clave en Configuración → API |
| 403 | FORBIDDEN | Scope insuficiente | Crea una clave nueva con el scope necesario |
| 403 | FORBIDDEN | Plan sin acceso a API | Actualiza tu plan a uno que incluya API dedicada |
| 404 | NOT_FOUND | Recurso no encontrado o no pertenece a tu org | Verifica el ID del recurso |
| 409 | CONFLICT | Duplicado (ej. mismo teléfono) | Busca el contacto existente antes de crear |
| 401 | KEY_EXPIRED | La clave tenía fecha de vencimiento y ya pasó | Genera una nueva desde Configuración → API |
| 403 | ORIGIN_NOT_ALLOWED | La clave tiene orígenes permitidos y el Origin no está en la lista | Agrega el dominio, o usa *.dominio.com para subdominios |
| 413 | PAYLOAD_TOO_LARGE | El cuerpo supera los 256 KB (el base64 de media cuenta) | Archivos de hasta ~190 KB por /api/v1/messages/media; más grandes, como enlace en un mensaje de texto |
| 422 | IDEMPOTENCY_KEY_REUSED | Misma Idempotency-Key con un cuerpo distinto | Usa una clave nueva por cada operación distinta |
| 429 | RATE_LIMITED | Demasiadas solicitudes | Espera lo que indica Retry-After. Ver Límites. |
| 424 | PROVIDER_ERROR | Meta rechazó el envío (token vencido, número inválido…) | El motivo viene en error.message. El mensaje queda en FAILED. |
Ejemplos de integración
Flujo completo: enviar un mensaje a un contacto
Este ejemplo muestra cómo obtener los canales, buscar un contacto y enviarle un mensaje usando JavaScript (Node.js / fetch):
const API_URL = "https://tu-dominio.com";
const API_KEY = "ak_tu_clave_aqui";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// 1. Obtener canales disponibles
const channelsRes = await fetch(`${API_URL}/api/v1/channels`, { headers });
const { data: channels } = await channelsRes.json();
const whatsappChannel = channels.find(ch => ch.type === "WHATSAPP");
console.log("Canal WhatsApp:", whatsappChannel.id);
// 2. Buscar contacto por teléfono
const contactsRes = await fetch(
`${API_URL}/api/v1/contacts?search=584121234567`,
{ headers }
);
const { data: contacts } = await contactsRes.json();
const contact = contacts[0];
console.log("Contacto:", contact.id, contact.name);
// 3. Enviar mensaje
const msgRes = await fetch(`${API_URL}/api/v1/messages`, {
method: "POST",
headers,
body: JSON.stringify({
contactId: contact.id,
channelId: whatsappChannel.id,
content: "Hola, tu pedido #1234 está listo para retiro.",
}),
});
const { data: message } = await msgRes.json();
console.log("Mensaje enviado:", message.id, "Estado:", message.status);Mismo flujo con cURL
# 1. Listar canales
curl -s -H "Authorization: Bearer ak_tu_clave" \
https://tu-dominio.com/api/v1/channels
# 2. Buscar contacto
curl -s -H "Authorization: Bearer ak_tu_clave" \
"https://tu-dominio.com/api/v1/contacts?search=584121234567"
# 3. Enviar mensaje
curl -X POST \
-H "Authorization: Bearer ak_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"contactId": "CONTACT_ID",
"channelId": "CHANNEL_ID",
"content": "Hola, tu pedido #1234 está listo para retiro."
}' \
https://tu-dominio.com/api/v1/messagesConsejo: Guarda los IDs de canales y contactos frecuentes en tu base de datos para no tener que consultarlos en cada envío.