connector-twilio

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Twilio Connector (experimental)

Twilio 连接器(实验性)

Send SMS / MMS and configure Twilio messaging from a Caffeine canister.
⚠️ Experimental (
twilio-client@0.1.2
) — no call has ever been made from this client.
Its write path could work at all only recently: before, every write discarded its arguments and posted an empty body. The wire format now matches what Twilio documents (form-encoded body, percent-encoded values, optional fields omitted) and all 118 files typecheck, but structurally correct is not verified. Treat the first successful send as the acceptance test, and do not present Twilio to a user as a fully supported platform feature until one has happened. Sends cost money, so a failed experiment is not free.
Scope — the package is the messaging surface only.
twilio-client
is pruned to 35 API modules (all of Messaging v1 plus the v2010 messaging path: Account, Message, Media, IncomingPhoneNumber and its variants, AvailablePhoneNumber, the A2P registries). Voice/calls, recordings, conferences, queues, applications, SIP and usage records are not in the package — if a build needs those, they are outside this connector. (Counts: 35 API modules, 82 models, 118 files, all typechecking.)
从Caffeine canister发送SMS/MMS并配置Twilio消息服务。
⚠️ 实验性版本(
twilio-client@0.1.2
)——此客户端从未发起过任何调用
。其写入路径直到最近才可能正常工作:在此之前,所有写入操作都会丢弃参数并提交空请求体。现在的有线格式已匹配Twilio文档要求(表单编码请求体、百分编码值、省略可选字段),且全部118个文件均通过类型检查,但“结构正确”并不等同于“已验证可用”。请将首次成功发送视为验收测试,在完成该测试前,请勿向用户将Twilio表述为完全支持的平台功能。发送消息会产生费用,因此失败的实验并非零成本。
范围说明——本包仅包含消息服务相关功能
twilio-client
已精简至35个API模块(涵盖Messaging v1及v2010消息路径:Account、Message、Media、IncomingPhoneNumber及其变体、AvailablePhoneNumber、A2P注册相关模块)。语音/通话、录音、会议、队列、应用、SIP及使用记录不在本包范围内——如果构建需要这些功能,需使用本连接器之外的方案。(统计:35个API模块、82个模型、118个文件,全部通过类型检查。)

Orchestrator routing notes

编排器路由说明

Load this skill when the user, spec, or a prior task mentions sending a text message, SMS/MMS, notifying someone by phone, buying or listing phone numbers, or any Twilio messaging concept. Raw
ic.http_request
to
*.twilio.com
is an anti-pattern that re-implements auth, host routing, percent-encoding and JSON parsing by hand — and, done naively, sends every message ~13 times.
Intent → capability mapping:
User intentCapability
Send an SMS
Api20100401MessageApi.createMessage
with
from
= a Twilio number
Send an MMS (image)same, with
mediaUrl = ["https://…"]
and
sendAsMms = true
Send via a Messaging Service (recommended for US traffic)same,
from = ""
+
messagingServiceSid
Check delivery status
fetchMessage
(
status
,
error_code
)
List / search sent messages
listMessage
(paginated)
Own or browse phone numbers
Api20100401IncomingPhoneNumberApi
,
…AvailablePhoneNumberCountryApi
Set up a Messaging Service
MessagingV1ServiceApi.createService
Register for US A2P 10DLC
MessagingV1BrandRegistrationApi
MessagingV1UsAppToPersonApi
MessagingV1PhoneNumberApi
(in that order — see US A2P 10DLC)
Verify a toll-free number
MessagingV1TollfreeVerificationApi
Twilio credentials are something a human must go and fetch from a console, so the build is not done when the backend compiles — it is done when the app tells the admin where to get the credential and gives them somewhere to paste it. See Auth model, then Frontend for the page that MUST ship, and repeat the steps in the completion message.
Ask before writing code: which number sends? A US-bound production app needs a Messaging Service + A2P registration (weeks of lead time, real fees); a demo/internal app can send from a single trial number to verified recipients only. Report the choice and its consequences back to the prompting user.
当用户、需求规格或先前任务提及发送短信、SMS/MMS、通过电话通知他人、购买或列出电话号码,或任何Twilio消息相关概念时,加载此技能。直接使用
ic.http_request
调用
*.twilio.com
是反模式,会手动重复实现认证、主机路由、百分编码和JSON解析——而且如果实现不当,会导致每条消息被发送约13次。
意图→能力映射:
用户意图对应能力
发送SMS使用
Api20100401MessageApi.createMessage
from
设为Twilio号码
发送MMS(图片)同上,设置
mediaUrl = ["https://…"]
sendAsMms = true
通过消息服务发送(美国流量推荐方案)同上,
from = ""
并设置
messagingServiceSid
查看送达状态
fetchMessage
(获取
status
error_code
列出/搜索已发送消息
listMessage
(分页)
管理或浏览电话号码
Api20100401IncomingPhoneNumberApi
…AvailablePhoneNumberCountryApi
设置消息服务
MessagingV1ServiceApi.createService
注册美国A2P 10DLC按顺序调用
MessagingV1BrandRegistrationApi
MessagingV1UsAppToPersonApi
MessagingV1PhoneNumberApi
(详见美国A2P 10DLC章节)
验证免费电话号码
MessagingV1TollfreeVerificationApi
Twilio凭证需要人工从控制台获取,因此后端编译完成并不代表构建完成——只有当应用告知管理员获取凭证的位置并提供粘贴入口后,构建才算完成。请先查看认证模型,再查看前端章节中必须交付的页面,并在完成消息中重复这些步骤。
编写代码前请确认:使用哪个号码发送?面向美国的生产应用需要消息服务+A2P注册(数周准备时间,真实费用);演示/内部应用可使用单个试用号码,且仅能发送给已验证的收件人。请将选择及其后果反馈给发起需求的用户。

Auth model — HTTP Basic, two flavours

认证模型——两种HTTP Basic认证方式

Both flavours are the same
#basicAuth { user; password }
credential and the client treats them identically; they differ in blast radius.
Flavour
user
/
password
When
API Key (default — prefer this)API Key SID (
SK…
) / its Secret
Production. Revocable and scoped: leaking one does not surrender the account.
Account SID + Auth TokenAccount SID (
AC…
) / Auth Token
Dev only. The Auth Token is the account — it can create sub-accounts, buy numbers, and spend money.
The Account SID (
AC…
) is also a required positional argument to every v2010 operation (it is in the URL path), regardless of which flavour is used. So an app using an API Key stores three values: Account SID, Key SID, Key Secret.
两种方式使用相同的
#basicAuth { user; password }
凭证,客户端对其处理逻辑一致,区别仅在于影响范围。
方式
user
/
password
使用场景
API密钥(默认推荐)API密钥SID(
SK…
)/ 密钥Secret
生产环境。可撤销且权限受限:泄露一个密钥不会导致整个账号失控。
账号SID + 认证令牌账号SID(
AC…
)/ 认证令牌
仅开发环境。认证令牌等同于账号权限——可创建子账号、购买号码、产生费用。
账号SID
AC…
)还是每个v2010操作的必填位置参数(位于URL路径中),无论使用哪种认证方式。因此,使用API密钥的应用需要存储三个值:账号SID、密钥SID、密钥Secret。

