mcpField
C
connector-twilio

connector-twilio

github.com/caffeinelabsその他211.7k 回インストール1 週間前に更新
リポジトリ

connector-twilio について

Caffeine canister から Twilio のメッセージ機能を利用する実験的な Motoko コネクターです。SMS、MMS、メッセージ設定に対応し、音声・通話 API は対象外です。送信はまだ検証されていません。

主な機能

  1. 01SMS・MMS メッセージの送信
  2. 02メッセージ記録の取得と一覧表示
  3. 03メッセージサービスと電話番号の管理
  4. 04米国 A2P 10DLC 登録フローへの対応
  5. 05管理者限定の認証情報設定

活用シーン

  1. 01Caffeine canister から通知を送信
  2. 02送信状況の確認や送信済みメッセージの検索
  3. 03電話番号と Twilio Messaging Service の設定
  4. 04米国向けメッセージの A2P 10DLC 登録

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/caffeinelabs/skills --skill connector-twilio

インストール後、Claude Code で/connector-twilioを入力して読み込まれたか確認します。

詳細

Twilio Connector(実験的)

Caffeine canister から SMS/MMS を送信し、Twilio のメッセージングを設定します。

⚠️ 実験的([email protected])—このクライアントから通話が行われたことは一度もありません。 書き込み処理が動作する可能性が出てきたのは、ごく最近のことです。それ以前は、すべての書き込みで引数が破棄され、空の本文が送信されていました。ワイヤ形式は現在、Twilio のドキュメントに記載された形式(フォームエンコードされた本文、パーセントエンコードされた値、省略可能なフィールドは省略)に一致し、118 個のファイルすべてで型チェックが通りますが、構造上正しいことは検証済みであることを意味しません。最初の送信成功を受け入れテストとし、それが実現するまでは、Twilio を完全にサポートされたプラットフォーム機能としてユーザーに提示しないでください。送信には費用がかかるため、実験に失敗しても無料ではありません。

対象範囲 — このパッケージが扱うのはメッセージング機能のみです。 twilio-client は 35 個の API モジュール(Messaging v1 全体と、v2010 のメッセージング関連パス:Account、Message、Media、IncomingPhoneNumber とそのバリアント、AvailablePhoneNumber、A2P レジストリ)に絞られています。Voice/通話、録音、会議、キュー、アプリケーション、SIP、使用状況レコードはパッケージに含まれていません。ビルドでこれらが必要な場合、このコネクターの対象範囲外です。(数:API モジュール 35 個、モデル 82 個、ファイル 118 個。すべて型チェック済み。)

オーケストレーターのルーティングに関する注意

ユーザー、仕様、または以前のタスクで、テキストメッセージの送信、SMS/MMS、電話での通知、電話番号の購入や一覧表示、または Twilio のメッセージング関連の概念に言及されている場合は、このスキルを読み込んでください。*.twilio.com への生の ic.http_request は、認証、ホストのルーティング、パーセントエンコーディング、JSON の解析を手作業で再実装するアンチパターンです。しかも、安易に実装すると、すべてのメッセージを約 13 回送信してしまいます。

意図 → 機能の対応関係:

ユーザーの意図機能
SMS を送信するfrom に Twilio の電話番号を指定した Api20100401MessageApi.createMessage
MMS(画像)を送信する同じ API で、mediaUrl = ["https://…"] と sendAsMms = true を指定
Messaging Service 経由で送信する(米国向けトラフィックでは推奨)同じ API で、from = "" と messagingServiceSid を指定
配信状況を確認するfetchMessage(status、error_code)
送信済みメッセージを一覧表示/検索するlistMessage(ページネーション対応)
電話番号を所有する/閲覧するApi20100401IncomingPhoneNumberApi、…AvailablePhoneNumberCountryApi
Messaging Service を設定するMessagingV1ServiceApi.createService
米国の A2P 10DLC に登録するMessagingV1BrandRegistrationApi → MessagingV1UsAppToPersonApi → MessagingV1PhoneNumberApi(この順番です。米国の A2P 10DLC を参照)
トールフリー番号を確認するMessagingV1TollfreeVerificationApi

Twilio の認証情報は、人がコンソールから取得しなければならないものです。そのため、バックエンドのコンパイルが通っただけではビルドは完了していません。アプリが管理者に認証情報の取得先を案内し、貼り付ける場所を用意して初めて完了です。認証モデル、次に必ず提供する必要があるページについては フロントエンド を参照し、完了メッセージでも手順を繰り返してください。

コードを書く前に確認してください: どの番号から送信しますか? 米国向けの本番アプリには、Messaging Service と A2P 登録が必要です(数週間のリードタイムと実費がかかります)。デモ/社内向けアプリでは、単一のトライアル番号から、確認済みの宛先にのみ送信できます。選択内容とその影響を、依頼したユーザーに報告してください。

認証モデル — HTTP Basic、2 種類

どちらの種類も同じ #basicAuth { user; password } 認証情報を使用し、クライアントは同じように扱います。違いは、漏えい時の影響範囲です。

種類user / password用途
API Key (デフォルト — こちらを推奨)API Key SID(SK…)/ その Secret本番環境。失効やスコープの設定が可能です。1 つ漏えいしても、アカウント全体が侵害されることはありません。
Account SID + Auth Tokenアカウント SID(AC…)/ Auth Token開発環境のみ。Auth Token はアカウントそのものです。サブアカウントの作成、電話番号の購入、料金の支払いが可能です。

Account SID(AC…)は、使用する種類にかかわらず、すべての v2010 操作で必須の位置引数でもあります(URL パスに含まれます)。そのため、API Key を使うアプリでは、Account SID、Key SID、Key Secret の3 つの値を保存します。

認証情報の取得
  1. https://console.twilio.com にサインインします。
  2. Account SID(AC…)はコンソールのダッシュボードに表示されています。コピーしてください。
  3. 本番環境では、Account → API keys & tokens → Create API key(Standard)を選び、SID(SK…)と Secret をコピーします。Secret が表示されるのは 1 回だけです。管理者が画面を移動すると復元できず、再発行のみ可能です。 開発環境のみ、代わりにダッシュボードから Auth Token を取得します。
  4. 送信元の電話番号を購入します。Phone Numbers → Manage → Buy a number を選び、SMS 機能にチェックが入っていることを確認してください(すべての番号で利用できるわけではありません)。
  5. トライアルアカウントでは、Phone Numbers → Verified Caller IDs で宛先ごとに認証する必要があります。認証しないと送信は 21608 エラーで失敗します。また、トライアルのメッセージには「Sent from your Twilio trial account」というプレフィックスが付加されます。
認証情報をキャニスターに渡す

