# 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](/docs/guides/whatsapp/message-types/interactive/reply-buttons) or [list menus](/docs/guides/whatsapp/message-types/interactive/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`:

<!-- bird:tabs typescript,python,go,php,cli,curl -->

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

```python
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)
```

```go
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)
}
```

```php
$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();
```

```cli
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
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"
      }
    }
  }'
```

<!-- /bird:tabs -->

`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:

```json
{
  "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](/docs/guides/whatsapp/message-types/interactive#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](/docs/guides/whatsapp/message-types/interactive#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:

```text
"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](/docs/guides/whatsapp/templates) 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](/docs/guides/whatsapp/message-types#the-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](#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](/docs/guides/whatsapp/message-types/interactive#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](/docs/api/errors/E15057) and [E15058](/docs/api/errors/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](/docs/guides/whatsapp/message-types/interactive#errors) and [How sending works](/docs/guides/whatsapp/sending-whatsapp).

## Next steps

- [WhatsApp interactive messages](/docs/guides/whatsapp/message-types/interactive): what all six interactive types share
- [WhatsApp templates](/docs/guides/whatsapp/templates): for a `url` button variable that WhatsApp personalizes per recipient
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries