Sign inGet Started

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);

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 es un único destinatario, dado como número de teléfono o como ID de usuario con alcance de negocio. Un número de teléfono está en formato E.164: un + inicial, código de país y número de abonado, por ejemplo +14155550100. Validamos el número, 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 que se cobre nada. Un mensaje va a un destinatario; no hay array de destinatarios ni envío por lotes, así que para alcanzar a muchas personas haz 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.

Plantilla

template nombra la plantilla preaprobada que se envía:
  • 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 refleja el idioma resuelto.
  • components: los valores que completan las variables de la plantilla (consulta Componentes y parámetros). Omítelo para una plantilla que no tiene 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 nombra un type (body o button) y un array parameters. Cada parámetro nombra su propio type (text, image, video, gif, document o location) y lleva el campo correspondiente: text una cadena de texto plano, 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 que la plantilla declara (consulta Referencia de campos). Una plantilla posicional omite name y toma sus valores en orden {{n}}, así que el primer parámetro completa {{1}}. En ambos casos, parámetros que no coincidan con lo que la plantilla declara devuelven un 422 WhatsAppTemplateParameterMismatch. También existe un tipo de componente header en el protocolo: 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 de medios 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, posicionalmente (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 propietario del remitente determina si lo nombras:
  • Una plantilla gestionada por Bird (su slug comienza con bird_) se envía desde el número que Bird mantiene para esa categoría, así que omite from. Establecerlo devuelve un 422 WhatsAppSenderNotAllowed.
  • Cualquier otro caso nombra 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 ser uno que tu espacio de trabajo posea. Omitirlo devuelve un 422 WhatsAppSenderRequired, y un número desde el que el espacio de trabajo no puede enviar devuelve un 422 WhatsAppSenderNotFound. Una plantilla creada por ti también debe estar en la misma cuenta WhatsApp Business Account que el número, o el envío devuelve un 422 WhatsAppSenderWABAMismatch.
Configuración de 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 por Bird no pueden utilizarse.
  • 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 obtiene en el momento del envío (url), así que una URL firmada debe seguir vigente después del envío. Una URL http se rechaza directamente. WhatsApp obtiene el archivo en sí, así que una URL que no puede alcanzar, una que sirve un tipo no soportado 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 otorga a esa tarjeta un botón que abre un chat con él.
  • interactive: texto del cuerpo más algo para pulsar, en uno de seis tipos: botones de respuesta, un menú de lista, un botón de enlace, un carrusel de medios o un botón individual que le pide al destinatario su ubicación o su número de teléfono. Mensajes interactivos cubre la estructura de cada tipo, las respuestas que produce 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, de la misma forma que pulsar responder en la app WhatsApp cita un mensaje. El destinatario ve tu mensaje con el citado encima, y el campo se devuelve 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 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 se puede renderizar hace fallar la solicitud en sí y no se crea ni se cobra nada. Un id que no nombra ningún mensaje que este espacio de trabajo posea, o uno más antiguo de los 15 días que un mensaje permanece citable, devuelve un 404 E15071 WhatsAppReferencedMessageNotFound. Uno que nombra un mensaje que nunca llegó a WhatsApp, o un mensaje de una conversación diferente a la de to y from de este envío, devuelve un 422 E15072 WhatsAppMessageNotQuotable. Si Bird no puede alcanzar el almacén que responde 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 límite 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

CampoTipoObligatorioLímites / notas
tostringUn destinatario por mensaje: un número de teléfono E.164 o un ID de usuario con alcance de negocio, que ninguna plantilla de código de verificación de un solo uso acepta
fromstring (E.164)no**Omite para una plantilla gestionada por Bird, que elige su propio remitente; 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.slugstringno**Un slug de plantilla que tu espacio de trabajo puede enviar; los slugs gestionados por Bird comienzan con bird_
template.languagestringno*Etiqueta de idioma de la plantilla (en, pt-BR); omite para enviar el idioma predeterminado de la plantilla
template.componentsarraynoCompleta las variables de la plantilla; el type del componente es body o button
template.components[].parameters[].namestringno†El marcador de posición que este valor completa, por ejemplo ref; obligatorio y debe coincidir con los nombres declarados de la plantilla para una plantilla con parámetros con nombre, se omite para una posicional
interactiveobjectno**Texto del cuerpo más un tipo de contenido que se puede pulsar; es un mensaje de servicio, así que necesita una ventana de servicio abierta. Consulta Mensajes interactivos
in_reply_to_message_idstringnoUn ID de mensaje WhatsApp que este espacio de trabajo posee, citado en el mensaje que envías; se refleja en las lecturas. Consulta Citar un mensaje
tagsarraynoHasta 20 etiquetas {name, value}; nombre ≤ 32 caracteres, valor ≤ 64, nombres únicos
metadataobjectnoJSON 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 silenciosamente. Las fallas definitivas que puedes corregir fallan inmediatamente con un 422: un destinatario inválido, un slug de plantilla o idioma desconocido, un desajuste de parámetros o un mensaje de servicio enviado a una ventana de servicio al cliente cerrada (WhatsAppServiceWindowClosed). Un monedero sin fondos no es una de ellas: el envío se acepta y el mensaje termina 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, luego a un estado terminal (delivered o failed) cuando llega el acuse de recibo, reportado a través de eventos, webhooks y los endpoints de lectura. Un acuse de lectura se muestra por separado como una marca de tiempo read_at y un evento whatsapp.read en lugar de como un estado.
Una nota de privacidad: para las plantillas de categoría authentication la API nunca devuelve los valores completados. El eco de 202 y cada lectura posterior llevan un array components vacío para esos mensajes, así 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 (tiempo de espera, conexión interrumpida), repetirla con la misma clave devuelve el resultado original en lugar de enviar y cobrar un mensaje duplicado. La respuesta repetida lleva un encabezado Idempotency-Replay. Consulta idempotencia para el formato de clave y la retención.

Recibir la respuesta

Los mensajes entrantes aterrizan en el mismo recurso que los salientes, y cada uno de ellos reinicia la ventana de servicio. Recibir mensajes de WhatsApp cubre cómo leerlos a través de la API, obtener los medios que envió un contacto y el webhook whatsapp.received.

Costo y facturación

WhatsApp se cobra por mensaje, en función de 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 reporta ambos:
CampoQué esCuándo llega
transaction_amountLa tarifa de Bird por gestionar el envíoCuando Bird procesa el envío aceptado, antes del despacho
passthrough_amountLa parte de Meta del precio del mensaje, que Bird transfiereCuando llega un acuse de recibo delivered o read aplicable
amountLa suma de los componentes cuyo precio se ha calculado hasta el momentoCrece a medida que llega cada componente
currency_codeLa moneda del monedero de tu organización, compartida por ambos componentesCon el primer componente
Ambos montos son cadenas decimales, netos de impuestos.
Los dos componentes se calculan con entradas diferentes. 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 con alcance de negocio, del prefijo de dos letras de ese ID. La parte de Meta usa la categoría que Meta mismo reporta en el acuse de recibo aplicable, que puede diferir de la de la plantilla: Meta puede reportar authentication-international cuando aplican sus reglas de destino, ubicación de 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. No se ha calculado ningún importe.
  • Después del procesamiento, transaction_amount tiene valor y amount es igual a él. passthrough_amount permanece null.
  • Después de un acuse de recibo 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 monto en esa proyección; no es prueba de que el mensaje fuera gratuito. Un componente con un precio explícito de cero muestra "0.00000".
Los dos cargos también fallan de forma diferente. Si falla el cobro de la tarifa de Bird, el envío se bloquea: cuando no puede completarse después del 202 porque el monedero no puede cubrir 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 distingue de failed. Un fallo en el cobro de la parte de Meta no bloquea la entrega: si el monedero tiene fondos insuficientes o la tarifa no existe cuando llega el acuse de recibo, el cargo se omite sin revertir el estado observado del mensaje. Tu entrega nunca se detiene 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 resolubles. Ambas rutas de callback usan la misma identidad de tarifa y dependen de la deduplicación del servicio de facturación. Concilia los acuses de recibo repetidos 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 sin resolver no es evidencia de que el mensaje fuera gratuito.
Usa el libro mayor de facturación para la conciliación financiera. Los campos cost del mensaje 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}.

Siguientes pasos