Envío de mensajes de WhatsApp
Esta guía cubre el endpoint de envío, POST /v1/whatsapp/messages. Construyes un payload JSON con un destinatario y exactamente un tipo de contenido: una plantilla preaprobada, o un mensaje de servicio con texto, una imagen, video, audio, un sticker, un documento, una ubicación, tarjetas de contacto o algo para pulsar. Bird devuelve 202 Accepted con un ID de mensaje y entrega de forma asíncrona. Cuál de los dos puedes enviar depende de la ventana de servicio al cliente. Cada solicitud envía un mensaje a un destinatario, y no existe un endpoint de envío por lotes.
Un envío mínimo
El payload válido más pequeño es un destinatario to y un template con su slug. Añade language si quieres uno específico; omitirlo envía el idioma predeterminado de la plantilla, y completa cualquier variable que la plantilla declare a través de components.
La llamada curl indica el host de EE. UU.; si tu clave empieza con bk_eu1_, llama a https://eu1.platform.bird.com en su lugar. Los SDK leen la región de tu clave, así que no configuran host.
const msg = await bird.whatsapp.send({
to: "+15551234567",
template: {
slug: "bird_otp",
components: [{ type: "body", parameters: [{ type: "text", text: "123456" }] }],
},
});
console.log(msg.id, msg.status);msg = client.whatsapp.send(
to="+31612345678",
template="bird_otp",
language="en",
components=[{"type": "body", "parameters": [{"type": "text", "text": "123456"}]}],
)
print(msg.id, msg.status)code := "123456"
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+15551234567",
Template: "bird_otp",
Language: "en",
Components: []bird.WhatsAppMessageTemplateComponent{{
Type: "body",
Parameters: &[]bird.WhatsAppMessageTemplateComponentParameter{{Type: "text", Text: &code}},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)$message = $bird->whatsapp->send(
to: '+15551234567',
template: 'bird_otp',
language: 'en',
components: [
(new WhatsAppMessageTemplateComponent())
->setType('body')
->setParameters([
(new WhatsAppMessageTemplateComponentParameter())->setType('text')->setText('123456'),
]),
],
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--components '[{"parameters":[{"text":"1234","type":"text"}],"type":"body"},{"parameters":[{"text":"1234","type":"text"}],"type":"button"}]' \
--language en \
--template bird_otp \
--to +31612345678curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+31612345678",
"template": {
"slug": "bird_otp",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "1234"
}
]
},
{
"type": "button",
"parameters": [
{
"type": "text",
"text": "1234"
}
]
}
]
}
}'La ventana de servicio al cliente
Cuál de los dos puedes enviar depende de un dato de estado: si la ventana de servicio al cliente está abierta.
El contacto abre la ventana al enviar un mensaje o llamar a tu número de negocio, y permanece abierta durante 24 horas, reiniciándose cada vez que te escribe de nuevo. Mientras está abierta puedes enviar un mensaje de servicio, es decir, cualquier contenido libre: texto, imagen, video, audio, sticker, documento, ubicación o interactivo. Una vez que caduca, solo una plantilla preaprobada llega al contacto, y su respuesta a ella reabre la ventana.
Bird rastrea la ventana por ti, así que un mensaje de servicio enviado a una ventana cerrada se rechaza antes de que se cree o se cobre nada: la solicitud devuelve un 422 E15044 WhatsAppServiceWindowClosed. La comprobación se realiza con la información disponible y permite continuar si no puede completarse, así que un 202 no es prueba de que la ventana estuviera realmente abierta en el momento del envío; una ventana que caduca entre la aceptación y el envío falla de forma asíncrona, con service_window_expired en el last_error del mensaje.
Consulta la ventana de servicio al cliente para el ciclo de vida completo: qué la abre, qué la reinicia y cómo interactúa con los precios.
Construir el payload
Destinatario
to indica un destino, dado como número de teléfono, ID de usuario en el ámbito del negocio o ID de grupo. Un número de teléfono va en formato E.164: un + inicial, código de país y número de abonado, por ejemplo +14155550100. El número se valida, así que un valor que no puede ser un número real y marcable (longitud incorrecta, prefijo no asignado) se rechaza con un 422 WhatsAppInvalidRecipient antes de generar ningún cargo. No hay array de destinatarios ni envío por lotes, así que para llegar a muchas personas que no están en un mismo grupo necesitas una llamada por destinatario.
Un ID de usuario con alcance de negocio como US.13491208655302741918 se dirige a un contacto cuyo número de teléfono no tienes, que es la forma de responder a un contacto que te escribió sin uno. Dos cosas cambian: el número de envío debe pertenecer al mismo portafolio de negocio al que está asociado el ID, y una plantilla de código de verificación de un solo uso necesita un número de teléfono. Una gestionada por Bird se rechaza en la aceptación con un 422 WhatsAppRecipientNotSupportedForTemplate; una plantilla de autenticación creada por tu espacio de trabajo se acepta y luego falla, ya que Meta requiere un número de teléfono para ella.
to admite una forma más: un ID de grupo WhatsApp como wag_01krdgeqcxet5s7t44vh8rt9mg, que envía a cada participante de ese chat grupal. Un envío grupal omite from e informa la entrega a nivel del grupo en lugar de contra un solo destinatario, así que Enviar a un grupo WhatsApp lo cubre en su propia página.
Plantilla
template indica la plantilla preaprobada que se enviará:
- slug (obligatorio): el slug de la plantilla, por ejemplo bird_order_confirmation. Debe coincidir con una plantilla de tu catálogo (letras minúsculas, dígitos y guiones bajos).
- language: la etiqueta de idioma de la plantilla, por ejemplo en o pt-BR. Omítelo para enviar el idioma predeterminado de la plantilla; indicar un idioma que la plantilla no tiene devuelve un 422 que lista los disponibles. El mensaje aceptado repite el idioma resuelto.
- components: los valores que llenan las variables de la plantilla (consulta Componentes y parámetros). Omítelo para una plantilla sin variables.
Explora tus plantillas, sus idiomas y una vista previa renderizada de cada una en la página de Plantillas.
Componentes y parámetros
Las plantillas llevan variables, con nombre ({{ref}}, {{amount}}) o numeradas ({{1}}, {{2}}). Proporcionas sus valores a través de components. Cada componente indica un type (body o button) y un array parameters. Cada parámetro indica su propio type (text, image, video, gif, document o location) y lleva el campo correspondiente: text una cadena de texto plana, image/video/gif/document una https url pública, y location un punto en el mapa. Una plantilla con parámetros con nombre requiere un name en cada parámetro, que coincida exactamente con los nombres declarados en la plantilla (consulta Referencia de campos). Una plantilla posicional omite name y toma sus valores en orden {{n}}, de modo que el primer parámetro llena {{1}}. En ambos casos, parámetros que no coinciden con lo que la plantilla declara devuelven un 422 WhatsAppTemplateParameterMismatch. También existe en la transmisión un tipo de componente header: en una plantilla gestionada por Bird se descarta, ya que ninguna plantilla gestionada por Bird declara una variable de encabezado, pero en una plantilla creada por tu espacio de trabajo se reenvía, que es como una plantilla de utilidad o marketing con encabezado multimedia obtiene su imagen.
Por ejemplo, una plantilla de código de verificación de un solo uso cuyo cuerpo dice {{1}} is your verification code y cuyo botón copia el código toma el código como parámetro de cuerpo y como parámetro de botón, de forma posicional (sin name):
Ejemplo de código
{
"components": [
{ "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
{ "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
]
}Categoría y remitente
La categoría de una plantilla (authentication, utility o marketing) determina cómo WhatsApp trata el mensaje y, junto con el país de destino, cuánto cuesta.
Quién es dueño del remitente determina si lo indicas:
- Una plantilla gestionada por Bird (su slug empieza con bird_) se envía desde el número que Bird mantiene para esa categoría, así que omite from. Indicarlo devuelve un 422 WhatsAppSenderNotAllowed.
- Todo lo demás indica su propio remitente en from: un mensaje de servicio de cualquier tipo y cualquier plantilla creada por tu espacio de trabajo. El número debe pertenecer a tu espacio de trabajo. Omitirlo devuelve un 422 WhatsAppSenderRequired, y un número desde el que el espacio de trabajo no puede enviar devuelve un 422 WhatsAppSenderNotFound. Además, una plantilla creada por ti debe estar en la misma WhatsApp Business Account que el número, o el envío devuelve un 422 WhatsAppSenderWABAMismatch.
Configuración del número de teléfono cubre ambos tipos de número y cómo se conecta un número propio.
Mensajes de servicio
En lugar de template, incluye exactamente uno de text, image, video, audio, sticker, document, location, contact_cards o interactive. Los nueve son mensajes de servicio, así que necesitan una ventana de servicio al cliente abierta. Todos requieren también from, un número que tu espacio de trabajo posea; los números gestionados de Bird no pueden usarse.
- text: { "body": "..." }, hasta 4096 caracteres. Añade "preview_url": true para mostrar una vista previa de enlace de la primera URL en body.
- image, video, audio, sticker, document: cada uno toma una URL https pública que WhatsApp descarga en el momento del envío (url), así que una URL firmada debe seguir vigente tras el envío. Una URL http se rechaza directamente. WhatsApp descarga el archivo, así que una URL a la que no puede acceder, una que sirve un tipo no compatible o un archivo que supera el límite de tamaño para su tipo se acepta y luego falla, con media_rejected en el last_error del mensaje y la razón propia de WhatsApp en description. image, video y document también aceptan un caption opcional; document también acepta un filename opcional; audio acepta un flag voice opcional para renderizar como nota de voz.
- location: { "latitude": ..., "longitude": ... } (ambos obligatorios, grados decimales) más name y address opcionales.
- contact_cards: un array de hasta cinco contactos compartidos en un mensaje. El name de cada tarjeta necesita formatted_name más al menos otra parte (first_name, last_name, middle_name, prefix o suffix); phone_numbers, emails, urls y addresses aceptan hasta diez entradas cada uno, y org y birthday (como YYYY-MM-DD) son opcionales. Un phone_number en E.164 le da a esa tarjeta un botón que abre un chat con él.
- interactive: texto del cuerpo más algo para tocar, en uno de seis tipos: botones de respuesta, un menú de lista, un botón de enlace, un carrusel multimedia o un botón único que pide al destinatario su ubicación o su número de teléfono. Mensajes interactivos cubre la estructura de cada tipo, las respuestas que genera un toque y los límites.
Ejemplo de código
{
"to": "+16505551234",
"from": "+13124495648",
"text": { "body": "Your order shipped: https://example.com/track/A1B2C3", "preview_url": true }
}Una solicitud sin contenido, o con más de un tipo, se rechaza con un 422.
Citar un mensaje
Establece in_reply_to_message_id con un ID de mensaje WhatsApp para enviar tu mensaje como respuesta a él, de la misma forma que tocar responder en la app de WhatsApp cita un mensaje. El destinatario ve tu mensaje con el citado encima, y el campo aparece en cada lectura del mensaje.
También funciona a la inversa: un mensaje entrante que WhatsApp marca como respuesta lleva el ID del mensaje citado en el mismo campo, que es como identificas a cuál de tus mensajes responde. Un mensaje entrante que WhatsApp no marca no lleva ID, y la resolución también puede fallar. Para una correlación fiable, usa identificadores explícitos de respuesta interactiva junto con el estado de conversación o tarea almacenado en tu aplicación. El metadata saliente permanece en el registro saliente y no se copia automáticamente en la respuesta.
La cita se resuelve antes de aceptar el envío, así que una cita que no puede renderizarse hace fallar la solicitud y no se crea ni se cobra nada. Un id que no corresponde a ningún mensaje del espacio de trabajo, o uno anterior a los 15 días en que un mensaje es citable, devuelve un 404 E15071 WhatsAppReferencedMessageNotFound. Uno que nombra un mensaje que nunca llegó a WhatsApp, o un mensaje de una conversación distinta a la de to y from de este envío, devuelve un 422 E15072 WhatsAppMessageNotQuotable. Si Bird no puede contactar el almacén que responde a la consulta, el envío devuelve un 503 E15073 WhatsAppMessageLookupUnavailable, que vale la pena reintentar. La cita funciona tanto en un envío de plantilla como en uno de formato libre.
Ejemplo de código
{
"to": "+16505551234",
"from": "+13124495648",
"in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
"text": { "body": "Yes, that slot is still free." }
}Etiquetas y metadatos
Dos campos opcionales adjuntan tu propio contexto a un mensaje; ambos se devuelven en las lecturas de API y viajan en cada evento de webhook del mensaje:
- tags: hasta 20 etiquetas { "name": ..., "value": ... } estructuradas para dimensiones de baja cardinalidad por las que filtras e informas (una campaña, una variante de experimento). Los nombres y valores aceptan letras ASCII, dígitos, guion bajo y guion; los nombres tienen un máximo de 32 caracteres y son únicos dentro de un envío, los valores de 64. Filtra la lista de mensajes por etiqueta (?tag=campaign o ?tag=campaign:launch-week), y la página de Métricas desglosa la entrega por etiqueta.
- metadata: un objeto JSON arbitrario, hasta 2 KB serializado, para contexto por envío que no necesitas como dimensión de filtro (un ID de pedido interno, una referencia de sesión).
Ejemplo de código
{
"tags": [{ "name": "campaign", "value": "order-confirmations" }],
"metadata": { "order_id": "ord_8271" }
}Referencia de campos
| Campo | Tipo | Obligatorio | Límites / notas |
|---|---|---|---|
| to | string | sí | Un destinatario por mensaje: un número de teléfono E.164, un ID de usuario en el ámbito del negocio, que ninguna plantilla de código de verificación de un solo uso acepta, o un ID de grupo WhatsApp (wag_…), que envía a cada participante de ese grupo |
| from | string (E.164) | no** | Omítelo para una plantilla gestionada por Bird, que elige su propio remitente, y para un envío grupal, que usa el número propio del grupo; obligatorio para un mensaje de servicio y para una plantilla creada por tu espacio de trabajo, y debe ser un número que tu espacio de trabajo posea |
| template.slug | string | no** | Un slug de plantilla que tu espacio de trabajo puede enviar; los slugs gestionados por Bird empiezan con bird_ |
| template.language | string | no* | Etiqueta de idioma de la plantilla (en, pt-BR); omítelo para enviar el idioma predeterminado de la plantilla |
| template.components | array | no | Llena las variables de la plantilla; el type del componente es body o button |
| template.components[].parameters[].name | string | no† | El marcador de posición que este valor llena, por ejemplo ref; obligatorio y debe coincidir con los nombres declarados en la plantilla para una plantilla con parámetros con nombre, omitido para una posicional |
| interactive | object | no** | Texto del cuerpo más un tipo de contenido que se puede tocar; es un mensaje de servicio, así que necesita una ventana de servicio abierta. Consulta Mensajes interactivos |
| in_reply_to_message_id | string | no | Un ID de mensaje WhatsApp que tu espacio de trabajo posee, citado en el mensaje que envías; se repite en lecturas. Consulta Citar un mensaje |
| tags | array | no | Hasta 20 etiquetas {name, value}; nombre ≤ 32 caracteres, valor ≤ 64, nombres únicos |
| metadata | object | no | JSON arbitrario, hasta 2 KB serializado |
* language es opcional; omitirlo envía el idioma predeterminado de la plantilla.
† name es obligatorio en cada parámetro para una plantilla con parámetros con nombre. Omítelo para una posicional. Consulta Componentes y parámetros.
** Incluye exactamente uno de template o un campo de contenido de mensaje de servicio (text, image, video, audio, sticker, document, location, interactive); consulta Mensajes de servicio.
El modelo asíncrono: qué significa 202
Un envío exitoso devuelve 202 Accepted con un ID de mensaje y status: accepted. El 202 se devuelve solo después de que el envío se acepta de forma duradera; nunca se acepta y luego se descarta en silencio. Los fallos graves que puedes corregir fallan de inmediato con un 422: un destinatario no válido, un slug o idioma de plantilla desconocido, una discrepancia de parámetros o un mensaje de servicio enviado a una ventana de servicio al cliente cerrada (WhatsAppServiceWindowClosed). Un monedero sin fondos no es uno de ellos: el envío se acepta y el mensaje termina en rejected con insufficient_balance cuando Bird intenta cobrarlo. La entrega real ocurre de forma asíncrona: el mensaje pasa a sent cuando lo entregamos a WhatsApp, y luego a un estado terminal (delivered o failed) cuando llega el acuse de recibo, notificado a través de eventos, webhooks y los endpoints de lectura. Un acuse de lectura se expone por separado como una marca de tiempo read_at y un evento whatsapp.read, no como un estado.
Una nota de privacidad: para las plantillas de categoría authentication el API nunca devuelve los valores completados. El eco del 202 y cada lectura posterior llevan un array components vacío para esos mensajes, de modo que un código de verificación nunca reaparece.
Reintentar de forma segura
Envía el encabezado Idempotency-Key con un valor único por envío lógico, y los reintentos se vuelven seguros. Si tu primera solicitud tuvo éxito pero nunca viste la respuesta (timeout, conexión caída), reenviarla con la misma clave devuelve el resultado original en lugar de enviar, y cobrar, un mensaje duplicado. La respuesta reenviada lleva un encabezado Idempotency-Replay. Consulta idempotencia para el formato de clave y la retención.
Recibir la respuesta
Los mensajes entrantes se encuentran en el mismo recurso que los salientes, y cada uno de ellos restablece la ventana de servicio. Recibir mensajes WhatsApp cubre cómo leerlos a través del API, descargar los archivos multimedia que un contacto envió y el webhook whatsapp.received.
Costo y facturación
WhatsApp se cobra por mensaje, según la categoría de la plantilla y el país del destinatario; consulta Precios de WhatsApp. Un mensaje se cobra en dos pasos, en dos momentos distintos, y el objeto cost del mensaje informa ambos:
| Campo | Qué es | Cuándo aparece |
|---|---|---|
| transaction_amount | Tarifa de Bird por gestionar el envío | Cuando Bird procesa el envío aceptado, antes del despacho |
| passthrough_amount | Parte de Meta del precio del mensaje, que Bird traslada | Cuando llega un acuse delivered o read aplicable |
| amount | La suma de los componentes cobrados hasta el momento | Crece a medida que llega cada componente |
| currency_code | La moneda del monedero de tu organización, compartida por ambos componentes | Con el primer componente |
Ambos importes son cadenas decimales, netos de impuestos.
Note: a service message carries no Meta share until September 30th 2026, and neither does a utility template delivered inside an open customer service window. From October 1st 2026 Meta charges for both, except inside a 72-hour free entry point window, and with 1,000 free service messages per business phone number per month: October 2026 pricing changes.
Los dos componentes se calculan con entradas distintas. La tarifa de Bird usa la categoría de la plantilla que enviaste y el país del destinatario, que proviene del código de país del número de teléfono o, en un envío dirigido a un ID de usuario en el ámbito del negocio, del prefijo de dos letras de ese ID. La parte de Meta usa la categoría que Meta informa en el acuse aplicable, que puede diferir de la de la plantilla: Meta puede reportar authentication-international cuando aplican sus reglas de destino, ubicación del negocio y elegibilidad. Consulta Tarifas authentication-international de WhatsApp.
Lo que cost muestra depende de cuánto ha avanzado el mensaje:
- En el 202, cost es null. Nada se ha cobrado.
- Después del procesamiento, transaction_amount está establecido y amount es igual a él. passthrough_amount permanece null.
- Después de un acuse delivered o read aplicable, un cargo de Meta registrado correctamente completa passthrough_amount, y amount refleja los componentes registrados.
Un componente null significa que no se ha registrado ningún importe en esa proyección; no es prueba de que el mensaje fue gratuito. Un componente explícitamente cobrado en cero muestra "0.00000".
Los dos cargos también fallan de manera distinta. La tarifa de Bird falla de forma cerrada: cuando no puede completarse después del 202 porque el monedero no cubre el envío o la ruta no tiene un precio configurado, el mensaje termina rejected con el código de error insufficient_balance o price_not_found, y no se cobra nada. Un mensaje rejected nunca llegó a WhatsApp, que es lo que lo diferencia de failed. La parte de Meta falla de forma abierta: si el monedero no tiene fondos suficientes o falta la tarifa cuando llega el acuse, el cargo se omite sin revertir el estado observado del mensaje. Tu entrega nunca se retiene por el segundo cargo.
Un mensaje cobrado por Bird conserva ese cargo saliente si la entrega falla después. La tarifa de Meta se procesa a partir de un callback delivered o read aplicable cuando Meta reporta precios regulares con una categoría y destino resolvibles. Ambas rutas de callback usan la misma identidad de tarifa y dependen de la deduplicación del servicio de facturación. Reconcilia los acuses reenviados contra los registros de facturación en lugar de tratar la proyección del mensaje como un recibo de débito permanente. Los precios de servicio o de entrada gratuita pueden hacer que el componente de Meta sea cero; un componente no resuelto no es evidencia de que el mensaje fue gratuito.
Usa el libro mayor de facturación para la reconciliación financiera. Los campos cost de los mensajes son proyecciones de los cargos y pueden retrasarse o quedar incompletos. Consulta Métricas de WhatsApp para la distinción entre observaciones de mensajes y registros de facturación.
Los eventos WhatsApp no incluyen costo. Para leer cualquiera de los componentes, vuelve a leer el mensaje con GET /v1/whatsapp/messages/{id}.
Próximos pasos
- Mensajes de servicio: envía texto, multimedia y contenido interactivo dentro de la ventana de servicio al cliente
- Plantillas: elige una plantilla y proporciona sus variables
- Recibir mensajes de WhatsApp: lee respuestas y obtén multimedia entrante
- Webhooks de estado de mensajes: recibe actualizaciones de entrega y fallos
- Idempotencia: reintenta de forma segura con el encabezado Idempotency-Key
Recursos relacionados
Continúa con la documentación, guías y ejemplos de este tema.