管理者は 管理者権限で保護された セッターを通じて認証情報を貼り付けます。保護は AccessControl.hasPermission(state, caller, #admin) によって行います。認証情報はキャニスター内にのみ保持され、フロントエンドに返されることはありません。

⚠️ 最初に呼び出した人が所有権を取得する方式で、セッターを保護しないでください。 IC では、認証されていないすべての呼び出し元は同じ匿名プリンシパルです。そのため、匿名呼び出しが先に所有権を取得すると、すべての匿名呼び出し元が caller == owner のチェックを通過し、認証情報を上書きできます。この認証情報を使うと料金が発生します。

キャニスターからクライアントに認証情報を渡す方法は、config.auth = ?#basicAuth { user; password } のみです。各メソッドはこれを Authorization: Basic … ヘッダーに変換します。認証情報を引数に取るメソッドはなく、URL に含めるメソッドもないため、ログに記録されるクエリ文字列から漏えいすることはありません。

アウトコールはすでに非複製です — 以前の説明を訂正します

defaultConfig には is_replicated = ?false が設定されているため、レコード更新によってこれを元に構成した場合も、そのままで正しい状態です。覚えておくことも、追加することもありません。

⚠️ ?true や null に設定しないでください。また、そうするよう勧める以前の説明は無視してください。 以前の SKILL のバージョンでは、書き込みは「IC のコンセンサスでリトライを重複排除できる」ため、複製されたままにすべきだと述べていました。これは誤りであり、コストもかかります。 複製アウトコールはサブネット内のすべてのノードで実行されます。リクエストが約 13 回送信されるため、SMS は約 13 通送信され、約 13 通分の料金が請求されます。認証情報はすべてのノードに渡り、Twilio が各応答に一意の sid を付与するため、応答がバイト単位で一致せず、コンセンサスも失敗します。これは、Gmail コネクタでメールが約 13 件重複して送信され、slack-client 0.1.0 で修正されたのと同じ問題です。

読み取り(fetch* / list*)も非複製で問題ありません。メッセージログについては、1 つのノードのビューが必要なものであり、こちらのほうが低コストです。

バックエンド

依存関係を追加する

以下のレシピで管理者権限を確認するには、クライアントとあわせて認可コンポーネントが必要です。

mops add [email protected]
mops add [email protected]

呼び出し形式 — フリー関数またはクラスファサード

各モジュールでは両方を利用できます。フリー関数は最初に config を受け取り、async* です。 module class は config を保持し、async です:

// 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, /* … */);

すべてのパラメーターは位置引数で、createMessage には27個あります。 使用しないパラメーターには "" / false / 0 / 0.0 / [] / null を渡してください。省略可能な enum パラメーターが ?T なのは、null を渡すとワイヤー上で省略されるためです。 数を慎重に確認してください。空文字列の位置を間違えると、誤ったフィールドが送信されても気づきにくいことがあります。順序は次のとおりです。

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

手順

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.AccessControlState;
  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);
};

マイグレーションチェーンのヘッド:

import AccessControl "mo:caffeineai-authorization/access-control";

module {
  type NewActor = {
    accessControlState : AccessControl.AccessControlState;
    twilioConfig : {
      var accountSid : Text;
      var keySid : Text;
      var keySecret : Text;
      var fromNumber : Text;
    };
  };

  public func migration(_old : {}) : NewActor {
    {
      accessControlState = AccessControl.initState();
      twilioConfig = {
        var accountSid = "";
        var keySid = "";
        var keySecret = "";
        var fromNumber = "";
      };
    };
  };
};
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;
  };
};
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, // attempt
      0, // validityPeriod
      false, // forceDelivery
      null, // contentRetention (omitted)
      null, // 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 "" };
  };
};

MMS の場合:mediaUrl = ["https://example.com/image.jpg"] と sendAsMms = true を設定します。 Messaging Service 経由で送信するには、from = "" のままにして、 代わりに messagingServiceSid を設定します。

アドレス指定 — E.164 と送信者の選択

  • to は E.164 形式である必要があります:+、国番号を含め、スペース、ハイフン、括弧は使用しません — "+15551234567"。"555-1234" は 21211 エラーになります。フロントエンドで正規化し、 さらに canister でも正規化してください。どちらか一方だけを信頼してはいけません。
  • from と messagingServiceSid のどちらか一方のみを指定してください。 両方を設定するとエラーになります。 from に番号を直接指定する方法は、米国以外へのトラフィックやデモでは問題ありませんが、米国宛ての本番トラフィックは Messaging Service 経由にしてください(送信者プールと固定送信者を利用でき、A2P 登録の関連付け先でもあります)。
  • 送信番号には SMS 機能が必要です。購入可能な番号すべてに備わっているわけではありません。番号を検索する際に、 Api20100401AvailablePhoneNumberCountryApi でこの機能を条件に指定してください。

米国 A2P 10DLC — 3つのリソースをこの順で作成

