# Sending SMS

This guide covers the single-send endpoint, `POST /v1/sms/messages`. You build one JSON payload with a recipient, a sender, a body, and a category; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Each request sends one message to one recipient. To send many at once, use [batch sending](#batch-sending); to send a pre-registered [template](/docs/guides/sms/templates) instead of your own text, supply a `template` object in place of `text`, `category`, and `from`.

## Before you send: enable the destination country

Every workspace has a destination allowlist. It is default-deny and typically starts with only your organization's home country enabled: a send to any other country is rejected with a `422` (`SMSDestinationNotEnabled`) before Bird resolves a sender or applies any compliance checks. Enable the countries you send to under [**SMS → Destinations**](https://bird.com/dashboard/w/sms/destinations) in the dashboard. The allowlist is deliberate friction: it keeps a leaked key or a bad loop from spraying messages to high-cost destinations you never intended to serve.

## A minimal send

The smallest valid free-text payload is a `to` recipient, a `from` sender, a `text` body, and a `category`.

```bash
curl -X POST https://eu1.platform.bird.com/v1/sms/messages \
  -H "Authorization: Bearer bk_eu1_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+31612345678",
    "from": "Bird",
    "text": "Your Bird verification code is 481920. It expires in 10 minutes.",
    "category": "authentication"
  }'
```

Use your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`) with a matching `bk_{region}_...` key. The response is the accepted message:

```json
{
  "id": "sms_01ky7qmwgpfkybj9ecrnwjx714",
  "direction": "outbound",
  "status": "accepted",
  "to": "+31612345678",
  "from": "Bird",
  "text": "Your Bird verification code is 481920. It expires in 10 minutes.",
  "category": "authentication",
  "segments": { "count": 1, "encoding": "GSM_7BIT", "characters": 64 },
  "cost": null,
  "carrier": null,
  "mcc_mnc": null,
  "sent_at": null,
  "delivered_at": null,
  "created_at": "2026-07-23T14:56:34.326Z"
}
```

`status: accepted` means Bird has the message and is working on it; `cost` is `null` because pricing happens during processing. What happens next is covered in [the async model](#the-async-model-what-202-means).

## Building up the payload

### Recipient

`to` is a single recipient in [E.164](https://en.wikipedia.org/wiki/E.164) format: a leading `+`, country code, and subscriber number, e.g. `+31612345678`. One message goes to one recipient, with no `cc`/`bcc` and no recipient array. To reach many people, send a [batch](#batch-sending).

### Sender

`from` is required on a free-text send and is the sender the recipient sees. It takes one of three shapes, and which ones work depends on the destination country:

- **An alphanumeric sender ID**: 1 to 11 letters, digits, or spaces (e.g. `Bird`). Accepted where the destination country permits one; some countries require a registration first, and some (including the US) don't support alphanumeric senders at all. Alphanumeric senders are one-way; recipients can't reply to them.
- **A phone number** in E.164. A numeric sender must be a number your workspace owns; an arbitrary number is rejected.
- **A short code**: a 5 or 6 digit number.

A sender that isn't valid for the destination is rejected with a `422` naming the reason (for example `SMSAlphaNotSupported` where alphanumeric senders aren't available). On a [template send](/docs/guides/sms/templates), `from` is not accepted: Bird selects a sender for the destination and category.

### Body and category

`text` is the message body, at least one character. It is billed and delivered in [segments](#segments-and-encoding); a send is capped at **12 segments** (about 1,836 GSM-7 characters, or 804 if the body uses the extended UCS-2 encoding). A body over the cap is rejected with a `422` rather than truncated.

`category` is required on a free-text send and classifies the message: `transactional`, `marketing`, `authentication`, or `service`. The category tells Bird and carriers why you're sending, and it is how per-country compliance rules (opt-out policy, quiet hours) are applied as they roll out, so a one-time passcode (`authentication`) and a promotion (`marketing`) classify differently. Choose the category that matches the message's purpose.

### Tags and metadata

Both attach your own data to a send, but they serve different jobs:

- **`tags`** are structured `{name, value}` pairs (max 20 per send; name 1 to 32 characters, value 1 to 64, ASCII `[A-Za-z0-9_-]` only, case-sensitive, names unique within a send). They are first-class filter dimensions: filter the [message list](/docs/guides/sms/sms-log) by tag. Use them for low-cardinality labels like `campaign` or `experiment_variant`.
- **`metadata`** is an arbitrary JSON object (max 2 KB serialized). It is stored, returned on API reads, and echoed on every [webhook event](/docs/guides/sms/events), but it is not a filter dimension. Use it for round-trip context: internal IDs, foreign keys, anything you want handed back with each event.

```json
{
  "tags": [{ "name": "campaign", "value": "spring-2026" }],
  "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" }
}
```

### Field reference

| Field      | Type              | Required | Limits / notes                                                                      |
| :--------- | :---------------- | :------- | :---------------------------------------------------------------------------------- |
| `to`       | string (E.164)    | yes      | One recipient per message                                                           |
| `from`     | string            | yes\*    | Owned E.164 number, alphanumeric sender ID (1–11 chars), or short code (5–6 digits) |
| `text`     | string            | yes\*    | At least 1 character; capped at 12 [segments](#segments-and-encoding)               |
| `category` | string            | yes\*    | `transactional`, `marketing`, `authentication`, or `service`                        |
| `tags`     | `{name, value}[]` | no       | Max 20; name 1–32 chars, value 1–64 chars; `[A-Za-z0-9_-]` only                     |
| `metadata` | object            | no       | Arbitrary JSON, max 2 KB serialized                                                 |

\* Required on a free-text send. A [template send](/docs/guides/sms/templates) supplies the body, category, and sender from the template instead, and rejects these three fields.

## Sending with a template

Instead of composing `text`, set the send's `template` object to reference one of Bird's built-in templates. The template supplies the body, the category, and the sender, so `text`, `category`, `from`, and `media_urls` are not accepted alongside it. The catalogue, each template's variables, and the full template-send contract live in [SMS templates](/docs/guides/sms/templates#sending-with-a-template).

## Segments and encoding

SMS carries a fixed number of characters per segment, and the limit depends on the encoding Bird picks for your body:

- **GSM-7**: the default 7-bit alphabet (Latin letters, digits, common punctuation). A single-segment message fits **160** characters; longer messages are split at **153** characters per segment (the split reserves a few characters for reassembly headers).
- **UCS-2**: used automatically when the body contains any character outside GSM-7, such as an emoji, a CJK character, or some accented letters. The limits drop to **70** characters single-segment and **67** per segment when split.

One out-of-range character switches the whole message to UCS-2 and more than halves its capacity, so a stray smart quote or emoji can turn a one-segment message into two. Every message response reports the resolved `segments`: the billable `count`, the `encoding` (`GSM_7BIT` or `UCS2`), and the character count. Segments are the unit you're billed on; see [cost](#cost-and-billing).

## Batch sending

`POST /v1/sms/batches` sends up to 100 independent messages in one request, the bulk-SMS pattern of one call for many recipients. Batch requests draw from the `sms_batch` [rate-limit group](/docs/guides/rate-limits#groups), separate from the `sms_send` group used by single sends, so batching also raises how many messages you can hand over per minute. The body is a JSON array of the message objects from [Building up the payload](#building-up-the-payload):

```bash
curl -X POST https://eu1.platform.bird.com/v1/sms/batches \
  -H "Authorization: Bearer bk_eu1_..." \
  -H "Content-Type: application/json" \
  -d '[
    { "to": "+31612345678", "from": "Bird", "text": "Order shipped.", "category": "transactional" },
    { "to": "+31687654321", "from": "Bird", "text": "Order shipped.", "category": "transactional" }
  ]'
