smsenlinea.com API

Bienvenido a la documentación de la API de smsenlinea.com. Con nuestra API RESTful, puedes integrar mensajería de WhatsApp en tu aplicación, gestionar conversaciones y automatizar flujos de comunicación.

Características Principales

✨ API RESTful
Endpoints modernos y bien documentados para todas las operaciones
🔐 Autenticación Segura
API Keys con permisos granulares y soporte para múltiples ambientes
⚡ Webhooks en Tiempo Real
Recibe notificaciones instantáneas de eventos en tu cuenta
📊 Rate Limiting Justo
Límites generosos con tiers escalables para cualquier tamaño de negocio
Nota: Esta documentación aplica a smsenlinea.com API v1. Mantenemos compatibilidad hacia atrás con versiones anteriores.

Asignación y Ruteo de conversaciones

Cómo el sistema decide a qué asesor se asigna una conversación entrante, y cómo configurarlo desde el panel y la API.

Departamentos

Un departamento (ej. Ventas, Soporte) agrupa asesores y tiene palabras clave para detectar la intención del cliente. Un asesor puede pertenecer a varios departamentos. Se configuran en Asesor IA → editar, y cada asesor elige sus departamentos en /admin/agents.

Estrategias de asignación

Campo assignment_strategy del departamento. La rotación ocurre solo entre los asesores de ese departamento:

ValorComportamiento
least_busyPor disponibilidad — el asesor con menos chats abiertos (prioriza en línea). Default.
round_robinSecuencial — uno tras otro en cola circular (1º, 2º, … y vuelve al primero).
rotate_availableCombinada — rotación secuencial solo entre los asesores en línea (heartbeat < 3 min).

Seleccionar la estrategia vía API

POST /api/v1/departments
PUT  /api/v1/departments/{id}
Authorization: Bearer <API_KEY>   (scope departments:write)
Content-Type: application/json

{
  "name": "Ventas",
  "keywords": ["comprar", "precio", "cotización"],
  "assignment_strategy": "round_robin"
}

Asignar/quitar asesores de un departamento:

POST   /api/v1/departments/{id}/agents          { "user_id": 5, "is_primary": true }
DELETE /api/v1/departments/{id}/agents/{userId}

Ruteo por intención

Al escalar a humano, se elige departamento por: 1) palabra clave del mensaje, y 2) clasificación por IA (si handoff_rules_json.ai_routing = true). Resuelto el departamento, se aplica su estrategia de asignación.

Código de asesor directo + enlace wa.me

