Sign inGet Started

WhatsApp link buttons

A link button puts one tappable button under a WhatsApp message that opens a URL in the recipient's browser. Use it when the next step lives on the web, such as a checkout page or a set of workshop dates, rather than in the chat itself. For a choice the recipient answers inside WhatsApp, use reply buttons or list menus instead.

Your Goldcrest order A1B2C3 is on its way. Follow its journey here.
Track order

Set interactive.type to cta_url, with a body_text and a cta_url object carrying the button's text and url:

const msg = await bird.whatsapp.send({
  to: "+16505551234",
  from: "+13124495648",
  interactive: {
    type: "cta_url",
    body_text: "Tap the button below to see the available dates.",
    cta_url: { text: "See dates", url: "https://example.com/workshops?click_id=a1b2c3" },
  },
});
console.log(msg.id, msg.status);

from is required on every service message: a number your workspace owns, not a Bird-managed one. The full shape adds an optional header, footer, and a quote of an earlier message:

Code example
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive": {
    "type": "cta_url",
    "header": {
      "type": "image",
      "url": "https://cdn.example.com/banners/workshop.png"
    },
    "body_text": "Tap the button below to see the available dates.",
    "footer_text": "Dates are subject to change.",
    "cta_url": {
      "text": "See dates",
      "url": "https://example.com/workshops?click_id=a1b2c3"
    }
  },
  "tags": [{ "name": "campaign", "value": "autumn-workshops" }],
  "metadata": { "order_id": "A-4192" }
}

in_reply_to_message_id quotes an earlier message in the same conversation. See the hub's quoting a message to correlate a reply for how resolution works and what it can miss.

This type sends exactly one cta_url button and cannot carry buttons, list, or cards alongside it. See the hub's buttons section for the shared button shape, which a carousel card's own link button also reuses.

Headers and footers

A header is optional, and it is one of four shapes:

Code example
"header": { "type": "text",     "text": "New workshop dates" }
"header": { "type": "image",    "url": "https://cdn.example.com/a.png" }
"header": { "type": "video",    "url": "https://cdn.example.com/a.mp4" }
"header": { "type": "document", "url": "https://cdn.example.com/a.pdf" }

A media header (image, video, or document) carries its file as a public https URL that WhatsApp fetches at send time, rather than an uploaded media handle. footer_text is optional and adds a line below the button.

Limits

FieldBound
cta_url buttonsexactly one
cta_url.text (label)required, 1 to 20 characters
cta_url.urlrequired, 1 to 2000 characters
body_textrequired, 1 to 1024 characters
footer_textoptional, 1 to 60 characters
header.text1 to 60 characters

The 2000-character cap on url is Bird's own: Meta publishes no length limit for this field. url also asserts format: uri, an absolute address with a scheme, but Bird does not check which scheme: an http:// address passes Bird's validation, and Meta is the only judge of whether it delivers.

What a click reports

A tap opens the address in the recipient's browser and nothing comes back to you over the API. A link button's tap is not an interactive_reply: the inbound mapper that produces interactive_reply handles only a reply-button tap and a list-row tap, and a cta_url link has no equivalent inbound shape. You can track outbound message status events (whatsapp.sent, whatsapp.delivered, and whatsapp.read), but read_at tells you the message was opened, not that the button was tapped. There is no click event, no timestamp, and no per-recipient tap signal from WhatsApp or from Bird.

Two ways to get attribution back, since the send itself will not give it to you:

  • Instrument the landing page. The only click evidence available is on your own destination server, from the URL you handed out.
  • Vary the URL yourself, per recipient. The url you send is a literal string: Bird stores it and passes it to Meta unchanged, with no substitution and no variable syntax. It is identical for every recipient of one send, so per-recipient attribution means generating your own query parameter, such as ?click_id=<value>, and issuing one POST /v1/whatsapp/messages call per recipient. The endpoint already takes a single to per call, so this is bookkeeping on your side rather than a missing API feature.

A third option exists outside this type entirely: a template with a url button variable is personalized per recipient by WhatsApp itself, supplied through the send's button component. That variable must sit at the end of the address, written as {{1}}, so it can vary a trailing path segment or query value but never the host or the middle of the URL. The trade-off: a template buys per-recipient URLs and delivery outside the customer service window, at the cost of Meta's review and a fixed approved shape, where a cta_url send buys free-form, review-free sending inside an open window with a URL you vary yourself.

Limits and edge cases

  • The customer service window has to be open. A link button is a service message, deliverable only inside an open window; see the hub's customer service window. The window check fails open, so a 202 is not proof the window was actually open when the send goes out.
  • from must be a number your workspace owns. Omitting it, or naming a number that isn't a connected sender, is rejected before the send is created.
  • The URL is static for the whole send, and identical for every recipient. There is no per-recipient variable on this type. See What a click reports for how to attribute clicks anyway.
  • No tap signal, ever. A link button's tap produces no inbound message and no webhook event. Do not build a feature that promises click metrics from this type alone.
  • Bird checks the URL's shape, not its scheme. url must be an absolute address with a scheme, but Bird does not require https, and Meta publishes no scheme restriction either. Contrast a media header's url, which is documented as requiring https.
  • A media header URL that WhatsApp can't fetch fails after the send is accepted. WhatsApp fetches the header asset at send time and caches it for 10 minutes; a signed URL has to outlive the send, and an unreachable URL fails asynchronously, with media_rejected on the message's last_error.

None of the shape checks the hub's errors table lists can fire on this type: they inspect a list's rows, a buttons array, or a carousel's cards, and a cta_url message has none of the three. A shape mistake, such as a text label over 20 characters, comes back as a generic request-validation error rather than one of those codes. A quote that doesn't resolve fails the request before anything is created or charged: 404 E15071 when the id names no message this workspace holds, 422 E15072 when it names one that cannot be quoted. For the errors any WhatsApp send can hit, a closed window, a missing or invalid sender, or an invalid recipient, see the hub's errors and Sending WhatsApp messages.

Next steps