Twilio SMS and MMS Setup for OpenClaw

Configure SMS and MMS messaging in OpenClaw through a Twilio phone number or Messaging Service. Covers webhook setup, request signature checks, sender access policies, and delivery status callbacks.

Read this when

  • You want to connect OpenClaw to SMS or MMS through Twilio
  • You need SMS/MMS webhook or allowlist setup

OpenClaw handles SMS and MMS traffic through a Twilio phone number or Messaging Service. The Gateway registers a webhook route (/webhooks/sms by default), checks Twilio request signatures unless you disable that check, delivers replies via Twilio's Messages API, and stores outbound delivery callbacks.

Status: this is an official plugin, installed on its own. SMS text and MMS attachments, direct messages only.

Before you begin

What you need:

  • The official SMS plugin, installed with openclaw plugins install @openclaw/sms.
  • A Twilio account holding a number that can send SMS, or a Twilio Messaging Service. MMS needs an MMS-capable sender, and native MMS delivery also hinges on the destination country and carrier.
  • The Twilio Account SID and Auth Token.
  • A public HTTPS URL that reaches your OpenClaw Gateway.
  • A choice of sender policy: pairing (the default) for private use, allowlist for phone numbers approved in advance, or open only when you deliberately want public SMS access.

A single Twilio number can handle both SMS and Voice Call when it carries both capabilities. Twilio configures the SMS webhook and the Voice webhook separately, and each uses its own Gateway path; only the SMS webhook is covered here.

US A2P / 10DLC delivery

When an application sends SMS or MMS from a US local 10DLC number to US recipients, US A2P 10DLC registration is required. Toll-free numbers and short codes go through different verification processes. None of this overlaps with OpenClaw channel setup: webhook signature validation, pairing, and outbound credentials may all be correct while carriers still block or filter delivery.

Before you depend on a US 10DLC sender, verify the following in Twilio:

  • The account is paid: Twilio trial accounts are not eligible to register for A2P 10DLC.
  • Twilio Trust Hub shows an approved Primary or Secondary Compliance Profile.
  • The Brand and Campaign are registered and approved.
  • The Twilio phone number carries A2P status REGISTERED and sits in the Sender Pool of the Messaging Service tied to the approved Campaign, or the messagingServiceSid you set here is that approved service.
  • The Campaign reflects the actual OpenClaw message use case and carries sample messages that match it.
  • Every opt-in path is fully described: website, keyword, offline, paper, or QR code. Where the flow cannot be seen publicly, supply publicly accessible screenshots or other evidence.
  • Messaging consent is voluntary and kept apart from required service terms, account creation, or purchase, and it includes the privacy policy, terms, frequency, rates, and opt-out disclosures Twilio requires.
  • You keep proof of consent, identify the sender, honor standard one-step opt-out keywords, and never buy, rent, sell, or transfer consent. Once a recipient opts out, send a single confirmation only, unless they opt in again.

For current requirements, treat Twilio as the source of truth: A2P 10DLC overview, registration quickstart, and required business and campaign information. What follows is setup guidance, not legal advice.

Should Twilio reject the Brand or Campaign during registration review, resolve it in Twilio before pointing the sender at OpenClaw. 30909 signals that the message flow or call to action is incomplete or cannot be verified. 30923 signals that messaging consent is demanded as a condition of service, account creation, or purchase, or is bundled into service terms. 30893 signals that the sample messages do not match the declared use case.

Quick Setup

Install the plugin

openclaw plugins install @openclaw/sms

Create or choose a Twilio sender

Inside Twilio, go to Phone Numbers > Manage > Active numbers and pick a number that can send SMS. If you plan to send attachments, pick one that is MMS-capable too. Note down:

  • Account SID, for example ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Auth Token
  • Sender phone number, for example +15551234567

When a Messaging Service stands in for a fixed sender number, note down the Messaging Service SID instead, for example MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.

Configure the SMS channel

Store this as sms.patch.json5 and replace the placeholders:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: "twilio-auth-token",
      fromNumber: "+15551234567",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
      dmPolicy: "pairing",
    },
  },
}

Then apply it:

openclaw config patch --file ./sms.patch.json5 --dry-run
openclaw config patch --file ./sms.patch.json5

Point Twilio at the Gateway webhook

In the Twilio number settings, go to Messaging and point A message comes in at:

https://gateway.example.com/webhooks/sms

Use HTTP POST. The local path defaults to /webhooks/sms; set channels.sms.webhookPath if you want a different route.