Obtaining credentials

获取凭证步骤

  1. Sign in at https://console.twilio.com.
  2. The Account SID (
    AC…
    ) is on the console dashboard — copy it.
  3. For production, Account → API keys & tokens → Create API key (Standard); copy the SID (
    SK…
    ) and the Secret. The Secret is shown once — if the admin navigates away it cannot be recovered, only replaced. For dev only, take the Auth Token from the dashboard instead.
  4. Buy a sending number: Phone Numbers → Manage → Buy a number, with the SMS capability ticked (not every number has it).
  5. On a trial account: verify each recipient under Phone Numbers → Verified Caller IDs, or sends fail with
    21608
    ; trial messages also carry a "Sent from your Twilio trial account" prefix.
  1. 登录https://console.twilio.com
  2. 在控制台仪表板上找到账号SID
    AC…
    )并复制。
  3. 生产环境:进入Account → API keys & tokens → Create API key(选择Standard);复制SID
    SK…
    )和SecretSecret仅显示一次——如果管理员离开当前页面,将无法找回,只能重新生成。仅开发环境可直接从仪表板获取认证令牌
  4. 购买发送号码:进入Phone Numbers → Manage → Buy a number,勾选SMS功能(并非所有号码都具备此功能)。
  5. 试用账号:在Phone Numbers → Verified Caller IDs下验证每个收件人,否则发送会失败并返回
    21608
    ;试用账号发送的消息还会带有“Sent from your Twilio trial account”前缀。

Handing the credentials to the canister

将凭证交付给canister

The admin pastes them through an admin-gated setter — gated on
AccessControl.hasPermission(state, caller, #admin)
. They are held by the canister only and never returned to the frontend.
⚠️ Never gate the setter on a first-caller-claims-ownership scheme. On the IC every unauthenticated caller is the same anonymous principal, so if an anonymous call claims ownership first, every anonymous caller passes the
caller == owner
check and can overwrite the credential — and this one spends money.
The canister hands them to the client only through
config.auth = ?#basicAuth { user; password }
, which every method turns into an
Authorization: Basic …
header. No method takes a credential argument and none puts it in the URL, so it cannot leak through a logged query string.
管理员需通过管理员权限 gated的设置器粘贴凭证——权限校验基于
AccessControl.hasPermission(state, caller, #admin)
。凭证仅由canister存储,绝不会返回给前端。
⚠️ 切勿使用“首次调用者获取所有权”的机制来保护设置器。在IC网络中,所有未认证调用者都是同一个匿名主体,因此如果匿名调用者率先获取所有权,所有匿名调用者都能通过
caller == owner
校验并覆盖凭证——而这会产生实际费用。
canister仅通过
config.auth = ?#basicAuth { user; password }
将凭证传递给客户端,每个方法都会将其转换为
Authorization: Basic …
请求头。没有方法会将凭证作为参数接收,也不会将其放入URL,因此不会通过日志中的查询字符串泄露。

Outcalls are already non-replicated — and this CORRECTS earlier guidance

出站调用已默认非复制式——修正早期指导意见

defaultConfig
ships
is_replicated = ?false
, so anything derived from it by record update is correct as-is. Nothing to remember, nothing to add.
⚠️ Do not set it to
?true
or
null
, and disregard any older advice to do so.
An older version of this SKILL claimed writes should stay replicated "so IC consensus dedups retries". That is false and expensive. A replicated outcall is performed by every node in the subnet: the request is sent ~13 times, so ~13 SMS are sent and ~13 are billed, the credential leaves every node, and consensus fails anyway because Twilio stamps each reply with a unique
sid
(so the responses never agree byte-for-byte). This is the same defect that produced ~13 duplicate emails via the Gmail connector and drove
slack-client
0.1.0.
Reads (
fetch*
/
list*
) are equally fine non-replicated: one node's view of a message log is what you want, and it is the cheaper path.
defaultConfig
默认设置
is_replicated = ?false
,因此通过记录更新派生的任何配置均无需额外修改。无需额外记忆或添加设置。
⚠️ 请勿将其设置为
?true
null
,并忽略任何要求这么做的旧建议
。此技能的旧版本声称写入操作应保持复制式“以便IC共识去重重试”。这是错误且昂贵的。复制式出站调用会由子网中的每个节点执行:请求会被发送约13次,因此约13条SMS会被发送并计费,凭证会从每个节点传出,且由于Twilio会为每个回复标记唯一的
sid
(因此回复内容无法完全一致),共识最终会失败。这与Gmail连接器产生约13封重复邮件、以及
slack-client
0.1.0版本的问题根源相同。
读取操作(
fetch*
/
list*
)同样适合使用非复制式:单个节点的消息日志视图就是所需内容,且成本更低。

Backend

后端

Add dependencies

添加依赖

The admin gate in the recipe below needs the authorization component alongside the client:
bash
mops add twilio-client@0.1.2
mops add caffeineai-authorization@1.0.1
以下方案中的管理员权限校验需要授权组件与客户端配合使用:
bash
mops add twilio-client@0.1.2
mops add caffeineai-authorization@1.0.1

Calling shape — free functions or the class facade

调用形式——自由函数或类 facade

Every module offers both. The free function takes
config
first and is
async*
; the
module class
captures
config
and is
async
:
<!-- motoko-check:skip -->
motoko
// Illustrative sketch, not a file to copy: `cfg`/`accountSid` are assumed to
// exist and the argument lists are elided. Marked motoko-check:skip for that
// reason — the compiled examples are the three mixins below.
import MessageApi "mo:twilio-client/Apis/Api20100401MessageApi";

// free function — config passed explicitly
let m = await* MessageApi.createMessage(cfg, accountSid, /* … */);

// class facade — config captured once
let messages = MessageApi.Api20100401MessageApi(cfg);
let m2 = await messages.createMessage(accountSid, /* … */);
All parameters are positional and there are 27 of them on
createMessage
.
Pass
""
/
false
/
0
/
0.0
/
[]
/
null
for the ones you do not use — the optional enum parameters are
?T
precisely so that
null
omits them from the wire. Count carefully; a misplaced empty string silently sends the wrong field. The order is:
config, accountSid, to, statusCallback, applicationSid, maxPrice, provideFeedback, attempt, validityPeriod, forceDelivery, contentRetention, addressRetention, smartEncoded, persistentAction, trafficType, shortenUrls, scheduleType, sendAt, sendAsMms, contentVariables, riskCheck, from, fallbackFrom, messagingServiceSid, body, mediaUrl, contentSid
每个模块都提供两种调用方式。自由函数需先传入
config
,且为
async*
module class
会捕获
config
,且为
async
<!-- motoko-check:skip -->
motoko
// 示例草图,无需复制:假设`cfg`/`accountSid`已存在,参数列表已省略。标记为motoko-check:skip
import MessageApi "mo:twilio-client/Apis/Api20100401MessageApi";

// 自由函数——显式传入config
let m = await* MessageApi.createMessage(cfg, accountSid, /* … */);

// 类 facade——一次性捕获config
let messages = MessageApi.Api20100401MessageApi(cfg);
let m2 = await messages.createMessage(accountSid, /* … */);
createMessage
共有27个位置参数
。对于未使用的参数,请传入
""
/
false
/
0
/
0.0
/
[]
/
null
——可选枚举参数为
?T
类型,因此传入
null
会在请求中省略该字段。请仔细核对参数顺序; misplaced的空字符串会静默发送错误字段。参数顺序如下:
config, accountSid, to, statusCallback, applicationSid, maxPrice, provideFeedback, attempt, validityPeriod, forceDelivery, contentRetention, addressRetention, smartEncoded, persistentAction, trafficType, shortenUrls, scheduleType, sendAt, sendAsMms, contentVariables, riskCheck, from, fallbackFrom, messagingServiceSid, body, mediaUrl, contentSid

The recipe

实现方案

motoko
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinTwilioConfig "mixins/twilio-config";
import MixinTwilioMessaging "mixins/twilio-messaging";

actor {
  let accessControlState = AccessControl.initState();
  include MixinAuthorization(accessControlState, null);

  // Admin-held Twilio credentials — never returned to the frontend.
  let twilioConfig = {
    var accountSid : Text = "";   // AC… — also a positional arg on every v2010 call
    var keySid : Text = "";       // SK… (or the Account SID again, in dev)
    var keySecret : Text = "";    // the API-key secret (or the Auth Token, in dev)
    var fromNumber : Text = "";   // E.164, e.g. "+15551234567"
  };
  include MixinTwilioConfig(accessControlState, twilioConfig);
  include MixinTwilioMessaging(twilioConfig);
};
motoko
import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";

mixin (
  accessControlState : AccessControl.AccessControlState,
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // All THREE are required, and this must agree with the guard in
  // twilio-messaging.mo: `keySid` is the Basic-Auth *username*, so a blank one
  // means every request goes out unauthenticated and Twilio answers 20003 —
  // while the UI cheerfully reports "Configured".
  public query func isTwilioConfigured() : async Bool {
    twilioConfig.accountSid.size() > 0 and twilioConfig.keySid.size() > 0 and twilioConfig.keySecret.size() > 0;
  };

  // The sending number is not a secret — the UI may display it.
  public query func getTwilioFromNumber() : async Text {
    twilioConfig.fromNumber;
  };

  // Admin-only. NOTE `#admin` — never a first-caller-claims-ownership check,
  // which the shared anonymous principal would defeat.
  public shared ({ caller }) func setTwilioCredentials(
    accountSid : Text,
    keySid : Text,
    keySecret : Text,
  ) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set Twilio credentials");
    };
    twilioConfig.accountSid := accountSid;
    twilioConfig.keySid := keySid;
    twilioConfig.keySecret := keySecret;
  };

  public shared ({ caller }) func setTwilioFromNumber(number : Text) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set the sending number");
    };
    twilioConfig.fromNumber := number;
  };
};
motoko
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import { createMessage } "mo:twilio-client/Apis/Api20100401MessageApi";
import { defaultConfig; type Config } "mo:twilio-client/Config";

