Botyconnect API v1
Guia completa para consumir la API
Botyconnect expone una API REST para que tu backend conecte numeros de WhatsApp en modo coexistencial, envie campanas masivas, consulte creditos y reciba eventos por webhook. Tu empresa maneja su propio login, CRM y reglas internas; Botyconnect provee la infraestructura oficial de WhatsApp.
BASE_URL = https://api.botyconnect.com/api/v1/botyconnectOpenAPI: validacion y SDKs
La especificacion openapi.yaml describe las 17 operaciones de la API, los 9 eventos de webhook con sus payloads, todos los schemas y los codigos de error. Usala como contrato en tu integracion:
Generar un SDK cliente
# TypeScript (fetch)
npx @openapitools/openapi-generator-cli generate \
-i https://developers.botyconnect.com/openapi.yaml \
-g typescript-fetch -o ./botyconnect-sdk
# PHP
npx @openapitools/openapi-generator-cli generate \
-i https://developers.botyconnect.com/openapi.yaml \
-g php -o ./botyconnect-sdk-php
# Python
npx @openapitools/openapi-generator-cli generate \
-i https://developers.botyconnect.com/openapi.yaml \
-g python -o ./botyconnect-sdk-pythonValidar tus requests en desarrollo
# Mock server local para probar tu integracion sin gastar creditos
npx @stoplight/prism-cli mock https://developers.botyconnect.com/openapi.yaml
# Validar que tus requests cumplen el contrato (proxy de validacion)
npx @stoplight/prism-cli proxy https://developers.botyconnect.com/openapi.yaml \
https://api.botyconnect.com/api/v1/botyconnectExplorar con UI
# Documentacion navegable con Redoc
npx @redocly/cli preview-docs https://developers.botyconnect.com/openapi.yaml
# O importa openapi.yaml directo en Postman / Insomnia / Bruno
# (File → Import → URL) para obtener la coleccion completa. Los eventos de webhook estan definidos en la seccion webhooks del spec (OpenAPI 3.1), con los headers de firma X-Botyconnect-Signature documentados por evento.
Flujo recomendado
- Solicita acceso desde /register. Un admin de Botyconnect aprueba la empresa.
- Recibes correo de activacion, defines password y entras al portal.
- Obtienes tu API key (en la aprobacion o creandola en el portal). Se muestra una sola vez.
- Registras tu webhook endpoint y guardas el
signing_secret. - Generas un link de conexion coexistencial por cada numero y lo completas en Meta.
- Listas tus numeros y templates aprobados.
- Creas campanas indicando
account_id; consultas estados o los recibes por webhook.
Solicitar acceso
El registro publico crea una solicitud pendiente. No entrega API key ni crea acceso directo.
POST /access-requests
Accept: application/json
{
"company_name": "Empresa Demo",
"tax_id": "900000000-1",
"country": "Colombia",
"contact_name": "Laura Gomez",
"contact_email": "laura@empresa.com",
"contact_phone": "+57 300 000 0000",
"estimated_monthly_messages": 50000,
"estimated_numbers_count": 3,
"use_case": "Mensajeria masiva desde backend propio.",
"webhook_url": "https://empresa.com/webhooks/botyconnect"
}// 201 Created
{
"success": true,
"message": "Botyconnect access request received.",
"data": { "request_id": "bcar_xxxxxxxxxxxx", "status": "pending" }
}Autenticacion
Toda la API publica usa una API key con formato bc_live_{public_id}.{secret}. El secret completo se muestra una sola vez al crear o rotar la key; en Botyconnect solo se guarda un hash. La API key identifica a tu empresa, no a un numero especifico.
Authorization: Bearer bc_live_bck_xxxxxxxx.xxxxxxxxxxxxxxxx
Accept: application/json Cada key tiene scopes explicitos. Un request sin el scope requerido responde 403.
| Scope | Permite |
|---|---|
| accounts:read | Listar numeros y ver su detalle. |
| accounts:connect | Generar links de conexion coexistencial. |
| templates:read | Listar templates aprobados de un numero. |
| campaigns:read | Listar campanas, ver detalle y recipients. |
| campaigns:write | Crear campanas masivas. |
| campaigns:cancel | Cancelar campanas en cola. |
| messages:write | Enviar mensajes individuales (texto, multimedia e interactivos). |
| webhooks:read | Listar webhook endpoints. |
| webhooks:write | Crear y eliminar webhook endpoints. |
| credits:read | Consultar saldo y movimientos. |
Opcionalmente la key puede restringirse por IPs permitidas y tiene un rate limit propio por minuto.
Idempotencia
Las operaciones de escritura criticas aceptan el header Idempotency-Key y es obligatorio usarlo en integraciones serias: si tu request sufre timeout, repites el mismo request con la misma key y recibes la misma respuesta sin duplicar la operacion.
Idempotency-Key: erp-campaign-9981- Misma key + mismo body: devuelve la respuesta original.
- Misma key + body diferente: responde
409 Conflict. - Las keys expiran a las 48 horas.
- Aplica en: crear campana, cancelar campana, enviar mensaje individual, crear link de conexion, crear/eliminar webhook endpoint.
Health check
GET /health
Authorization: Bearer bc_live_xxx
// 200 OK
{ "success": true, "data": { "service": "botyconnect_api", "version": "v1", "status": "ok" } }Conectar numeros (coexistencia)
Una empresa puede conectar uno o varios numeros bajo la misma API key. Cada numero conserva la app de WhatsApp Business funcionando (modo coexistencial). Solicita un link de conexion y redirige al usuario:
POST /accounts/coexistence-link
Authorization: Bearer bc_live_xxx // scope: accounts:connect
Idempotency-Key: connect-sucursal-bogota-001
{
"label": "Ventas Bogota",
"external_reference": "sucursal-bogota",
"redirect_url": "https://empresa.com/botyconnect/callback"
}// 201 Created
{
"success": true,
"data": {
"onboarding_url": "https://business.facebook.com/messaging/whatsapp/onboard/...",
"attempt_id": "oba_123",
"expires_at": "2026-06-07T18:00:00Z"
}
} Cuando Meta completa el flujo, Botyconnect procesa el callback, activa el numero y redirige a tu redirect_url con ?success=true&account_id=wacc_xxx&attempt_id=oba_123. Tambien emite el webhook account.connected.
Listar numeros
GET /accounts // scope: accounts:read
GET /accounts/{account_id}
// 200 OK
{
"data": [
{
"id": "wacc_101",
"label": "Ventas Bogota",
"external_reference": "sucursal-bogota",
"phone_number": "+57 300 111 2233",
"status": "active",
"connection_mode": "coexistence",
"quality_rating": "GREEN",
"daily_limit": 1000,
"messages_sent_today": 120
}
]
}Templates aprobados de un numero
GET /accounts/{account_id}/templates // scope: templates:read
// 200 OK
{
"data": [
{ "name": "promo_junio", "language": "es_CO", "status": "approved", "category": "MARKETING" }
]
}Crear campana
Tu backend crea campanas masivas indicando el numero (account_id), un template aprobado y los destinatarios. Maximo 5.000 destinatarios por campana.
POST /campaigns
Authorization: Bearer bc_live_xxx // scope: campaigns:write
Idempotency-Key: erp-campaign-9981
{
"account_id": "wacc_101",
"external_reference": "erp-campaign-9981",
"template_name": "promo_junio",
"language": "es_CO",
"header_type": "image", // opcional: none | image | video | document
"header_url": "https://cdn.empresa.com/promo.jpg", // opcional, si el template tiene header
"body_placeholders": ["Cliente", "20%"], // variables por defecto
"dedupe": true,
"recipients": [
{
"wa_id": "573001112233",
"external_contact_id": "crm-1001",
"vars": ["Laura", "25%"] // array posicional; reemplaza body_placeholders para este destinatario
},
{
"wa_id": "573004445566",
"external_contact_id": "crm-1002" // sin vars: usa body_placeholders
}
]
}// 201 Created
{
"data": {
"id": "cmp_123",
"external_reference": "erp-campaign-9981",
"account_id": "wacc_101",
"status": "queued",
"total_recipients": 2,
"reserved_credits": 2
}
}wa_idse normaliza a solo digitos;dedupe: trueelimina duplicados.varses un array posicional y debe coincidir con la cantidad de variables del template.- Si el saldo disponible no alcanza para todos los destinatarios, la campana se rechaza.
- Al crearla se reservan creditos; cada mensaje aceptado por Meta consume uno; lo que nunca llega a Meta se libera.
Consultar campanas
GET /campaigns?status=completed&per_page=50 // scope: campaigns:read
GET /campaigns/{campaign_id}
GET /campaigns/{campaign_id}/recipients?per_page=50// GET /campaigns/cmp_123 → 200 OK
{
"data": {
"id": "cmp_123",
"status": "completed",
"total_recipients": 1000,
"reserved_credits": 1000,
"consumed_credits": 990,
"released_credits": 10,
"sent": 990,
"delivered": 940,
"read": 510,
"failed": 10,
"started_at": "2026-06-07T10:00:00Z",
"finished_at": "2026-06-07T10:05:00Z"
}
}Cancelar campana
POST /campaigns/{campaign_id}/cancel // scope: campaigns:cancel
Idempotency-Key: cancel-erp-campaign-9981Solo se cancela si sigue en queued y no tiene mensajes enviados.
Enviar mensaje individual
Ademas de campanas con template, puedes enviar un mensaje suelto a un contacto: texto libre, multimedia (imagen, video, documento, audio) o interactivo (botones o lista). Es lo que se usa para responder a un cliente.
422 outside_24h_window sin consumir creditos. Para iniciar conversaciones fuera de ventana usa una campana con template. Cada mensaje aceptado por Meta consume 1 credito. Texto
POST /messages
Authorization: Bearer bc_live_xxx // scope: messages:write
Idempotency-Key: ticket-555-reply-1
{
"account_id": "wacc_101",
"to": "573001112233",
"type": "text",
"external_reference": "ticket-555",
"text": { "body": "Hola, ya te atiendo." }
}Multimedia (url o media_id)
{
"account_id": "wacc_101",
"to": "573001112233",
"type": "image", // image | video | document | audio
"image": { "url": "https://empresa.com/files/comprobante.jpg", "caption": "Tu comprobante" }
}- Envia
urlpublica omedia_idya subido a Meta; uno de los dos. captionopcional (no aplica aaudio);documentadmitefilename.
Interactivo: botones
{
"account_id": "wacc_101",
"to": "573001112233",
"type": "interactive",
"interactive": {
"kind": "buttons",
"body": "Confirmas tu cita de manana 10:00 am?",
"buttons": [
{ "id": "confirm", "title": "Si, confirmo" },
{ "id": "reschedule", "title": "Reprogramar" }
]
}
}Maximo 3 botones; title hasta 20 caracteres.
Interactivo: lista
{
"account_id": "wacc_101",
"to": "573001112233",
"type": "interactive",
"interactive": {
"kind": "list",
"body": "Elige una opcion",
"button": "Ver opciones",
"sections": [
{
"title": "Soporte",
"rows": [
{ "id": "billing", "title": "Facturacion", "description": "Dudas de pago" },
{ "id": "tech", "title": "Tecnico", "description": "Fallas del servicio" }
]
}
]
}
}Respuesta (202 Accepted)
El envio es asincrono: la API valida, reserva 1 credito y encola el mensaje. Responde al instante con status: queued y aun sin wam_id.
// 202 Accepted
{
"data": {
"id": "msg_abc123",
"wam_id": null,
"account_id": "wacc_101",
"to": "573001112233",
"type": "text",
"status": "queued",
"external_reference": "ticket-555",
"credits_reserved": 1,
"created_at": "2026-06-14T10:00:01Z"
}
} El envio real a Meta ocurre en background; el resultado llega por webhook: message.sent (consume el credito) o message.failed (lo libera), y luego message.delivered / message.read segun avance. Asi un pico de trafico nunca bloquea el backend.
Creditos
El saldo es global por empresa: tu decides como repartirlo entre tus numeros. 1 mensaje aceptado por Meta = 1 credito.
GET /credits/balance // scope: credits:read
// 200 OK
{
"success": true,
"data": {
"balance": 50000,
"reserved_balance": 2000,
"available_balance": 48000,
"status": "active",
"low_balance_threshold": 1000
}
}GET /credits/ledger?per_page=50 // scope: credits:read| Tipo de movimiento | Significado |
|---|---|
| purchase | Compra de paquete confirmada. |
| manual_grant | Creditos asignados por el admin de Botyconnect. |
| manual_adjust | Ajuste operativo manual. |
| reserve | Reserva al crear una campana. |
| consume | Consumo real: Meta acepto el mensaje y devolvio wam_id. |
| release | Liberacion de reserva por mensajes que no llegaron a Meta. |
| refund | Devolucion acordada comercialmente. |
Webhooks
Registra una URL HTTPS de tu backend. Botyconnect devuelve un signing_secretuna sola vez y envia cada evento firmado. Esta llamada debe hacerla tu backend, nunca el navegador.
POST /webhooks/endpoints // scope: webhooks:write
Idempotency-Key: webhook-prod-001
{
"url": "https://empresa.com/webhooks/botyconnect",
"events": [
"account.connected", "campaign.created", "campaign.completed",
"message.sent", "message.delivered", "message.read", "message.failed",
"message.inbound.created", "message.echo.created"
]
}// 201 Created
{
"success": true,
"data": {
"id": "whend_123",
"url": "https://empresa.com/webhooks/botyconnect",
"events": ["..."],
"status": "active",
"signing_secret": "whsec_xxxxxxxxxxxx" // guardalo: no se vuelve a mostrar
}
}GET /webhooks/endpoints // scope: webhooks:read
DELETE /webhooks/endpoints/{endpoint_id} // scope: webhooks:write (lo desactiva)Eventos disponibles
| Evento | Cuando se emite |
|---|---|
| account.connected | Un numero completo la conexion coexistencial y quedo activo. |
| campaign.created | Se creo una campana y quedo en cola. |
| campaign.completed | La campana termino de procesarse. |
| message.sent | Meta acepto el mensaje (devuelve wam_id; consume 1 credito). |
| message.delivered | El mensaje llego al dispositivo del destinatario. |
| message.read | El destinatario leyo el mensaje. |
| message.failed | Meta reporto fallo de entrega (incluye codigo de error). |
| message.inbound.created | Un cliente escribio a tu numero. |
| message.echo.created | Alguien respondio manualmente desde la app de WhatsApp Business (coexistencia). |
Validar la firma HMAC
Cada entrega incluye estos headers:
X-Botyconnect-Event-Id: evt_123
X-Botyconnect-Timestamp: 1780840000
X-Botyconnect-Signature: sha256=...Tu backend debe leer el body crudo (antes de parsear JSON) y comparar:
// Node.js
const crypto = require("crypto");
const expected = "sha256=" + crypto
.createHmac("sha256", SIGNING_SECRET)
.update(`${req.headers["x-botyconnect-timestamp"]}.${rawBody}`)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-botyconnect-signature"])
);- Rechaza eventos con timestamp muy antiguo (replay attack).
- Guarda
X-Botyconnect-Event-Idpara descartar duplicados. - Responde
2xxrapido; si tu endpoint falla, Botyconnect reintenta con backoff exponencial. - Un webhook caido nunca bloquea tus envios.
Ejemplo de evento
{
"event": "message.delivered",
"account_id": "wacc_101",
"campaign_id": "cmp_123",
"recipient": "573001112233",
"external_contact_id": "crm-1001",
"wam_id": "wamid.xxx",
"status": "delivered",
"timestamp": "2026-06-07T10:00:10Z"
} En coexistencia, message.echo.created te avisa cuando alguien de tu equipo responde manualmente desde la app de WhatsApp Business, para que tu sistema registre esa intervencion.
Rate limits
| Ambito | Limite |
|---|---|
| Por API key | Configurable; 120 req/min por defecto |
| Por empresa (tenant) | 1000 req/min |
| Lecturas (GET) | 600 req/min por endpoint |
| Escrituras (POST/DELETE) | 120 req/min por endpoint |
| Crear campana | 60 req/min |
| Links de conexion | 20 req/min |
| Webhook endpoints | 30 req/min |
Al exceder un limite recibes 429; reintenta con backoff exponencial respetando Retry-After si viene.
Errores
| Codigo | Caso | Accion |
|---|---|---|
| 401 | API key ausente, invalida o revocada | Verificar el header Authorization o rotar la key. |
| 403 | La key no tiene el scope requerido | Crear una key con el scope correcto. |
| 404 | El recurso no existe o pertenece a otra empresa | Verificar el ID publico (wacc_, cmp_, whend_). |
| 409 | Idempotency-Key reutilizada con body diferente | Usar una key nueva o repetir el body original. |
| 422 | Payload invalido o regla de negocio (template, variables, saldo, ventana de 24h) | Revisar el detalle en errors o el code (outside_24h_window, insufficient_credits, ...). |
| 429 | Rate limit excedido | Reintentar con backoff exponencial. |
| 502 | Meta rechazo un mensaje individual (whatsapp_send_failed) | Revisar destino/contenido; el credito reservado se libera automaticamente. |
| 503 | Almacenamiento de auth/idempotencia no disponible | Reintentar; si persiste, contactar soporte. |