Documentation
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.
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:
Przykład kodu
{
  "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:
Przykład kodu
"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. What you do see is the ordinary outbound lifecycle, the message's sent, delivered, and read statuses, 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. E15057 and E15058 can fire on any type that carries in_reply_to_message_id, when the quoted message doesn't resolve or can't 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 How sending works.

Next steps