Expose the exact SMS webhook path

Your public URL has to route the SMS path to the Gateway process (port 18789 by default). That same path handles inbound Twilio webhooks and the short-lived, tokenized attachments OpenClaw sends with MMS. If Tailscale Funnel is what you use for local testing, expose /webhooks/sms explicitly:

tailscale funnel --bg --set-path /webhooks/sms http://127.0.0.1:<gateway-port>/webhooks/sms
tailscale funnel status

Voice Call and SMS each have their own webhook path. When one Twilio number serves both, leave both routes configured in Twilio and in your tunnel.

Start the Gateway and approve first sender

openclaw gateway

Text the Twilio number. Your first message triggers a pairing request. Approve it:

openclaw pairing list sms
openclaw pairing approve sms <CODE>

Pairing codes are valid for 1 hour.

Configuration Examples

Every key sits under channels.sms (and, per account, under channels.sms.accounts.<id>):

KeyDefaultPurpose
enabledtrueTurns the channel/account on or off.
accountSid,SID for the Twilio Account (AC...).
authToken,Twilio Auth Token, given as a plaintext string or a SecretRef.
fromNumber,Sender number in E.164 format.
messagingServiceSid,Messaging Service SID (MG...), applied when no fromNumber resolves.
defaultTo,Fallback destination used when a send flow leaves out an explicit target.
webhookPath/webhooks/smsHTTP path on the Gateway that receives inbound Twilio webhooks.
publicWebhookUrl,Public webhook URL for Twilio; needed for signature validation and for hosting outbound MMS.
dangerouslyDisableSignatureValidationfalseBypasses X-Twilio-Signature checks; intended only for local tunnel testing.
dmPolicy"pairing"One of pairing, allowlist, open, or disabled.
allowFrom[]Sender numbers permitted in E.164, or "*" together with dmPolicy: "open".
textChunkLimit1500Upper bound on characters in each outbound SMS chunk.
accounts, defaultAccount,Map of multiple accounts, plus the default account id.

Config file

Reach for config-file setup when the channel definition should be carried along with the Gateway config:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: "twilio-auth-token",
      fromNumber: "+15551234567",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
      dmPolicy: "pairing",
    },
  },
}

Environment variables

Environment variables affect the default account only, and values set in config win over env values.

VariableMaps to
TWILIO_ACCOUNT_SIDaccountSid
TWILIO_AUTH_TOKENauthToken
TWILIO_PHONE_NUMBER (alias TWILIO_SMS_FROM)fromNumber
TWILIO_MESSAGING_SERVICE_SIDmessagingServiceSid
SMS_PUBLIC_WEBHOOK_URLpublicWebhookUrl
SMS_WEBHOOK_PATHwebhookPath
SMS_ALLOWED_USERSallowFrom (comma-separated)
SMS_TEXT_CHUNK_LIMITtextChunkLimit
SMS_DANGEROUSLY_DISABLE_SIGNATURE_VALIDATIONdangerouslyDisableSignatureValidation ("true")
export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="<twilio-auth-token>"
export TWILIO_PHONE_NUMBER="+15551234567"
export SMS_PUBLIC_WEBHOOK_URL="https://gateway.example.com/webhooks/sms"

Next, switch the channel on in config:

{
  channels: {
    sms: {
      enabled: true,
      dmPolicy: "pairing",
    },
  },
}

SecretRef auth token

A SecretRef can be supplied for authToken (source: "env" | "file" | "exec" | "store"). Choose this option when the Twilio Auth Token should be resolved by the Gateway from the OpenClaw secrets runtime rather than kept as plaintext in config:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: { source: "env", provider: "default", id: "TWILIO_AUTH_TOKEN" },
      fromNumber: "+15551234567",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
      dmPolicy: "pairing",
    },
  },
}

The Gateway runtime needs visibility of whichever environment variable or secret provider you reference. After you change host environment variables, restart any managed Gateway processes.

Messaging Service sender

Choose messagingServiceSid rather than fromNumber when Twilio is to pick the sender via a Messaging Service:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: "twilio-auth-token",
      messagingServiceSid: "MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
      dmPolicy: "pairing",
    },
  },
}

Should both fromNumber and messagingServiceSid survive config and env resolution, the value from fromNumber takes effect.

Default outbound target

Configure defaultTo when automation or agent-initiated delivery needs a default destination for send flows that leave out an explicit target:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: "twilio-auth-token",
      fromNumber: "+15551234567",
      defaultTo: "+15557654321",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
    },
  },
}

