关于 send-message
send-message 是面向开发者的 Zavu API 消息发送技能,说明如何选择渠道、设置收件人及处理多种消息类型,并涵盖错误和回复关联。
主要功能
- 01按收件人选择消息渠道
- 02支持多种收件人标识格式
- 03发送文本及 WhatsApp 富媒体
- 04配置引用回复和发送方
- 05使用幂等键发送消息
适用场景
- 01从应用发送短信或邮件
- 02向 WhatsApp 联系人发送媒体或交互消息
- 03通过 Telegram 或社交渠道回复对话
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zavudev/ zavu-skills --skill send-message安装后在 Claude Code 中输入/send-message确认已加载。
详细介绍
发送消息
适用场景
构建通过 Zavu API 发送消息的代码时使用此技能。涵盖渠道选择、收件人(to)格式、消息类型和错误处理。
渠道
auto、sms、sms_oneway、whatsapp、telegram、email、instagram、messenger、voice。
发送方与账户(一段说明)
Sender 是作为 Zavu-Sender 传入的 API 句柄;账户(WhatsApp Business Account、Facebook Page、Telegram 机器人、电话号码)则是消息路由所经过的连接,也是计费对象。Sender 免费。在控制面板中连接账户会自动创建对应的 sender;使用 GET /v1/senders 查找,并以其 channels 数组为准,确认它可以发送哪些内容。完整模型请参阅 channel-setup 技能。
渠道选择决策树
Is recipient an email address?
-> YES: channel = "email" (the sender needs an email channel: a verified domain)
Is message type non-text (image, video, buttons, list, template, etc.)?
-> YES: channel = "whatsapp" (auto-selected)
Need voice call / TTS?
-> YES: channel = "voice"
Recipient is a numeric chat ID (Instagram / Messenger / Telegram)?
-> YES: channel = "instagram" | "messenger" | "telegram"
Need one-way SMS (no inbound replies)?
-> YES: channel = "sms_oneway"
Need guaranteed delivery to a specific channel?
-> YES: channel = "sms" | "whatsapp" | "telegram" | "instagram" | "messenger"
Want Zavu to pick the channel?
-> YES: channel = "auto" (first channel the sender has configured
that the recipient's address can receive; not cost-based)
Default?
-> channel = "sms" (or omit for default)
收件人(to)格式
通用的 to 字段接受多种标识符格式。消息将根据 channel 路由(如果省略 channel,则根据标识符自动选择)。
| 格式 | 示例 | 说明 |
|---|---|---|
| E.164 电话号码 | +14155551234 | SMS、WhatsApp、语音、Telegram。 |
| 电子邮件地址 | [email protected] | 默认使用 email。 |
| WhatsApp BSUID | US.13491208655302741918 | 特定于业务的用户 ID。路由到 WhatsApp;如果联系人使用了用户名并隐藏了电话号码,可使用此格式向其发送消息。 |
| 数字聊天 ID | 123456789 | Telegram、Instagram 或 Messenger 聊天/用户 ID。 |
基本消息
SMS(默认)
TypeScript:
const result = await zavu.messages.send({
to: "+14155551234",
text: "Your verification code is 123456",
});
console.log(result.message.id);
Python:
result = zavu.messages.send(
to="+14155551234",
text="Your verification code is 123456",
)
print(result.message.id)
Go:
result, err := client.Messages.Send(context.TODO(), zavudev.MessageSendParams{
To: zavudev.String("+14155551234"),
Text: zavudev.String("Your verification code is 123456"),
})
fmt.Println(result.Message.ID)
Ruby:
result = client.messages.send(to: "+14155551234", text: "Your verification code is 123456")
puts result.message.id
PHP:
$result = $client->messages->send([
'to' => '+14155551234',
'text' => 'Your verification code is 123456',
]);
echo $result->message->id;
WhatsApp 文本
const result = await zavu.messages.send({
to: "+14155551234",
channel: "whatsapp",
text: "Hello from Zavu!",
});
电子邮件
const result = await zavu.messages.send({
to: "[email protected]",
channel: "email",
subject: "Your order has shipped",
text: "Hi John, your order #12345 has shipped.",
htmlBody: "<h1>Order Shipped</h1><p>Your order #12345 has shipped.</p>",
replyTo: "[email protected]",
});
语音(文本转语音)
const result = await zavu.messages.send({
to: "+14155551234",
channel: "voice",
text: "Your verification code is 1 2 3 4 5 6",
voiceLanguage: "en-US", // optional, auto-detected from country code
});
单向 SMS
无需接收回复(没有向你回传消息的入站路径)时,使用 sms_oneway。
await zavu.messages.send({
to: "+14155551234",
channel: "sms_oneway",
text: "Your appointment is confirmed for 3pm.",
});
Instagram / Messenger
指定数字聊天/用户 ID(来自入站对话)。
// Instagram Direct
await zavu.messages.send({
to: "17841400000000000",
channel: "instagram",
text: "Hello from Zavu via Instagram!",
});
// Messenger (Facebook Page / Marketplace chat)
await zavu.messages.send({
to: "24025631120151183",
channel: "messenger",
text: "Hello from Zavu via Messenger!",
});
WhatsApp 富媒体消息
图片
await zavu.messages.send({
to: "+14155551234",
messageType: "image",
text: "Check out this product!", // caption
content: { mediaUrl: "https://example.com/image.jpg" },
});
文档
await zavu.messages.send({
to: "+14155551234",
messageType: "document",
content: {
mediaUrl: "https://example.com/invoice.pdf",
filename: "invoice.pdf",
},
});
视频 / 音频
// Video
await zavu.messages.send({
to: "+14155551234",
messageType: "video",
text: "Watch this!",
content: { mediaUrl: "https://example.com/video.mp4" },
});
// Audio
await zavu.messages.send({
to: "+14155551234",
messageType: "audio",
content: { mediaUrl: "https://example.com/audio.mp3" },
});
贴纸
await zavu.messages.send({
to: "+14155551234",
messageType: "sticker",
content: { mediaUrl: "https://example.com/sticker.webp" },
});
位置
await zavu.messages.send({
to: "+14155551234",
messageType: "location",
content: {
latitude: 37.7749,
longitude: -122.4194,
locationName: "San Francisco",
locationAddress: "123 Main St, San Francisco, CA",
},
});
联系人名片
await zavu.messages.send({
to: "+14155551234",
messageType: "contact",
content: {
contacts: [
{ name: "John Doe", phones: ["+14155551234", "+14155555678"] },
],
},
});
交互式按钮(最多 3 个)
await zavu.messages.send({
to: "+14155551234",
messageType: "buttons",
text: "How would you rate your experience?",
content: {
buttons: [
{ id: "great", title: "Great!" },
{ id: "okay", title: "It was okay" },
{ id: "poor", title: "Not good" },
],
},
});
列表消息
await zavu.messages.send({
to: "+14155551234",
messageType: "list",
text: "Select an option:",
content: {
listButton: "View Options",
sections: [{
title: "Products",
rows: [
{ id: "prod_1", title: "Product A", description: "$10.00" },
{ id: "prod_2", title: "Product B", description: "$20.00" },
],
}],
},
});
模板消息
templateVariables 用于填充正文占位符(按位置以 1、2 等为键,或按名称为键;请勿混用)。templateButtonVariables 用于填充动态 URL/OTP 按钮占位符(按按钮索引 0、1、2 为键)。templateHeaderVariables 用于设置文本标题变量(以 1 为键)。在 WhatsApp 中,正文和标题值不能包含换行符、制表符或 4 个以上连续空格;否则发送会被拒绝,并返回 400 invalid_request,在 details.field 中指出相应值。
await zavu.messages.send({
to: "+14155551234",
messageType: "template",
content: {
templateId: "tpl_abc123",
templateVariables: { "1": "John", "2": "ORD-12345" },
templateButtonVariables: { "0": "abc-report-token" }, // optional: dynamic URL/OTP buttons
},
});
CTA URL(行动号召按钮)
await zavu.messages.send({
to: "+14155551234",
messageType: "cta_url",
text: "Check out our latest collection",
content: {
ctaDisplayText: "View Products", // max 20 chars
ctaUrl: "https://example.com/products",
ctaHeaderType: "image", // optional: text | image | video | document
ctaHeaderMediaUrl: "https://example.com/header.jpg",
footerText: "Limited time offer", // optional, max 60 chars
},
});
位置请求(请联系人分享其位置)
发送一条带固定“发送位置”按钮的消息。仅限 WhatsApp。不接受 content 对象——提示词放在 text 中(最多 1024 个字符)。
SDK 尚未生成此功能,请使用 REST:
curl -X POST https://api.zavu.dev/v1/messages \
-H "Authorization: Bearer $ZAVU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155551234",
"channel": "whatsapp",
"messageType": "location_request",
"text": "To finish your order, share the delivery address."
}'
回复会以普通入站 location 消息的形式到达(不是新类型),其中
content.replyToMessageId 会设为请求的 ID。根据此字段进行匹配,将坐标与
你询问的订单/工单关联起来:
if (
event.type === "message.inbound" &&
event.data.messageType === "location" &&
event.data.content?.replyToMessageId === pendingRequestId
) {
const { latitude, longitude } = event.data.content;
}
content.name 和 content.address 为可选字段——只有联系人选择已保存地点而非放置图钉时才会包含。请始终以经纬度为准。
联系信息请求(请联系人分享其电话号码)
发送一条带有固定“分享联系信息”按钮的消息。仅限 WhatsApp。无需 content 对象——提示内容放在 text 中(最多 1024 个字符)。对于已采用 WhatsApp 用户名、且只能通过 BSUID 识别的联系人,这一功能至关重要。
SDK 尚未生成此功能,请使用 REST:
curl -X POST https://api.zavu.dev/v1/messages \
-H "Authorization: Bearer $ZAVU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "US.13491208655302741918",
"channel": "whatsapp",
"messageType": "request_contact_info",
"text": "Share your phone number so our team can call you back."
}'
回复会作为普通的入站 contact 消息到达,共享的号码位于
content.contacts[0].phones。如果发送者之前只能通过 BSUID 识别,Zavu 会自动将共享的电话号码关联到该联系人——无需手动更新。
注意:带有一键式/零点击/复制代码按钮的身份验证模板无法发送给 BSUID(WhatsApp 错误 131062)——请先获取电话号码。
群发也会拒绝 BSUID/@username 收件人。
表情回应
await zavu.messages.react({
messageId: "msg_abc123",
emoji: "\ud83d\udc4d",
});
引用回复(WhatsApp 和 Telegram)
将 content.replyToMessageId 设置为你要回复的消息的 Zavu ID,例如 message.inbound webhook 中的 data.messageId。收件人会看到你发送的消息上方引用了该消息。
const telegram = event.data.channel === "telegram";
await zavu.messages.send({
// Telegram's `data.from` is "telegram:<chatId>"; send to the bare chat id.
to: telegram ? event.data.telegramChatId : event.data.from,
channel: telegram ? "telegram" : "whatsapp",
text: "Your order ships tomorrow.",
content: { replyToMessageId: event.data.messageId },
'Zavu-Sender': event.senderId, // the sender that received it
});
- WhatsApp:适用于除
reaction外的所有消息类型。Telegram:适用于所有消息类型;按钮点击(入站消息中的data.isCallbackQuery: true)并非消息,无法引用。 - 被引用的消息必须属于与
to相同的对话,并且必须由发送此消息所用的发送者发送或接收。请在Zavu-Sender中传入该发送者。 - 请显式设置
channel。若使用auto,电话号码可能会路由到 SMS,而 SMS 不支持引用。 - 无法投递的引用会被拒绝,不会作为普通消息发送:消息不属于此项目时返回
404;属于其他渠道、其他对话、其他发送者,或引用的是表情回应、Telegram 按钮点击、失败的消息,或尚无providerMessageId的消息时返回400。 - 请求中只读取
replyToMessageId。Zavu 会在存储的消息上填入replyToProviderMessageId、replyToFrom、replyToText和replyToMessageType。
正在输入指示器
将一条入站 WhatsApp 消息标记为已读,并在你准备回复时显示正在输入指示器(POST /v1/messages/{messageId}/typing)。发送回复后或 25 秒后,指示器会自动清除。仅适用于入站 WhatsApp 消息。当回复需要超过几秒时使用(例如 LLM Agent、工具调用、信息查询)。
curl -X POST https://api.zavu.dev/v1/messages/MESSAGE_ID/typing \
-H "Authorization: Bearer $ZAVU_API_KEY" \
-H "Zavu-Sender: sender_12345"
发送者覆盖
await zavu.messages.send({
to: "+14155551234",
text: "Hello!",
'Zavu-Sender': "snd_abc123",
});
幂等性
await zavu.messages.send({
to: "+14155551234",
text: "Payment confirmed",
idempotencyKey: "payment_confirm_order_123",
});
使用项目中已有密钥重试时,不会发送任何内容。系统会返回 409,其正文为 { message }:即首次请求创建的消息及其当前状态,请从中读取 id 和 status。重放请求会在任何速率、每日或余额限制检查之前得到响应,因此,即使在每位收件人每分钟仅允许发送一条 SMS 的时间窗口内重试,仍会收到 409 重放响应,而非 429。SDK 会将该 409 抛为 ConflictError(不会重试);重放的消息位于错误的 error 属性中:
import Zavudev from "@zavudev/sdk";
try {
await zavu.messages.send({ to: "+14155551234", text: "Payment confirmed", idempotencyKey: key });
} catch (err) {
if (err instanceof Zavudev.ConflictError) {
const { message } = err.error as { message: { id: string; status: string } };
// Already sent: message.id and message.status describe the first attempt.
} else {
throw err;
}
}
自动回退到 SMS
发送者也支持 SMS 时,失败的 WhatsApp 文本或模板消息会通过 SMS 重试。是否回退取决于渠道的选择方式:
- 未提供
channel或设为"auto":启用回退。设置fallbackEnabled: false可关闭。 - 显式设置
channel(例如"whatsapp"):关闭回退。设置fallbackEnabled: true可启用。
// Explicit WhatsApp, with the SMS safety net
await zavu.messages.send({
to: "+14155551234",
channel: "whatsapp",
text: "Your order has shipped.",
fallbackEnabled: true,
});
媒体、按钮、列表及其他仅限 WhatsApp 的类型不会回退。
获取状态和列出消息
// Get single message
const msg = await zavu.messages.get({ messageId: "msg_abc123" });
console.log(msg.message.status); // queued | sending | sent | delivered | read | failed
// List with filters + pagination
let cursor: string | undefined;
do {
const result = await zavu.messages.list({ status: "delivered", limit: 50, cursor });
for (const message of result.items) {
console.log(message.id, message.status);
}
cursor = result.nextCursor ?? undefined;
} while (cursor);
常见错误
| 错误代码 | 含义 | 解决方法 |
|---|---|---|
whatsapp_window_closed | 24 小时窗口未开启 | 改用模板消息 |
a2p_limit_exceeded | HTTP 429:Free 套餐的月度额度已用完。WhatsApp、Telegram、Instagram 和 Messenger 共用每月 2,000 条消息额度,双向消息均计入——联系人发给你的一条消息和你发出的一条消息一样,都会消耗一个额度,因此,一个发出 300 条、收到 1,700 条消息的项目就会达到上限。在共存模式下,通过 WhatsApp Business App 发送的消息不计入额度,发送失败的消息也不计入。达到上限后,入站消息也会被拒绝:不会存储、不会出现在收件箱中,也不会触发 message.inbound webhook。此限制与下方的每日上限相互独立;每日上限不会阻止达到此月度额度 | 升级到付费套餐(无上限),或等待每月 1 日重置 |
insufficient_balance | HTTP 402,与其他错误一样,顶层包含 { code, message }(已知时,details 中包含 requiredCents 和 availableCents):预付余额不足以支付此次发送费用。电子邮件按 1,000 条为一组从余额扣费(事务性邮件每 1,000 条 $0.40,营销邮件每 1,000 条 $0.80);短信和语音按条计费 | 在控制面板中充值,然后重试 |
sms_compliance_blocked | HTTP 422,仅适用于短信和单向短信:内容检查拒绝了该文本(details.complianceFailures、details.requiredFixes,以及适用时的 suggestedCompliantVersion),或检查无法运行,错误消息会说明原因。不会发送消息,也不会扣费。一次性验证码始终会通过检查 | 按照 requiredFixes 修改(通常需要说明企业名称和联系原因),然后重新发送;如果检查无法运行,请在几分钟后重试 |
url_not_verified | 消息中包含未经验证的网址 | 先通过 /v1/urls 提交网址 |
url_shortener_blocked | 检测到网址缩短服务 | 使用完整的目标网址 |
ten_dlc_required | HTTP 403:目标号码为美国或加拿大(+1)号码,且项目没有获批的 10DLC 活动。这是运营商的要求,并非 Zavu 的要求;注册品牌和活动需要通过企业验证 | 在控制面板中注册 10DLC 品牌和活动,或通过其他渠道向该联系人发送消息 |
sending_suspended | HTTP 403:Zavu 的自动审核拒绝了该账户的多条消息后,暂停了该账户的发送功能。暂停期间,所有渠道的发送都会被拒绝;details.dashboardUrl 指向支持页面。对 POST /v1/calls 也会返回相同代码 | 联系支持团队;等待不会解除暂停 |
RISK_BLOCKED / RISK_REJECTED / RISK_REVIEW_EXPIRED | 出现在发送失败的消息上(异步),而非响应中:自动审核直接拒绝了消息、审核人员未批准被暂扣的消息,或审核窗口先行关闭。发送失败的短信不会扣费 | 修改消息内容;如果认为拒绝有误,请联系支持团队并提供消息 ID |
SENDING_SUSPENDED | 出现在发送失败的消息上(异步):消息排队期间,账户的发送功能被暂停 | 联系支持团队 |
daily_limit_exceeded | HTTP 429:渠道组的每日上限,details.limit 会说明具体上限。未完成任何验证:sms + sms_oneway 合计每天 25 条,voice 每天 5 条,WhatsApp + Telegram + Instagram + Messenger 合计每天 100 条。超过此基础额度后:短信每天 200 条;身份或企业验证获批后可提升至每天 10,000 条;Free 套餐的语音每天 50 条、会话式消息每天 250 条。付费套餐的语音和会话式消息均无上限。电子邮件受套餐额度限制(Free 套餐每天 100 条)。计数于 00:00 UTC 重置。此外,所有套餐均按账户年龄设有滚动电子邮件上限(前 7 天为每 24 小时 1,000 条、每小时 200 条;截至第 30 天为每 24 小时 10,000 条、每小时 1,000 条;之后为每 24 小时 50,000 条、每小时 5,000 条):超过此上限时,消息会被接受,但随后会以 EMAIL_RATE_LIMIT_DAILY 或 EMAIL_RATE_LIMIT_HOURLY 失败,而不是返回此 429 | 等待重置,或验证身份/添加付款方式以提高上限(电子邮件需升级套餐) |
EMAIL_INVALID_RECIPIENT | 电子邮件地址格式错误(异步,出现在发送失败的消息上) | 修正地址;可通过 POST /v1/introspect/email 预先检查列表 |
EMAIL_DOMAIN_NOT_FOUND | 收件人域名没有 MX 或 A 记录(异步) | 移除该地址;发送会导致硬退信 |
EMAIL_RECIPIENT_SUPPRESSED | 此地址此前曾退信或投诉(异步) | 从列表中移除此地址 |
| 验证会改变什么,以及不会改变什么。 账户从启用后的第一分钟起,就能通过任何渠道向任何目标发送消息。发送消息不需要任何渠道要求进行身份或企业验证。验证改变的是每日上限:身份验证(KYC)、已保存的付款方式、已结算的存款或付费套餐都能让账户脱离最低档;而要将短信额度提升至每天 10,000 条,则必须完成身份或企业验证。注册 10DLC 品牌和营销活动仍然需要企业验证(KYB),每个 +1 短信目标都需要注册。电子邮件没有验证门槛:发件人只要有经过验证的域名,从第一天起就能在套餐额度内发送(Free 套餐为每天 100 封、每月 3,000 封)。参考:https://docs.zavu.dev/concepts/sending-limits |
每条 sms、sms_oneway、email 和 voice 消息在发送前都会经过审核——审核消息内容及其周围的发送模式。审核有三种结果。正常情况下,消息会直接发送。消息也可能被暂缓以进行短暂审核:消息保持 queued 状态(不会出现新状态,MessageStatus 不变),获批后便会正常发送;审核窗口约为几小时。或者消息会被拒绝:消息发送失败,errorCode 中包含 RISK_BLOCKED、RISK_REJECTED 或 RISK_REVIEW_EXPIRED,并触发 message.failed;被拒绝的短信不会收费。呼叫绝不会被暂缓——审核拦截的呼叫会直接失败,而不是延迟发起。多次被拒绝会导致账户发送功能被暂停(403 sending_suspended)。广播只会根据其文本审核一次,不会按收件人逐一审核。不要编写重试逻辑来重新发送因这些代码而失败的消息:被拒绝的是消息内容,因此重试发送完全相同的内容也会被同样拒绝。
电子邮件会在发送时自动进行预验证:对于确定会导致硬退信的情况(语法错误、域名无效、地址已被抑制),消息会以以上代码标记为失败,而不会发送,因此不会影响退信率。这些结果会异步显示在消息上(status: "failed" + errorCode),并通过 message.failed Webhook 发送。提示性信号(如 info@ 这类角色地址、一次性邮箱域名)不会阻止发送——请先通过 POST /v1/introspect/email 检查。
限制
- 按钮标题:最多 20 个字符,最多 3 个按钮
- 列表行标题:最多 24 个字符,描述:最多 72 个字符,每个分区最多 10 行
- 位置请求正文:最多 1024 个字符,不得包含
content对象,仅限 WhatsApp - 电子邮件主题:最多 998 个字符
- 语音语言代码:
en-US、es-ES、pt-BR等(省略时自动检测) - 媒体消息会自动选择 WhatsApp 渠道