米国のロングコードから米国内の宛先にテキストメッセージを送信するには、事前に3つすべてを作成する必要があります。これらがない場合、米国の通信事業者はトラフィックを全面的に拒否します。

  1. ブランド登録 — Trust Hub の customerProfileBundleSid と a2PProfileBundleSid(別途作成)を参照して、MessagingV1BrandRegistrationApi.createBrandRegistrations を呼び出します。開発時には mock = true を渡すと料金を省略できます。ステータスは PENDING から始まり、数時間から数日で APPROVED または FAILED になります。事業者情報が不完全、表記に一貫性がない、または登録情報と一致しない場合は失敗します。
  2. A2P キャンペーン — Messaging Service とブランドの両方を参照して、MessagingV1UsAppToPersonApi.createUsAppToPerson を呼び出します。オンボーディングの失敗の多くはここで発生します。 T-Mobile は、messageFlow にオプトインの説明がないキャンペーンや、messageSamples が宣言された usAppToPersonUsecase と一致しないキャンペーンを拒否します。
  3. 番号からサービスへの割り当て — MessagingV1PhoneNumberApi.createPhoneNumber(cfg, serviceSid, phoneNumberSid)。 番号を同時に割り当てられる Messaging Service は1つだけです。再割り当てするには、まず deletePhoneNumber を呼び出す必要があります。

適用中の登録期限: privacyPolicyUrl と termsAndConditionsUrl の両方が有効でないキャンペーンは、2026-06-30 以降、ハードエラー 400 になります。どちらも createUsAppToPerson の位置引数であり、"" では失敗します。登録時に Twilio が取得するため、URL は公開 HTTPS ページに解決される必要があります。

トールフリー番号には A2P とは別のフロー(MessagingV1TollfreeVerificationApi)を使用します。

利用可能な API サーフェス

このレシピでは、ドキュメント化されたメッセージング関連の機能を扱います。

モジュール用途
Api20100401MessageApiメッセージの送信/取得/一覧表示/更新/削除
Api20100401MediaApi, …MediaInstanceApiメッセージの MMS メディア
Api20100401IncomingPhoneNumberApi(Local/Mobile/TollFree を含む)所有する番号。削除すると解放されます
Api20100401AvailablePhoneNumberCountryApi購入可能な番号の検索
Api20100401BalanceApi, …AccountApiアカウント残高とアカウントレコード
Api20100401UserDefinedMessageApi(Subscription を含む)ユーザー定義のメッセージイベント
MessagingV1ServiceApiMessaging Services(送信者プール)
MessagingV1BrandRegistrationApi(Otp、BrandVettingApi を含む)A2P ブランド
MessagingV1UsAppToPersonApi(UsecaseApi を含む)A2P キャンペーン
MessagingV1PhoneNumberApi, …ShortCodeApi, …AlphaSenderApi, …ChannelSenderApi送信者プールへの登録
MessagingV1TollfreeVerificationApiフリーダイヤル番号の認証
MessagingV1Linkshortening*, …DomainConfig*, …DomainCertsApiブランド付きリンクの短縮
MessagingV1DeactivationsApiキャリアの無効化リスト

パッケージに含まれない機能(生成されたサーフェスから除外):通話、録音、 会議、参加者、キュー、アプリケーション、SIP ドメインと認証情報、 使用状況レコードとトリガー、住所、キー、トークン、残高トランザクション。パッケージに含まれるのはメッセージングのサーフェスのみです。上記の機能については、このコネクターは利用できません。