mixin (
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // Credentials ride config.auth; defaultConfig is already non-replicated.
  func twilioClientConfig() : Config {
    {
      defaultConfig with
      auth = ?#basicAuth { user = twilioConfig.keySid; password = twilioConfig.keySecret };
      max_response_bytes = ?(200_000 : Nat64);
    };
  };

  /// Send an SMS to `to` (E.164). Returns the message SID.
  public shared ({ caller }) func sendSms(to : Text, body : Text) : async Text {
    if (caller.isAnonymous()) Runtime.trap("Sign in to send messages");
    // Same three-way check as isTwilioConfigured(): accountSid goes in the URL
    // path, keySid is the Basic-Auth user, keySecret the password. Missing any
    // one of them fails at Twilio, not here, so check before spending cycles.
    if (
      twilioConfig.accountSid.size() == 0 or twilioConfig.keySid.size() == 0 or twilioConfig.keySecret.size() == 0
    ) {
      Runtime.trap("Twilio is not configured (an admin must set all three credentials)");
    };
    let msg = await* createMessage(
      twilioClientConfig(),
      twilioConfig.accountSid, // accountSid — in the URL path, not the credential
      to,                      // to (E.164)
      "", "",                  // statusCallback, applicationSid
      0.0,                     // maxPrice (0 = no cap)
      false,                   // provideFeedback
      0, 0,                    // attempt, validityPeriod
      false,                   // forceDelivery
      null, null,              // contentRetention, addressRetention (omitted)
      false,                   // smartEncoded
      [],                      // persistentAction
      null,                    // trafficType (omitted)
      false,                   // shortenUrls
      null,                    // scheduleType — MUST be null for an immediate send
      "",                      // sendAt (scheduled sends only)
      false,                   // sendAsMms
      "",                      // contentVariables
      null,                    // riskCheck (omitted)
      twilioConfig.fromNumber, // from  (use EITHER from OR messagingServiceSid)
      "",                      // fallbackFrom
      "",                      // messagingServiceSid
      body,                    // body
      [],                      // mediaUrl (set for MMS)
      "",                      // contentSid (Content API templates)
    );
    // `sid` is optional in the generated model because the spec marks it
    // nullable, though Twilio always sets it on a successful create. Fall back
    // to "" rather than trapping: the outcall has already happened, so a trap
    // would roll back this canister's own state while the SMS stays delivered.
    switch (msg.sid) { case (?sid) sid; case null "" };
  };
};
For MMS:
mediaUrl = ["https://example.com/image.jpg"]
and
sendAsMms = true
. To send through a Messaging Service, leave
from = ""
and set
messagingServiceSid
instead.
motoko
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinTwilioConfig "mixins/twilio-config";
import MixinTwilioMessaging "mixins/twilio-messaging";

actor {
  let accessControlState = AccessControl.initState();
  include MixinAuthorization(accessControlState, null);

  // 管理员持有的Twilio凭证——绝不会返回给前端。
  let twilioConfig = {
    var accountSid : Text = "";   // AC…——同时是每个v2010调用的位置参数
    var keySid : Text = "";       // SK…(开发环境中可再次使用账号SID)
    var keySecret : Text = "";    // API密钥的Secret(开发环境中可使用认证令牌)
    var fromNumber : Text = "";   // E.164格式,例如 "+15551234567"
  };
  include MixinTwilioConfig(accessControlState, twilioConfig);
  include MixinTwilioMessaging(twilioConfig);
};
motoko
import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";

