Documentation
Sign inGet started

Sending SMS

This guide covers the single-send endpoint, POST /v1/sms/messages. Build a JSON payload with a recipient, sender, body, and category. Bird returns 202 Accepted with a message ID and delivers asynchronously. Each request sends one message to one recipient. To send many messages at once, use batch sending. To send a 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 default-deny destination allowlist that starts with only your organization's home country enabled. Bird rejects a send to any other country with 422 SMSDestinationNotEnabled before resolving a sender. Enable the countries you serve under SMS > Destinations in the dashboard.

A minimal send

The smallest valid free-text payload is a to recipient, a from sender, a text body, and a category.
const msg = await bird.sms.send({
  from: "+15557654321",
  to: "+14155550100",
  text: "Your verification code is 123456.",
  category: "authentication",
});
console.log(msg.id, msg.status);
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:
Exemple de code
{
  "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 one recipient in E.164 format: a leading +, country code, and subscriber number, such as +31612345678. One message goes to one recipient, with no cc, bcc, or 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, spaces, dashes, or underscores, with at least one letter and no separator at either end, such as Bird or Acme-Co. Digits alone represent a phone number, so Bird rejects 555555 and 555 555 as sender IDs. Some countries require registration, and others, including the US, do not support alphanumeric senders. Recipients cannot 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 as transactional, marketing, authentication, or service. It tells Bird and carriers why you are sending. A one-time passcode uses authentication; a promotion uses marketing. 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.
Exemple de code
{
  "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, one letter minimum), 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
optionsobjectnoPer-message processing settings. smart_encoding is the only one available; see segments and encoding
* 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 catalog, each template's variables, and the full template-send contract live in SMS templates.

Segments and encoding

SMS is billed per segment. A message that fits GSM-7 encoding gets 160 characters per single segment; UCS-2 (triggered by emoji, CJK, or other non-GSM characters) drops to 70. Longer messages are split into multipart segments with slightly lower per-segment limits. Every response reports the resolved segments: the billable count, the encoding, and the character count. Segments are the unit you're billed on; see cost.
When typographic characters are the only reason a body falls outside GSM-7, smart encoding can reduce its segment count. Set options.smart_encoding to true and Bird replaces curly quotes, dashes, ellipses, and similar characters with GSM-7 equivalents before sending. It is off by default because it changes the body you composed.
For the full character set, extension-table characters that cost two slots, emoji sizing, what smart encoding replaces, and the segment arithmetic, see Character limits.

Batch sending

POST /v1/sms/batches sends up to 100 independent messages in one request. Batch requests use the sms_batch rate-limit group, separate from the sms_send group for single sends. The body is a JSON array of the message objects from Building up the payload:
const result = await bird.sms.sendBatch([
  {
    from: "+15557654321",
    to: "+15551111111",
    text: "Hi Alice!",
    category: "marketing",
  },
  {
    from: "+15557654321",
    to: "+15552222222",
    text: "Hi Bob!",
    category: "marketing",
  },
]);
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. Request failures return immediately: an invalid field, a body over the segment cap, a destination country you have not enabled, or an invalid sender returns a 422. A workspace with no wallet balance receives a 402.
Delivery happens asynchronously. The message moves to sent when Bird hands it to the carrier. A delivery receipt then sets delivered, undelivered, failed, or expired through events and webhooks and the read endpoints. This design has three consequences:
  • 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

Bird currently rejects the following request fields with 422 SMSUnsupportedFeature:
scheduled_at, validity_period, media_urls, messaging_profile_id, broadcast_id, campaign_id, audience_id, contact_id, topic_id, personalization, options.max_price_per_segment, options.track_clicks
Do not include these fields in a send.

Retrying safely

Send the Idempotency-Key header with a unique value per logical send. If a request succeeds without returning a response, replay the same request and key. Bird 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 and currency_code. The message reference documents the per-segment rate, billed segments, destination country, and carrier surcharge. Review cost and segments per message in the SMS log.

Next steps

  • SMS templates: send a built-in template and let Bird pick the sender.
  • SMS log: find a message and inspect its lifecycle, segments, and cost.
  • Events: receive delivery events in your systems.
  • SMS metrics: monitor delivery rate, failure rate, and accepted volume.
  • Idempotency: retry safely with the Idempotency-Key header.