Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Twilio

Send and receive SMS and MMS through Twilio Programmable Messaging with the SMS SDK twilio() adapter: setup, senders, webhooks, errors, and limits.

The twilio() adapter sends SMS and MMS through Twilio Programmable Messaging and verifies Twilio’s status and inbound webhooks. To compare providers, see Choose a provider.

Support status

Supported. Requests, responses, error codes, statuses, and webhook signing follow Twilio’s official documentation, and the adapter passes the shared contract tests. Limits to know:

  • Scheduling (sendAt) works only from a Messaging Service sender.
  • Content Templates (ContentSid) are not supported.
  • Webhook verification needs the Auth Token of the account that owns the number. An API key cannot verify webhooks.
  • Twilio signs no timestamp, so webhooks have no replay window. Deduplicate on dedupeKey.

Setup

npm install @opencoredev/sms-sdk
pnpm add @opencoredev/sms-sdk
yarn add @opencoredev/sms-sdk
bun add @opencoredev/sms-sdk
nub add @opencoredev/sms-sdk
aube add @opencoredev/sms-sdk
Variable Required Used for
TWILIO_ACCOUNT_SID Yes Account SID, AC plus 32 hex characters.
TWILIO_AUTH_TOKEN Yes, unless you use an API key API auth, and webhook verification.
TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET Instead of the Auth Token API auth with a revocable key. Twilio recommends this for production.
TWILIO_FROM or TWILIO_MESSAGING_SERVICE_SID One of them Default sender. The service SID is MG plus 32 hex characters.

The adapter reads no environment variables itself; the examples and the doctor CLI use these names. Keep the secrets on the server.

import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio";

const sms = createSmsClient({
  adapters: [
    twilio({
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
      // Or, instead of authToken: apiKeySid and apiKeySecret.
      from: "+15550100001",
    }),
  ],
});

const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // "SM..." ("MM..." for MMS)

