docs/channels/sms.md
OpenClaw receives and sends SMS through a Twilio phone number or Messaging Service. The Gateway registers an inbound webhook route (default /webhooks/sms), validates Twilio request signatures by default, and sends replies back through Twilio's Messages API.
Status: official plugin, installed separately. Text only: no MMS/media, direct messages only.
<CardGroup cols={3}> <Card title="Pairing" icon="link" href="/channels/pairing"> Default DM policy for SMS is pairing. </Card> <Card title="Gateway security" icon="shield" href="/gateway/security"> Review webhook exposure and sender access controls. </Card> <Card title="Channel troubleshooting" icon="wrench" href="/channels/troubleshooting"> Cross-channel diagnostics and repair playbooks. </Card> </CardGroup>You need:
openclaw plugins install @openclaw/sms.pairing (default) for private use, allowlist for preapproved phone numbers, or open only for intentionally public SMS access.One Twilio number can serve both SMS and Voice Call if it has both capabilities. The SMS webhook and Voice webhook are configured separately in Twilio and use separate Gateway paths; this page only covers the SMS webhook.
- 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`.
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
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.
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.
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.
All keys live under channels.sms (and per account under channels.sms.accounts.<id>):
| Key | Default | Purpose |
|---|---|---|
enabled | true | Enable or disable the channel/account. |
accountSid | — | Twilio Account SID (AC...). |
authToken | — | Twilio Auth Token; plaintext string or SecretRef. |
fromNumber | — | E.164 sender number. |
messagingServiceSid | — | Messaging Service SID (MG...) used when no fromNumber resolves. |
defaultTo | — | Default destination when a send flow omits an explicit target. |
webhookPath | /webhooks/sms | Gateway HTTP path for inbound Twilio webhooks. |
publicWebhookUrl | — | Public URL configured in Twilio; required for signature validation. |
dangerouslyDisableSignatureValidation | false | Skip X-Twilio-Signature checks; local tunnel testing only. |
dmPolicy | "pairing" | pairing, allowlist, open, or disabled. |
allowFrom | [] | Allowed sender numbers in E.164, or "*" with dmPolicy: "open". |
textChunkLimit | 1500 | Maximum characters per outbound SMS chunk. |
accounts, defaultAccount | — | Multi-account map and default account id. |
Use config-file setup when you want the channel definition to travel 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 apply to the default account only; config values take precedence over env values.
| 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"
Then enable the channel in config:
{
channels: {
sms: {
enabled: true,
dmPolicy: "pairing",
},
},
}
authToken can be a SecretRef (source: "env" | "file" | "exec"). Use this when the Gateway should resolve the Twilio Auth Token from the OpenClaw secrets runtime instead of storing plaintext 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 referenced environment variable or secret provider must be visible to the Gateway runtime. Restart managed Gateway processes after changing host environment variables.
Use messagingServiceSid instead of fromNumber when Twilio should choose the sender through 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 are present after config and env resolution, fromNumber is used.
Set defaultTo when automation or agent-initiated delivery should have 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",
},
},
}
channels.sms.dmPolicy controls direct SMS access:
pairing (default): unknown senders get a pairing code; approve with openclaw pairing approve sms <CODE>.allowlist: only senders in allowFrom are processed. An empty allowFrom rejects every sender (the Gateway logs a startup warning).open: config validation requires allowFrom to include "*". Without the wildcard, only listed numbers can chat.disabled: all inbound DMs are dropped.allowFrom entries should be E.164 phone numbers such as +15551234567. sms: and twilio-sms: prefixes are accepted and normalized. For a private assistant, prefer 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"],
},
},
}
With the SMS channel selected, targets accept bare E.164 numbers or the sms: prefix:
openclaw message send --channel sms --target sms:+15551234567 --message "hello"
When channel selection is implicit, the twilio-sms: prefix selects this channel without taking over the sms: service prefix, which iMessage uses to pick carrier SMS delivery for its own targets:
openclaw message send --target twilio-sms:+15551234567 --message "hello"
The CLI requires an explicit --target. defaultTo is for automation and agent-initiated delivery paths where the target can be resolved from channel config.
Agent replies from inbound SMS conversations automatically go back to the sender through the configured Twilio sender.
SMS output is plain text. OpenClaw strips markdown, flattens fenced code blocks, rewrites links as label (url), and splits long replies into chunks of at most textChunkLimit characters (default 1500) before sending them through Twilio.
After the Gateway starts:
openclaw channels capabilities --channel sms
openclaw channels status --channel sms --probe --json
openclaw pairing list sms.openclaw pairing approve sms <CODE>.For outbound-only testing, use:
openclaw message send --channel sms --target sms:+15557654321 --message "OpenClaw SMS test"
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.
By default, OpenClaw validates X-Twilio-Signature using publicWebhookUrl and authToken. Keep publicWebhookUrl byte-for-byte aligned with the URL configured in Twilio, including scheme, host, path, and query string.
The webhook route also enforces, independent of signature validation:
POST only.AccountSid must match the configured accountSid (HTTP 403 otherwise).MessageSid values are deduplicated for 10 minutes.For local tunnel testing only, you can set:
{
channels: {
sms: {
dangerouslyDisableSignatureValidation: true,
},
},
}
Do not use disabled signature validation on a public Gateway.
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.
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.
A 403 with Invalid account means the inbound payload's AccountSid does not match the configured accountSid; check that the webhook points at the account that owns the number.
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:
publicWebhookUrl.POST.webhookPath; for Tailscale Funnel, run tailscale funnel status and confirm /webhooks/sms is listed.publicWebhookUrl uses 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.
Confirm accountSid, authToken, and either fromNumber or messagingServiceSid are resolved. If you use a trial Twilio account, the destination number may need to be verified in Twilio before outbound SMS will send.
Check dmPolicy and allowFrom. With the default pairing policy, the sender must be approved before normal agent turns are processed.