エラーとページネーション

  • メソッドは 2xx ではデコード済みレコードを返し、4xx/5xx では Error.reject("HTTP <status> body[…]: …") をスローします。 diagnostics が有効なため、reject テキストには Twilio 独自のエラーボディ (code、message、more_info)が含まれます。try { … } catch (e) { Error.message(e) } で ラップしてください。
  • 実際の UI テキストへのマッピングを検討すべきコード:20003 認証に失敗しました(認証情報が無効)、 21211 To が無効、21408 地域の権限がありません(コンソールで送信先の国の地域権限を有効にしてください)、 21608 トライアルアカウントでは未認証の宛先です、21610 宛先が配信を停止しました(STOP)、 21703 送信者プールが枯渇しています、21704 Messaging Service に番号がありません、 21714 プールのサイズ上限に達しています。
  • 2xx でも配信済みとは限りません。 createMessage は status = #queued または #accepted を返します。配信は非同期です。fetchMessage をポーリングして #delivered/#undelivered/#failed を確認し、error_code を読み取ってください。または、 statusCallback URL を設定してください(受信 HTTP エンドポイントが必要です。この範囲では扱いません)。
  • ページネーションの形式は 2 つの API バージョンで異なります。 v2010 の一覧取得(listMessage およびその他すべての Api20100401* の一覧取得)は、トップレベルの next_page_uri/ previous_page_uri を返します(?Text と、完全な URL ではなく /2010-04-01/… のような パス)。一方、Messaging v1 の一覧取得(listService、listPhoneNumber、A2P レジストリ)では、 ページネーション情報が meta 内にネストされ、next_page_url/previous_page_url(完全な URL)と page_size として返されます。70 個の一覧レスポンスのうち、meta 形式を使用するのは 10 個のみです。 listMessage はその中に含まれません。meta フィールドは ListServiceResponse を含むすべての v1 一覧で ?ListAlphaSenderResponseMeta と型付けされています。同一のレコードはコード生成時に 1 つの共有モジュールに重複排除されるため、この名前は呼び出したエンドポイントではなく、 最初にソートされた一覧を反映しています。
  • pageSize のデフォルトは 50 で、上限は 1000 です。すべての一覧呼び出しに上限を設けてください。 アカウントの利用が多い場合、listMessage を上限なしで呼び出すと max_response_bytes を超過します。

2 つの形式を読む方法:

// 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 };

フィールドでの注意点

  • 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 による 回避策はありません。仕様に記載されていない値は表現できません。 即時送信で scheduleType に ?#fixed を渡すと 400 エラーになります。 Twilio はこれを予約メッセージとして解釈し、その後 SendAt が見つからずエラーになります。 この動作を明示的に必要とする場合を除き、これらの引数にはすべて null を指定するのが正しい値です。
  • maxPrice は 0.0 の場合に省略されます。これが望ましい動作です。 MaxPrice=0 を送信すると、 メッセージ料金の上限がゼロになり、Twilio は有料配信を拒否します。省略すると「上限なし」になります。 省略するには 0.0 を渡してください。
  • xTwilioApiVersion(UsAppToPerson メソッド用)は、Twilio サポートから別途指示がない限り "" を渡してください。
  • スループットは送信者ごとに異なります。 ロングコードは毎秒 1 通、フリーダイヤル番号は約 3 通、 国際ロングコードは約 10 通、ショートコードは 100 通です。番号ごとの MPS は引き上げられません。 Messaging Service の送信者プールに番号を追加して拡張してください。
  • stickySender/areaCodeGeomatch は米国とカナダでのみ利用できます。
  • Config.baseUrl は使用されません。各操作には、統合された仕様からコード生成時に固定されたホスト (v2010 では api.twilio.com、v1 では messaging.twilio.com)が指定されています。 これを設定しないでください。また、トラフィックがリダイレクトされることも期待しないでください。

フロントエンド

