Enviar un broadcast

Si quieres mandar una plantilla aprobada a muchos destinatarios a la vez (lista de contactos, segmento por etiqueta, o un conjunto explícito de IDs), usa la API de broadcasts. Para enviar a un único número, mira 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 quieres 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

Asegúrate de tener los contactos cargados

Puedes 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 usas 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), usa POST /contacts/import con un array. Detalles en referencia de contacts.

3

Define la audiencia

El broadcast acepta tres formas de elegir destinatarios (puedes combinar lista + IDs):

Opción A · Lista de contactos

Crea una lista y agrégale miembros. Después pasas 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 etiquetas tus contactos (en el dashboard o vía POST /contacts/bulk-tag), puedes poblar una lista a partir de una etiqueta. Útil para segmentos dinámicos tipo "manda 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), pasa un array de contactIds al crear el broadcast — sin listId.

4

Crea el broadcast

Esto solo lo crea (estado DRAFT o, si pasas 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 usas en POST /messages en modo experto. Si la plantilla no tiene variables, déjalo como [] u omítelo.
  • 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 omites, 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 necesitas llamar /send tú. Si quieres adelantarlo, puedes igual disparar /send manualmente; si quieres abortarlo antes de la hora, usa /cancel.
5

Dispáralo

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

Sigue 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, usa 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), suscríbete al evento message.status en webhooks salientes. El payload firmado con HMAC trae el messageId que enlazas 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, crea un seguimiento: una difusión cuya audiencia se deriva de los destinatarios de otra. Pasa 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). Prográmalo 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 tienes política de overage activa, la request rechaza con 400 antes de marcar SENDING — no quedas 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 necesitas 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. Necesitas al menos uno (o ambos — se mergean y deduplican).
  • 400 "La plantilla debe estar APROBADA": la plantilla está en PENDING o REJECTED. Espera 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. Contrata un add-on, sube de plan, activa overage, o reduce destinatarios.
  • 403 con API key sin scope: tu key tiene scopes restringidos y no incluyen messages:send. Recréala con scopes: [] (acceso total) o suma 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). Pídeles que escriban primero a tu número o que opt-in vía un canal de Meta.

Referencias rápidas