Cada asesor tiene un código único (users.direct_code). Si el primer mensaje del cliente empieza con asesor:CODE (también asesor: CODE o asesor#CODE), el chat se asigna directo a ese asesor y se pausa el bot.

https://wa.me/<NUMERO_DEL_NEGOCIO>?text=asesor:<CODE>

Cada asesor copia su enlace desde el menú del chat → "Mi enlace directo de contacto".

Widget público (burbuja de WhatsApp)

La burbuja usa la API pública (la public_key va en el path, auth widgetAuth):

EndpointDescripción
GET /api/v1/widget/{public_key}/configConfig + departamentos + asesores (id, nombre, avatar — sin datos sensibles).
POST /api/v1/widget/{public_key}/leadLead con department_id y preferred_agent_id (el cliente elige depto/asesor).
POST /api/v1/widget/{public_key}/eventTracking (bubble_opened, form_submitted, …).

Relación con el ruteo: departamento elegido → estrategia del departamento; preferred_agent_id o código asesor:CODE → asesor directo; sin elección → ruteo por intención. Las widget keys se gestionan en /admin/integrations/wordpress/widget-keys.

Interruptor global del bot: tenants.settings.bot_enabled. Si está en false, el Asesor IA no atiende ninguna conversación (solo humanos). Se controla desde el menú del chat → "Asesor IA (bot)".

Inicio Rápido

Comienza a usar la API de smsenlinea.com en 5 minutos.

1. Crear una Clave API

Dirígete a Configuración → Claves API y haz clic en "Nueva Clave". Selecciona los permisos que necesites.

2. Primer Llamado

Usa tu clave API para hacer un llamado HTTP simple:

curl -X GET "https://panel.smsenlinea.com/api/v1/external/conversations" \
  -H "X-API-Key: whasaas_live_..." \
  -H "Content-Type: application/json"

3. Manejar Respuestas

Todas las respuestas son JSON. Las respuestas exitosas retornan 200 OK:

{
  "ok": true,
  "data": {
    "conversations": [
      {
        "id": "conv_123",
        "phone": "573001234567",
        "status": "active",
        "created_at": "2026-05-22T14:30:00Z"
      }
    ]
  }
}

4. Configurar Webhooks

Para recibir notificaciones en tiempo real, configura un webhook en Integraciones → Webhooks.

¡Listo! Ya estás preparado para usar la API de smsenlinea.com.

Autenticación

smsenlinea.com soporta dos métodos de autenticación para la API:

Tu clave se muestra una sola vez

Al generar una clave la verás una única vez: guárdala en un lugar seguro. Nosotros solo almacenamos una huella criptográfica, de modo que ni nosotros podemos recuperarla. Si la pierdes o crees que se filtró, genera una nueva y revoca la anterior desde Ajustes → Developer API Keys. En el panel solo verás su principio y sus últimos dígitos, para reconocerla.

Comprueba con qué cuenta estás hablando

Toda respuesta de la API incluye tres encabezados que identifican la cuenta a la que pertenece la clave usada:

X-Account-Id: 46
X-Account-Name: Compuservices Jr
X-Api-Key-Name: Clientes Compuservices

Son la forma más rápida de detectar el error más común al integrar: usar la clave de otra cuenta. Si ves datos que no esperabas —conversaciones, contactos o números que no reconoces—, mira primero X-Account-Name: la API siempre devuelve lo que pertenece a la cuenta dueña de la clave, así que un nombre distinto al tuyo significa que la clave configurada no es la correcta, no que estés viendo datos ajenos.

También puedes consultar GET /api/v1/account para ver la cuenta completa, su plan y su consumo.

Método 1: API Key Header

Envía tu clave API en el encabezado X-API-Key:

GET /api/v1/external/conversations HTTP/1.1
Host: panel.smsenlinea.comX-API-Key: whasaas_live_1a2b3c4d5e6f7g8h
Content-Type: application/json

Método 2: Bearer Token

Alternativamente, usa un Bearer token en el encabezado Authorization:

GET /api/v1/external/conversations HTTP/1.1
Host: panel.smsenlinea.comAuthorization: Bearer whasaas_live_1a2b3c4d5e6f7g8h
Content-Type: application/json

Permisos (Scopes)

Cada clave API tiene permisos específicos. Asigna sólo los que tu integración necesita — principio de menor privilegio. Hay 33 scopes disponibles, organizados por categoría:

📨 Mensajería (messages)

ScopePermite
messages:readListar y consultar mensajes; verificar números en WhatsApp
messages:writeEnviar mensajes (texto, media, audio, documento, botones, lista, encuesta, ubicación, contacto, reacción, forward, bulk)
messages:editEditar el contenido de un mensaje enviado
messages:deleteEliminar mensajes para todos (recall)

💬 Conversaciones (conversations)

ScopePermite
conversations:readListar y consultar conversaciones, notas y eventos
conversations:writeCrear/modificar conversaciones, archivar, marcar leído, etiquetar
conversations:assignAsignar y transferir conversaciones a asesores
conversations:update_statusCambiar estado (open / pending / resolved / archived)
conversations:noteAgregar notas internas a conversaciones

📱 Instancias de WhatsApp (instances)

ScopePermite
instances:readListar, ver y consultar estado / estadísticas de instancias
instances:writeCrear y eliminar instancias
instances:connectGenerar QR, conectar y reiniciar instancias
instances:disconnectDesconectar / cerrar sesión de WhatsApp

👥 Contactos & Asesores

ScopePermite
contacts:readListar contactos, listas, foto de perfil
contacts:writeCrear/editar/eliminar/bloquear contactos; importar CSV
agents:readListar asesores, ver su disponibilidad y estadísticas

🛒 Comercio (products / orders)

ScopePermite
products:readConsultar catálogo de productos y categorías
products:writeCrear/modificar productos, variantes, imágenes y categorías
orders:readListar órdenes y descargar recibos
orders:writeCrear órdenes, cambiar estado, refunds, enviar link de pago

📣 Marketing & Plantillas

ScopePermite
campaigns:readConsultar campañas masivas y sus destinatarios
campaigns:writeCrear, programar, iniciar/pausar campañas (requiere tier ≥ Starter)
templates:readLeer plantillas y respuestas rápidas
templates:writeCrear y modificar plantillas y respuestas rápidas

🏷️ Organización (labels)

ScopePermite
labels:readListar etiquetas del tenant
labels:writeCrear, editar y eliminar etiquetas

📦 Archivos (media)

ScopePermite
media:readDescargar archivos multimedia
media:writeSubir archivos para usar en envíos

🪝 Webhooks & Reportes

ScopePermite
webhooks:readListar webhooks y ver entregas
webhooks:writeCrear y editar webhooks; lanzar tests
webhooks:manageEliminar webhooks y reintentar entregas
analytics:readAcceder a métricas, reportes y dashboards
account:readInformación del tenant: plan, vencimiento, cuotas, rate-limit

🧾 Cotizaciones y Facturas (quotes / invoices)

ScopePermite
quotes:readConsultar cotizaciones: listado, detalle, timeline y estadísticas (enviadas/leídas/aceptadas)
invoices:readConsultar facturas: listado, detalle, pagos y estadísticas de cartera
tickets:readConsultar tickets, órdenes de trabajo, plantillas y estadísticas
tickets:writeCrear tickets/órdenes, responder, cambiar estado y asignar
reminders:readConsultar recordatorios recurrentes y su historial de envíos
reminders:writeCrear, editar, pausar/reanudar y eliminar recordatorios recurrentes

Ambos requieren además que el módulo correspondiente esté incluido en tu plan; de lo contrario la API responde 403 plan_feature_required.

ℹ️ Llaves sin scopes: las API Keys creadas desde el panel sin lista de scopes tienen acceso a todos los endpoints. Si tu llave fue creada con una lista explícita de scopes (antes de que existieran quotes:read / invoices:read) y recibes 403 insufficient_scope, edita o rota la llave para incluirlos.

IP Whitelist

Para mayor seguridad, puedes restringir el acceso a IPs específicas. Configura esto desde la página de detalles de tu clave API.

Catálogo de Endpoints

smsenlinea.com API v2 expone más de 120 endpoints activos bajo el prefijo /api/v1, más los 9 endpoints legacy bajo /api/v1/external. Todos requieren autenticación con API Key.

📌 Convención: los endpoints nuevos viven bajo /api/v1/... y devuelven el formato {ok, data, message}. Los legacy bajo /api/v1/external/... mantienen el formato {success, data, message} por compatibilidad.

📱 Instancias de WhatsApp

Scope base: instances:read / instances:write / instances:connect / instances:disconnect

GET/api/v1/instances

Lista todas las instancias del tenant.

GET/api/v1/instances/{id}

Detalle completo de una instancia.

POST/api/v1/instances

Crea una nueva instancia y devuelve el QR de conexión.

GET/api/v1/instances/{id}/qr

Refresca el QR para conectar la instancia.

GET/api/v1/instances/{id}/status

Estado actual (open / connecting / disconnected) + teléfono.

POST/api/v1/instances/{id}/connect

Reconectar la instancia (genera QR fresco).

POST/api/v1/instances/{id}/disconnect

Cierra la sesión de WhatsApp.

POST/api/v1/instances/{id}/restart

Reinicia el proceso de Evolution para esa instancia.

DELETE/api/v1/instances/{id}

Elimina la instancia (también en Evolution).

GET/api/v1/instances/{id}/stats?period=24h

Métricas: mensajes enviados/recibidos, conversaciones activas (periodos: 1h, 24h, 7d, 30d).

📨 Mensajes

Scope base: messages:read / messages:write / messages:edit / messages:delete

Envío

📱 ¿Desde qué número sale el mensaje? Todos los endpoints de envío aceptan instance_id (número) o instance_name (texto) en el body para elegir la instancia de WhatsApp. Si no lo envías, se usa la primera instancia conectada de tu cuenta. Con más de una instancia, especifícala siempre. Ver la guía completa en Selección de instancia.
POST/api/v1/messages/send

Envía un mensaje de texto. Body: {phone, message, instance_id?, instance_name?}.

POST/api/v1/messages/send-media

Envía imagen, video, audio, documento o sticker. Body: {phone, media_url, media_type, caption?, filename?, mime_type?}. media_type admite image, video, audio, document y sticker (también los alias ptt, voice, gif, photo). En media_url puedes pasar una URL pública o el archivo en base64.

POST/api/v1/messages/send-audio

Envía un audio / nota de voz. Body: {phone, audio_url}.

POST/api/v1/messages/send-document

Envía un documento (PDF, DOC, XLSX, etc.).

Multimedia en números oficiales de Meta

En una instancia meta_cloud el envío se comprueba antes de llamar a WhatsApp, así que un fallo llega como 422 explicando qué corregir en vez de un error genérico del proveedor:

CódigoCuándo
outside_24h_windowEl contacto no te ha escrito en las últimas 24 h. Fuera de esa ventana WhatsApp solo permite plantillas aprobadas: usa send-template.
unsupported_media_typeEl formato no está en la lista que admite WhatsApp para ese tipo.
media_too_largeEl archivo supera el límite (solo se comprueba cuando lo mandas en base64).

Formatos y tamaños que acepta WhatsApp Cloud API:

TipoFormatosMáximo
imageimage/jpeg, image/png5 MB
videovideo/mp4, video/3gp16 MB
audioaudio/aac, amr, mpeg, mp4, ogg (opus)16 MB
stickerimage/webp500 KB animado / 100 KB estático
documentcualquiera100 MB

Estos límites son de WhatsApp, no del panel. En instancias Evolution (números por QR) no se aplica ninguna de estas comprobaciones.

Archivos que recibes: media_url caduca en 24 h

Cuando alguien te envía un archivo, el panel lo descarga y lo guarda. La media_url que ves en el webhook message.received y en GET /api/v1/messages es un enlace firmado que expira a las 24 horas. No la guardes como si fuera permanente.

Tienes dos formas de trabajar con ella:

  • Descárgate el archivo al recibir el webhook y guárdalo en tu sistema. Es lo más robusto.
  • Renueva el enlace cuando lo necesites con el media_key que acompaña a cada mensaje: GET /api/v1/media/url?key=EL_MEDIA_KEY devuelve una URL nueva.

GET /api/v1/messages y GET /api/v1/messages/{id} firman el enlace en cada lectura, así que la media_url que te devuelven siempre viene recién emitida.

Ojo con HEAD: el enlace se firma para GET. Una petición HEAD responde 403 aunque el enlace siga vigente — si compruebas disponibilidad, hazlo con GET.

// message.received con archivo
{
  "event": "message.received",
  "data": {
    "type": "image",
    "media_url": "https://...s3...?X-Amz-Expires=86400",   // caduca en 24 h
    "media_key": "tenants/46/media/media_6a8c7b6e.jpg",     // permanente
    "media_mime": "image/jpeg",
    "media_filename": "foto.jpg",
    "media_size": 26388
  }
}
POST/api/v1/messages/send-location

Envía ubicación geográfica. Body: {phone, latitude, longitude, name?, address?}.

POST/api/v1/messages/send-contact

Envía una tarjeta de contacto (vCard).

POST/api/v1/messages/send-template

Envía una plantilla local con variables {{var}}.

POST/api/v1/messages/send-buttons

Mensaje con botones de respuesta rápida. Body: {phone, body, buttons[], footer?, header?}. Funciona con los dos tipos de número.

En WhatsApp oficial (Meta): máximo 3 botones, título de 20 caracteres, cuerpo de 1024 y pie de 60. Si envías de más, se recortan a esos límites en lugar de fallar. Requiere la ventana de 24 horas abierta.

curl -X POST https://panel.smsenlinea.com/api/v1/messages/send-buttons \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"573001234567","body":"¿Confirmas tu cita?",
       "buttons":[{"id":"si","title":"Sí"},{"id":"no","title":"No"}]}'
