Sending

One call out. One kind of content.

Set up in:
Cursor

A WhatsApp send is one POST carrying a recipient and exactly one kind of content: a template, or free-form text, image, video, audio, sticker, document, or location. The response is a 202 with a message ID, and delivery reports itself on webhooks you already handle.

send-notification.ts
202 · 480ms
import { BirdClient } from "@messagebird/sdk";

const bird = new BirdClient({
  apiKey: process.env.BIRD_API_KEY!,
});

const msg = await bird.whatsapp.send({
  to: "+15551234567",
  template: {
    slug: "bird_delivery_update",
    components: [{ type: "body", parameters: [
      { type: "text", name: "ref",  text: "#4821" },
      { type: "text", name: "date", text: "Wednesday" },
    ] }],
  },
});

console.log(msg.id, msg.status);
// → "wam_01krdgeqcxet5s7t44vh8rt9mg", "accepted"
Reminder: you have an appointment on 3 Sep at 14:30. We look forward to seeing you.9:42 AM
Reschedule
Your order #4821 is out for delivery, arriving Wednesday. Thanks for shopping with us.9:43 AM
Your subscription renews on 3 Sep for €12.00. No action is needed.9:44 AM
View plan

The same send in every runtime.

Sending is the core verb of the Bird WhatsApp API. The SDKs cover Node.js, Python, Go, and PHP, and the CLI and raw HTTP take the same body. A first send can name a Bird-managed template, which picks its own sender, so nothing has to be provisioned before you watch a real message land.

1
2
3
4
5
6
7
8
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);

Exactly one content kind, never two.

A request carrying no content is refused with a 422, and so is one carrying two. A template is the only content WhatsApp delivers outside an open customer service window, which is why it is what starts a conversation. Free-form content is a reply, and it always names the number it comes from.

send.ts
202 · accepted
// A template: the only content deliverable outside an open window.
await bird.whatsapp.send({
  to:       "+15551234567",
  template: { slug: "bird_delivery_update", components },
  tags:     [{ name: "campaign", value: "order-updates" }],
  metadata: { order_id: "BRD-49217" },
});

// Free-form text: deliverable only inside one, and `from` is required.
await bird.whatsapp.send({
  to:   "+15551234567",
  from: "+13124495648",
  text: { body: "Your order shipped: https://example.com/track/A1B2C3", preview_url: true },
});

// An image, on its own send field. One content kind per request, never two.
await bird.whatsapp.send({
  to:    "+15551234567",
  from:  "+13124495648",
  image: { url: "https://example.com/receipt.png", caption: "Your receipt" },
});

What every send carries.

The parts of the request that are not the message: who it is for, who it is from, how a retry behaves, and what rides back to you on every event.

  1. 01

    A recipient, by number or by ID

    An E.164 phone number, or the contact's Meta business-scoped user ID when you do not hold their number. One-time-passcode templates need a phone number.

  2. 02

    A sender, unless the template picks one

    Omit from for a Bird-managed template: its category selects the number. Everything else requires a from your workspace owns, on the same business account as the template.

  3. 03

    An idempotency key, if you want one

    A retry carrying the same Idempotency-Key replays the original response rather than sending twice. Without one, a retry is a new message and a duplicate.

  4. 04

    Up to 20 tags

    Structured name-value labels that become query dimensions: filter the message list by tag, and group metrics by one. Keep them low-cardinality.

  5. 05

    Up to 2 KB of metadata

    Arbitrary JSON stored on the message and returned on API reads and events. This is where an order ID or a foreign key belongs.

  6. 06

    A 202, not a delivery

    The response is the accepted message, echoing the content it resolved. Delivery happens afterwards, and reports itself on the message and its events.

A retry that does not double-send.

A timeout tells you nothing about whether the message went out. Send the retry with the same idempotency key and the API replays the original response instead of processing the request again, and marks the replay on the way back so your logs can tell the two apart.

retry.sh
Idempotency-Key
curl -X POST https://api.bird.com/v1/whatsapp/messages \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Idempotency-Key: order-shipped-BRD-49217" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15551234567",
    "template": { "slug": "bird_delivery_update" }
  }'