Access control

Direct SMS access is governed by channels.sms.dmPolicy:

  • pairing (default): senders OpenClaw does not recognize receive a pairing code, which you approve using openclaw pairing approve sms <CODE>.
  • allowlist: processing covers only the senders listed in allowFrom. Every sender is rejected when allowFrom is empty (the Gateway writes a startup warning to the log).
  • open: validation of the config demands that allowFrom contain "*". Chat is limited to the listed numbers unless that wildcard is present.
  • disabled: every inbound DM is discarded.

Each allowFrom entry should be an E.164 phone number, for example +15551234567. The sms: and twilio-sms: prefixes are accepted and normalized. For a private assistant, dmPolicy: "allowlist" with explicit phone numbers is the better choice:

{
  channels: {
    sms: {
      enabled: true,
      accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authToken: "twilio-auth-token",
      fromNumber: "+15551234567",
      publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
      dmPolicy: "allowlist",
      allowFrom: ["+15557654321"],
    },
  },
}

Sending SMS

Once the SMS channel is selected, a target may be a bare E.164 number or carry the sms: prefix:

openclaw message send --channel sms --target sms:+15551234567 --message "hello"

Where channel selection is implicit, the twilio-sms: prefix picks this channel and does not take over the sms: service prefix, which iMessage relies on to choose carrier SMS delivery for its own targets:

openclaw message send --target twilio-sms:+15551234567 --message "hello"

An explicit --target is required by the CLI. defaultTo serves automation and agent-initiated delivery paths, where channel config can resolve the target.

Replies the agent produces from inbound SMS conversations return to the sender automatically, using the configured Twilio sender.

SMS output is plain text. OpenClaw removes markdown, collapses fenced code blocks, converts links to label (url), and breaks long replies into chunks no larger than textChunkLimit characters (1500 by default) before handing them to Twilio.

Sending MMS

Either use the standard structured media field or pass the CLI --media option:

openclaw message send \
  --channel sms \
  --target sms:+15551234567 \
  --message "photo" \
  --media ./photo.jpg

The attachment goes through the shared outbound-media policy, is held temporarily in plugin-scoped SQLite state, and is presented to Twilio as a tokenized HTTPS URL on the configured publicWebhookUrl path. Sends that carry media alone are allowed.

channels.sms.mediaMaxMb caps each inbound and outbound attachment, measured in MiB. The mediaMaxMb of the selected account takes precedence over the channel root, and agents.defaults.mediaMaxMb provides the fallback. This per-attachment setting does not override the Twilio total-message and media-type limits described below. Outbound images may be optimized before they are sent.

The generated media URL works as a bearer capability and expires 10 minutes after creation. Its entire query string must be treated as a secret: set reverse-proxy and access logs to drop the query string or to redact all query values. OpenClaw Gateway route diagnostics capture the pathname only and have no control over upstream proxy logs.

One media item is attached to each outbound OpenClaw delivery. JPEG, JPG, PNG, and GIF attachments are capped by OpenClaw at 5,000,000 bytes, while other supported media types are limited to 500,000 bytes. Attachments of type application/vcard must be media-only, since Twilio rejects them when a caption is present. Carriers at the destination may impose lower limits or refuse formats they do not support. Twilio has to fetch the generated URL without HTTP authentication, so publicWebhookUrl cannot embed userinfo; tokens supplied by a reverse proxy through the query are kept.

For incoming MMS, no more than 10 attachments are processed by OpenClaw, and the total download is capped at 5 MiB. Attachments beyond that, or ones that cannot be fetched, trigger a visible unavailable-media notice; the signed message is never discarded and no empty turn is delivered silently. Nothing is downloaded until the sender is authorized, and the fetch uses Twilio authentication plus an api.twilio.com host restriction.

Delivery status

Whenever an outbound send succeeds, OpenClaw saves the initial Twilio API status if the response carries one. If publicWebhookUrl is valid, each outbound message also supplies Twilio with a derived StatusCallback URL that keeps its base URL and connection overrides and adds the delivery-callback retry settings Twilio requires. Derived URLs that are invalid or too large are left out.

Delivery callbacks that arrive later update the same plugin-scoped SQLite record. Semantic retries are deduplicated, a terminal state cannot be regressed by older transitions, and terminal observations that conflict are reported as conflicted rather than picking a false winner. Records hold message SIDs, status and error metadata, and timestamps, but no message bodies or phone-number addresses. Each record lives up to 30 days from its latest observation, within the plugin-wide 5,000-message cap and oldest-record eviction.