POST/api/v1/messages/send-list

Lista interactiva con secciones. Body: {phone, body, button_text, sections[], footer?, title?}. Funciona con los dos tipos de número.

En WhatsApp oficial (Meta): hasta 10 secciones y 10 filas en total entre todas; título de fila 24 caracteres y descripción 72. Lo que sobre se recorta. Requiere la ventana de 24 horas abierta.

POST/api/v1/messages/send-template

Este endpoint hace dos cosas distintas según lo que envíes:

  • template_nameplantilla aprobada de WhatsApp (Meta). Es la única forma de escribir fuera de la ventana de 24 horas, y admite encabezado, botones y carrusel.
  • template_id → plantilla de texto guardada en tu panel, que se envía como mensaje normal.

Paso 1: consulta qué necesita la plantilla. GET /api/v1/instances/{id}/templates devuelve cada plantilla con un bloque requires:

{
  "name": "promo_agosto", "language": "es", "status": "APPROVED",
  "requires": {
    "header":    { "type": "IMAGE", "vars": 0, "needs_media": true },
    "body_vars": 2,
    "buttons": [
      { "index": 0, "type": "URL", "needs_value": true,
        "hint": "Parte final de la URL (se añade a https://tienda.com/)" },
      { "index": 1, "type": "QUICK_REPLY", "needs_value": false }
    ],
    "carousel": { "cards": 0 }
  }
}

Paso 2: envíala con esos datos.

curl -X POST https://panel.smsenlinea.com/api/v1/messages/send-template \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "phone": "573001234567",
    "instance_id": 50,
    "template_name": "promo_agosto",
    "language": "es",
    "header": "https://midominio.com/banner.jpg",
    "variables": ["Ana", "20%"],
    "buttons": { "0": "oferta-agosto" }
  }'

Cómo se rellena cada parte:

CampoPara qué
variablesValores del cuerpo, en orden ({{1}}, {{2}}…)
headerURL del archivo si el encabezado es imagen, vídeo o documento; el texto si es de tipo texto
buttonsValor por índice de botón. Solo lo piden los de URL dinámica y los de copiar código; los de respuesta rápida lo admiten como identificador opcional
cardsCarrusel: por tarjeta, {header, variables, buttons}

Si falta algún dato, la API responde 422 diciendo exactamente qué falta, sin llegar a gastar el envío. Meta rechaza el mensaje entero cuando el número de parámetros no coincide, así que se comprueba antes.

Errores propios: template_not_approved (no existe o Meta no la aprobó), not_supported (intentaste una plantilla aprobada en un número por QR) y empty_template.

POST/api/v1/instances/{id}/templates/ai

Redacta una plantilla con IA a partir de una descripción en lenguaje normal, respetando las reglas de Meta. Devuelve el borrador ya validado; no lo crea en Meta: lo revisas y después lo envías con POST /instances/{id}/templates.

curl -X POST https://panel.smsenlinea.com/api/v1/instances/50/templates/ai \\
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \\
  -d '{
    "descripcion": "Avisar al cliente que su equipo ya está reparado y puede pasar a recogerlo, con el número de orden y el horario",
    "negocio": "Compuservices JR",
    "con_botones": true
  }'

Respuesta:

{
  "template": {
    "name": "equipo_reparado_listo",
    "category": "UTILITY",
    "language": "es",
    "body": "Hola {{1}}, te informamos que tu equipo con orden {{2}} ya está reparado...",
    "body_examples": ["Carlos", "A-123"],
    "header_type": "TEXT", "header_text": "Tu equipo está listo",
    "footer": "Compuservices JR",
    "buttons": [],
    "explicacion": "Por qué eligió esa categoría y qué representa cada variable"
  },
  "ready": true,
  "errors": [],
  "warnings": [],
  "fixes": ["Ajusté el nombre a ..."]
}
CampoPara qué
descripcionObligatorio. Qué quieres decir, en tus palabras
negocioNombre del negocio: nombrarlo reduce rechazos
categoriaSugerencia. Si el texto no encaja, la IA elige la correcta y lo explica
con_botonesSi false, no propone botones
readyLéelo antes de enviar: true = pasó todas las reglas
warningsCosas que la API acepta pero al revisor no le gustan (categoría dudosa, marketing sin opción de baja…)
fixesRetoques que se aplicaron solos (nombre, numeración de variables…)

Redacta con DeepSeek. Si tienes tu propia clave de DeepSeek en Ajustes → Inteligencia Artificial se usa esa; si no, la de la plataforma. Consume saldo de IA de tu cuenta.

Validación previa al crear plantillas

POST /instances/{id}/templates comprueba la plantilla antes de mandarla a Meta y responde 422 con la lista exacta de lo que falla. Meta tarda hasta 24 h en revisar, así que un fallo de forma que se detecta aquí te ahorra un día.

Reglas comprobadas contra la API de Meta:

Meta RECHAZAMeta ACEPTA
Variable al principio o al final del cuerpo
Variables no correlativas ({{1}} y {{3}})
Número de ejemplos distinto del de variables
Cuerpo formado solo por variables
Más de dos saltos de línea seguidos
Más de 1 variable en el encabezado
Cualquier variable en el pie
Dos variables seguidas: Hola {{1}} {{2}}
(se suele creer lo contrario, pero la API las admite)
POST/api/v1/messages/send-flow

Envía un formulario nativo de WhatsApp: el cliente lo rellena dentro del chat, sin abrir un navegador ni escribir sus datos en varios mensajes. Cuando lo envía, la respuesta llega a tu webhook como evento flow.response y aparece en el chat.

Solo en WhatsApp oficial (Meta). En números conectados por código QR no existe: usa /messages/send-buttons o /messages/send-list.

Paso 1: crea el formulario (o hazlo desde el panel, en Formularios de WhatsApp). Se crea como borrador.

curl -X POST https://panel.smsenlinea.com/api/v1/instances/50/flows \\
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \\
  -d '{ "preset": "cita" }'

Modelos listos: cita, cotizacion, soporte, encuesta, registro. O defínelo campo a campo:

{
  "name": "Agendar visita",
  "title": "Agendar visita",
  "submit_label": "Confirmar",
  "categories": ["APPOINTMENT_BOOKING"],
  "fields": [
    { "type": "text",   "name": "nombre",  "label": "Tu nombre", "required": true },
    { "type": "date",   "name": "fecha",   "label": "Fecha preferida", "required": true },
    { "type": "select", "name": "franja",  "label": "Franja horaria", "required": true,
      "options": [ {"id":"am","title":"Mañana"}, {"id":"pm","title":"Tarde"} ] }
  ]
}