# A retry carrying the same key replays the original 202 and sets
# Idempotency-Replay: true. Without one, the recipient gets a duplicate.

Six events, and the two that mean failure.

Rejected means Bird refused the message before submitting it, so it is not charged. Failed means it was submitted and WhatsApp refused delivery. Both carry an error code, a description, and Meta's own code when there is one.

POST /webhooks/bird
signed
{
  "type": "whatsapp.failed",
  "timestamp": "2026-05-19T15:42:08.114Z",
  "data": {
    "whatsapp_id":  "wam_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "direction":    "outbound",
    "from":         { "phone_number": "+15557654321" },
    "to":           { "phone_number": "+15551234567" },
    "tags":         [{ "name": "campaign", "value": "order-updates" }],
    "metadata":     { "order_id": "BRD-49217" },
    "error": {
      "code":        "service_window_expired",
      "description": "The 24-hour service window closed; send a template.",
      "occurred_at": "2026-05-19T15:42:08.031Z"
    }
  }
}

The delivered event is also when Meta's share of the price is charged, though no event payload carries a cost. Read the message back to see what it cost.

  • whatsapp.acceptedAccepted by the API and queued for send. This is what the 202 said.
  • whatsapp.sentHanded to the WhatsApp network.
  • whatsapp.deliveredWhatsApp confirmed the message reached the recipient's device.
  • whatsapp.readThe recipient opened it. The status stays delivered; the read is recorded separately.
  • whatsapp.rejectedRefused before submission, and not charged: reason code in the payload.
  • whatsapp.failedSubmitted, then refused by WhatsApp: reason code in the payload.

One recipient per call.

There is no batch endpoint on WhatsApp: fan out in your own loop and pace against the rate-limit headers on each response rather than a constant you picked. Sends are metered per message, and pricing moves with the destination country and the template's category.

Go deeper in the docs.

Read sending WhatsApp for the full request contract, WhatsApp events for the lifecycle and its payloads, and webhooks for endpoints, signatures, and retries.

Sending questions, answered.

The request, retries, tags and metadata, and delivery status.

¿Cómo envío un mensaje de WhatsApp?
Haz un POST a /v1/whatsapp/messages con el número de teléfono E.164 del destinatario, un slug de plantilla y los valores para las variables de la plantilla. Bird valida la solicitud, devuelve un 202 con un ID de mensaje y lo entrega de forma asíncrona.
¿Qué ocurre si reintento un envío tras un timeout?
Incluye un encabezado Idempotency-Key y una solicitud reintentada devolverá el resultado original en lugar de enviar dos veces. Sin él, un reintento se trata como un mensaje nuevo y el destinatario recibe un duplicado.
¿Puedo adjuntar etiquetas o metadatos a un mensaje?
Sí. Las etiquetas son hasta 20 labels estructurados por los que puedes filtrar y agrupar en el registro de mensajes y las métricas. Los metadatos son JSON arbitrario (hasta 2 KB) que se devuelven en el mensaje y sus eventos, útiles para correlacionar envíos con tus propios sistemas.
¿Cómo sé si un mensaje fue entregado?
Cada cambio de estado dispara un evento de webhook: accepted, sent, delivered, read, failed o rejected. También puedes consultar la línea de tiempo de eventos del mensaje a través de la API. Un estado delivered significa que WhatsApp confirmó que el dispositivo del destinatario lo recibió.

Your first WhatsApp send, today.

Sending is one capability of the Bird WhatsApp API: templates, inbound, numbers, and analytics ship with it, on infrastructure we have run for a decade.

Empieza con un canal.
Añade los demás cuando estés listo.

Una clave API de prueba es tuya de inmediato. El acceso a producción se desbloquea cuando añades un método de pago y verificas un remitente.

¿Usas Claude Code, Cursor o Codex? Copia un prompt de configuración y tu agente instalará el Bird CLI y las habilidades por ti. Elige el tuyo:

Cursor