SMS and MMS Setup with Twilio for OpenClaw
Learn how to configure Twilio SMS and MMS for OpenClaw, including webhooks, security, and delivery status. This guide is for administrators setting up the official SMS plugin.
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 either a Twilio phone number or a Messaging Service. The Gateway sets up a webhook route by default at /webhooks/sms, checks Twilio request signatures unless disabled, dispatches responses via the Twilio Messages API, and captures outbound delivery status updates.
This is an official plugin that needs separate installation. It covers SMS text plus MMS attachments, and only direct messages.
-
Pairing, SMS defaults to the pairing DM policy.
-
Gateway security, Check webhook exposure and sender access settings.
-
Channel troubleshooting, Playbooks for cross-channel diagnostics and fixes.
Before you begin
These are the prerequisites:
- The official SMS plugin installed with
openclaw plugins install @openclaw/sms. - A Twilio account holding an SMS-capable phone number, or a Twilio Messaging Service. MMS needs an MMS-capable sender; native MMS delivery also depends on the recipient's country and carrier.
- The Twilio Account SID and Auth Token.
- A public HTTPS URL pointing to your OpenClaw Gateway.
- A sender policy selection:
pairing(the default) for personal use,allowlistfor approved phone numbers, oropenonly when SMS access is deliberately public.
A single Twilio number can handle both SMS and Voice Call if it supports both. The SMS webhook and Voice webhook are configured independently in Twilio and use separate Gateway paths; this page addresses only the SMS webhook.
US A2P / 10DLC delivery
Applications sending SMS and MMS from a US local 10DLC number to US recipients must complete US A2P 10DLC registration. Toll-free numbers and short codes follow different verification procedures. This sits apart from OpenClaw channel configuration: webhook signature validation, pairing, and outbound credentials can all be correct while carriers still block or filter delivery.
Before relying on a US 10DLC sender, verify in Twilio that:
- The account is paid; Twilio trial accounts cannot register for A2P 10DLC.
- A Primary or Secondary Compliance Profile is approved in Twilio Trust Hub.
- The Brand and Campaign are registered and approved.
- The Twilio phone number shows A2P status
REGISTEREDand sits in the Sender Pool of the Messaging Service tied to the approved Campaign, or themessagingServiceSidyou set here is that approved service. - The Campaign describes the real OpenClaw message use case and includes matching sample messages.
- Every website, keyword, offline, paper, or QR-code opt-in path is described completely. If the flow is not publicly visible, provide publicly accessible screenshots or other evidence.
- Messaging consent is voluntary and separate from required service terms, account creation, or purchase, with the privacy policy, terms, frequency, rates, and opt-out disclosures Twilio requires.
- You retain proof of consent, identify the sender, honor standard one-step opt-out keywords, and do not buy, rent, sell, or transfer consent. After an opt-out, send only one confirmation unless the recipient opts in again.
Use Twilio as the source of truth for current requirements: A2P 10DLC overview, registration quickstart, and required business and campaign information. This section is setup guidance, not legal advice.
If Twilio rejects the Brand or Campaign during registration review, fix that in Twilio before using the sender with OpenClaw. 30909 means the message flow or call to action is incomplete or unverifiable. 30923 means messaging consent is required as a condition of service, account creation, or purchase, or is bundled with service terms. 30893 means 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
In Twilio, open Phone Numbers > Manage > Active numbers and pick an SMS-capable number. To send attachments, choose one that is also MMS-capable. Save:
- Account SID, for example
ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - Auth Token
- Sender phone number, for example
+15551234567
If you use a Messaging Service instead of a fixed sender number, save the Messaging Service SID, for example MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Configure the SMS channel
Save this as sms.patch.json5 and change the placeholders:
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "pairing",
},
},
}
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 phone number settings, open Messaging and set A message comes in to:
https://gateway.example.com/webhooks/sms
Use HTTP POST. The default local path is /webhooks/sms; change channels.sms.webhookPath if you need a different route.
Expose the exact SMS webhook path
Your public URL must route the SMS path to the Gateway process (default port 18789). The same path serves inbound Twilio webhooks and short-lived, tokenized attachments when OpenClaw sends MMS. If you use Tailscale Funnel 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 use separate webhook paths. If the same Twilio number handles both, keep both routes configured in Twilio and in your tunnel.
Start the Gateway and approve first sender
openclaw gateway
Send a text message to the Twilio number. The first message creates a pairing request. Approve it:
openclaw pairing list sms
openclaw pairing approve sms <CODE>
Pairing codes expire after 1 hour.
Configuration Examples
All keys live under channels.sms (and per account under channels.sms.accounts.<id>):
| Key | Default | Purpose |
|---|---|---|
enabled | true | Turns the channel or account on or off. |
accountSid | , | Twilio Account SID (AC...). |
authToken | , | Twilio Auth Token; can be a plaintext string or a SecretRef. |
fromNumber | , | Sender number in E.164 format. |
messagingServiceSid | , | Messaging Service SID (MG...) applied when no fromNumber is available. |
defaultTo | , | Fallback target used by a send flow that does not specify one explicitly. |
webhookPath | /webhooks/sms | Gateway HTTP path that handles inbound Twilio webhooks. |
publicWebhookUrl | , | Public Twilio webhook URL; needed for signature validation and outbound MMS hosting. |
dangerouslyDisableSignatureValidation | false | Bypass X-Twilio-Signature checks; intended for local tunnel testing only. |
dmPolicy | "pairing" | One of pairing, allowlist, open, or disabled. |
allowFrom | [] | Permitted sender numbers in E.164, or "*" combined with dmPolicy: "open". |
textChunkLimit | 1500 | Max characters allowed in each outbound SMS segment. |
accounts, defaultAccount | , | Mapping for multiple accounts and the default account identifier. |
Config file
If you want the channel definition to move along with the Gateway config, rely on the config-file approach:
{
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 only affect the default account; values from config override anything set via env.
| Variable | Maps to |
|---|---|
TWILIO_ACCOUNT_SID | accountSid |
TWILIO_AUTH_TOKEN | authToken |
TWILIO_PHONE_NUMBER (alias TWILIO_SMS_FROM) | fromNumber |
TWILIO_MESSAGING_SERVICE_SID | messagingServiceSid |
SMS_PUBLIC_WEBHOOK_URL | publicWebhookUrl |
SMS_WEBHOOK_PATH | webhookPath |
SMS_ALLOWED_USERS | allowFrom (comma-separated) |
SMS_TEXT_CHUNK_LIMIT | textChunkLimit |
SMS_DANGEROUSLY_DISABLE_SIGNATURE_VALIDATION | dangerouslyDisableSignatureValidation ("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"
After that, activate the channel through config:
{
channels: {
sms: {
enabled: true,
dmPolicy: "pairing",
},
},
}
SecretRef auth token
authToken accepts a SecretRef (source: "env" | "file" | "exec" | "store"). Choose this option when the Gateway needs to pull the Twilio Auth Token from the OpenClaw secrets runtime rather than keeping 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 must have access to the referenced environment variable or secret provider. After you modify host environment variables, restart managed Gateway processes.
Messaging Service sender
Use messagingServiceSid in place of fromNumber when Twilio should 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",
},
},
}
If both fromNumber and messagingServiceSid survive config and env resolution, fromNumber takes precedence.
Default outbound target
Set defaultTo when automated or agent-initiated sends should fall back to a default destination if a send flow omits 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): unknown senders receive a pairing code; approve it withopenclaw pairing approve sms <CODE>.allowlist: only senders listed inallowFromare handled. An emptyallowFromblocks every sender (the Gateway logs a startup warning).open: config validation demands thatallowFrominclude"*". Without the wildcard, only listed numbers can converse.disabled: all inbound DMs are discarded.
Entries in allowFrom should be E.164 phone numbers, for example +15551234567. The sms: and twilio-sms: prefixes are accepted and normalized. For a private assistant, choose dmPolicy: "allowlist" with explicit phone numbers:
{
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
When the SMS channel is active, targets accept bare E.164 numbers or the sms: prefix:
openclaw message send --channel sms --target sms:+15551234567 --message "hello"
With implicit channel selection, the twilio-sms: prefix selects this channel without overriding 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"
The CLI requires an explicit --target. defaultTo serves automation and agent-initiated delivery paths where the target can be derived from channel config.
Agent replies from inbound SMS conversations automatically return to the sender via the configured Twilio sender.
SMS output is plain text. OpenClaw removes markdown, flattens fenced code blocks, converts links to label (url), and breaks long replies into chunks of at most textChunkLimit characters (default 1500) before sending through Twilio.
Sending MMS
Use the standard structured media field or the CLI --media option:
openclaw message send \
--channel sms \
--target sms:+15551234567 \
--message "photo" \
--media ./photo.jpg
OpenClaw fetches the attachment through the shared outbound-media policy, stores it briefly in plugin-scoped SQLite state, and hands Twilio a tokenized HTTPS URL on the configured publicWebhookUrl path. Media-only sends are supported.
channels.sms.mediaMaxMb caps each inbound and outbound attachment in MiB.
The selected account's mediaMaxMb takes precedence over the channel root, then
agents.defaults.mediaMaxMb provides the fallback. This per-attachment limit
does not replace Twilio's total-message or media-type ceilings below. Outbound
images may be optimized before sending.
The generated media URL is a bearer capability expiring after 10 minutes. Treat its full query string as confidential: configure reverse-proxy and access logs to strip the query string or redact every query value. OpenClaw Gateway route diagnostics log only the pathname, but cannot govern upstream proxy logs.
Outbound OpenClaw deliveries attach one media item. OpenClaw limits JPEG, JPG, PNG, and GIF attachments to 5,000,000 bytes; other supported media types are limited to 500,000 bytes. application/vcard attachments must be media-only; Twilio rejects them with a caption. Destination carriers may impose smaller limits or refuse unsupported formats. Twilio must fetch the generated URL without HTTP authentication, so publicWebhookUrl cannot contain embedded userinfo; query-based reverse-proxy tokens are preserved.
For incoming MMS, OpenClaw processes at most 10 attachments and downloads at most 5 MiB total. Any additional or unavailable attachments produce a visible unavailable-media notice instead of discarding the signed message or silently delivering an empty turn. Downloads occur only after sender authorization, with Twilio authentication and an api.twilio.com host restriction.
Delivery status
After each successful outbound send, OpenClaw stores the initial Twilio API status when the response includes one. When publicWebhookUrl is valid, every outbound message also gives Twilio a derived StatusCallback URL that keeps its base URL and connection overrides while adding the required delivery-callback retry settings. Invalid or oversized derived URLs are omitted.
Later delivery callbacks update the same plugin-scoped SQLite record. Semantic retries are deduplicated, older transitions cannot regress a terminal state, and conflicting terminal observations are reported as conflicted instead of picking a false winner. Records contain message SIDs, status/error metadata, and timestamps, but not message bodies or phone-number addresses. Each record is kept for up to 30 days after its latest observation, subject to the plugin-wide 5,000-message cap and oldest-record eviction.
Verify Setup
After the Gateway starts:
- Verify the Gateway log shows the SMS webhook route.
- Run a Twilio-side probe (checks the configured Twilio webhook URL/method, recent inbound errors, and the most recent stored outbound delivery state):
openclaw channels capabilities --channel sms
openclaw channels status --channel sms --probe --json
- Send an SMS to the Twilio number from your phone.
- Run
openclaw pairing list sms. - Approve the pairing code with
openclaw pairing approve sms <CODE>. - Send another SMS and confirm the agent replies.
For outbound-only testing, use:
openclaw message send --channel sms --target sms:+15557654321 --message "OpenClaw SMS test"
End-to-end test from macOS iMessage/SMS
On a Mac that can send carrier SMS through Messages, you can use imsg to drive the sender side without touching your phone:
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 should create a pairing request. The second message should receive the agent reply through Twilio.
Webhook security
By default, OpenClaw validates X-Twilio-Signature with publicWebhookUrl and authToken. The endpoint portion of publicWebhookUrl must match the URL set in Twilio exactly, covering scheme, host, path, and query string, byte for byte. OpenClaw omits Twilio connection-override fragments (#...) from signature computation, as required by Twilio.
The webhook route also enforces these rules independently of signature validation:
POSTonly.- A failed-request budget of 300 requests per minute per SMS account, webhook route, and resolved client address. Every request counts toward this budget, but HTTP 429 is triggered only after body parsing or Twilio signature validation fails.
- Signed delivery callbacks are categorized before inbound sender quotas and write to bounded, plugin-scoped SQLite state before returning HTTP 200. They do not draw from inbound dispatch quotas, which protect raw inbound message admission and downstream agent dispatch. Delivery persistence instead has a separate 3,000-callback-per-minute safety fuse per SMS account route, returning HTTP 503 without the durable-acceptance marker above that limit. This is fail-closed overload protection, not lossless backpressure. When signature validation is off, delivery callbacks first apply the stricter 30/minute resolved-client-address cap before persistence.
- Dispatchable callback rate limit of 30 accepted callbacks per minute per SMS account, webhook route, and validated sender after body parsing and signature validation pass (HTTP 429 above that). The sender key is the canonicalized, signature-covered
Fromvalue, so equivalent SMS/RCS address forms share one budget, one flooding sender exhausts only its own budget, and callbacks from other senders behind Twilio's shared egress addresses remain dispatchable. Invalid or missing sender values share a separate empty-sender budget. - Aggregate validated-callback ceiling of 300 accepted callbacks per minute per SMS account and webhook route. This bounds durable-ingress pressure from many distinct signed senders without recreating shared-egress cross-throttling. If signature validation is disabled, nothing authenticates
From; the stricter 30/min resolved-client-address dispatch cap applies instead of the validated sender and aggregate policy. - Client addresses are resolved through the shared Gateway trusted-proxy rules. If
gateway.trustedProxiescontains the reverse proxy that forwards Twilio callbacks, OpenClaw keys the address-based limits from the forwarded client address; otherwise it falls back to the direct socket address. - Inbound payloads must include a nonempty
AccountSidthat matches the configuredaccountSidexactly. Direct-number callbacks must target the configuredfromNumber; Messaging Service callbacks must carry the configuredMessagingServiceSid. The raw callback is first committed to the durable ingress queue and acknowledged; an identity mismatch is then marked as a permanent invalid-payload failure during drain and is never dispatched or allowed to download media. - Delivery callbacks with a missing or different
AccountSidare acknowledged, logged, and intentionally not stored. - Replayed
MessageSidvalues are deduplicated by the durable ingress queue. Completed-message tombstones are retained for 24 hours (up to 20,000 entries per account); permanent-failure tombstones are retained for 30 days (up to 1,000 entries). - Delivery observations use a semantic, non-PII fingerprint of source, message SID, normalized status, error code, and carrier completion date. Multiple states for one outbound message remain distinct. Records expire 30 days after their latest observation, while the 5,000-message cap can evict older records sooner.
- Request bodies over 32 KB are rejected.
OpenClaw adds the 5xx retry policy and a retry count to generated delivery StatusCallback URLs so Twilio can retry a failed SQLite commit or an overloaded delivery-state route. Twilio does not retry HTTP 429 by default. The #rp=4xx and #rp=all connection overrides opt into 4xx retries, but Twilio caps the complete retry transaction at 15 seconds. Neither a 429 nor a delivery-state 503 guarantees later recovery; use reconciliation when final-state completeness matters. Missed intermediate transitions cannot be reconstructed.
For completeness-sensitive workflows, persist Message SIDs and reconcile stale nonterminal records by polling Twilio's Message resource. Twilio's delivery logging guidance recommends polling when a message has not reached delivered or undelivered within 12 hours because a status callback may not have arrived. The SMS fallback URL is not a substitute: it only handles failures retrieving or executing the inbound SMS TwiML webhook.
For local tunnel testing only, you can set:
{
channels: {
sms: {
dangerouslyDisableSignatureValidation: true,
},
},
}
Do not use disabled signature validation on a public Gateway.
Multi-account config
Use 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"],
},
},
},
},
}
Each account must use a distinct webhookPath; the Gateway refuses to register a webhook route whose path is already owned by another account. TWILIO_*/SMS_* environment fallbacks apply only to the default account; set defaultAccount to change which account that is.
Troubleshooting
Twilio returns 403 or OpenClaw rejects the webhook
Check that publicWebhookUrl exactly matches the URL configured in Twilio, including scheme, host, path, and query string. Twilio signs the public URL string, so proxy rewrites and alternate hostnames can break signature validation.
If Twilio receives a durable acknowledgement but no pairing request appears, check the Gateway log for a permanent invalid-payload failure. Confirm 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
Check the Twilio number's Messaging webhook URL and method. It must point to the SMS webhook URL and use POST. Also confirm the Gateway is reachable from the public internet or through your tunnel.
If the Twilio message log shows 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, runtailscale funnel statusand confirm/webhooks/smsis listed. publicWebhookUrluses the same scheme, host, path, and query string 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 accountSid, authToken, and either fromNumber or messagingServiceSid are resolved. Twilio trial accounts can send only to verified recipients in the account's sign-up country and must use Twilio's predefined content; custom SMS bodies are not supported. Trial accounts also cannot register for A2P 10DLC, so upgrade before registering a US 10DLC sender.
Twilio accepts the send but delivery later fails
Start with OpenClaw's stored delivery observation:
openclaw channels status --channel sms --probe --json
When the most recent outbound status reads failed or undelivered, look at its messageSid to determine the final Message status and the error code reported by Twilio. If you see 30034, the sender has not been registered, or it does not belong to the Sender Pool of the Messaging Service tied to the approved Campaign. A status of 30035 indicates that Twilio is still in the process of registering, deregistering, or reassigning the number; hold off on sending until the status changes to REGISTERED.
Messages arrive but the agent does not answer
Review dmPolicy and allowFrom. Under the default pairing policy, the sender must gain approval before regular agent turns can proceed.