Campañas
Una campaña envía una plantilla aprobada a un público: todos los contactos que selecciona un filtro, con las variables de cada uno rellenas. Por la API montas, revisas, lanzas y sigues la campaña — el envío en sí corre en un proceso aparte, al ritmo que definas, sin gastar el límite de envío de tu clave.
El camino de una campaña
materializing es el armado de la lista de destinatarios: el público se congela en el
lanzamiento, y quien entre en la base después no la recibe. Una campaña también puede
terminar en canceled o, cuando nadie del público puede recibirla, en failed.
1. Crear el borrador
curl -X POST https://api.joinotify.com/broadcasts \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"name": "Black Friday",
"phoneNumberId": "106540352242922",
"templateName": "promo_black_friday",
"templateLanguage": "es",
"audienceId": "cm1aud1ence01",
"variables": {
"body": {
"1": { "source": "contact", "key": "first_name", "fallback": "cliente" },
"2": { "source": "static", "value": "BLACK20" }
}
},
"sendWindow": { "days": [1, 2, 3, 4, 5], "start": "09:00", "end": "20:00" },
"timezone": "Europe/Madrid"
}'
Solo name es obligatorio; el resto puede venir después, por PATCH. Nada se revisa al
guardar — la revisión ocurre en la estimación, la prueba y el lanzamiento.
Variables. Cada marcador de la plantilla necesita una fuente: contact (un dato del
contacto, como first_name), attribute (un campo personalizado) o static (un valor
fijo). Las dos primeras exigen un fallback para quien no tiene el dato. Una plantilla
con encabezado de medio necesita headerMedia — sube el archivo con
POST /broadcasts/media y usa el id.
Público. Un público guardado (audienceId) o un filtro solo de esta campaña
(audienceFilter), con el mismo árbol de condiciones. Crea públicos reutilizables con
POST /audiences y revisa a quién seleccionan con POST /audiences/preview.
2. Estimar y probar
POST /broadcasts/{id}/estimate revisa toda la campaña y responde a quién alcanzaría
ahora:
{
"data": {
"recipients": 4800,
"total": 5230,
"skipped": { "opted_out": 120, "no_consent": 290, "suppressed": 20 },
"cap": 50000,
"firstCampaign": false,
"overCap": 0,
"remainingQuota": 18000,
"portfolioLimit": 10000,
"estimatedCost": { "micros": 297600000, "currency": "eur", "byCountry": [], "unpriced": 0 }
}
}
Después, envíate la campaña con POST /broadcasts/{id}/test y
{ "to": "+5541987111527" }. La prueba sale en el momento, fuera de las métricas, y
muestra las variables rellenas con los valores de respaldo.
Solo los contactos con consentimiento (opted_in) y fuera de la lista de supresión. Las
plantillas de utilidad no dependen del consentimiento, pero siguen omitiendo números
inválidos. Ver Contactos y consentimiento.
3. Lanzar
curl -X POST https://api.joinotify.com/broadcasts/cm1bro4dc4st01/launch \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "scheduledAt": "2026-11-27T12:00:00+01:00" }'
Sin scheduledAt, la campaña empieza en el momento. La respuesta es 202. El
lanzamiento rechaza con 422:
type | Por qué |
|---|---|
audience_empty | Nadie del público puede recibirla |
broadcast_limit_reached | El público supera el techo de destinatarios (limit dice cuál) |
quota_exceeded | El plan no tiene más mensajes este mes |
El techo crece con el historial. La primera campaña de la cuenta va a hasta 1.000 personas; las siguientes, a hasta diez veces la mayor campaña ya terminada — siempre dentro del límite del plan. Es lo que protege la calidad de un número nuevo de un envío demasiado grande.
Editar una campaña programada la devuelve a borrador: hay que lanzarla de nuevo.
4. Seguir
GET /broadcasts/{id}/stats— contadores, tasas, clics por botón, fallos agrupados, costo por país y la calidad del número antes y ahora.GET /broadcasts/{id}/timeseries— la evolución en franjas de 15 minutos.GET /broadcasts/{id}/recipientsy/export— cada destinatario, con estado y error.- Los eventos
broadcast.pausedybroadcast.completeden tu webhook.
Pausas automáticas
Una campaña se detiene sola cuando seguir perjudicaría al número: calidad roja,
plantilla pausada por Meta, cuenta restringida, cuota del plan agotada, demasiados errores
seguidos. El motivo queda en pauseReason y llega en el evento broadcast.paused — la
tabla completa está en el catálogo de eventos.
Dos pausas se resuelven solas y traen resumeAfter: send_window (fuera de la ventana
de envío) y throughput (Meta retuvo el caudal; reintenta a los 15 minutos, 1 hora y 4
horas). Las demás esperan a que alguien corrija la causa y llame a
POST /broadcasts/{id}/resume.
Envíos sin respuesta
Cuando Meta no responde a un envío, el destinatario queda unknown. No se repite
solo: el mensaje puede haber llegado, y repetir lo enviaría dos veces. Si decides
reenviar, POST /broadcasts/{id}/resend-unknown con { "confirm": true } devuelve esos
destinatarios a la cola.
Alcance de la clave
Una API key restringida a algunos números solo ve y crea campañas en esos números. Los
públicos guardados son de toda la cuenta: /audiences responde 403 para ella.
El público de una campaña estimada o lanzada con esa clave — guardado o escrito como filtro —
queda restringido a los contactos que tienen conversación en sus números, y el recorte se congela
en el lanzamiento, junto con el público. En el envío de prueba, un contactId fuera de ese
recorte responde 404. Una campaña lanzada con una clave sin restricción, o desde el panel, va a
toda la base.