mixin (
  accessControlState : AccessControl.AccessControlState,
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // 三个值均为必填,且需与twilio-messaging.mo中的校验逻辑一致:`keySid`是Basic-Auth的用户名,因此空值会导致所有请求未认证,Twilio会返回20003——而UI会错误地显示“已配置”。
  public query func isTwilioConfigured() : async Bool {
    twilioConfig.accountSid.size() > 0 and twilioConfig.keySid.size() > 0 and twilioConfig.keySecret.size() > 0;
  };

  // 发送号码并非机密——UI可显示该号码。
  public query func getTwilioFromNumber() : async Text {
    twilioConfig.fromNumber;
  };

  // 仅管理员可用。注意使用`#admin`权限——绝不要使用“首次调用者获取所有权”的校验,这会被共享匿名主体绕过。
  public shared ({ caller }) func setTwilioCredentials(
    accountSid : Text,
    keySid : Text,
    keySecret : Text,
  ) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set Twilio credentials");
    };
    twilioConfig.accountSid := accountSid;
    twilioConfig.keySid := keySid;
    twilioConfig.keySecret := keySecret;
  };

  public shared ({ caller }) func setTwilioFromNumber(number : Text) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set the sending number");
    };
    twilioConfig.fromNumber := number;
  };
};
motoko
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import { createMessage } "mo:twilio-client/Apis/Api20100401MessageApi";
import { defaultConfig; type Config } "mo:twilio-client/Config";

mixin (
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // 凭证通过config.auth传递;defaultConfig已默认非复制式。
  func twilioClientConfig() : Config {
    {
      defaultConfig with
      auth = ?#basicAuth { user = twilioConfig.keySid; password = twilioConfig.keySecret };
      max_response_bytes = ?(200_000 : Nat64);
    };
  };

  /// 向`to`(E.164格式)发送SMS。返回消息SID。
  public shared ({ caller }) func sendSms(to : Text, body : Text) : async Text {
    if (caller.isAnonymous()) Runtime.trap("Sign in to send messages");
    // 与isTwilioConfigured()相同的三重校验:accountSid会放入URL路径,keySid是Basic-Auth用户名,keySecret是密码。缺少任何一个都会在Twilio端失败,因此在消耗cycles前先进行校验。
    if (
      twilioConfig.accountSid.size() == 0 or twilioConfig.keySid.size() == 0 or twilioConfig.keySecret.size() == 0
    ) {
      Runtime.trap("Twilio is not configured (an admin must set all three credentials)");
    };
    let msg = await* createMessage(
      twilioClientConfig(),
      twilioConfig.accountSid, // accountSid——位于URL路径,而非凭证中
      to,                      // to(E.164格式)
      "", "",                  // statusCallback, applicationSid
      0.0,                     // maxPrice(0表示无上限)
      false,                   // provideFeedback
      0, 0,                    // attempt, validityPeriod
      false,                   // forceDelivery
      null, null,              // contentRetention, addressRetention(省略)
      false,                   // smartEncoded
      [],                      // persistentAction
      null,                    // trafficType(省略)
      false,                   // shortenUrls
      null,                    // scheduleType——立即发送时必须为null
      "",                      // sendAt(仅定时发送时使用)
      false,                   // sendAsMms
      "",                      // contentVariables
      null,                    // riskCheck(省略)
      twilioConfig.fromNumber, // from (使用from或messagingServiceSid中的一个)
      "",                      // fallbackFrom
      "",                      // messagingServiceSid
      body,                    // body
      [],                      // mediaUrl(发送MMS时设置)
      "",                      // contentSid(Content API模板)
    );
    // 生成的模型中`sid`为可选类型,因为规范标记其可为null,但Twilio在创建成功时总会设置该值。返回空字符串而非抛出异常:出站调用已完成,因此抛出异常会回滚canister自身状态,但SMS已被发送。
    switch (msg.sid) { case (?sid) sid; case null "" };
  };
};
发送MMS时:设置
mediaUrl = ["https://example.com/image.jpg"]
sendAsMms = true
。如需通过消息服务发送,将
from = ""
并设置
messagingServiceSid

Addressing — E.164, and which sender

地址规范——E.164格式与发送方选择

  • to
    must be E.164
    :
    +
    , country code, no spaces, dashes or parentheses —
    "+15551234567"
    .
    "555-1234"
    fails with
    21211
    . Normalize in the frontend and again in the canister; do not trust either alone.
  • from
    vs
    messagingServiceSid
    — exactly one.
    Setting both is an error. A bare
    from
    number is fine for non-US traffic and demos; US-bound production traffic should go through a Messaging Service (sender pool, sticky sender, and it is what A2P registration attaches to).
  • The sending number needs the SMS capability, which not every purchasable number has. Filter on it when browsing
    Api20100401AvailablePhoneNumberCountryApi
    .
  • to
    必须为E.164格式
    :以
    +
    开头,后跟国家代码,无空格、短横线或括号——例如
    "+15551234567"
    "555-1234"
    会失败并返回
    21211
    。请在前端和canister中分别进行格式标准化,不要单独信任其中一方。
  • from
    messagingServiceSid
    ——二选一
    。同时设置两者会报错。单独使用
    from
    号码适用于非美国流量和演示场景;面向美国的生产流量应通过消息服务发送(支持发送方池、粘性发送方,且A2P注册需绑定到消息服务)。
  • 发送号码需具备SMS功能,并非所有可购买的号码都具备此功能。浏览
    Api20100401AvailablePhoneNumberCountryApi
    时请筛选该功能。

US A2P 10DLC — three resources, in this order

美国A2P 10DLC——三个资源,按顺序配置

Before any US long code can text US destinations, all three must exist. Without them US carriers reject the traffic outright.
  1. Brand registration
    MessagingV1BrandRegistrationApi.createBrandRegistrations
    , referencing Trust Hub
    customerProfileBundleSid
    +
    a2PProfileBundleSid
    (created out of band). Pass
    mock = true
    in dev to skip the fee. Status starts
    PENDING
    and settles to
    APPROVED
    /
    FAILED
    over hours to days; it fails if business details are incomplete, inconsistently formatted, or do not match registry data.
  2. A2P campaign
    MessagingV1UsAppToPersonApi.createUsAppToPerson
    , referencing both the Messaging Service and the brand. Most onboarding failures land here. T-Mobile rejects campaigns whose
    messageFlow
    does not describe opt-in, or whose
    messageSamples
    do not match the declared
    usAppToPersonUsecase
    .
  3. Number → service assignment
    MessagingV1PhoneNumberApi.createPhoneNumber(cfg, serviceSid, phoneNumberSid)
    . A number lives in exactly one Messaging Service at a time; reassignment needs
    deletePhoneNumber
    first.