Tipos de campo: text, textarea, email, phone, number, date, select, radio, checkbox, optin. Máximo 20 campos.

Paso 2: publicícalo. Un borrador no se puede enviar, y uno publicado ya no se puede modificar (se crea otro).

curl -X POST https://panel.smsenlinea.com/api/v1/instances/50/flows/FLOW_ID/publish \\
  -H "Authorization: Bearer TU_API_KEY"

Paso 3: envíalo.

curl -X POST https://panel.smsenlinea.com/api/v1/messages/send-flow \\
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \\
  -d '{
    "phone": "573001234567",
    "instance_id": 50,
    "flow_id": "1543663357804290",
    "body": "Agenda tu visita técnica en menos de un minuto.",
    "button_text": "Agendar visita"
  }'
CampoPara qué
bodyTexto del mensaje que acompaña al botón
button_textTexto del botón. Máximo 30 caracteres y sin emojis (se limpian solos)
flow_tokenTu identificador del envío. WhatsApp te lo devuelve en la respuesta, y es lo que permite saber a qué envío corresponde. Si no lo mandas, se genera y se devuelve
screenPantalla inicial. Por omisión, la del formulario
dataDatos con los que abrir esa pantalla (opcional)
footer, headerPie y encabezado de texto (opcionales)

Errores propios: flow_not_found, flow_not_published (sigue en borrador), missing_screen y not_supported (lo intentaste en un número por QR). Se comprueban antes de gastar el envío.

GET/api/v1/instances/{id}/flows

Lista tus formularios. Añade ?sync=1 para refrescarlos contra Meta (por si los creaste en WhatsApp Manager). El campo sendable te dice de un vistazo cuáles puedes enviar ya.

{
  "flow_id": "1543663357804290",
  "name": "Agendar cita",
  "status": "PUBLISHED",
  "initial_screen": "FORMULARIO",
  "responses_count": 42,
  "sendable": true,
  "preview_url": "https://business.facebook.com/wa/manage/flows/..."
}
GET/api/v1/flows/responses

Las respuestas que han enviado tus clientes. Filtra con flow_id, conversation_id o since.

{
  "id": 128,
  "flow_id": "1543663357804290",
  "flow_name": "Agendar cita",
  "flow_token": "ft_9a3c...",
  "conversation_id": 1126,
  "from": "573001234567",
  "answers": { "nombre": "María Ríos", "fecha": "1755734400000", "franja": "am" },
  "created_at": "2026-08-21 15:04:22"
}

Las respuestas van tal cual las manda WhatsApp: las fechas en milisegundos desde epoch y las listas por el id de la opción, no por su texto. Es a propósito, para no alterar el dato original. En el panel se muestran ya traducidas.

DELETE/api/v1/instances/{id}/flows/{flow_id}

Borra el formulario si es un borrador. Si ya estaba publicado Meta no permite borrarlo: se retira de circulación y deja de poder enviarse.

POST/api/v1/messages/send-cta

Botón que abre un enlace. En vez de pegar la URL en el texto, el cliente ve un botón con la etiqueta que elijas. Body: {phone, body, url, button_text?, footer?, header?}.

curl -X POST https://panel.smsenlinea.com/api/v1/messages/send-cta \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"573001234567",
       "body":"Tu cotización COT-2026-0071 está lista.",
       "url":"https://panel.smsenlinea.com/q/abc123",
       "button_text":"Ver cotización"}'

Solo en WhatsApp oficial (Meta) y dentro de la ventana de 24 horas. La etiqueta del botón admite 20 caracteres. La URL debe empezar por http:// o https://; si no, responde 422 indicando el campo. En números por QR usa /messages/send con la URL en el texto.

POST/api/v1/messages/request-location

Pide la ubicación con un botón: el cliente la comparte con un toque, sin explicarle cómo adjuntarla. Útil para visitas técnicas, entregas y órdenes de trabajo. Body: {phone, body}. Solo en WhatsApp oficial (Meta).

POST/api/v1/messages/send-poll

Encuesta de WhatsApp. Body: {phone, question, options[], multiple_answers?}. Solo en números conectados por QR: la API oficial de Meta no tiene encuestas y la llamada responde not_supported.

POST/api/v1/messages/send-reaction

Reacciona con emoji a un mensaje existente. Body: {evolution_msg_id, emoji}.

POST/api/v1/messages/forward

Reenvía un mensaje a múltiples destinatarios.

POST/api/v1/messages/send-bulk

Envío en lote (hasta 500 mensajes con delay configurable). Requiere tier ≥ Starter.

Lectura y gestión

GET/api/v1/messages

Lista mensajes con filtros: conversation_id, instance_id, direction, type, date_from, date_to, paginación.

GET/api/v1/messages/{id}

Detalle completo de un mensaje.

POST/api/v1/messages/edit

Edita un mensaje enviado. Body: {evolution_msg_id, content}. Solo en números conectados por QR: la API oficial de Meta no permite editar mensajes.

POST/api/v1/messages/delete

Elimina un mensaje para todos (recall). Solo en números conectados por QR: la API oficial de Meta no permite eliminar mensajes ya enviados.

Qué puede hacer cada tipo de número

AcciónPor QROficial (Meta)
Texto, imagen, audio, documento, ubicación, contacto
Plantillas aprobadasSí (obligatorias fuera de 24 h)
Plantillas con encabezado, botones y carrusel
Formularios dentro de WhatsApp (Flows)
Botones y listas interactivasSí (con límites de Meta)
Botón que abre un enlace (send-cta)No
Pedir la ubicación con un botónNo
Frases de inicio y comandos del númeroNo
Reacciones, reenviar
Acuse de lectura (✓✓ azul) y “escribiendo…”
EncuestasNo — no existe en la API de Meta
Editar / eliminar un mensaje enviadoNo — no existe en la API de Meta
Ver si el cliente está en línea o escribiendoNo — Meta no publica ese dato

Cuando una acción no existe en el número oficial, la API responde 422 con el código not_supported y una explicación, en lugar de fallar de forma silenciosa.

POST/api/v1/messages/{id}/read-receipt

Marca un mensaje recibido como leído.

Helpers WhatsApp

POST/api/v1/whatsapp/check

Verifica qué números tienen cuenta de WhatsApp. Body: {numbers: []}.

POST/api/v1/whatsapp/profile-picture

URL de foto de perfil de un contacto.

💬 Conversaciones

Scope base: conversations:read / conversations:write / conversations:assign / conversations:update_status / conversations:note

GET/api/v1/conversations

Lista conversaciones. Filtros: status, assigned_to (id o "unassigned"), instance_id, unread_only, search.

GET/api/v1/conversations/{id}

Detalle de una conversación con contact, instance, assigned_to.

GET/api/v1/conversations/{id}/messages

Mensajes paginados de una conversación.

POST/api/v1/conversations

Inicia una conversación nueva. Body: {phone, instance_id?, initial_message?}.

POST/api/v1/conversations/assign

Asigna a uno o varios asesores. Body: {conversation_id|phone, agent_id|agent_email|agent_ids[]}. Pasa null para desasignar.

POST/api/v1/conversations/status

Cambia estado: open, pending, resolved, archived.

POST/api/v1/conversations/notes

Agrega nota interna a una conversación.

GET/api/v1/conversations/{id}/notes

Lista todas las notas internas de la conversación.

POST/api/v1/conversations/{id}/labels

Agrega una etiqueta. Body: {label_id}.

