mcpField
S
send-message

send-message

github.com/zavudev生产力1351 次安装2 天前更新
代码仓库

关于 send-message

send-message 是面向开发者的 Zavu API 消息发送技能,说明如何选择渠道、设置收件人及处理多种消息类型,并涵盖错误和回复关联。

主要功能

  1. 01按收件人选择消息渠道
  2. 02支持多种收件人标识格式
  3. 03发送文本及 WhatsApp 富媒体
  4. 04配置引用回复和发送方
  5. 05使用幂等键发送消息

适用场景

  1. 01从应用发送短信或邮件
  2. 02向 WhatsApp 联系人发送媒体或交互消息
  3. 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 电话号码+14155551234SMS、WhatsApp、语音、Telegram。
电子邮件地址[email protected]默认使用 email。
WhatsApp BSUIDUS.13491208655302741918特定于业务的用户 ID。路由到 WhatsApp;如果联系人使用了用户名并隐藏了电话号码,可使用此格式向其发送消息。
数字聊天 ID123456789Telegram、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_closed24 小时窗口未开启改用模板消息
a2p_limit_exceededHTTP 429:Free 套餐的月度额度已用完。WhatsApp、Telegram、Instagram 和 Messenger 共用每月 2,000 条消息额度,双向消息均计入——联系人发给你的一条消息和你发出的一条消息一样,都会消耗一个额度,因此,一个发出 300 条、收到 1,700 条消息的项目就会达到上限。在共存模式下,通过 WhatsApp Business App 发送的消息不计入额度,发送失败的消息也不计入。达到上限后,入站消息也会被拒绝:不会存储、不会出现在收件箱中,也不会触发 message.inbound webhook。此限制与下方的每日上限相互独立;每日上限不会阻止达到此月度额度升级到付费套餐(无上限),或等待每月 1 日重置
insufficient_balanceHTTP 402,与其他错误一样,顶层包含 { code, message }(已知时,details 中包含 requiredCents 和 availableCents):预付余额不足以支付此次发送费用。电子邮件按 1,000 条为一组从余额扣费(事务性邮件每 1,000 条 $0.40,营销邮件每 1,000 条 $0.80);短信和语音按条计费在控制面板中充值,然后重试
sms_compliance_blockedHTTP 422,仅适用于短信和单向短信:内容检查拒绝了该文本(details.complianceFailures、details.requiredFixes,以及适用时的 suggestedCompliantVersion),或检查无法运行,错误消息会说明原因。不会发送消息,也不会扣费。一次性验证码始终会通过检查按照 requiredFixes 修改(通常需要说明企业名称和联系原因),然后重新发送;如果检查无法运行,请在几分钟后重试
url_not_verified消息中包含未经验证的网址先通过 /v1/urls 提交网址
url_shortener_blocked检测到网址缩短服务使用完整的目标网址
ten_dlc_requiredHTTP 403:目标号码为美国或加拿大(+1)号码,且项目没有获批的 10DLC 活动。这是运营商的要求,并非 Zavu 的要求;注册品牌和活动需要通过企业验证在控制面板中注册 10DLC 品牌和活动,或通过其他渠道向该联系人发送消息
sending_suspendedHTTP 403:Zavu 的自动审核拒绝了该账户的多条消息后,暂停了该账户的发送功能。暂停期间,所有渠道的发送都会被拒绝;details.dashboardUrl 指向支持页面。对 POST /v1/calls 也会返回相同代码联系支持团队;等待不会解除暂停
RISK_BLOCKED / RISK_REJECTED / RISK_REVIEW_EXPIRED出现在发送失败的消息上(异步),而非响应中:自动审核直接拒绝了消息、审核人员未批准被暂扣的消息,或审核窗口先行关闭。发送失败的短信不会扣费修改消息内容;如果认为拒绝有误,请联系支持团队并提供消息 ID
SENDING_SUSPENDED出现在发送失败的消息上(异步):消息排队期间,账户的发送功能被暂停联系支持团队
daily_limit_exceededHTTP 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 渠道

评论

支持 Markdown