```

Validation is all-or-nothing: if any message in the batch is invalid, the whole request is rejected with a `422` and nothing is sent, so a batch never partially applies. On success the `202` response carries each accepted message in submission order under `data`, plus a `summary` with the `accepted_count`. Each message is independent from there: one recipient's failure never affects the others.

## The async model: what 202 means

A successful send returns `202 Accepted` with a message ID and `status: accepted`. The `202` is returned only after the send is durably accepted; it is never accepted and then silently dropped. Hard failures you can fix fail immediately with a `422`: an invalid field, a body over the segment cap, a destination country [you haven't enabled](#before-you-send-enable-the-destination-country), or a sender that isn't valid for the destination. A send from a workspace with no wallet balance fails with a `402`.

Delivery happens asynchronously: the message moves to `sent` when Bird hands it to the carrier, then to a terminal status (`delivered`, `undelivered`, `failed`, or `expired`) when the delivery receipt arrives, reported through [events and webhooks](/docs/guides/sms/events) and the read endpoints. Three consequences of the async design worth knowing:

- **Cost is priced after acceptance.** The `cost` on a message is `null` at accept time and is populated once Bird prices the send during processing. Read the message back (or wait for the delivery event) to see the final charge.
- **A message can be rejected after the 202.** If the charge fails during processing, the message ends `rejected` with an `sms.rejected` webhook and you are not billed; an exhausted wallet surfaces as `last_error.code: insufficient_balance`.
- **Reads can briefly trail the 202.** The message becomes visible on the read endpoints shortly after the `202`, so a `404` immediately after a send resolves itself within moments.

## Reserved fields

The following fields are part of the request vocabulary but are not yet available. Including any of them returns a `422` (`SMSUnsupportedFeature`); they are reserved now so SDKs and integrations converge on one name before launch:

`scheduled_at`, `validity_period`, `media_urls`, `messaging_profile_id`, `broadcast_id`, `campaign_id`, `audience_id`, `contact_id`, `topic_id`, `max_price_per_segment`, `personalization`, `track_clicks`

Don't build around these yet.

## Retrying safely

Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result instead of sending a duplicate message. See [idempotency](/docs/guides/idempotency) for key format and retention.

## Cost and billing

Outbound SMS is billed per segment. The final charge depends on the destination country and carrier; some routes add a surcharge (for example, US 10DLC carrier fees). Once priced, each message read carries the total `cost` as a decimal `amount` with its `currency_code`; the [message reference](/docs/api/reference/get-sms-message) documents the per-component breakdown (per-segment rate, billed segments, destination country, and carrier surcharge) returned on single-message reads. Aggregate spend and segment counts across a range are on the [Metrics](/docs/guides/sms/tracking-and-metrics) page.

## Next steps

- [SMS templates](/docs/guides/sms/templates): send a pre-registered template and let Bird pick the sender
- [SMS log](/docs/guides/sms/sms-log): find a message, follow its lifecycle, and read its segments and cost
- [Events](/docs/guides/sms/events): the webhook events that report delivery to your systems
- [SMS metrics](/docs/guides/sms/tracking-and-metrics): delivery rate, failure rate, segments, and spend
- [Idempotency](/docs/guides/idempotency): safe retries with the `Idempotency-Key` header