Enviar un broadcast

Si querés mandar una plantilla aprobada a muchos destinatarios a la vez (lista de contactos, segmento por etiqueta, o un conjunto explícito de IDs), usá la API de broadcasts. Para enviar a un único número, mirá la guía Enviar una plantilla.

¿Cuándo usar broadcasts? Cuando el mismo mensaje (misma plantilla, mismas variables o variables por contacto) va a 2+ destinatarios y querés trazabilidad por envío: estado por destinatario, conteos sent / failed, posibilidad de cancelar, y un único cargo de conversaciones validado contra tu cuota antes de empezar.
1

Pre-requisitos

  • Una API key de tu org con scope messages:send (o scopes vacíos = acceso total). Andá a Configuración → Integraciones → API Keys en el dashboard.
  • Una plantilla en estado APPROVED. Las PENDING o REJECTED bloquean el envío. Listalas con GET /organizations/{orgId}/templates.
  • El phone_number_id (UUID de Mosend) del número desde el que vas a enviar. Lo ves debajo de cada número en el dashboard, o vía GET /organizations/{orgId}/phone-numbers.
  • Los contactos deben tener optInStatus !== 'OPTED_OUT'. Los que estén opted-out se filtran automáticamente del envío.
2

Asegurate de tener los contactos cargados

Podés crear contactos uno por uno, importar por CSV, o pasarlos por waId (teléfono en formato E.164 sin +) — la API hace upsert por waId. Cada contacto recibe un id (UUID) que es lo que usás después en el broadcast.

POST /organizations/{orgId}/contacts
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/contacts' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "waId": "573001234567",
    "name": "Juan Pérez",
    "language": "es"
  }'

# Respuesta: { "data": { "id": "abc-uuid-...", ... } }
# El opt-in se gestiona aparte: POST /contacts/bulk-opt-in-status o el módulo opt-ins.

Para volúmenes grandes (cientos / miles), usá POST /contacts/import con un array. Detalles en referencia de contacts.

3

Definí la audiencia

El broadcast acepta tres formas de elegir destinatarios (podés combinar lista + IDs):

Opción A · Lista de contactos

Creá una lista y agregale miembros. Después pasás solo el listId al broadcast — ideal para audiencias recurrentes (clientes-vip, prospects-2026, etc.).

Crear lista + agregar contactos
# 1. Crear la lista
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/contact-lists' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Clientes Q1", "description": "Compradores ene-mar 2026" }'

# Respuesta: { "data": { "id": "<listId>", ... } }

# 2. Agregar contactos por id
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/contact-lists/<listId>/members' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "contactIds": ["uuid-1", "uuid-2", "uuid-3"] }'

Opción B · Lista poblada por etiqueta

Si ya etiquetás tus contactos (en el dashboard o vía POST /contacts/bulk-tag), podés poblar una lista a partir de una etiqueta. Útil para segmentos dinámicos tipo "mandá a todos los que tengan tag vip".

POST /contact-lists/{listId}/add-by-tag
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/contact-lists/<listId>/add-by-tag' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "tagId": "<tagId>" }'

# Respuesta: { "data": { "added": 47, "alreadyMembers": 3 } }

Opción C · Pasar contactIds directo

Para envíos ad-hoc sin crear lista (p.ej. integración que arma la audiencia desde tu propio CRM), pasá un array de contactIds al crear el broadcast — sin listId.

4

Creá el broadcast

Esto solo lo crea (estado DRAFT o, si pasás scheduledAt, SCHEDULED). Aún no se manda nada. La audiencia se resuelve al disparar el envío en el paso 5.

POST /organizations/{orgId}/broadcasts
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/broadcasts' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Anuncio promo Q1",
    "phoneNumberId": "<UUID-de-tu-numero>",
    "templateId": "<UUID-de-la-plantilla>",
    "templateLanguage": "es_CO",
    "listId": "<listId>",
    "contactIds": [],
    "templateVariables": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Juan" }
        ]
      }
    ]
  }'

# Respuesta
# {
#   "data": {
#     "id": "<broadcastId>",
#     "status": "DRAFT",
#     "totalRecipients": 0,    // se calcula al hacer /send
#     ...
#   }
# }
  • templateVariables sigue el formato oficial de Meta — el mismo array components que usás en POST /messages en modo experto. Si la plantilla no tiene variables, dejalo como [] o omitilo.
  • Plantillas con botones dinámicos (URL con {{1}} o COPY_CODE): el array tiene que incluir un component{ type: "button", sub_type: "url" | "copy_code", index: "0", parameters: [...] } además del body. Si lo omitís, Meta rechaza con #131008. Ver el ejemplo OTP en /enviar-plantilla.
  • Las mismas variables aplican a todos los destinatarios. Para personalización por-contacto (cada uno con su nombre, su pedido, etc.) hoy hay que crear N broadcasts o usar POST /messages en loop — vamos a soportar variables por-contacto pronto.
  • scheduledAt (ISO 8601) deja el broadcast en SCHEDULED y Mosend lo dispara solo a esa hora — un job interno corre cada minuto y ejecuta los SCHEDULED cuya hora ya pasó. No necesitás llamar /send vos. Si querés adelantarlo, podés igual disparar /send manualmente; si querés abortarlo antes de la hora, usá /cancel.
5

Disparalo

POST /broadcasts/{id}/send resuelve la audiencia (deduplica entre listId + contactIds, descarta opt-outs), valida tu cuota de conversaciones del plan antes de empezar, y manda secuencialmente con throttle de ~50 msg/s. Retorna el resumen final.

POST /organizations/{orgId}/broadcasts/{id}/send
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/broadcasts/<broadcastId>/send' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>'

# Respuesta (cuando termina)
# {
#   "data": {
#     "sent": 198,
#     "failed": 2,
#     "total": 200
#   }
# }

La request bloquea hasta que termine — un broadcast de 200 contactos a 50/s tarda ~4 segundos. Para volúmenes grandes (>500), aumentá tu timeout HTTP del lado del cliente o consultá el estado con polling al endpoint GET /broadcasts/{id}.

6

Seguí el progreso y los estados

Cada destinatario tiene su propio BroadcastRecipient con estado individual (PENDING / SENT / FAILED / DELIVERED / READ). El status del broadcast en sí queda en COMPLETED (al menos un sent) o FAILED (todos fallaron).

GET /organizations/{orgId}/broadcasts/{id}
curl 'https://api.mosend.dev/organizations/{orgId}/broadcasts/<broadcastId>' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>'

# Respuesta
# {
#   "data": {
#     "id": "...",
#     "status": "COMPLETED",
#     "totalRecipients": 200,
#     "sentCount": 198,
#     "failedCount": 2,
#     "startedAt": "...",
#     "completedAt": "...",
#     "counts": {              // agregados listos para tu dashboard
#       "total": 200,
#       "sent": 198,           // SENT + DELIVERED + READ (salió OK)
#       "delivered": 180,      // DELIVERED + READ (llegó al teléfono)
#       "read": 95,            // el destinatario lo abrió
#       "failed": 2,           // no les llegó
#       "replied": 23          // contestaron tras recibirlo
#     }
#   }
# }

Los counts son acumulativos por embudo: cada READ también cuenta como delivered y sent; cada DELIVERED cuenta como sent. replied marca a quienes te escribieron de vuelta dentro de los 30 días posteriores al envío.

Listar destinatarios por estado

Para traer el detalle por destinatario (sin cargar el broadcast entero) y filtrar por estado, usá GET /broadcasts/{id}/recipients. Acepta ?filter= con uno de replied · read · delivered · sent · failed, y pagina por cursor (?cursor=, ?limit= hasta 200). Sin filter devuelve todos.

GET /organizations/{orgId}/broadcasts/{id}/recipients?filter=replied
curl 'https://api.mosend.dev/organizations/{orgId}/broadcasts/<broadcastId>/recipients?filter=replied&limit=100' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>'

