Skip to content

Broadcasts

Los broadcasts son campañas masivas que envían un template aprobado de WhatsApp a muchos destinatarios. Son solo para WhatsApp y basados en templates (un canal de Instagram devuelve 422 channel_not_whatsapp).

Todas las rutas /platform/v1/broadcasts/* requieren send:broadcasts. El scope es accesible desde una API key (server-to-server) o desde una sesión cookie del dashboard.

Dos límites gobiernan los broadcasts (columnas en platform_plans):

PlanDestinatarios por campañaCampañas por períodoEfecto
free00Deshabilitado: POST /broadcasts devuelve 403 broadcasts_not_in_plan.
lite100010Hasta 1000 destinatarios por campaña, 10 campañas por período.
pro10 000100Hasta 10 000 destinatarios por campaña, 100 campañas por período.
businessSin límite.
  • El cap de campañas por período (broadcasts_per_month) cuenta las campañas creadas dentro del período de facturación actual (aniversario de Polar o mes calendario). Pasarte devuelve 429 broadcasts_per_month_quota al crear.
  • El cap de destinatarios por campaña (broadcast_recipients_limit) se aplica al cargar destinatarios: los que sobran van al array errors[] con recipients_limit_reached (no es un 422 duro).
crear ──► [draft] ──schedule──► [scheduled] ──(vence / send)──► [sending] ──► [completed] (≥1 enviado)
│ │ │
│ └── cancel ──► [draft] (limpia scheduled_at) └─(0 enviados)─► [failed]
└── send ─────────────────────────────────────────────────────────────┘
  • cancel es scheduled → draft (reanudable, limpia scheduled_at). NO hay un estado terminal cancelled: cancelás un agendado y vuelve a borrador, listo para editar o reenviar.
  • Un broadcast sending o terminal NO se puede cancelar (409 status_locked).
  • Un segundo POST /{id}/send es idempotente a nivel FSM: una vez en sending, devuelve 409 status_locked.
Método + rutaDescripción
POST /platform/v1/broadcasts (201)Crear un borrador. Body {channel_id, name (≤200), template_name (≤512), language_code="es"}. Errores: 403 broadcasts_not_in_plan (free), 429 broadcasts_per_month_quota, 422 channel_not_found / channel_not_active / channel_not_whatsapp.
GET /platform/v1/broadcastsListar tus broadcasts (más nuevo primero).
GET /platform/v1/broadcasts/{id}Detalle de uno (404 opaco si no es tuyo).
POST /platform/v1/broadcasts/{id}/recipientsCargar ≤1000 destinatarios por llamada. Devuelve {added, duplicates, errors[]}. Errores: 404, 409 status_locked (no está en draft).
POST /platform/v1/broadcasts/{id}/scheduleAgendar para un timestamp futuro ({scheduled_at} ISO-8601, tolerancia de 60s de skew; un timestamp claramente pasado → 422). draft → scheduled. Errores: 404, 409 status_locked, 422 no_recipients.
POST /platform/v1/broadcasts/{id}/cancelscheduled → draft (limpia scheduled_at). Errores: 404, 409 status_locked.
POST /platform/v1/broadcasts/{id}/send (202)Pasar a sending; el worker lo toma en su próximo ciclo (cada 30s). Errores: 404, 409 status_locked, 422 no_recipients.
GET /platform/v1/broadcasts/{id}/metricsAgregado por destinatario (ver abajo).

Hasta 1000 por llamada (llamá varias veces para campañas más grandes, dentro del cap del plan). Cada destinatario es {to, variables}:

{
"recipients": [
{ "to": "59891234567", "variables": { "nombre": "Ana", "codigo": "PIZZA20" } },
{ "to": "59899876543", "variables": { "nombre": "Luis", "codigo": "PIZZA20" } }
]
}
  • to: el teléfono del destinatario (cualquier formato; se normaliza a dígitos). Un teléfono inválido va a errors[].
  • variables: los parámetros del template. Solo valores planos string-coercibles: un valor dict o list anidado devuelve 422 (el worker mapea cada valor a un parámetro posicional del componente BODY de Meta, en orden de inserción, con str(valor); una estructura anidada no tiene encoding válido en Meta).

La respuesta separa los tres resultados:

{ "added": 2, "duplicates": 0, "errors": [] }
  • added: filas creadas.
  • duplicates: destinatarios repetidos (dentro del batch o ya cargados; hay un UNIQUE (broadcast_id, to)).
  • errors[]: teléfonos inválidos (invalid_phone) y los que sobrepasan el cap del plan (recipients_limit_reached).
{
"total": 1000,
"sent": 980,
"failed": 20,
"delivered": 940,
"read": 610,
"responded": 85,
"pending": 0,
"response_rate": 0.0867
}
CampoSignificado
totalDestinatarios cargados.
sentEnviados con éxito a Meta (incluye delivered / read / responded).
failedFallaron (cuota de template, token, error de Meta).
deliveredMeta confirmó entrega al device.
readEl usuario leyó el mensaje.
respondedEl destinatario respondió después del envío.
pendingTodavía sin enviar (total - sent - failed).
response_rateresponded / sent (0.0 si sent == 0).

delivered / read / responded se derivan del data plane uniendo por (canal, meta_message_id); son consultas batcheadas, no una por destinatario.

El envío es asíncrono: POST /{id}/send devuelve 202 de inmediato y el worker procesa la campaña en su ciclo de 30s, mandando el template por destinatario y comiteando fila por fila (reanudable si el proceso muere a mitad).

Entrega at-least-once. Si el proceso muere entre la llamada a Meta y el commit de la fila, ese destinatario queda pending y se reintenta en el próximo ciclo, así que puede recibir el template dos veces. La ventana es de milisegundos (round-trip a Meta). El UNIQUE (broadcast_id, to) evita filas duplicadas, no llamadas a Meta duplicadas. Diseñá tu template asumiendo at-least-once.

  • Templates: cómo crear y aprobar el template que vas a usar en la campaña.
  • Webhooks normalizados: recibí message.delivered / message.read en tiempo real en lugar de pollear las métricas.