Registration deadline in force: campaigns without working
privacyPolicyUrl
and
termsAndConditionsUrl
hard-400 since 2026-06-30. Both are positional arguments on
createUsAppToPerson
and
""
fails; the URLs must resolve to public HTTPS pages, because Twilio fetches them during registration.
Toll-free numbers use a separate flow —
MessagingV1TollfreeVerificationApi
— not A2P.
任何美国长途号码要向美国号码发送短信,必须完成以下三个资源的配置。否则美国运营商会直接拒绝流量。
  1. 品牌注册——调用
    MessagingV1BrandRegistrationApi.createBrandRegistrations
    ,引用Trust Hub的
    customerProfileBundleSid
    +
    a2PProfileBundleSid
    (需提前创建)。开发环境中传入
    mock = true
    可跳过费用。状态初始为
    PENDING
    ,数小时至数天后会变为
    APPROVED
    FAILED
    ;如果企业信息不完整、格式不一致或与注册数据不匹配,注册会失败。
  2. A2P活动——调用
    MessagingV1UsAppToPersonApi.createUsAppToPerson
    ,引用消息服务和品牌。大多数上线失败都出现在此步骤。如果
    messageFlow
    未描述 opt-in流程,或
    messageSamples
    与声明的
    usAppToPersonUsecase
    不匹配,T-Mobile会拒绝该活动。
  3. 号码→服务绑定——调用
    MessagingV1PhoneNumberApi.createPhoneNumber(cfg, serviceSid, phoneNumberSid)
    。一个号码同一时间只能属于一个消息服务;重新绑定需先调用
    deletePhoneNumber
注册截止要求:自2026年6月30日起,未配置有效
privacyPolicyUrl
termsAndConditionsUrl
的活动会直接返回400错误。两者均为
createUsAppToPerson
的位置参数,传入
""
会失败;URL必须指向可公开访问的HTTPS页面,因为Twilio会在注册过程中获取这些页面内容。
免费号码使用独立流程——
MessagingV1TollfreeVerificationApi
——无需A2P注册。

Available API surface

可用API范围