# Respuesta
# {
#   "data": {
#     "items": [
#       {
#         "id": "<recipientId>",
#         "contactId": "...",
#         "contact": { "id": "...", "name": "Juan Pérez", "waId": "573001234567" },
#         "status": "READ",
#         "metaMessageId": "wamid....",
#         "sentAt": "...", "deliveredAt": "...", "readAt": "...",
#         "repliedAt": "2026-05-20T14:02:11.000Z"
#       }
#     ],
#     "nextCursor": "<recipientId>"   // null cuando no hay más
#   }
# }
  • replied → contestaron (repliedAt no nulo).
  • read → estado READ.
  • deliveredDELIVERED o READ.
  • sentSENT, DELIVERED o READ (salió OK).
  • failed → no les llegó (FAILED), con errorMessage.

Para enterarte del cambio SENT → DELIVERED → READ de cada mensaje individual en tiempo real (que llega después, vía webhook de Meta), suscribite al evento message.status en webhooks salientes. El payload firmado con HMAC trae el messageId que enlazás con el recipient correspondiente. Mosend también propaga esos estados al BroadcastRecipient, así que los counts de arriba se actualizan solos.

Seguimiento (segundo toque)

Para re-impactar a quienes no se engancharon, creá un seguimiento: una difusión cuya audiencia se deriva de los destinatarios de otra. Pasá sourceBroadcastId + followUpAudience en vez de listId/contactIds. Debe usar el mismo número que la difusión original.

  • NOT_ENGAGED — no leyeron ni respondieron (status SENT/DELIVERED, sin reply).
  • NOT_REPLIED — no respondieron (aunque hayan leído).
  • NOT_DELIVERED — no les llegó (FAILED) — reintento.

La audiencia se resuelve al enviar (no al crear): quien lea o conteste mientras tanto queda excluido automáticamente. Como va fuera de la ventana de 24 h, el seguimiento también usa una plantilla aprobada (un recordatorio). Programalo 24–48 h después con scheduledAt.

POST /organizations/{orgId}/broadcasts (seguimiento)
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/broadcasts' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Recordatorio promo Q1",
    "phoneNumberId": "<UUID-del-mismo-numero>",
    "templateId": "<UUID-plantilla-recordatorio>",
    "sourceBroadcastId": "<broadcastId-original>",
    "followUpAudience": "NOT_ENGAGED",
    "scheduledAt": "2026-05-23T14:00:00.000Z"
  }'

Cuota y cancelación

  • Antes de empezar a enviar, el backend calcula cuántas conversaciones nuevas implica tu broadcast (destinatarios sin hilo activo en el periodo actual) y lo compara con tu cuota del plan. Si no alcanza y no tenés política de overage activa, la request rechaza con 400 antes de marcar SENDING — no quedás a medias.
  • Para abortar un broadcast en curso, POST /broadcasts/{id}/cancel. Los que ya se mandaron no se desmandan (Meta no lo permite), pero los pendientes no se envían.
  • Los opt-outs se filtran automáticamente del envío — no necesitás limpiar la lista manualmente. Si un contacto se opta-out después de crear el broadcast pero antes de /send, queda fuera.

Errores comunes

  • 400 "Audiencia requerida (lista o contactos)": no pasaste listId ni contactIds. Necesitás al menos uno (o ambos — se mergean y deduplican).
  • 400 "La plantilla debe estar APROBADA": la plantilla está en PENDING o REJECTED. Esperá a que Meta la apruebe (AUTHENTICATION suelen tardar minutos, MARKETING horas).
  • 400 "Este broadcast abriría N conversación(es) nueva(s)…": chocaste con tu cuota del plan. Contratá un add-on, subí de plan, activá overage, o reducí destinatarios.
  • 403 con API key sin scope: tu key tiene scopes restringidos y no incluyen messages:send. Recreala con scopes: [] (acceso total) o sumá el scope.
  • recipient FAILED con error Meta 131056: ese contacto nunca confirmó opt-in con tu negocio (Meta lo bloquea aunque tu API lo tenga OPTED_IN). Pediles que escriban primero a tu número o que opt-in vía un canal de Meta.

Referencias rápidas