Verify Setup

Once the Gateway is running:

  1. Check that the Gateway log lists the SMS webhook route.
  2. Run a Twilio-side check (it verifies the configured Twilio webhook URL and method, recent inbound errors, and the latest stored outbound delivery state):
openclaw channels capabilities --channel sms
openclaw channels status --channel sms --probe --json
  1. From your phone, text the Twilio number.
  2. Execute openclaw pairing list sms.
  3. Use openclaw pairing approve sms <CODE> to approve the pairing code.
  4. Text again and verify that the agent answers.

To test outbound only, use:

openclaw message send --channel sms --target sms:+15557654321 --message "OpenClaw SMS test"

End-to-end test from macOS iMessage/SMS

On a Mac able to send carrier SMS through Messages, imsg lets you drive the sender side while leaving your phone alone:

imsg send --to "+15551234567" --service sms --text "OpenClaw SMS E2E $(date -u +%Y%m%dT%H%M%SZ)" --json
openclaw pairing list sms
openclaw pairing approve sms <CODE>
imsg send --to "+15551234567" --service sms --text "reply exactly SMS pong" --json

The first message ought to create a pairing request. The second message ought to come back as the agent reply via Twilio.

Webhook security

OpenClaw checks X-Twilio-Signature against publicWebhookUrl and authToken by default. The endpoint part of publicWebhookUrl must line up byte-for-byte with the URL you configured in Twilio, covering scheme, host, path, and query string. As Twilio requires, Twilio connection-override fragments (#...) are left out of signature computation by OpenClaw.

Separately from signature validation, the webhook route also enforces:

  • POST only.
  • A failed-request budget of 300 requests per minute, counted per SMS account, webhook route, and resolved client address. Every request counts against this budget, yet HTTP 429 is returned only once body parsing or Twilio signature validation has failed.
  • Signed delivery callbacks get classified before inbound sender quotas apply, and they commit to bounded, plugin-scoped SQLite state before HTTP 200 is returned. Inbound dispatch quotas are not consumed by them: those quotas exist to protect raw inbound message admission and downstream agent dispatch. Instead, delivery persistence has its own safety fuse of 3,000 callbacks per minute per SMS account route, and beyond that limit it returns HTTP 503 without the durable-acceptance marker. Excess callbacks are rejected this way to protect the service; they are not queued for later delivery. When signature validation is disabled, delivery callbacks are first subject to the stricter cap of 30 per minute per resolved client address before persistence.
  • A dispatchable callback rate limit of 30 accepted callbacks per minute, per SMS account, webhook route, and validated sender, applied after body parsing and signature validation succeed (HTTP 429 beyond that). The sender key is the normalized From value covered by the signature, so equivalent SMS/RCS address forms draw on one budget, a single flooding sender exhausts only its own budget, and callbacks from other senders behind Twilio's shared egress addresses stay dispatchable. Invalid or missing sender values draw on a separate empty-sender budget.
  • An aggregate validated-callback ceiling of 300 accepted callbacks per minute per SMS account and webhook route. This limits durable-ingress pressure from many distinct signed senders without bringing back cross-throttling on shared egress. With signature validation disabled, nothing authenticates From; the stricter 30/min resolved-client-address dispatch cap then replaces the validated sender and aggregate policy.
  • Client addresses are resolved using the shared Gateway trusted-proxy rules. When gateway.trustedProxies contains the reverse proxy that forwards Twilio callbacks, OpenClaw derives the address-based limits from the forwarded client address; if not, it uses the direct socket address.
  • Inbound payloads must carry a nonempty AccountSid that matches the configured accountSid exactly. Direct-number callbacks must be addressed to the configured fromNumber; Messaging Service callbacks must carry the configured MessagingServiceSid. The raw callback is committed to the durable ingress queue and acknowledged first; an identity mismatch is then marked during drain as a permanent invalid-payload failure, and it is never dispatched or permitted to download media.
  • Delivery callbacks whose AccountSid is missing or different are acknowledged, logged, and deliberately not stored.
  • Replayed MessageSid values are deduplicated by the durable ingress queue. Tombstones for completed messages are kept for 24 hours (up to 20,000 entries per account); tombstones for permanent failures are kept for 30 days (up to 1,000 entries).
  • Delivery observations rely on a semantic, non-PII fingerprint built from source, message SID, normalized status, error code, and carrier completion date. Distinct states of one outbound message stay separate. Records expire 30 days after their latest observation, though the 5,000-message cap may evict older records earlier.
  • Request bodies larger than 32 KB are rejected.

Generated delivery StatusCallback URLs carry the 5xx retry policy plus a retry count, which lets Twilio retry a failed SQLite commit or an overloaded delivery-state route. By default, Twilio does not retry HTTP 429. The #rp=4xx and #rp=all connection overrides opt into 4xx retries, but the whole retry transaction is capped by Twilio at 15 seconds. Neither a 429 nor a delivery-state 503 guarantees later recovery; when final-state completeness matters, use reconciliation. Intermediate transitions that are missed cannot be reconstructed.

For workflows that are sensitive to completeness, persist Message SIDs and reconcile stale nonterminal records by polling Twilio's Message resource. Twilio's delivery logging guidance advises polling when a message has not reached delivered or undelivered within 12 hours, since a status callback may never have arrived. The SMS fallback URL does not cover this: it handles only failures when retrieving or executing the inbound SMS TwiML webhook.

For local tunnel testing only, you can set:

{
  channels: {
    sms: {
      dangerouslyDisableSignatureValidation: true,
    },
  },
}

Never run a public Gateway with signature validation disabled.

Multi-account config

Reach for accounts when you operate more than one Twilio number:

{
  channels: {
    sms: {
      accounts: {
        support: {
          enabled: true,
          accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
          authToken: "twilio-auth-token",
          fromNumber: "+15551234567",
          publicWebhookUrl: "https://gateway.example.com/webhooks/sms/support",
          webhookPath: "/webhooks/sms/support",
          dmPolicy: "allowlist",
          allowFrom: ["+15557654321"],
        },
      },
    },
  },
}

Every account needs its own distinct webhookPath; a webhook route whose path another account already owns is refused registration by the Gateway. The TWILIO_*/SMS_* environment fallbacks apply to the default account only; to change which account that is, set defaultAccount.

Troubleshooting

Twilio returns 403 or OpenClaw rejects the webhook

Verify that publicWebhookUrl matches the URL configured in Twilio exactly, including scheme, host, path, and query string. Because Twilio signs the public URL string, proxy rewrites and alternate hostnames can cause signature validation to fail.

If Twilio gets a durable acknowledgement but no pairing request shows up, look in the Gateway log for a permanent invalid-payload failure. Check that the callback's AccountSid and To match the configured account and fromNumber, or that its MessagingServiceSid matches the configured Messaging Service.

No pairing request appears

Inspect the Twilio number's Messaging webhook URL and method. It must use POST and point at the SMS webhook URL. Also verify the Gateway can be reached from the public internet or through your tunnel.

When the Twilio message log reports error 11200, Twilio accepted the inbound SMS but could not reach your webhook. Check:

  • Twilio Messaging > A message comes in points at publicWebhookUrl.
  • The method is POST.
  • The tunnel or reverse proxy exposes the exact webhookPath; for Tailscale Funnel, run tailscale funnel status and confirm /webhooks/sms is listed.
  • publicWebhookUrl uses the same scheme, host, path, and query string that Twilio sends, so signature validation can reproduce the signed URL.

openclaw channels status --channel sms --probe surfaces both mismatched Twilio webhook settings and recent 11200 errors.

Outbound sends fail

Confirm that accountSid, authToken, and either fromNumber or messagingServiceSid are resolved. Trial accounts on Twilio can send only to verified recipients in the country where the account signed up, and they must use Twilio's predefined content; custom SMS bodies are not supported. Such accounts also cannot register for A2P 10DLC, so upgrade first if you plan to register a US 10DLC sender.

Twilio accepts the send but delivery later fails

Begin with the delivery observation OpenClaw has stored:

openclaw channels status --channel sms --probe --json

When the latest outbound status shows failed or undelivered, open its messageSid to check the final Message status and error code in Twilio. A 30034 result indicates the sender is either unregistered or absent from the Sender Pool of the Messaging Service tied to the approved Campaign. If you get 30035, Twilio is still in the process of registering, deregistering, or reassigning the number, so hold off sending until that status reads REGISTERED.

Messages arrive but the agent does not answer

Verify dmPolicy and allowFrom. Under the default pairing policy, approval of the sender is required before normal agent turns can be processed.

3,731 words · updated Oct 25, 2026