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.
Send a link button
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);msg = client.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"},
},
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+16505551234",
From: "+13124495648",
Interactive: &bird.WhatsAppInteractiveSend{
Type: "cta_url",
BodyText: "Tap the button below to see the available dates.",
CtaUrl: &bird.WhatsAppInteractiveCtaUrlSend{Text: "See dates", Url: "https://example.com/workshops?click_id=a1b2c3"},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$interactive = (new WhatsAppMessageSendRequestInteractive())
->setType('cta_url')
->setBodyText('Tap the button below to see the available dates.')
->setCtaUrl(
(new WhatsAppInteractiveSendCtaUrl())
->setText('See dates')
->setUrl('https://example.com/workshops?click_id=a1b2c3'),
);
$message = $bird->whatsapp->send(
to: '+16505551234',
from: '+13124495648',
interactive: $interactive,
);
echo $message->getId(), ' ', $message->getStatus();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"}}'curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"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"
}
}
}'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:
Ejemplo de código
{
"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:
Ejemplo de código
"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
| Field | Bound |
|---|---|
| cta_url buttons | exactly one |
| cta_url.text (label) | required, 1 to 20 characters |
| cta_url.url | required, 1 to 2000 characters |
| body_text | required, 1 to 1024 characters |
| footer_text | optional, 1 to 60 characters |
| header.text | 1 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
- WhatsApp interactive messages: what all six interactive types share
- WhatsApp templates: for a url button variable that WhatsApp personalizes per recipient
- How sending works: the request envelope, the 202 model, and safe retries