Twilio に OAuth は不要です。認証情報は管理者が貼り付ける長期間有効なペアであるため、 リダイレクト URI も /connect/twilio ルートも、ユーザーごとのハンドシェイクも必要ありません。実装しないでください。 Twilio を使うアプリに必須なのは、管理者が認証情報を取得して入力するためのページです。 これは提案ではなく受け入れ基準です。これらが欠けたビルドは、不完全なだけでなく、壊れています。

  • 認証情報のページが存在し、アクセス可能であること。 認証情報の入力場所がない「SMS を送信する」機能は使用できません。 サインイン済みの管理者は、ナビゲーションまたは未設定時のプロンプトから /settings/twilio にアクセスできる必要があります。
  • コンソールでの手順を UI に表示すること。 チャットの返信に記載するだけでは不十分です。管理者はチャットが消えた後、 数週間経ってから戻ってくることがあります。
  • API キーのシークレットは Twilio で一度しか表示されません。 入力欄の横にその旨を記載してください。 そうしないと、管理者は別の画面に移動した後、2 つ目のキーを作成しなければならなくなります。
  1. ログインフロー — 必須です。 setTwilioCredentials は #admin を条件とするため、 アプリは匿名ではない呼び出し元を必要とします。ログイン、useInternetIdentity/useActor の接続処理、 および管理者ロールのゲートには、extension-authorization を利用してください。

  2. 管理者設定ページ — /settings/twilio(管理者のみアクセス可能)。以下が必要です。

    • 入力欄の上に「Twilio 認証情報の取得方法」パネルを配置し、1 回限りの約 5 分のセットアップとして、以下の番号付き手順を記載します(完了メッセージには、これらの手順を一字一句そのまま繰り返し記載する必要があります)。
      1. https://console.twilio.com にサインインします。
      2. ダッシュボードから Account SID(AC…)をコピーします。
      3. Account → API keys & tokens → Create API key(Standard)を選択します。SID(SK…)と Secret をコピーします。Secret は 1 回しか表示されません。
      4. Phone Numbers → Manage → Buy a number から、SMS 機能を備えた番号を購入します。
      5. 3 つの値と番号を下に貼り付けて、保存します。
      6. トライアルアカウントの場合は、Verified Caller IDs で各受信者を確認します。 Twilio コンソールを開く便利なリンクも含めます。
    • 3 つの入力欄:Account SID(プレーンテキスト。シークレットではありません)、Key SID、Key Secret(パスワード入力)。setTwilioCredentials にバインドし、成功したらシークレットを消去します。キーはローテーションされるため、フォームは再送信できる状態に保ちます。
    • setTwilioFromNumber にバインドされた送信元番号のフィールドを設け、その横に E.164 形式の例(+15551234567)を表示し、クライアント側で検証します。
    • isTwilioConfigured()(Bool)に基づいて、ステータスを「設定済み」/「未設定」と表示します。この判定では、Key SID を含む3 つすべての値が必要です。Key SID は Basic 認証のユーザー名なので、空欄だとすべてのリクエストが未認証となり、ページが設定済みと表示しているにもかかわらず、Twilio が 20003 を返します。シークレットは、マスクした場合も含め、絶対に再表示しないでください。送信元番号は表示してもかまいません(getTwilioFromNumber)。シークレットではありません。
    • ページにアクセスできるようにします。 共有 Layout のナビゲーションには、isCallerAdmin が true の場合にこのページへのリンクを必ず表示し、それ以外の場合は非表示にします。リンクはこのページ内ではなく、ナビゲーションの定義箇所に追加します。
  3. 未設定状態での案内。 isTwilioConfigured() が false の場合、機能しない「送信」ボタンを絶対に表示しないでください。管理者には /settings/twilio への「Twilio を設定」リンクを表示し、匿名ではない非管理者には説明を表示します。例:「テキストメッセージはまだ設定されていません。管理者が Settings で Twilio の認証情報を追加する必要があります。」

  4. Twilio のエラーを翻訳します。 失敗は、Twilio の code を含む 拒否された 呼び出しとして届きます。拒否内容をそのまま表示するのではなく、少なくとも以下のコードを対処方法に置き換えてください。

    • 20003 → 「Twilio の認証情報が正しくありません。管理者に再入力を依頼してください」
    • 21211 → 「電話番号が無効です。+15551234567 形式を使用してください」
    • 21408 → 「この Twilio アカウントでは、その国へのテキスト送信が有効になっていません」
    • 21608 → 「トライアルアカウントでは、まず Twilio で受信者を確認する必要があります」
    • 21610 → 「この番号は STOP と返信しているため、テキストを送信できません」
  5. 配信を約束しないでください。 呼び出しの成功は、配信ではなくキューへの登録を意味します。UI にはそれに応じて「メッセージをキューに登録しました」と表示し、配信状況が重要な場合は、fetchMessage から取得したポーリング済みの status を表示します。

ルートのレイアウト例:

