Documentation
Sign inGet started

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; to send a pre-registered template 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 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.
Codebeispiel
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:
Codebeispiel
{
  "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.

Building up the payload

Recipient

to is a single recipient in 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.

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, 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; 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 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, 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.
Codebeispiel
{
  "tags": [{ "name": "campaign", "value": "spring-2026" }],
  "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" }
}

Field reference

FieldTypeRequiredLimits / notes
tostring (E.164)yesOne recipient per message
fromstringyes*Owned E.164 number, alphanumeric sender ID (1–11 chars), or short code (5–6 digits)
textstringyes*At least 1 character; capped at 12 segments
categorystringyes*transactional, marketing, authentication, or service
tags{name, value}[]noMax 20; name 1–32 chars, value 1–64 chars; [A-Za-z0-9_-] only
metadataobjectnoArbitrary JSON, max 2 KB serialized
* Required on a free-text send. A template send 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.

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.

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, 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:
Codebeispiel
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, 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 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 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 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 page.

Next steps

  • SMS templates: send a pre-registered template and let Bird pick the sender
  • SMS log: find a message, follow its lifecycle, and read its segments and cost
  • Events: the webhook events that report delivery to your systems
  • SMS metrics: delivery rate, failure rate, segments, and spend
  • Idempotency: safe retries with the Idempotency-Key header