Pass either authToken or apiKeySid with apiKeySecret, not both. twilio() throws ConfigurationError at startup when the Account SID is malformed or a credential is empty. Two more options exist for tests and proxies: baseUrl (default https://api.twilio.com) and fetch. To test without a Twilio account, see Test without a provider.

The adapter calls POST /2010-04-01/Accounts/{AccountSid}/Messages.json with a form body. A 2xx without a valid SM or MM SID is an unknown outcome.

Senders

Sender Write Sent as
Long code or toll-free number "+15550100001" From
Short code { shortCode: "12345" } From
Alphanumeric sender ID { senderId: "Acme" } From
Messaging Service { messagingService: "MG…" } MessagingServiceSid

Which senders work depends on the destination country, and a sender Twilio refuses is a sender rejection. In the US, long codes need A2P 10DLC registration, toll-free numbers need verification before they reach the US or Canada, and alphanumeric sender IDs are not supported. Twilio’s SMS guidelines list the rules for each country. SMS SDK does not check them before sending; see Registration and consent.

Features

Feature Twilio
MMS Up to 10 mediaUrls
Scheduling (sendAt) Only with a { messagingService } sender, inside Twilio’s scheduling window
Validity period 1 to 36000 seconds
Per-message webhookUrl Yes, sent as StatusCallback
Body length Up to 1600 characters, checked before sending

Provider options

Pass other Twilio Message parameters in providerOptions.twilio:

import { sms } from "./sms";

await sms.send({
  to: "+14155550123",
  body: "Track your order: https://example.com/orders/123",
  providerOptions: { twilio: { smartEncoded: true, messageIntent: "delivery" } },
});
Option Twilio parameter Type
applicationSid ApplicationSid string, AP plus 32 hex characters
provideFeedback ProvideFeedback boolean
attempt Attempt integer, 1 or more
contentRetention ContentRetention "retain" | "discard"
addressRetention AddressRetention "retain" | "obfuscate"
smartEncoded SmartEncoded boolean
shortenUrls ShortenUrls boolean. Needs a { messagingService } sender, or the send throws UnsupportedFieldError.
sendAsMms SendAsMms boolean
messageIntent MessageIntent "otp" | "notifications" | "marketing" | "fraud" | "security" | "customercare" | "delivery" | "education" | "polling" | "announcements" | "events"
riskCheck RiskCheck "enable" | "disable"
extra any other parameter Record<string, string | number | boolean>, sent as is

Unknown keys and wrong types throw before any request, and the options go out only when the twilio adapter sends. extra cannot set the fields SMS SDK owns: To, From, MessagingServiceSid, Body, MediaUrl, StatusCallback, ValidityPeriod, SendAt, ScheduleType, ContentSid, and ContentVariables. MaxPrice, ForceDelivery, and TrafficType have no typed option; send them through extra. See providerOptions for how options interact with fallback and idempotency.

Webhooks

import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

export async function POST(request: Request): Promise<Response> {
  try {
    const event = await parseSmsWebhook({
      provider: "twilio",
      request,
      publicUrl: "https://example.com/webhooks/sms/twilio",
      credentials: { authToken: process.env.TWILIO_AUTH_TOKEN ?? "" },
    });
    if (event.type === "message.delivered" || event.type === "message.undelivered") {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    } else if (event.type === "recipient.opted_out") {
      await db.suppressions.add(event.from);
    }
    // Empty TwiML: no automatic reply.
    return new Response("<Response></Response>", { headers: { "content-type": "text/xml" } });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}
  • Credential: the Auth Token of the account that owns the number. A subaccount’s number is signed with the subaccount’s token. API keys cannot verify.
  • Point Twilio at it: for status, pass webhookUrl per message or set a status callback on the Messaging Service. For inbound, set “A message comes in” on the number, or the incoming message webhook on the Messaging Service, with HTTP POST.
  • publicUrl: pass the exact URL configured in Twilio, or verification fails behind a proxy. SMS SDK also tries it with and without :443 or :80. See Webhook security.
  • Signing: X-Twilio-Signature is an HMAC-SHA1 over the URL and the sorted form parameters. For JSON bodies, Twilio signs only the URL and adds a bodySHA256 query parameter, which SMS SDK checks against the raw body.
  • Replay window: none, because Twilio signs no timestamp. Deduplicate on dedupeKey.
Twilio status Event
queued, accepted, scheduled, sending message.queued
sent message.sent
delivered message.delivered
undelivered, failed message.undelivered, or message.filtered with error 30007
read, canceled, others unrecognized
Inbound message message.received
Inbound with OptOutType STOP, START, HELP recipient.opted_out, recipient.opted_in, recipient.help (source: "provider")

OptOutType appears when Advanced Opt-Out is on for the Messaging Service. dedupeKey is twilio:{MessageSid}:{status} for status updates and twilio:{MessageSid}:received for inbound messages.

Limitations

  • Scheduling needs a Messaging Service sender. To schedule from a plain number, use Telnyx.
  • Content Templates are not supported, because a template replaces body, which SMS SDK needs for segment estimates, policy checks, and idempotency. WhatsApp and Conversations are out of scope.
  • Twilio enforces its own scheduling window and MMS country coverage, and rejects sends outside them.

Error codes

Twilio response Category Falls back
401, or error 20003 auth Yes
403 with an unlisted code auth Yes
Error 20429 rate_limited Yes, after retries
21212, 21606, 21612, 21659, 21660, 21703 sender Yes
21408 (geographic permission), 21608 (trial account) account Yes
21211, 21614 recipient No
21610 (recipient unsubscribed) compliance Never
Any other 4xx request No
429 without error 20429 unknown Never
5xx, network error, timeout, 2xx without a valid SID unknown Never

Twilio documents that a 429 with error 20429 was not processed. A 429 without that code may come from a proxy, so SMS SDK treats it as unknown. error.provider.code holds Twilio’s error code and error.provider.requestId the Twilio-Request-Id header. For adapter authors, the subpath also exports buildTwilioForm, interpretTwilioResponse, twilioRejectionCategory, and twilioDelivery.

Official documentation

Checked on 2026-10-08. The full evidence list is in PROVIDERS.md.

Next steps