Documented and messaging-focused (this recipe):
ModuleFor
Api20100401MessageApi
send / fetch / list / update / delete messages
Api20100401MediaApi
,
…MediaInstanceApi
MMS media on a message
Api20100401IncomingPhoneNumberApi
(+
Local
/
Mobile
/
TollFree
)
numbers you own; delete = release
Api20100401AvailablePhoneNumberCountryApi
browse numbers to buy
Api20100401BalanceApi
,
…AccountApi
account balance and account records
Api20100401UserDefinedMessageApi
(+
Subscription
)
user-defined message events
MessagingV1ServiceApi
Messaging Services (sender pools)
MessagingV1BrandRegistrationApi
(+
Otp
,
BrandVettingApi
)
A2P brand
MessagingV1UsAppToPersonApi
(+
UsecaseApi
)
A2P campaigns
MessagingV1PhoneNumberApi
,
…ShortCodeApi
,
…AlphaSenderApi
,
…ChannelSenderApi
sender pool membership
MessagingV1TollfreeVerificationApi
toll-free verification
MessagingV1Linkshortening*
,
…DomainConfig*
,
…DomainCertsApi
branded link shortening
MessagingV1DeactivationsApi
carrier deactivation list
Not in the package (pruned from the generated surface): calls, recordings, conferences, participants, queues, applications, SIP domains and credentials, usage records and triggers, addresses, keys, tokens, balance transactions. The package ships the messaging surface only — for anything above, this connector is not the path.
已文档化且聚焦消息服务(本方案涉及):
模块用途
Api20100401MessageApi
发送/获取/列出/更新/删除消息
Api20100401MediaApi
,
…MediaInstanceApi
消息中的MMS媒体资源
Api20100401IncomingPhoneNumberApi
(+
Local
/
Mobile
/
TollFree
已拥有的号码;删除操作即释放号码
Api20100401AvailablePhoneNumberCountryApi
浏览可购买的号码
Api20100401BalanceApi
,
…AccountApi
账号余额和账号记录
Api20100401UserDefinedMessageApi
(+
Subscription
用户自定义消息事件
MessagingV1ServiceApi
消息服务(发送方池)
MessagingV1BrandRegistrationApi
(+
Otp
,
BrandVettingApi
A2P品牌
MessagingV1UsAppToPersonApi
(+
UsecaseApi
A2P活动
MessagingV1PhoneNumberApi
,
…ShortCodeApi
,
…AlphaSenderApi
,
…ChannelSenderApi
发送方池成员管理
MessagingV1TollfreeVerificationApi
免费号码验证
MessagingV1Linkshortening*
,
…DomainConfig*
,
…DomainCertsApi
品牌化短链接
MessagingV1DeactivationsApi
运营商停用列表
不在本包范围内(从生成的API范围中精简):通话、录音、会议、参与者、队列、应用、SIP域和凭证、使用记录和触发器、地址、密钥、令牌、余额交易。本包仅包含消息服务相关功能——如需上述功能,请勿使用本连接器。

Errors and pagination

错误处理与分页

  • Methods return the decoded record on 2xx and
    throw Error.reject("HTTP <status> body[…]: …")
    on 4xx/5xx.
    diagnostics
    is on, so the reject text carries Twilio's own error body (
    code
    ,
    message
    ,
    more_info
    ). Wrap in
    try { … } catch (e) { Error.message(e) }
    .
  • Codes worth mapping to real UI text: 20003 authenticate failed (bad credential), 21211 invalid
    To
    , 21408 region not permissioned (enable the destination country's geo permissions in the console), 21608 unverified recipient on a trial account, 21610 recipient has unsubscribed (STOP), 21703 sender pool exhausted, 21704 the Messaging Service has no numbers, 21714 pool size capped.
  • A 2xx does not mean delivered.
    createMessage
    returns
    status = #queued
    or
    #accepted
    ; delivery is asynchronous. Poll
    fetchMessage
    for
    #delivered
    /
    #undelivered
    /
    #failed
    and read
    error_code
    , or configure a
    statusCallback
    URL (needs an inbound HTTP endpoint — out of scope here).
  • Pagination differs between the two API versions. v2010 lists —
    listMessage
    and every other
    Api20100401*
    list — return top-level
    next_page_uri
    /
    previous_page_uri
    (
    ?Text
    , and a path such as
    /2010-04-01/…
    , not a full URL). Messaging v1 lists (
    listService
    ,
    listPhoneNumber
    , the A2P registries) instead nest pagination under
    meta
    , as
    next_page_url
    /
    previous_page_url
    (full URLs) plus
    page_size
    . Only 10 of the 70 list responses use the
    meta
    form;
    listMessage
    is not one of them. The
    meta
    field is typed
    ?ListAlphaSenderResponseMeta
    on every v1 list, including
    ListServiceResponse
    — identical records are deduplicated to one shared module at codegen time, so the name reflects whichever list sorted first, not the endpoint you called.
  • pageSize
    defaults to 50 and caps at 1000. Bound every list call — an unbounded
    listMessage
    on a busy account will blow
    max_response_bytes
    .
Reading the two shapes:
<!-- motoko-check:skip -->
motoko
// Illustrative sketch, not a file to copy — `res` is assumed to be the decoded
// list response. Marked motoko-check:skip for that reason.

// v2010 (listMessage and every other Api20100401* list): top-level, a path
switch (res.next_page_uri) { case (?path) { /* fetch the next page */ }; case null {} };

// Messaging v1 (listService, listPhoneNumber, the A2P registries): nested, a full URL
switch (res.meta) { case (?m) { m.next_page_url }; case null null };
  • 方法在2xx响应时返回解码后的记录,在4xx/5xx响应时抛出
    Error.reject("HTTP <status> body[…]: …")
    diagnostics
    已启用,因此拒绝信息会包含Twilio自身的错误体(
    code
    message
    more_info
    )。请使用
    try { … } catch (e) { Error.message(e) }
    进行捕获。
  • 值得映射为UI文本的错误码:20003认证失败(凭证错误)、21211无效
    To
    地址、21408区域未授权(需在控制台启用目标国家的地理权限)、21608试用账号的收件人未验证、21610收件人已退订(回复STOP)、21703发送方池耗尽、21704消息服务无可用号码、21714池大小受限。
  • 2xx响应不代表消息已送达
    createMessage
    返回的
    status
    #queued
    #accepted
    ;送达是异步过程。请轮询
    fetchMessage
    获取
    #delivered
    /
    #undelivered
    /
    #failed
    状态并读取
    error_code
    ,或配置
    statusCallback
    URL(需要入站HTTP端点——不在本连接器范围内)。
  • 两个API版本的分页逻辑不同。v2010的列表接口——
    listMessage
    及其他所有
    Api20100401*
    列表接口——返回顶层
    next_page_uri
    /
    previous_page_uri
    ?Text
    类型,为路径如
    /2010-04-01/…
    ,而非完整URL)。Messaging v1的列表接口(
    listService
    listPhoneNumber
    、A2P注册相关)则将分页信息嵌套在
    meta
    字段下,包含
    next_page_url
    /
    previous_page_url
    (完整URL)及
    page_size
    。70个列表响应中仅有10个使用
    meta
    格式;
    listMessage
    不在其中
    meta
    字段的类型为
    ?ListAlphaSenderResponseMeta
    ,适用于所有v1列表接口,包括
    ListServiceResponse
    ——代码生成时会将相同记录去重为一个共享模块,因此名称反映的是首个排序的列表接口,而非当前调用的端点。
  • pageSize
    默认值为50,最大值为1000。请为所有列表调用设置边界——繁忙账号的无限制
    listMessage
    会超出
    max_response_bytes
    限制。
两种分页格式的读取示例:
<!-- motoko-check:skip -->
motoko
// 示例草图,无需复制——假设`res`为解码后的列表响应。标记为motoko-check:skip

// v2010(listMessage及其他所有Api20100401*列表接口):顶层路径
switch (res.next_page_uri) { case (?path) { /* 获取下一页 */ }; case null {} };

// Messaging v1(listService、listPhoneNumber、A2P注册相关):嵌套完整URL
switch (res.meta) { case (?m) { m.next_page_url }; case null null };

Field gotchas

字段注意事项

  • usecase
    on
    createService
    is
    Text
    , not a variant
    . Valid:
    notifications
    ,
    marketing
    ,
    verification
    ,
    discussion
    ,
    poll
    ,
    undeclared
    . Anything else 400s.
  • usAppToPersonUsecase
    is a different, brand-tier-dependent enum — query
    MessagingV1UsAppToPersonUsecaseApi.fetchUsAppToPersonUsecase
    for what a given brand may use.
  • Optional enum arguments are
    ?T
    — pass
    null
    to omit them, and prefer that.
    The variants are closed:
    contentRetention
    #retain
    /
    #discard
    ,
    addressRetention
    #retain
    /
    #obfuscate
    ,
    trafficType
    #free
    ,
    scheduleType
    #fixed
    ,
    riskCheck
    #enable
    /
    #disable
    . There is no
    #Text
    escape hatch
    — a value the spec does not list cannot be expressed. Passing
    ?#fixed
    for
    scheduleType
    on an immediate send is a 400: Twilio reads it as a scheduled message and then finds no
    SendAt
    .
    null
    is the correct value for every one of these unless you specifically want the behaviour.
  • maxPrice
    is omitted when
    0.0
    , which is what you want.
    Sending
    MaxPrice=0
    would cap the message price at zero and make Twilio refuse paid delivery; omitting it means "no cap". Pass
    0.0
    to omit.
  • xTwilioApiVersion
    (on the
    UsAppToPerson
    methods) — pass
    ""
    unless Twilio support asks otherwise.
  • Throughput is per sender: long code 1 message/second, toll-free ~3, international long code ~10, short code 100. Per-number MPS cannot be raised — scale by adding numbers to the Messaging Service's sender pool.
  • stickySender
    /
    areaCodeGeomatch
    are US + Canada only.
  • Config.baseUrl
    is unused. Every operation carries a hardcoded host (
    api.twilio.com
    for v2010,
    messaging.twilio.com
    for v1), pinned at codegen time from the merged spec. Do not set it and do not expect it to redirect traffic.
  • createService
    中的
    usecase
    为**
    Text
    类型,而非枚举**。有效值:
    notifications
    marketing
    verification
    discussion
    poll
    undeclared
    。其他值会返回400错误。
  • usAppToPersonUsecase
    是一个不同的、依赖品牌等级的枚举——请调用
    MessagingV1UsAppToPersonUsecaseApi.fetchUsAppToPersonUsecase
    获取指定品牌可用的枚举值。
  • 可选枚举参数为
    ?T
    类型——传入
    null
    以省略字段,优先使用此方式
    。枚举为封闭类型:
    contentRetention
    #retain
    /
    #discard
    addressRetention
    #retain
    /
    #obfuscate
    trafficType
    #free
    scheduleType
    #fixed
    riskCheck
    #enable
    /
    #disable
    没有
    #Text
    逃逸机制
    ——规范中未列出的值无法表示。立即发送时传入
    ?#fixed
    作为
    scheduleType
    会返回400错误:Twilio会将其视为定时消息,但未找到
    SendAt
    字段。除非明确需要对应行为,否则这些参数的正确值均为
    null
  • maxPrice
    为0.0时会被省略,这正是所需行为
    。发送
    MaxPrice=0
    会将消息价格上限设为0,导致Twilio拒绝付费送达;省略该字段表示“无上限”。传入0.0即可省略该字段。
  • xTwilioApiVersion
    UsAppToPerson
    方法中的参数)——除非Twilio支持团队要求,否则传入
    ""
  • 吞吐量按发送方限制:长途号码1条/秒、免费号码约3条/秒、国际长途号码约10条/秒、短码100条/秒。单号码的MPS无法提升——需通过向消息服务的发送方池添加号码来扩展。
  • stickySender
    /
    areaCodeGeomatch
    仅适用于美国和加拿大。
  • Config.baseUrl
    未被使用
    。每个操作都带有硬编码的主机(v2010为
    api.twilio.com
    ,v1为
    messaging.twilio.com
    ),在代码生成时从合并的规范中固定。请勿设置该字段,也不要期望其能重定向流量。

Frontend

前端

Twilio needs no OAuth: the credential is a long-lived pair the admin pastes, so there is no redirect URI, no
/connect/twilio
route, and no per-user handshake. Do not build one. What a Twilio build MUST ship is the page that lets the admin get and enter the credentials — acceptance criteria, not suggestions; a build missing them is broken, not merely incomplete:
  • The credentials page exists and is reachable. A "send SMS" feature with nowhere to enter a credential is unusable. A signed-in admin must reach
    /settings/twilio
    from the nav or from the not-configured prompt.
  • The console steps are in the UI, not only in the chat reply — the admin returns weeks later, after the chat is gone.
  • The API-Key secret is shown once by Twilio. Say so next to the input, or admins will navigate away and have to create a second key.
  1. A login flow — required.
    setTwilioCredentials
    gates on
    #admin
    , so the app needs non-anonymous callers. Take login,
    useInternetIdentity
    /
    useActor
    plumbing and the admin-role gate from
    extension-authorization
    .
  2. An admin settings page
    /settings/twilio
    (admin-gated). Required:
    • A "How to get your Twilio credentials" panel above the inputs, framed as a one-time ~5-minute setup, with these numbered steps (the completion message must repeat them verbatim):
      1. sign in at https://console.twilio.com;
      2. copy the Account SID (
        AC…
        ) from the dashboard;
      3. Account → API keys & tokens → Create API key (Standard); copy the SID (
        SK…
        ) and the Secretthe Secret is displayed only once;
      4. Phone Numbers → Manage → Buy a number with the SMS capability;
      5. paste the three values plus the number below and save;
      6. on a trial account, verify each recipient under Verified Caller IDs. Include a convenience link that opens the Twilio console.
    • Three inputs: Account SID (plain text — not a secret), Key SID, Key Secret (password input). Bound to
      setTwilioCredentials
      ; clear the secret on success; keep the form re-submittable, because keys get rotated.
    • A sending-number field bound to
      setTwilioFromNumber
      , with an E.164 example (
      +15551234567
      ) beside it and client-side validation.
    • Status driven by
      isTwilioConfigured()
      (
      Bool
      ) — "Configured" / "Not configured". That predicate requires all three values, Key SID included: it is the Basic-Auth username, so a blank one means every request is unauthenticated and Twilio answers
      20003
      while the page claims to be configured. Never render the secret back, not even masked. The sending number may be displayed (
      getTwilioFromNumber
      ); it is not a secret.
    • Make the page reachable. The shared Layout nav MUST link here when
      isCallerAdmin
      is true and hide it otherwise. Add the link where the nav is defined, not inside this page.
  3. Empty-state nudges. When
    isTwilioConfigured()
    is
    false
    , never render a dead "Send" button: admins get a "Set up Twilio" link to
    /settings/twilio
    ; non-anonymous non-admins get an explanation — e.g. "Texting isn't set up yet — an administrator needs to add Twilio credentials in Settings."
  4. Translate Twilio's errors. Failures arrive as rejected calls carrying Twilio's
    code
    . Map at least these to an action rather than showing the raw reject:
    • 20003
      → "The Twilio credentials are wrong — an admin should re-paste them"
    • 21211
      → "That phone number isn't valid — use the +15551234567 format"
    • 21408
      → "Texting that country isn't enabled on this Twilio account"
    • 21608
      → "On a trial account the recipient must be verified in Twilio first"
    • 21610
      → "That number has replied STOP and cannot be texted"
  5. Never promise delivery. A successful call means queued, not delivered. Word the UI accordingly ("Message queued") and, if delivery matters, show the polled
    status
    from
    fetchMessage
    .
Suggested route layout:
/                 →  Main UI (any signed-in user; empty-state when unconfigured)
/settings/twilio  →  Admin credentials + sending number (admin-only)
Twilio无需OAuth:凭证是管理员粘贴的长期密钥对,因此无需重定向URI、
/connect/twilio
路由或每用户握手流程。请勿构建此类功能。Twilio构建必须交付的是让管理员获取并输入凭证的页面——这是验收标准,而非建议;缺少该页面的构建是损坏的,而非仅不完整
  • 凭证页面存在且可访问。没有凭证输入入口的“发送SMS”功能是无法使用的。已登录的管理员必须能从导航栏或未配置提示中访问
    /settings/twilio
  • 控制台操作步骤需内嵌在UI中,而非仅存在于聊天回复中——管理员可能在数周后返回,此时聊天记录已丢失。
  • API密钥的Secret仅由Twilio显示一次。请在输入框旁注明这一点,否则管理员离开页面后将不得不重新创建密钥。
  1. 登录流程——必填
    setTwilioCredentials
    基于
    #admin
    权限校验,因此应用需要非匿名调用者。请从
    extension-authorization
    中获取登录、
    useInternetIdentity
    /
    useActor
    plumbing及管理员角色校验逻辑。
  2. 管理员设置页面——
    /settings/twilio
    (仅管理员可访问)。必填内容:
    • “如何获取Twilio凭证”面板,位于输入框上方,描述为一次约5分钟的一次性设置,包含以下编号步骤(完成消息必须逐字重复这些步骤):
      1. 登录https://console.twilio.com
      2. 从仪表板复制账号SID
        AC…
        );
      3. 进入Account → API keys & tokens → Create API key(选择Standard);复制SID
        SK…
        )和Secret——Secret仅显示一次
      4. 进入Phone Numbers → Manage → Buy a number,勾选SMS功能;
      5. 将三个值及号码粘贴到下方并保存;
      6. 试用账号需在Verified Caller IDs下验证每个收件人。 请添加便捷链接以打开Twilio控制台。
    • 三个输入框:账号SID(明文——非机密)、密钥SID、密钥Secret(密码输入框)。绑定到
      setTwilioCredentials
      ;成功后清空Secret输入框;保持表单可重新提交,因为密钥可能需要轮换。
    • 发送号码输入框,绑定到
      setTwilioFromNumber
      ,旁附E.164格式示例(
      +15551234567
      )并进行客户端校验。
    • 基于
      isTwilioConfigured()
      Bool
      )显示状态——“已配置”/“未配置”。该断言要求三个值全部存在,包括密钥SID:它是Basic-Auth的用户名,因此空值会导致所有请求未认证,Twilio返回20003,但页面会显示“已配置”。绝不要回显Secret,即使是掩码形式。发送号码可显示(通过
      getTwilioFromNumber
      );它并非机密。
    • 确保页面可访问。共享布局导航栏必须在
      isCallerAdmin
      为true时显示该链接,否则隐藏。请在导航栏定义处添加链接,而非在该页面内部添加。
  3. 空状态提示。当
    isTwilioConfigured()
    false
    时,请勿显示无效的“发送”按钮:管理员会看到指向
    /settings/twilio
    的“设置Twilio”链接;非匿名非管理员用户会看到说明——例如“短信功能尚未设置——管理员需在设置中添加Twilio凭证”。
  4. 翻译Twilio错误信息。失败会以调用被拒绝的形式返回,携带Twilio的
    code
    。请至少将以下错误码映射为操作提示,而非显示原始拒绝信息:
    • 20003
      → “Twilio凭证错误——管理员应重新粘贴”
    • 21211
      → “电话号码无效——请使用+15551234567格式”
    • 21408
      → “此Twilio账号未启用向该国家发送短信的权限”
    • 21608
      → “试用账号的收件人需先在Twilio中验证”
    • 21610
      → “该号码已回复STOP,无法发送短信”
  5. 不要承诺消息已送达。调用成功仅表示消息已“排队”,而非已送达。UI措辞需相应调整(例如“消息已排队”);如果送达状态重要,请显示从
    fetchMessage
    轮询到的
    status
