发送 SMS
本指南介绍单条发送端点 POST /v1/sms/messages。构建一个包含收件人、发件人、正文和类别的 JSON 载荷。Bird 返回带有消息 ID 的 202 Accepted,并异步投递。每个请求向一个收件人发送一条消息。如需一次发送多条消息,请使用批量发送。如需发送模板而不是自定义文本,请提供 template 对象来代替 text 和 category。内置模板拒绝 from;工作区模板则必须提供它。
发送前:启用目的地国家
你的工作区只向其目的地允许列表中的国家发送 SMS。你的组织所在国家是其企业资料中注册地址所在的国家;如果注册地址没有国家,则为账单地址所在的国家。工作区的允许列表初始为空。每当设置或更改所在国家时,Bird 都会将该国家加入允许列表。如果发送目标国家不在允许列表中,Bird 会在解析发件人之前以 422 SMSDestinationNotEnabled 拒绝该发送。请在控制台的 SMS > Destinations 中启用你服务的国家。
在同一页面上,你可以为每个国家设置每日支出上限,币种为你钱包的货币。设置上限需要组织钱包以及读取该钱包的权限;移除上限则不需要。无论该国家是否启用,上限都会生效。当天发往该国家的支出达到上限后,Bird 会以 422 SMSDestinationDailySpendExceeded 拒绝发往该国家的新消息,直到 UTC 午夜。验证消息同样会被拒绝。支出在消息扣费时计入,因此已接受的消息可能按其费用超出上限。上限的更改最多需要一分钟才会对发送生效。
最简发送
最小的有效自由文本载荷包含一个 to 收件人、一个 from 发件人、一个 text 正文和一个 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);msg = client.sms.send(
from_="+15557654321",
to="+15551234567",
text="Your verification code is 123456.",
category="authentication",
)
print(msg.id, msg.status)msg, err := client.Sms.Send(context.Background(), bird.SmsSendParams{
From: "+15557654321",
To: "+15551234567",
Text: "Your verification code is 123456.",
Category: bird.SMSCategoryAuthentication,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)$message = $bird->sms->send(
from: '+15557654321',
to: '+15551234567',
text: 'Your verification code is 123456.',
category: 'authentication',
);
echo $message->getId(), ' ', $message->getStatus();bird sms send --body-file - <<'JSON'
{
"to": "+14155550100",
"from": "+15557654321",
"text": "Your verification code is 123456.",
"category": "authentication",
"options": {
"smart_encoding": true
},
"tags": [
{
"name": "campaign",
"value": "signup"
}
],
"metadata": {
"user_id": "usr_12345"
}
}
JSONcurl -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"
}'请使用你所在区域的主机(https://us1.platform.bird.com 或 https://eu1.platform.bird.com)以及与之匹配的 bk_{region}_... 密钥。响应为已接受的消息:
{
"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 表示 Bird 已收到消息并正在处理;cost 为 null,因为定价在处理过程中进行。后续流程见异步模型。
逐步构建载荷
收件人
to 是一个 E.164 格式的收件人:以 + 开头,后接国家代码和用户号码,例如 +31612345678。一条消息只发给一个收件人,不支持 cc、bcc 或收件人数组。如需发给多人,请使用批量发送。
发送方
from 是收件人看到的发送方。自由文本发送必须提供该字段,除非 messaging_profile_id 指定的消息配置文件为目标国家设置了发送方或默认发送方。它有两种形式,哪些形式可用取决于目标国家:
- 字母数字发送方 ID:由 3 到 11 个字母、数字、空格、连字符、下划线或点组成,至少包含一个字母,且首尾不能是分隔符,例如
Bird或Acme-Co。它必须包含字母,因此像555 555这样带标点的数字串会被拒绝。有些国家要求注册,另一些国家(包括美国)不支持字母数字发送方。收件人无法回复这类发送方。 - 你的工作区拥有的号码,使用 E.164 格式或纯数字。任何全数字的
from都会被视为号码,并在你的发送方中查找,因此你不拥有的任意号码会被拒绝。它作为长号码、免费号码还是短代码使用,取决于号码本身,而不是你写了多少位数字。6 位数的from并不因为有 6 位就是短代码;只有你拥有的号码本身是短代码时,它才是短代码。
对目标国家无效的发送方会被拒绝,并返回说明原因的 422(例如在不支持字母数字发送方的地区返回 SMSAlphaNotSupported)。在内置模板发送中,不接受 from:Bird 会根据目标国家和类别选择发送方。工作区模板要求提供你自己的 from 发送方,或由消息配置文件提供发送方。
如何申请发送方 ID、查看各国对它的要求以及按国家注册,请参阅 SMS 发送方 ID。
如需将一个项目的发送方和默认设置集中管理,请创建消息配置文件,并在 messaging_profile_id 中指定它。省略 from 时,Bird 会使用该配置文件为目标国家设置的发送方或其默认发送方发送。省略 category 时会采用配置文件的默认类别,options.smart_encoding、cost_center、validity_period 和 options.callback_url 也会采用配置文件中的设置。你在消息上设置的值优先。
正文和类别
text 是消息正文,至少一个字符。正文按分段计费和投递;每次发送最多 12 个分段(约 1,836 个 GSM-7 字符;如果正文使用扩展的 UCS-2 编码,则约 804 个)。超出上限的正文会被拒绝并返回 422,而不会被截断。
自由文本发送必须提供 category,除非 messaging_profile_id 中的消息配置文件设置了默认类别。它将消息归类为 transactional、marketing、authentication 或 service,以告知 Bird 和运营商你发送的原因。一次性验证码使用 authentication;促销消息使用 marketing。请选择与消息用途相符的类别。
标签和元数据
这些字段都用于在发送时附加你自己的数据,但用途不同:
tags是结构化的{name, value}键值对(每次发送最多 20 个;名称 1 到 32 个字符,值 1 到 64 个字符,仅限 ASCII[A-Za-z0-9_-],区分大小写,同一次发送内名称不可重复)。它们是一级筛选维度:可以按标签筛选消息列表。用于取值种类较少的标签,例如campaign或experiment_variant。metadata是任意 JSON 对象(序列化后最大 2 KB)。它会被存储,在 API 读取时返回,并在每个 webhook 事件中原样回传,但不能作为筛选维度。用它携带需要往返传递的上下文:内部 ID、外键,以及任何你希望随每个事件一并返回的数据。reference是一个 1 到 255 个字符的字符串,用于查找消息,例如订单号或工单号。消息列表上的?reference=会对它进行区分大小写的精确匹配,多条消息可以共用同一个值。它会在读取时返回,并在每个 webhook 事件中原样回传。仅在需要按它搜索时设置,其他数据请放入metadata。
{
"tags": [{ "name": "campaign", "value": "spring-2026" }],
"metadata": { "user_id": "usr_12345", "order_id": "ord_98765" },
"reference": "ord_98765"
}字段参考
| 字段 | 类型 | 必填 | 限制 / 说明 |
|---|---|---|---|
to | 字符串(E.164) | 是 | 每条消息一个收件人 |
from | 字符串 | 是* | 您拥有的 E.164 号码、字母数字发件人 ID(3–11 个字符,至少包含一个字母)或短代码(5–6 位数字) |
text | 字符串 | 是* | 至少 1 个字符;上限为 12 个分段 |
category | 字符串 | 是* | transactional、marketing、authentication 或 service |
template | 对象 | 是* | 用于内置模板或工作区模板的 {slug, parameters?, language?} |
tags | {name, value}[] | 否 | 最多 20 个;名称 1–32 个字符,值 1–64 个字符;仅限 [A-Za-z0-9_-] |
metadata | 对象 | 否 | 任意 JSON,序列化后最大 2 KB |
reference | 字符串 | 否 | 1–255 个字符;在消息列表中按 ?reference= 精确匹配 |
messaging_profile_id | 字符串 | 否 | 提供发送方和默认值的消息配置文件;消息上设置的值优先 |
validity_period | 整数 | 否 | 自接受起 60–172800 秒;省略时使用消息配置文件中的值或运营商自身的规则 |
options | 对象 | 否 | 单条消息的处理设置,例如 smart_encoding(参见分段与编码)和 callback_url(参见回调 URL) |
* 自由文本发送需要 from、text 和 category,但消息配置文件可以提供 from 和 category。模板发送需要 template 来代替 text 和 category(并拒绝这两者)。内置模板拒绝 from;工作区模板则要求提供它。
使用模板发送
设置 template 即可发送内置模板或已发布的工作区模板。工作区模板创作功能处于 API 预览阶段。模板目录、变量和发送方规则见 SMS 模板。
分段与编码
SMS 按分段计费。符合 GSM-7 编码的消息,单个分段可容纳 160 个字符;使用 UCS-2(由表情符号、中日韩文字或其他非 GSM 字符触发)时降为 70 个。较长的消息会拆分为多部分分段,每个分段的字符上限略低。每个响应都会报告解析后的 segments:计费的 count、encoding 以及字符数。分段是计费单位;参见费用。
如果消息正文仅因印刷字符而超出 GSM-7 范围,智能编码可以减少其分段数。将 options.smart_encoding 设为 true 后,Bird 会在发送前将弯引号、破折号、省略号等字符替换为对应的 GSM-7 字符。此功能默认关闭,因为它会更改你编写的正文。
完整字符集、占用两个位置的扩展表字符、emoji 的长度计算、智能编码会替换哪些字符以及分段计算方法,请参阅字符限制。
批量发送
POST /v1/sms/batches 可在一个请求中发送最多 100 条相互独立的消息,即通过一次 API 调用向多个收件人发送的批量 SMS 模式。批量请求使用 sms_batch 请求速率限制策略,与单条发送使用的 sms_send 策略相互独立。请求体是一个 JSON 对象,其 messages 数组包含构建载荷中所述的消息对象:
const result = await bird.sms.sendBatch({
messages: [
{
from: "+15557654321",
to: "+15551111111",
text: "Hi Alice!",
category: "marketing",
},
{
from: "+15557654321",
to: "+15552222222",
text: "Hi Bob!",
category: "marketing",
},
],
});batch = client.sms.send_batch(
messages=[
{
"from_": "+15557654321",
"to": "+15551111111",
"text": "Hi Alice!",
"category": "marketing",
},
{
"from_": "+15557654321",
"to": "+15552222222",
"text": "Hi Bob!",
"category": "marketing",
},
]
)
for msg in batch.data:
print(msg.id, msg.status)batch, err := client.Sms.SendBatch(context.Background(), bird.SmsSendBatchParams{
Messages: []bird.SmsSendParams{
{
From: "+15557654321", To: "+15551111111",
Text: "Hi Alice!", Category: bird.SMSCategoryMarketing,
},
{
From: "+15557654321", To: "+15552222222",
Text: "Hi Bob!", Category: bird.SMSCategoryMarketing,
},
},
})
if err != nil {
log.Fatal(err)
}
for _, msg := range batch.Data {
fmt.Println(msg.Id, *msg.Status)
}$batch = $bird->sms->sendBatch(messages: [
(new SMSMessageSendRequest())
->setFrom('+15557654321')
->setTo('+15551111111')
->setText('Hi Alice!')
->setCategory('marketing'),
(new SMSMessageSendRequest())
->setFrom('+15557654321')
->setTo('+15552222222')
->setText('Hi Bob!')
->setCategory('marketing'),
]);
foreach ($batch->getData() ?? [] as $item) {
echo $item->getId(), ' ', $item->getStatus(), "\n";
}curl -X POST "https://{region}.platform.bird.com/v1/sms/batches" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+15551111111",
"from": "+15557654321",
"text": "Hi Alice!",
"category": "marketing"
},
{
"to": "+15552222222",
"from": "+15557654321",
"text": "Hi Bob!",
"category": "marketing"
}
]
}'校验采用全有或全无的方式:只要批次中有任何一条消息无效,整个请求就会以 422 被拒绝,且不会发送任何消息,因此批次不会只部分生效。成功时,202 响应在 data 下按提交顺序返回每条已接受的消息,并附带包含 accepted_count 的 summary。此后每条消息相互独立:一个收件人发送失败不会影响其他收件人。
异步模型:202 的含义
发送成功时返回 202 Accepted,其中包含消息 ID 和 status: accepted。请求错误会立即返回:字段无效、消息体超过分段上限、目标国家尚未启用或发送方无效时,均返回 422。钱包没有余额的工作区会收到 402。
投递以异步方式进行。Bird 将消息交给运营商后,消息状态变为 sent。随后,投递回执会通过事件和 webhook 以及读取端点将状态设为 delivered、undelivered、failed 或 expired。这种设计带来三个结果:
- 费用在接受后计价。 消息的
cost在接受时为null,Bird 在处理过程中为发送计价后才会填入。要查看目前已计价的费用,请重新读取该消息或等待投递事件;费用与计费介绍了各费用组成部分,以及某一部分何时保持未计价。 - 消息在返回 202 后仍可能被拒绝。 如果处理过程中扣费失败,消息最终状态为
rejected,并触发sms.rejectedwebhook,你不会被计费;钱包余额耗尽时显示为last_error.code: insufficient_balance。 - 读取结果可能短暂滞后于 202。 消息在返回
202后不久才会在读取端点上可见,因此发送后立即出现的404会在片刻内自行消失。
保留字段
Bird 会以 422 SMSUnsupportedFeature 拒绝以下请求字段:
scheduled_at、media_urls、broadcast_id、campaign_id、audience_id、contact_id、topic_id、personalization、options.max_price_per_segment、options.track_clicks(当 true 时)
发送时请勿包含这些字段。
安全重试
每次逻辑发送都在 Idempotency-Key 请求头中传入唯一值。如果请求已成功但未返回响应,请使用相同的请求和键重新发送。Bird 会返回原始结果,而不会发送重复消息。键的格式和保留期限请参阅幂等性。
费用与计费
出站 SMS 按分段计费。费用取决于目标国家和运营商;部分线路会加收第三方附加费,例如美国 10DLC 运营商费用。
消息的 cost 将费用拆分为若干命名组成项。transaction_amount 是 Bird 为传送该消息收取的费用,passthrough_amount 是转嫁的第三方费用,amount 是已定价组成项的总和,以 currency_code 计价。尚未定价的组成项为 null 而不是 "0.00000",因此附加费始终未确定的消息,其 amount 仅包含传送费用。各字段说明请参阅消息参考。
附加费按尽力而为的方式确定。Bird 在记录送达回执时,于一个有限的时间窗口内确定附加费。如果在该窗口内未能确定,passthrough_amount 将永久保持为 null:Bird 不会重试,amount 仍等于传送费用。
入站 SMS 分两项计费:按分段计算的入站费率,以及适用时的入站运营商附加费。两者都记录在所收消息自身的 cost 中:费率为 transaction_amount,附加费为 passthrough_amount。与出站附加费不同,入站附加费在消息被接受时定价,而不是在送达时定价,因此不会在之后补填。
在 SMS 日志中查看每条消息的费用和分段数。
后续步骤
- SMS 模板:发送内置模板,并由 Bird 选择发送方。
- SMS 日志:查找消息,查看其生命周期、分段数和费用。
- 事件:在您的系统中接收投递事件。
- SMS 回调 URL:将单条消息或单个配置文件的事件发送到各自的 HTTPS 端点。
- SMS 指标:监控投递率、失败率和已接受的消息量。
- 幂等性:使用
Idempotency-Key请求头安全地重试。 - 发送您的第一条 SMS:一段在控制台中演示相同设置步骤的视频