/                 →  Main UI (any signed-in user; empty-state when unconfigured)
/settings/twilio  →  Admin credentials + sending number (admin-only)
# No /connect/twilio: Twilio uses pasted long-lived credentials, not a redirect flow.

コンポーザーが Caffeine ユーザーに伝えるべきこと

人間が Twilio アカウントを作成し、番号を購入して認証情報を貼り付けるまで、アプリからは何も送信できません。そのため、完了メッセージは成果物の一部であり、単なる要約ではありません。次の内容を、この順番で必ず含めてください。

  1. 認証情報が必要であり、誰が入力するのか — 管理者が、サインイン後にナビゲーションからアクセスできる /settings/twilio で入力します。
  2. Frontend の項目 2 にある番号付きの6つの手順を、一字一句そのまま含めてください。API キーの Secret は一度しか表示されないことも含みます。
  3. Twilio の利用には費用がかかること — メッセージごとの料金に加え、番号の月額料金がかかります。また、トライアルアカウントでは検証済みの番号にしかテキストを送信できず、すべてのメッセージにトライアルに関する通知が付加されます。
  4. 米国内宛ての通信では、A2P 10DLC 要件があること — 数週間の準備期間と追加料金が必要であることを明記し、3つのリソースを指定の順番で記載してください。そうしないと、米国内の携帯電話に届かないままアプリをリリースすることになります。
  5. エラーの対応表を1行ずつ記載してください:20003 → 認証情報を貼り直す;21211 → E.164 形式にする;21408 → 宛先の国を有効にする;21608 → 受信者を検証する(トライアル);21610 → 受信者が配信登録を解除済み。

これを「Settings で Twilio を設定してください」のように短縮したり、Twilio のドキュメントへのリンクで置き換えたりしないでください。設定ページのパネルと同じ文言を使い、両者に食い違いが生じないようにしてください。

既知の制限事項

  • 提供されるのはメッセージ機能のみです。 パッケージはメッセージ機能の経路に絞られており、音声、録音、SIP、利用状況など、それ以外の機能は含まれていません。
  • 受信メッセージは対象外です。 SMS の受信や statusCallback の配信確認には、カニスター上の受信用 HTTP エンドポイントが必要です。これはこのクライアントではなく、別のコンポーネントが担います。
  • バイナリメディアはアップロードできません。 mediaUrl には、Twilio が取得する公開 URLを指定します。このクライアント経由で、カニスターから画像データを POST することはできません。
  • 冪等性キーはありません。 Twilio のメッセージング API には冪等性キーがないため、タイムアウト後の再試行で二重送信になる可能性があります。むやみに再試行するのではなく、アプリケーション側で論理的な送信ごとの安定変数による重複排除キーを使って制御してください。デフォルトの非複製設定により約13倍の増幅はなくなりますが、再試行時の動作は変わりません。
  • フィールドが1つ除外されています。 POST …/IncomingPhoneNumbers/{Sid}.json は AccountSid をパスパラメーターとして使う一方、AccountSid の form フィールドも受け付けます(番号をサブアカウント間で移動する際に使用)。生成コードでは両方に同じ名前空間を使うため、form 側のフィールドは除外されており、このクライアントから番号をサブアカウントに移管することはできません。それ以外のすべてのエンドポイントには影響しません。
  • ここに記載した内容は、実際の Twilio 環境では検証されていません。 ワイヤ形式は少なくとも構造上は正しく、書き込み時には Twilio が要求する、パーセントエンコードされたパラメーターを含む application/x-www-form-urlencoded の本文が送信されます。ただし、実際の呼び出しは行われていません。最初に送信が成功した時点で、実際の受け入れテストが行われたとみなしてください。
  • 仕様のバージョン: Twilio が公開している OpenAPI 仕様(Messaging v1 と API v2010)を spec-merge で統合したものから生成し、その後メッセージ機能に絞っています。これらの仕様に含まれない新しい Twilio 機能は、ここにも含まれていません。

関連情報

コメント