建议路由布局:
/                 → 主UI(任何已登录用户;未配置时显示空状态)
/settings/twilio  → 管理员凭证+发送号码设置(仅管理员可访问)

No /connect/twilio: Twilio uses pasted long-lived credentials, not a redirect flow.

无需/connect/twilio:Twilio使用粘贴的长期凭证,而非重定向流程。

undefined
undefined

What the composer must tell the Caffeine user

编写者必须告知Caffeine用户的内容

The app cannot send anything until a human creates a Twilio account, buys a number and pastes credentials — so the completion message is part of the deliverable, not a summary of it. It MUST contain, in this order:
  1. That credentials are required, and who enters them — an admin, on
    /settings/twilio
    , reachable from the nav once signed in.
  2. The six numbered steps verbatim from Frontend item 2, including that the API-key Secret is shown only once.
  3. That Twilio costs money — per-message pricing plus a monthly number fee, and that a trial account can only text verified numbers and prefixes every message with a trial notice.
  4. For US-bound traffic: the A2P 10DLC requirement, named as weeks of lead time and additional fees, with the three ordered resources — otherwise the user will ship an app that silently fails to reach US phones.
  5. The failure map, one line each:
    20003
    → re-paste credentials;
    21211
    → E.164 format;
    21408
    → enable the destination country;
    21608
    → verify the recipient (trial);
    21610
    → recipient unsubscribed.