DELETE/api/v1/conversations/{id}/labels/{labelId}

Quita una etiqueta.

POST/api/v1/conversations/{id}/archive

Archiva o desarchiva. Body: {archived: true|false}.

POST/api/v1/conversations/{id}/read

Marca todos los mensajes entrantes como leídos y avisa a WhatsApp, de modo que el cliente ve los ✓✓ azules en su teléfono. La respuesta incluye whatsapp_notified: si es false, el marcado se hizo solo en tu panel (por ejemplo si la línea estaba desconectada).

POST/api/v1/conversations/{id}/typing

Muestra “escribiendo…” al cliente. Cuerpo: {"state": "typing"} — valores admitidos typing, recording (grabando audio) y paused (dejar de mostrarlo).

curl -X POST https://panel.smsenlinea.com/api/v1/conversations/1234/typing \
  -H "Authorization: Bearer TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"state":"typing"}'

Diferencias por proveedor. En números conectados por QR el indicador se activa y se apaga cuando tú quieras. En WhatsApp oficial (Meta) el indicador se envía respondiendo al último mensaje del cliente, expira solo a los ~25 segundos o al enviar tu respuesta, y solo funciona dentro de la ventana de atención de 24 horas: por eso paused se acepta pero no hace nada, y si el cliente nunca te ha escrito la llamada responde no_inbound_message.

GET/api/v1/instances/{id}/automation

Devuelve las frases de inicio y los comandos configurados en el número.

POST/api/v1/instances/{id}/automation

Configura ambos. Son ajustes del número, no mensajes:

  • Frases de inicio: hasta 4 sugerencias que se pueden tocar, visibles para quien abre el chat por primera vez. 80 caracteres, sin emojis.
  • Comandos: hasta 30, se ofrecen cuando el cliente escribe «/». Nombre de 32 caracteres, descripción de 256, sin emojis.
curl -X POST https://panel.smsenlinea.com/api/v1/instances/50/automation \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "prompts": ["Pedir una cotización", "Ver catálogo", "Hablar con un asesor"],
    "commands": [
      {"command_name":"cotizar","command_description":"Solicitar una cotización"},
      {"command_name":"soporte","command_description":"Hablar con un asesor"}
    ]
  }'

Lo que exceda los límites se recorta en lugar de fallar, porque Meta rechaza la configuración entera si algo se pasa. Solo en WhatsApp oficial (Meta).

POST/api/v1/instances/{id}/presence

Estado en línea de la línea completa (no de un chat). Cuerpo: {"state": "online"} u offline. Estar online es además lo que permite recibir la presencia de tus contactos. Solo aplica a conexiones por QR: en WhatsApp oficial no existe ese concepto y la respuesta llega con applied: false.

POST/api/v1/conversations/{id}/toggle-bot

Activa o pausa el chatbot en esta conversación.

DELETE/api/v1/conversations/{id}

Elimina (soft-delete: archiva).

