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/botyconnect
Descargar OpenAPI 3.1 (YAML) Especificacion completa para validar requests y generar SDKs.

OpenAPI: 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-python

Validar 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/botyconnect

Explorar 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

  1. Solicita acceso desde /register. Un admin de Botyconnect aprueba la empresa.
  2. Recibes correo de activacion, defines password y entras al portal.
  3. Obtienes tu API key (en la aprobacion o creandola en el portal). Se muestra una sola vez.
  4. Registras tu webhook endpoint y guardas el signing_secret.
  5. Generas un link de conexion coexistencial por cada numero y lo completas en Meta.
  6. Listas tus numeros y templates aprobados.
  7. 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.

ScopePermite
accounts:readListar numeros y ver su detalle.
accounts:connectGenerar links de conexion coexistencial.
templates:readListar templates aprobados de un numero.
campaigns:readListar campanas, ver detalle y recipients.
campaigns:writeCrear campanas masivas.
campaigns:cancelCancelar campanas en cola.
messages:writeEnviar mensajes individuales (texto, multimedia e interactivos).
webhooks:readListar webhook endpoints.
webhooks:writeCrear y eliminar webhook endpoints.
credits:readConsultar 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_id se normaliza a solo digitos; dedupe: true elimina duplicados.
  • vars es 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-9981

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

Ventana de 24 horas: solo puedes enviar un mensaje libre si el contacto te escribio en las ultimas 24 horas. Fuera de ventana responde 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 url publica o media_id ya subido a Meta; uno de los dos.
  • caption opcional (no aplica a audio); document admite filename.

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 movimientoSignificado
purchaseCompra de paquete confirmada.
manual_grantCreditos asignados por el admin de Botyconnect.
manual_adjustAjuste operativo manual.
reserveReserva al crear una campana.
consumeConsumo real: Meta acepto el mensaje y devolvio wam_id.
releaseLiberacion de reserva por mensajes que no llegaron a Meta.
refundDevolucion 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

EventoCuando se emite
account.connectedUn numero completo la conexion coexistencial y quedo activo.
campaign.createdSe creo una campana y quedo en cola.
campaign.completedLa campana termino de procesarse.
message.sentMeta acepto el mensaje (devuelve wam_id; consume 1 credito).
message.deliveredEl mensaje llego al dispositivo del destinatario.
message.readEl destinatario leyo el mensaje.
message.failedMeta reporto fallo de entrega (incluye codigo de error).
message.inbound.createdUn cliente escribio a tu numero.
message.echo.createdAlguien 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-Id para descartar duplicados.
  • Responde 2xx rapido; 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

AmbitoLimite
Por API keyConfigurable; 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 campana60 req/min
Links de conexion20 req/min
Webhook endpoints30 req/min

Al exceder un limite recibes 429; reintenta con backoff exponencial respetando Retry-After si viene.

Errores

CodigoCasoAccion
401API key ausente, invalida o revocadaVerificar el header Authorization o rotar la key.
403La key no tiene el scope requeridoCrear una key con el scope correcto.
404El recurso no existe o pertenece a otra empresaVerificar el ID publico (wacc_, cmp_, whend_).
409Idempotency-Key reutilizada con body diferenteUsar una key nueva o repetir el body original.
422Payload invalido o regla de negocio (template, variables, saldo, ventana de 24h)Revisar el detalle en errors o el code (outside_24h_window, insufficient_credits, ...).
429Rate limit excedidoReintentar con backoff exponencial.
502Meta rechazo un mensaje individual (whatsapp_send_failed)Revisar destino/contenido; el credito reservado se libera automaticamente.
503Almacenamiento de auth/idempotencia no disponibleReintentar; si persiste, contactar soporte.