Do not compress this to "configure Twilio in Settings" and do not substitute a link to Twilio's documentation. Use the same wording here as in the settings-page panel so the two cannot drift.
应用无法发送任何消息,直到人工创建Twilio账号、购买号码并粘贴凭证——因此完成消息是交付物的一部分,而非简单总结。必须按以下顺序包含:
  1. 说明需要凭证,以及谁来输入——管理员,在
    /settings/twilio
    页面,登录后可从导航栏访问。
  2. 逐字重复前端章节第2项中的6个编号步骤,包括API密钥的Secret仅显示一次
  3. 说明Twilio会产生费用——每条消息的定价加上号码月费;试用账号仅能向已验证号码发送短信,且每条消息会带有试用提示前缀。
  4. 面向美国的流量:A2P 10DLC要求,说明需要数周准备时间和额外费用,以及三个按顺序配置的资源——否则用户交付的应用将无法向美国号码发送短信,且无任何提示。
  5. 错误映射,每行一个
    20003
    → 重新粘贴凭证;
    21211
    → 使用E.164格式;
    21408
    → 启用目标国家权限;
    21608
    → 验证收件人(试用账号);
    21610
    → 收件人已退订。
请勿将其简化为“在设置中配置Twilio”,也不要替换为Twilio文档的链接。请使用与设置页面面板相同的措辞,避免两者内容不一致。

Known limitations

已知限制

  • Only the messaging surface is shipped. The package is pruned to the messaging path; voice/recordings/SIP/usage and the rest are not in it.
  • Inbound messages are out of scope. Receiving SMS, and
    statusCallback
    delivery receipts, need an inbound HTTP endpoint on the canister — a different component, not this client.
  • Binary media is not uploadable.
    mediaUrl
    takes a public URL Twilio fetches; the canister cannot POST image bytes through this client.
  • No idempotency key. Twilio's messaging API has none, so a retry after a timeout may send twice. Guard at the application level (a stable-variable dedupe key per logical send) rather than retrying blindly. The non-replicated default removes the ~13× amplification, not retry semantics.
  • One dropped field.
    POST …/IncomingPhoneNumbers/{Sid}.json
    accepts an
    AccountSid
    form field (used to move a number between subaccounts) while
    AccountSid
    is also its path parameter. The generator has a single namespace for both, so the form copy is dropped and transferring a number to a subaccount is not reachable through this client. Every other endpoint is unaffected.
  • Nothing here has been exercised against live Twilio. The wire format is at least structurally right — writes send an
    application/x-www-form-urlencoded
    body with percent-encoded parameters, which is what Twilio requires — but no call has been made. Treat a first successful send as the real acceptance test.
  • Spec vintage: generated from Twilio's published OpenAPI specs merged by
    spec-merge
    (Messaging v1 + API v2010), then pruned to the messaging surface. Newer Twilio features absent from those specs are absent here.
  • 仅包含消息服务相关功能。本包已精简至消息服务路径;语音/录音/SIP/使用记录等功能不在范围内。
  • 入站消息不在范围内。接收SMS和
    statusCallback
    送达回执需要canister上的入站HTTP端点——这是一个独立组件,而非本客户端的功能。
  • 无法上传二进制媒体
    mediaUrl
    接受Twilio可获取的公共URL;canister无法通过此客户端上传图片字节。
  • 无幂等键。Twilio的消息API无此功能,因此超时后重试可能导致重复发送。请在应用层面进行防护(每个逻辑发送操作使用稳定变量作为去重键),而非盲目重试。非复制式默认设置仅消除了约13倍的重复发送,并未改变重试语义。
  • 一个字段被遗漏
    POST …/IncomingPhoneNumbers/{Sid}.json
    接受
    AccountSid
    表单字段(用于在子账号间转移号码),同时
    AccountSid
    也是路径参数。生成器对两者使用同一命名空间,因此表单字段被遗漏,无法通过此客户端将号码转移到子账号。其他所有端点均不受影响。
  • 所有功能均未在实时Twilio环境中测试。有线格式至少结构正确——写入操作发送
    application/x-www-form-urlencoded
    请求体,参数为百分编码,符合Twilio要求——但尚未发起任何调用。请将首次成功发送视为真正的验收测试。
  • 规范版本:从Twilio发布的OpenAPI规范(Messaging v1 + API v2010)通过
    spec-merge
    合并生成,然后精简至消息服务相关功能。这些规范中未包含的Twilio新功能也不在本包中。

Related

相关资源