API pública v1
Documentación de la API de Conexla
Toda la documentación en español, con ejemplos de curl reales y copiables. Crea tus claves en Ajustes → API. Base URL: https://tuapp.conexla.tech.
Autenticación
Cada petición lleva tu clave en la cabecera Authorization, con el esquema Bearer. La clave tiene el formato cnx_live_<id>.<secreto> — la parte antes del punto identifica la clave (se muestra siempre en tu panel), la parte de después del punto es el secreto y solo se enseña UNA vez al crearla. Guárdala como si fuera una contraseña: no hay forma de volver a verla, solo de revocarla y crear una nueva.
curl https://tuapp.conexla.tech/api/v1/contactos \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Ámbitos (scopes)
Cada clave tiene uno o varios ámbitos. Una petición a una operación sin el ámbito necesario responde 403 (FORBIDDEN_SCOPE).
| Ámbito | Permite |
|---|---|
mensajes:enviar | POST /api/v1/mensajes |
contactos:leer | GET /api/v1/contactos |
contactos:escribir | POST /api/v1/contactos |
conversaciones:leer | GET /api/v1/conversaciones |
Gestionar tus propios webhooks (/api/v1/webhooks) no exige ningún ámbito de negocio concreto: cualquier clave activa puede administrar sus URLs.
Límites de uso
Cada clave tiene un límite de peticiones por minuto (ventana deslizante: se cuentan las peticiones de los últimos 60 segundos en cada momento, no un contador que resetea en punto). Toda respuesta incluye estas cabeceras:
| Cabecera | Significado |
|---|---|
X-RateLimit-Limit | Peticiones permitidas por minuto para esta clave. |
X-RateLimit-Remaining | Cuántas te quedan en la ventana actual. |
X-RateLimit-Reset | Instante (epoch, segundos) en que vuelve a haber cupo. |
Retry-After | Solo en 429: segundos que conviene esperar antes de reintentar. |
Superar el límite responde 429 con código RATE_LIMITED.
Enviar un mensaje
POST /api/v1/mensajes — requiere el ámbito mensajes:enviar. Identifica al destinatario por telefono (se crea el contacto si no existía) o por contactoId.
Regla de la ventana de 24 horas (igual que en la bandeja): si la última respuesta libre de tu negocio fue hace más de 24 horas desde el último mensaje entrante de ese contacto, solo puedes reabrir la conversación con tipo: "plantilla". Intentar tipo: "texto" fuera de ventana responde 422 WINDOW_CLOSED — es una regla de Meta, no una limitación nuestra: fuera de ventana, WhatsApp solo entrega plantillas aprobadas.
# Texto libre (dentro de la ventana de 24h)
curl -X POST https://tuapp.conexla.tech/api/v1/mensajes \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"telefono": "+34600111222",
"tipo": "texto",
"texto": "¡Hola! Tu pedido ya está listo para recoger."
}'# Plantilla (funciona también FUERA de la ventana de 24h)
curl -X POST https://tuapp.conexla.tech/api/v1/mensajes \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"telefono": "+34600111222",
"tipo": "plantilla",
"plantilla": { "nombre": "recordatorio_cita", "idioma": "es", "variables": ["Marta", "18:00"] }
}'Respuesta 201: { "ok": true, "datos": { "mensaje": {...}, "conversacionId": "...", "contactoId": "..." } }
Contactos
GET /api/v1/contactos — ámbito contactos:leer. Paginación por cursor: pasa el cursorSiguiente de una respuesta como ?cursor= en la siguiente petición hasta que venga null.
curl "https://tuapp.conexla.tech/api/v1/contactos?limite=20" \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Página siguiente:
curl "https://tuapp.conexla.tech/api/v1/contactos?limite=20&cursor=MjAyNi0wOC0xN1QxMDowMDowMFp8ZGVtby1jb250YWN0by0y" \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"POST /api/v1/contactos — ámbito contactos:escribir. Deduplica por teléfono normalizado: si ya existe, actualiza sus etiquetas en vez de duplicarlo.
curl -X POST https://tuapp.conexla.tech/api/v1/contactos \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "telefono": "+34600111222", "nombre": "Marta Gómez", "etiquetas": ["cliente_habitual"] }'Conversaciones
GET /api/v1/conversaciones — ámbito conversaciones:leer. Admite estado (abierta/pendiente/cerrada), busqueda y paginación por cursor.
curl "https://tuapp.conexla.tech/api/v1/conversaciones?estado=abierta&limite=10" \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Webhooks salientes
Registra una URL para recibir eventos en tiempo real (no necesitas hacer polling). Al registrarla, mandamos un reto de verificación firmado y exigimos que lo devuelvas — así comprobamos que controlas de verdad esa URL antes de confiarle nada. Solo aceptamos https:// públicas (nunca IPs privadas ni localhost).
curl -X POST https://tuapp.conexla.tech/api/v1/webhooks \
-H "Authorization: Bearer cnx_live_ab12cd34.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://miapp.com/webhooks/conexla", "eventos": ["mensaje_entrante"] }'Tu endpoint debe responder 200 con { "reto": "<el mismo valor recibido>" } durante la verificación. La respuesta de éxito trae el secreto de firma — se muestra UNA vez, guárdalo.
Verificar la firma de cada evento
Cada evento llega firmado en la cabecera X-Conexla-Signature con el formato sha256=<hex>: HMAC-SHA256 del cuerpo exacto (bytes crudos, antes de parsear JSON) con tu secreto.
// Node.js — verificación en tiempo constante
import { createHmac, timingSafeEqual } from "node:crypto";
function firmaValida(cuerpoCrudo, firmaRecibida, secreto) {
const esperada = "sha256=" + createHmac("sha256", secreto).update(cuerpoCrudo).digest("hex");
const a = Buffer.from(firmaRecibida);
const b = Buffer.from(esperada);
return a.length === b.length && timingSafeEqual(a, b);
}GET /api/v1/webhooks lista tus webhooks (sin el secreto). DELETE /api/v1/webhooks?id=<id> elimina uno.
Catálogo de errores
Toda respuesta de error tiene la forma { "ok": false, "error": { "codigo", "mensaje", "idPeticion" } }. codigo es estable (compara tu integración contra él, en inglés); mensaje es siempre español legible, listo para enseñárselo a tu cliente final sin traducir. idPeticion identifica la petición en nuestros logs si necesitas soporte.
| Código | HTTP | Mensaje (español) |
|---|---|---|
UNAUTHORIZED | 401 | La clave de API no es válida, está revocada o no se ha proporcionado. Revisa la cabecera 'Authorization: Bearer <clave>'. |
FORBIDDEN_SCOPE | 403 | Esta clave de API no tiene permiso para esta operación. Revisa sus ámbitos (scopes) en Ajustes → API. |
RATE_LIMITED | 429 | Has superado el límite de peticiones por minuto de esta clave. Reduce la frecuencia o espera al tiempo indicado en 'Retry-After'. |
VALIDATION_ERROR | 400 | Los datos enviados no son válidos. Revisa 'detalles' para saber qué campo falla. |
NOT_FOUND | 404 | No se encontró el recurso solicitado. |
WINDOW_CLOSED | 422 | La ventana de servicio de 24 horas está cerrada para esta conversación: solo se puede reabrir con una plantilla aprobada por Meta, no con texto libre. |
CONFLICT | 409 | El recurso ya existe o entra en conflicto con el estado actual. |
ACCOUNT_NOT_CONNECTED | 409 | Este tenant todavía no tiene ninguna cuenta de WhatsApp conectada. Conéctala desde /conectar antes de usar la API de mensajes. |
WEBHOOK_VERIFICATION_FAILED | 422 | No se pudo verificar que controlas esa URL: el reto de verificación no se respondió correctamente o a tiempo. |
URL_NOT_ALLOWED | 400 | Esa URL no está permitida por motivos de seguridad (solo HTTPS público, no direcciones internas o privadas). |
META_DELIVERY_FAILED | 502 | Meta rechazó el envío del mensaje. Revisa 'detalles' para el motivo exacto (número inválido, plantilla no aprobada, cuenta con restricciones...). |
INTERNAL_ERROR | 500 | Ha ocurrido un error interno. Si persiste, contacta con soporte indicando el identificador de esta petición (idPeticion). |