📜 API Legacy /api/v1/external/*

Estos endpoints siguen funcionando indefinidamente para no romper integraciones existentes. Para nuevas integraciones se recomienda usar los endpoints v2 de arriba.

POST/api/v1/external/messages/send
POST/api/v1/external/messages/edit
POST/api/v1/external/messages/delete
POST/api/v1/external/whatsapp/check
GET/api/v1/external/agents
POST/api/v1/external/conversations/assign
POST/api/v1/external/conversations/status
POST/api/v1/external/conversations/notes
POST/api/v1/external/contacts/update

⏱️ Mensajes programados

Scope: messages:schedule o messages:write. Cola central con throttle anti-baneo por instancia.

POST/api/v1/messages/schedule

Encola un mensaje diferido. Body: {phone, message|media_url, instance_id?|instance_name?, send_after?, dedupe_key?}.

GET/api/v1/messages/scheduled/{id}

Estado del mensaje encolado.

POST/api/v1/messages/scheduled/{id}/cancel

Cancela un mensaje aún no enviado.

🧑‍💼 Asesores y Departamentos

Scope: agents:read / departments:read / departments:write

GET/api/v1/agents

Lista los asesores del tenant (nombre, email, rol, estado).

GET/api/v1/agents/{id}

Detalle de un asesor.

GET/api/v1/agents/{id}/metrics

Métricas del asesor: conversaciones atendidas, mensajes, tiempos de respuesta.

GET/api/v1/agents/{id}/availability

Disponibilidad actual del asesor.

GET/api/v1/agents/{id}/conversations

Conversaciones asignadas al asesor.

GET/api/v1/departments

Lista departamentos. CRUD completo con POST/PUT/DELETE en /departments/{id} y gestión de miembros en /departments/{id}/agents.

👥 Contactos y Listas

Scope: contacts:read / contacts:write

GET/api/v1/contacts

Lista contactos con búsqueda y paginación.

GET/api/v1/contacts/search?phone=

Busca un contacto por teléfono.

POST/api/v1/contacts

Crea un contacto. También: POST /contacts/update (upsert por teléfono), POST /contacts/import, GET /contacts/export, block/unblock, y listas en /contact-lists.

🧾 Cotizaciones NUEVO

Scope: quotes:read · Requiere el módulo de Cotizaciones en tu plan (Pro/Enterprise). Solo lectura: consulta el funnel completo — enviadas, leídas, aceptadas, rechazadas, vencidas.

GET/api/v1/quotes

Lista cotizaciones. Filtros: status (draft, sent, viewed, accepted, rejected, expired, cancelled), date_from, date_to, search (número/cliente), paginación. Cada fila incluye views_count, first_viewed_at, acceptance y public_url.

GET/api/v1/quotes/{id}

Detalle completo: ítems, opciones (good/better/best), aceptación (quién y cuándo) y events — el timeline completo (created, sent_wa, viewed, accepted...).

GET/api/v1/quotes/stats?date_from=&date_to=

Funnel agregado: by_status, sent_count, viewed_count, accepted_count, view_rate, acceptance_rate, accepted_amount.

// GET https://panel.smsenlinea.com/api/v1/quotes/stats
{
  "ok": true,
  "data": {
    "total": 42,
    "by_status": {"sent": 10, "viewed": 18, "accepted": 9, "rejected": 3, "draft": 2},
    "sent_count": 40,
    "viewed_count": 27,
    "accepted_count": 9,
    "view_rate": 0.675,
    "acceptance_rate": 0.225,
    "accepted_amount": 18500000
  }
}

💵 Facturas NUEVO

Scope: invoices:read · Requiere el módulo de Facturación en tu plan. Solo lectura: ciclo de cobro completo — enviadas, leídas (apertura del correo/página), pagos parciales, pagadas, vencidas.

GET/api/v1/invoices

Lista facturas. Filtros: status (draft, sent, viewed, partial, paid, overdue, cancelled, void), date_from/date_to (fecha de emisión), search, overdue_only=1. Incluye amount_paid, balance_due, first_viewed_at y public_url.

GET/api/v1/invoices/{id}

Detalle completo: ítems, retenciones y payments (cada pago registrado con fecha, monto y método).

GET/api/v1/invoices/stats?date_from=&date_to=

Resumen de cartera: by_status, viewed_count, overdue_count, total_invoiced, total_collected, total_outstanding.

🎫 Tickets y Órdenes de Trabajo NUEVO

Scopes: tickets:read / tickets:write · Requiere el módulo de Tickets en tu plan. Soporta plantillas con campos personalizados y órdenes de servicio en campo (dirección + horario + empleados + firma del cliente).

GET/api/v1/ticket-templates

Plantillas activas con su definición de campos (fields: key, label, type, required, options). Consúltalas antes de crear para saber qué datos enviar en custom_fields.

GET/api/v1/tickets

Lista tickets. Filtros: status, service_status (scheduled, en_route, in_progress, completed), priority, assigned_to, date_from/date_to, search.

GET/api/v1/tickets/{id}

Detalle: mensajes públicos, eventos, asignados y bloque work_order (estado de servicio, dirección, campos custom, firmado por/cuándo, work_order_url). Las notas internas y la imagen de la firma nunca se exponen.

GET/api/v1/tickets/stats

Totales por estado + resumen de órdenes de trabajo por estado de servicio.

POST/api/v1/tickets

Crea un ticket. Body: {subject, description?, priority?, phone?, contact_name?, contact_email?, template_id?, custom_fields?, service_address?, service_date?, service_time?, service_end_time?, assignee_ids?, notify_customer?}. Con plantilla field_service genera la orden de trabajo, notifica al cliente, envía a cada empleado su enlace de ejecución y programa recordatorios automáticos (1 día antes y 2 horas antes, ajustados al horario hábil configurado por el negocio).

POST/api/v1/tickets/{id}/status

Cambia el estado. Body: {status} (open, in_progress, waiting_customer, resolved, closed) o {service_status} para órdenes (solo la siguiente transición válida; completed requiere la firma del cliente en el portal del empleado).

POST/api/v1/tickets/{id}/reply

Responde al cliente (le notifica por WhatsApp/email) o registra nota interna con {private: true}.

POST/api/v1/tickets/{id}/assign

Asigna asesores. Body: {assignee_ids: []}. En órdenes de trabajo re-notifica los enlaces de ejecución.

🗓️ Recordatorios recurrentes NUEVO

Scopes: reminders:read / reminders:write. Programa mensajes de WhatsApp que se repiten (semanal, mensual, cada N días…) hacia un cliente — ideal para recordatorios de cobro de cuotas. El envío pasa por la cola anti-baneo y respeta el horario hábil (nunca de madrugada).

GET/api/v1/reminders

Lista tus planes. Filtro status (active, paused, finished) + paginación. Cada uno incluye next_run_at (UTC) y occurrences_sent.

POST/api/v1/reminders

Crea un plan. Body: {title, phone, display_name?, template_id?|body?, variables?, instance_id?, frequency, day_of_week?, day_of_month?, every_n_days?, send_time, timezone, skip_weekends?, starts_on, ends_on?, max_occurrences?}.

GET/api/v1/reminders/{id}

Detalle del plan.

PUT/api/v1/reminders/{id}

Edita el plan (recalcula el próximo envío).

POST/api/v1/reminders/{id}/pause · POST/api/v1/reminders/{id}/resume

Pausar / reanudar (reanudar recalcula la próxima ocurrencia hacia el futuro).

GET/api/v1/reminders/{id}/runs

Historial de ocurrencias con el estado real del envío (queued/sent/failed).

DELETE/api/v1/reminders/{id}

Elimina el plan.

Ejemplo — cobrar la cuota mensual el día 5 a las 9:00 (Bogotá):

curl -X POST "https://panel.smsenlinea.com/api/v1/reminders" \
  -H "Authorization: Bearer TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Cuota mensual plan hosting",
    "phone": "573001112233",
    "display_name": "María Gómez",
    "template_id": 12,
    "variables": { "monto": "$50.000", "referencia": "CT-2024-11" },
    "frequency": "monthly",
    "day_of_month": 5,
    "send_time": "09:00",
    "timezone": "America/Bogota",
    "skip_weekends": true,
    "starts_on": "2026-08-01"
  }'

// Respuesta
{ "ok": true, "data": { "id": 42, "next_run_at": "2026-08-05 14:00:00" } }

La plantilla puede usar {{nombre}} (del contacto) y tus variables ({{monto}}, {{referencia}}…). next_run_at viene en UTC; si la hora local cae fuera del horario hábil, se corre al inicio de la jornada. Para "día 31" en meses cortos se usa el último día del mes.

👤 Cuenta y Plan

Scope: account:read

GET/api/v1/account

Información del tenant (nombre, email, fecha de alta).

GET/api/v1/account/plan

Plan vigente: nombre, límites (instancias, mensajes/mes, asesores) y features incluidas.

GET/api/v1/account/subscription

Estado de la suscripción y fecha de renovación/vencimiento.

GET/api/v1/account/usage

Consumo del período: mensajes usados vs límite del plan, instancias activas.

GET/api/v1/account/api-key/info

Información de la API Key usada (nombre, scopes, último uso).

GET/api/v1/account/rate-limit

Límites de tasa vigentes y consumo actual.

📊 Analíticas

Scope: analytics:read. Todas aceptan date_from/date_to.

GET/api/v1/analytics/overview

Panorama general: mensajes, conversaciones, contactos nuevos, ventas.

GET/api/v1/analytics/messages

Mensajes enviados y recibidos por día, con totales del rango.

GET/api/v1/analytics/conversations

Conversaciones nuevas/resueltas y tiempos de atención.

GET/api/v1/analytics/instances

Actividad por instancia (útil con varias líneas de WhatsApp).

GET/api/v1/analytics/agents

Desempeño por asesor.

GET/api/v1/analytics/sales

Ventas del período (módulo e-commerce).

GET/api/v1/analytics/top-products

Productos más vendidos.

GET/api/v1/analytics/api-usage

Uso de la propia API (llamadas por endpoint y por día).

📣 Marketing

Plantillas, respuestas rápidas y campañas masivas.

GET/api/v1/templates

CRUD de plantillas de mensaje (POST/PUT/DELETE en /templates/{id}; categorías en /templates/categories).

Crear plantillas desde la API

Hay dos tipos distintos y no se crean igual:

1 · Plantilla del panel — texto guardado que se envía como mensaje normal. No pasa por Meta, está disponible al instante y sirve para números por QR y oficiales. Admite los marcadores {{name}}, {{phone}}, {{company}}, {{advisor}} y {{date}}.

curl -X POST https://panel.smsenlinea.com/api/v1/templates \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Aviso de recoleccion",
    "message_text": "Hola {{name}}, tu recoleccion de {{company}} quedo agendada.",
    "category": "Operaciones",
    "description": "Aviso al cliente"
  }'

// Respuesta: la API detecta sola las variables del texto
{ "ok": true, "data": { "id": 12, "variables": ["name", "company"], ... } }

Se listan con GET /api/v1/templates, se editan con PUT /api/v1/templates/{id} y se borran con DELETE. Para enviarlas: POST /api/v1/messages/send-template con template_id.

2 · Plantilla aprobada de WhatsApp (Meta) — la única que permite escribir fuera de la ventana de 24 h. Se crea en la WABA de una instancia meta_cloud y la revisa Meta (24-48 h).

curl -X POST https://panel.smsenlinea.com/api/v1/instances/50/templates \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Aviso de Recoleccion",
    "language": "es",
    "category": "UTILITY",
    "header_type": "TEXT",
    "header_text": "Recoleccion programada",
    "body": "Hola {{1}}, tu recoleccion quedo agendada para el {{2}}. Te esperamos.",
    "body_examples": ["Ana", "01/09/2026"],
    "footer": "Gracias por confiar en nosotros",
    "buttons": [
      { "type": "QUICK_REPLY", "text": "Confirmar" },
      { "type": "URL", "text": "Ver detalle", "url": "https://tusitio.com/ot/{{1}}" }
    ]
  }'
CampoPara qué
nameSe normaliza a minúsculas con guiones bajos. La respuesta trae el nombre final en name_sent.
categoryUTILITY, MARKETING o AUTHENTICATION.
bodyVariables posicionales {{1}}, {{2}}No puede terminar en una variable ni ser casi solo variables.
body_examplesUn valor de ejemplo por cada variable del cuerpo. Meta los exige.
header_typeNONE, TEXT, IMAGE, VIDEO o DOCUMENT.
buttonsArray de QUICK_REPLY, URL o COPY_CODE. Los de URL dinámica llevan {{1}} al final.
footerOpcional. No admite variables.

Antes de llamar a Meta se validan las reglas: si algo no cumple responde 422 con la lista exacta de errores, sin gastar el intento. Errores propios: not_supported (la instancia no es de Meta) y template_not_created (Meta la rechazó).

Después: GET /api/v1/instances/{id}/templates para ver su estado (PENDINGAPPROVED) y el bloque requires; POST /api/v1/instances/{id}/templates/sync para refrescar desde Meta; y POST /api/v1/instances/{id}/templates/ai para que la IA redacte el borrador.

GET/api/v1/quick-replies

CRUD de respuestas rápidas.

GET/api/v1/campaigns

CRUD de campañas + control: start, pause, resume, cancel, recipients, stats.

GET/api/v1/labels

CRUD de etiquetas para clasificar conversaciones.

🛒 E-commerce

Requiere el módulo e-commerce en tu plan.

GET/api/v1/products

CRUD de productos + POST /products/{id}/images + POST /products/{id}/send-to-chat.

GET/api/v1/categories

CRUD de categorías del catálogo.

GET/api/v1/orders

Órdenes: listar, crear, cambiar estado/pago, refund, send-payment-link.

POST/api/v1/coupons/validate

Valida un cupón; CRUD completo en /coupons.

📦 Media y 🪝 Webhooks (gestión)

POST/api/v1/media/upload

Sube un archivo y devuelve la URL para usar en send-media/send-document. También GET /media/url, GET /media/download, GET /media/quota, DELETE /media.

GET/api/v1/webhooks

CRUD de webhooks + POST /webhooks/{id}/test + GET /webhooks/{id}/deliveries + reintento de entregas. Catálogo de eventos en GET /webhooks/events.

Selección de instancia

Tu cuenta puede tener varias instancias de WhatsApp (varias líneas/números). Todos los endpoints de envío te permiten elegir desde cuál sale cada mensaje.

Cómo funciona

Parámetro del bodyTipoComportamiento
instance_idnúmeroEnvía por esa instancia. Debe ser tuya y estar connected.
instance_nametextoAlternativa amigable: el nombre exacto de la instancia (como aparece en el panel). Se ignora si también envías instance_id.
(ninguno)Se usa la primera instancia conectada de tu cuenta. Cómodo con una sola línea; impredecible con varias — especifícala siempre si tienes más de una.

Paso 1 — Consulta tus instancias

curl -H "Authorization: Bearer TU_API_KEY" \
  "https://panel.smsenlinea.com/api/v1/instances?status=connected"

// Respuesta (resumida)
{
  "ok": true,
  "data": [
    { "id": 12, "instance_name": "ventas-principal", "status": "connected", "phone_number": "573001112233" },
    { "id": 15, "instance_name": "soporte",          "status": "connected", "phone_number": "573004445566" }
  ]
}

El filtro ?status=connected devuelve solo las instancias listas para enviar.

Paso 2 — Envía indicando la instancia

// Por ID
curl -X POST "https://panel.smsenlinea.com/api/v1/messages/send" \
  -H "Authorization: Bearer TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "573009998877",
    "message": "Hola, tu pedido está listo 🎉",
    "instance_id": 15
  }'

// O por nombre (equivalente)
curl -X POST "https://panel.smsenlinea.com/api/v1/messages/send" \
  -H "Authorization: Bearer TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "573009998877",
    "message": "Hola, tu pedido está listo 🎉",
    "instance_name": "soporte"
  }'

Aplica a todos los envíos

instance_id / instance_name funcionan igual en: send, send-media, send-audio, send-document, send-location, send-contact, send-template, send-buttons, send-list, send-poll, send-bulk, forward y messages/schedule. También en POST /conversations al iniciar una conversación nueva.

Si la instancia no está disponible

Cuando la instancia pedida no existe, no es tuya o está desconectada, la API responde 400 con el código no_connected_instance — e incluye tus instancias disponibles para que corrijas sin adivinar:

{
  "ok": false,
  "error": "no_connected_instance",
  "message": "La instancia instance_id=99 no existe en tu cuenta o no está conectada.",
  "details": {
    "available_instances": [
      { "id": 12, "instance_name": "ventas-principal", "status": "connected", "phone_number": "573001112233" },
      { "id": 15, "instance_name": "soporte", "status": "disconnected", "phone_number": "573004445566" }
    ],
    "hint": "Envía instance_id (número) o instance_name (texto) de una instancia con status \"connected\". Si tienes varias instancias, especifícala siempre."
  }
}
💡 Buena práctica: guarda el instance_id en la configuración de tu integración (no lo resuelvas en cada envío) y refresca la lista solo si recibes no_connected_instance. Los IDs son estables; el nombre puede cambiar si lo renombras en el panel.

Campañas masivas

Envíos masivos a una lista de destinatarios, con control de ritmo, ventana horaria y rotación entre números.

POST/api/v1/campaigns

Crea la campaña en estado draft. Body: {name, message_text, instance_ids[], recipients[], scheduled_at?, send_window_start?, send_window_end?, min_delay?, max_delay?, batch_size?, batch_interval?, rotation_type?}.

POST/api/v1/campaigns/{id}/start

Pone la campaña en cola de envío. Exige al menos una instancia y un destinatario.

GET/api/v1/campaigns/{id}/stats

Enviados, entregados, leídos, fallidos y respuestas.

También: GET /campaigns, GET|PUT|DELETE /campaigns/{id}, /pause, /resume, /cancel y GET /campaigns/{id}/recipients.

Campañas por número oficial de Meta

En un envío masivo todos los destinatarios están fuera de la ventana de 24 horas, así que WhatsApp solo admite plantillas aprobadas. Si las instancias de la campaña son oficiales, hay que enviar la plantilla; el texto libre se rechaza al crearla en lugar de fallar destinatario por destinatario.

curl -X POST https://panel.smsenlinea.com/api/v1/campaigns \
  -H "Authorization: Bearer TU_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Promo agosto",
    "instance_ids": [50],
    "meta_template_name": "recordatorio_cuota",
    "meta_template_lang": "es",
    "meta_template_vars": ["{{name}}", "Compuservices", "Asesor 1"],
    "message_text": "(se rellena con el cuerpo de la plantilla)",
    "recipients": [{"phone":"573001112233","name":"Ana"}]
  }'

Cada valor de meta_template_vars ocupa un hueco de la plantilla ({{1}}, {{2}}…) y admite los mismos tokens que el texto libre: {{name}}, {{phone}} o las variables de cada destinatario.

Errores propios de campañas

CódigoQué significa
mixed_providersMezclaste números oficiales y por QR en la misma campaña: admiten contenido distinto
template_requiredInstancia oficial sin meta_template_name / meta_template_lang
template_not_approvedLa plantilla no existe para esa instancia o Meta aún no la aprobó
template_vars_missingFaltan valores en meta_template_vars para los huecos de la plantilla

Bajas de marketing (obligatorio conocerlo)

Cuando un contacto escribe BAJA, STOP, «no más mensajes» o similar, el sistema lo registra, le confirma automáticamente y lo excluye de todas las campañas de ese negocio, sea cual sea el número por el que se envíe. Vuelve a entrar escribiendo ALTA.

El filtro se aplica al cargar los destinatarios, también por API: si tu lista trae 500 números y 12 están dados de baja, la campaña se crea con 488. No hay forma de saltárselo, y es lo que protege la calidad del número frente a los reportes de los usuarios.

Webhooks

Los webhooks permiten que tu aplicación reciba notificaciones en tiempo real cuando ocurren eventos importantes.

Qué cambia según el tipo de número

Los acuses de entrega y lectura funcionan igual con cualquier número. La presencia del cliente (en línea / escribiendo) solo la entrega WhatsApp en las líneas conectadas por QR; si migras a WhatsApp oficial (Meta) dejarás de recibir contact.presence, porque Meta no publica ese dato. Tenlo en cuenta si tu integración depende de él.

Eventos Disponibles

Evento Descripción
message.received Se recibió un mensaje nuevo
message.sent Se envió un mensaje
message.delivered El mensaje llegó al teléfono del cliente (✓✓ gris)
message.read El cliente leyó el mensaje (✓✓ azul)
message.failed WhatsApp no pudo entregar el mensaje
message.updated Cualquier cambio de estado del mensaje. Equivale a los tres anteriores juntos: úsalo si prefieres un solo evento y filtrar por el campo status
contact.presence El cliente está en línea o escribiendo. Campos: state (composing, recording, available, unavailable, paused) e is_typing. Solo llega en líneas conectadas por QR: WhatsApp oficial (Meta) no informa la presencia de los clientes
flow.response Un cliente rellenó y envió un formulario de WhatsApp. Campos: flow_token (el que mandaste al enviarlo, para saber a qué envío corresponde), answers con las respuestas, conversation_id, from y message_id. Solo en números oficiales de Meta
chat.assigned Una conversación fue asignada a un agente
chat.created Se creó una nueva conversación
contact.updated La información de un contacto fue actualizada
instance.connected Una instancia de WhatsApp se conectó

Verificar Webhooks

Cada webhook incluye una firma HMAC-SHA256 en el encabezado X-Webhook-Signature. Siempre verifica la firma para garantizar que el webhook proviene de smsenlinea.com:

Python

import hmac
import hashlib

def verify_webhook(request_body, signature, secret):
    expected = hmac.new(
        secret.encode(),
        request_body.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

PHP

<?php

function verifyWebhook($payload, $signature, $secret) {
    // Calcular la firma esperada
    $expected = hash_hmac(
        'sha256',
        $payload,
        $secret,
        false // retornar como string hexadecimal
    );

    // Comparar de forma segura
    return hash_equals($expected, $signature);
}

// Ejemplo de uso en un endpoint de webhook
$secret = 'wh_1a2b3c4d5e6f7g8h'; // Tu secret token del webhook

// Obtener el cuerpo de la solicitud
$payload = file_get_contents('php://input');

// Obtener la firma del encabezado
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

// Verificar la firma
if (!verifyWebhook($payload, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// La firma es válida, procesar el webhook
$data = json_decode($payload, true);

if ($data['event'] === 'message.received') {
    // Procesar mensaje recibido
    $phone = $data['data']['phone'];
    $message = $data['data']['message'];

    // Tu lógica aquí
    echo "Mensaje recibido de {$phone}: {$message}";
}

http_response_code(200);
echo json_encode(['success' => true]);

?>

Configurar un Webhook

Dirígete a Integraciones → Webhooks y crea un nuevo webhook con tu URL.

Rate Limiting

La API de smsenlinea.com aplica límites de velocidad basados en tu plan.

Límites por Plan

Plan Req/Minuto Req/Hora Req/Día
Free 100 1,000 10,000
Starter 500 10,000 100,000
Professional 2,000 50,000 1,000,000
Enterprise Ilimitado Ilimitado Ilimitado

Encabezados de Rate Limit

Cada respuesta incluye encabezados que indican tu uso:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1653225600

Manejar Límites Excedidos

Cuando excedes el límite, recibirás una respuesta 429 Too Many Requests:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "ok": false,
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Please try again in 60 seconds."
}

Manejo de Errores

La API usa códigos de estado HTTP estándar para indicar el resultado de las solicitudes.

Códigos de Estado

Código Significado
200 Solicitud exitosa
201 Recurso creado
400 Solicitud inválida (parámetros incorrectos)
401 No autorizado (clave API inválida)
403 Prohibido (permisos insuficientes)
404 No encontrado
429 Demasiadas solicitudes (rate limit)
500 Error del servidor

Respuesta de Error

Los errores incluyen más información útil:

{
  "ok": false,
  "error": "invalid_phone",
  "message": "El número de teléfono proporcionado no es válido",
  "code": "INVALID_INPUT"
}

Ejemplos de Código

JavaScript/Node.js

const fetch = require('node-fetch');

async function sendMessage() {
  const response = await fetch('https://panel.smsenlinea.com/api/v1/external/messages/send', {
    method: 'POST',
    headers: {
      'X-API-Key': 'whasaas_live_...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      phone: '573001234567',
      message: 'Hola desde smsenlinea.com'
    })
  });

  const data = await response.json();
  console.log(data);
}

sendMessage();

Python

import requests

def send_message():
    response = requests.post(
        'https://panel.smsenlinea.com/api/v1/external/messages/send',
        headers={
            'X-API-Key': 'whasaas_live_...',
            'Content-Type': 'application/json'
        },
        json={
            'phone': '573001234567',
            'message': 'Hola desde smsenlinea.com'
        }
    )

    print(response.json())

send_message()

PHP

<?php

function sendMessage() {
    $apiKey = 'whasaas_live_...';
    $url = 'https://panel.smsenlinea.com/api/v1/external/messages/send';

    $data = [
        'phone' => '573001234567',
        'message' => 'Hola desde smsenlinea.com'
    ];

    $options = [
        'http' => [
            'header' => [
                'X-API-Key: ' . $apiKey,
                'Content-Type: application/json'
            ],
            'method' => 'POST',
            'content' => json_encode($data),
            'timeout' => 10
        ]
    ];

    $context = stream_context_create($options);
    $response = file_get_contents($url, false, $context);
    $result = json_decode($response, true);

    print_r($result);
}

sendMessage();

// Alternativa usando cURL (recomendado):
function sendMessageWithCurl() {
    $apiKey = 'whasaas_live_...';
    $url = 'https://panel.smsenlinea.com/api/v1/external/messages/send';

    $data = [
        'phone' => '573001234567',
        'message' => 'Hola desde smsenlinea.com'
    ];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'X-API-Key: ' . $apiKey,
        'Content-Type: application/json'
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = json_decode($response, true);
    echo "HTTP Status: " . $httpCode . "\n";
    print_r($result);
}

sendMessageWithCurl();

?>

cURL

curl -X POST https://panel.smsenlinea.com/api/v1/external/messages/send \
  -H "X-API-Key: whasaas_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "573001234567",
    "message": "Hola desde smsenlinea.com"
  }'

Mejores Prácticas

🔐 Seguridad

  • Nunca compartas tus claves API públicamente
  • Usa diferentes claves para dev, staging y production
  • Rota tus claves cada 90 días
  • Usa IP whitelist para claves de producción
  • Siempre verifica las firmas de webhooks

⚡ Rendimiento

  • Usa batch operations cuando sea posible
  • Implementa caché para reducir llamadas API
  • Pagina los resultados grandes (limit, offset)
  • Usa webhooks en lugar de polling
  • Implementa reintentos exponenciales para resiliencia

📊 Monitoring

  • Log todas las llamadas API importantes
  • Monitora la tasa de errores 5xx
  • Alerta cuando se acerque al límite de rate limit
  • Rastrear latencia de webhooks
  • Mantén métricas de tasa de entrega de webhooks