Twilio SMS Global Settings
How Strike sysadmins configure SMS delivery callbacks and Twilio opt-out handling without guessing the webhook URL · ~13 min read ~– min read · Suggested by Laura engineeringoperations
The SMS tab in System Configuration looks simple: provider, account SID, auth token, and webhook base URL. The trap is that this base is not the Strike UI domain and not the backend gateway. Strike derives both Twilio webhook URLs from it: the delivery-status callback and the inbound-reply URL. The adjacent trap is in Twilio itself: Messaging Service Opt-out Management owns reserved keyword behavior that affects Strike's opt-in and opt-out flows.
The SMS tab is global configuration
The System Configuration SMS tab holds deployment-global SMS settings: provider, account SID, auth token, and the public webhook base URL. The account SID identifies the Twilio account, the token is encrypted and never shown again, and the provider decides whether Strike sends through the mock path, mockserver path, or live Twilio. Strike derives the delivery StatusCallback URL and inbound-reply URL from the one stored base.
The webhook base is not the UI domain
The webhook base is not the UI URL. From https://osprey-<env>.constructured.ai/, take <env> and build https://webhooks-<env>.constructured.com. For osprey-main.constructured.ai, the base is https://webhooks-main.constructured.com; Strike appends /webhooks/sms for delivery status and /webhooks/sms/inbound for replies.
Why exactness matters
Twilio signatures bind to the exact URL. Strike derives the outbound message StatusCallback from the stored base, then validates incoming callbacks against that derived URL. The Cloudflare Worker also validates the Twilio signature before forwarding to <env>-gw.constructured.com.
What happens during delivery tracking
The callback is delivery status only. Strike attaches the configured URL to outbound messages, Twilio posts form-encoded status changes to /webhooks/sms, and the API records transitions by message SID. STOP, HELP, and inbound replies use a different endpoint.
Where opt-out management lives
Twilio Messaging Service settings still matter. In the Twilio Console, open Messaging -> Services -> Osprey Strike Customer Messaging -> Opt-out. This page owns carrier-standard reserved keyword behavior. Strike records YES, STOP, START, UNSTOP, and HELP through /webhooks/sms/inbound, but HELP is a special case: the app audits it and stays silent because Twilio sends the configured HELP reply.
How to determine the right value
Use the env slug and the provisioned host list. Confirm webhooks-<env>.constructured.com exists in the Worker webhook_hostnames list, then test with a Twilio-signed POST. A 503 with a fake message SID proves both signature gates passed; 401 points at the Worker/front-door layer; 403 means the app rejected the stored callback string or token.
Where to check logs
Use Twilio logs for provider-side evidence. In the Twilio Console, open Messaging -> Logs -> Messages to inspect SMS activity by timestamp, sender, recipient, status, direction, and message SID. Use those logs alongside Strike audit rows when debugging consent replies or delivery callbacks.
The operator model
The SMS tab is where the layers meet: infrastructure provides Worker hostnames, System Configuration stores the chosen webhook base, the sender gives the derived callback to Twilio, the Worker validates and forwards, and the API validates again before recording delivery status.
The screenshot is intentionally boring. It shows a sysadmin-only System Configuration tab with provider, account SID, auth token, and webhook URL fields. That is exactly the right product shape. SMS configuration should be visible to the people operating the deployment, not hidden in process environment variables that require an engineer and a redeploy.
The field that deserves explanation is the one that looks most like a normal URL.
The delivery callback value shown in the screenshot is:
https://webhooks-main.constructured.com/webhooks/sms
That value is correct for the main Strike environment. Current code now stores the base host, https://webhooks-main.constructured.com, and derives this delivery-status URL by appending /webhooks/sms. It derives the inbound-reply URL by appending /webhooks/sms/inbound. The base is not copied from the browser address bar and is not the API gateway hostname.
For a Strike UI domain https://osprey-<env>.constructured.ai/, the SMS webhook base is https://webhooks-<env>.constructured.com. Strike derives https://webhooks-<env>.constructured.com/webhooks/sms and https://webhooks-<env>.constructured.com/webhooks/sms/inbound from that base.
The SMS tab is global configuration
Issue 1049 moved SMS setup out of process startup configuration and into System Configuration. Issue 1053 narrowed the URL setting to one webhook base host, then made Strike derive the two API-owned route paths from that base. That matters operationally: a sysadmin can switch the provider, set the Twilio account, replace the encrypted auth token, and update both Twilio webhook targets without waiting for a redeploy.
The shape is intentionally hybrid.
The SMS provider is global. It decides which sending backend Strike uses for the deployment: mock for inert local behavior, mockserver for Twilio-shaped local callback testing, or live Twilio for real delivery. The webhook base URL is also global because Twilio needs one public front door per deployment environment.
Per-NOC identity stays elsewhere. NOC-scoped phone numbers, active Twilio instance selection, and tenant identity are part of the NOC configuration surface. That is why the earlier cairn NOC-Scoped Does Not Mean NOC-Owned is still the ownership model: Strike owns the telephony plumbing; the NOC has scoped identity inside it.
That boundary now matters for voice as well as SMS. Pager flow no longer resolves voice configuration from a deployment-global Twilio setting; it uses the NOC’s active Twilio instance. The SMS tab still owns the deployment-wide provider, account credentials, and webhook base used for SMS delivery and replies. Do not infer pager voice routing from this page.
In the SMS tab, the fields mean:
| Field | What it controls | Operational note |
|---|---|---|
| Provider | Whether SMS uses mock, mockserver, or live Twilio behavior | This is deployment-global, not per NOC. |
| Account SID | The public AC... Twilio account identifier used by the SMS sender |
It is an identifier, not a secret, but it should match the token. |
| Auth Token | The Twilio signing and API secret | Store encrypted; never render the previous value back in plaintext. |
| Webhook Base URL | The public Worker base host used to derive Twilio webhook URLs | Store the base host only, such as https://webhooks-main.constructured.com. |
| Delivery Callback URL | Derived from the base plus /webhooks/sms |
Sent to Twilio as StatusCallback; must match the URL Twilio calls exactly. |
| Inbound Reply URL | Derived from the base plus /webhooks/sms/inbound |
Paste this into the Twilio Messaging Service inbound request URL. |
Twilio’s SMS StatusCallback is the URL Strike asks Twilio to POST delivery state changes to after a message is accepted: sent, delivered, failed, undelivered, and related transitions.
The webhook base is not the UI domain
The browser in the screenshot is on the Strike UI:
https://osprey-main.constructured.ai/...
That tells you the environment slug is main. It does not tell you the webhook host directly.
The webhook base uses a different subdomain and a different top-level domain:
https://webhooks-main.constructured.com
The derivation has three steps:
- Start with the Strike UI domain, such as
https://osprey-main.constructured.ai/. - Take the environment slug between
osprey-and.constructured.ai; here,main. - Build
https://webhooks-<env>.constructured.com.
That gives the common environments:
| Strike UI domain | Environment slug | Webhook base | Delivery callback | Inbound reply |
|---|---|---|---|---|
https://osprey-main.constructured.ai/ |
main |
https://webhooks-main.constructured.com |
/webhooks/sms |
/webhooks/sms/inbound |
https://osprey-demo.constructured.ai/ |
demo |
https://webhooks-demo.constructured.com |
/webhooks/sms |
/webhooks/sms/inbound |
The slug convention is not enough by itself. The Worker host must also exist in the Cloudflare Worker configuration. In the infrastructure source, infrastructure/opentofu/cf-worker/terraform.tfvars carries the authoritative webhook_hostnames list. If webhooks-<env>.constructured.com is not in that list and applied, there is no front door for Twilio to reach.
Do not point Twilio webhook settings at osprey-<env>.constructured.ai, and do not point them at <env>-gw.constructured.com. Twilio calls the public Worker host. The Worker forwards to the protected gateway.
Why exactness matters
There are two signature checks in the live path.
First, the Cloudflare Worker receives the Twilio webhook request at webhooks-<env>.constructured.com. It validates X-Twilio-Signature using the Twilio auth token. Then it derives the environment from the hostname and forwards the request to the protected backend gateway:
webhooks-main.constructured.com -> main-gw.constructured.com
webhooks-demo.constructured.com -> demo-gw.constructured.com
Second, the Strike API validates the Twilio signature again inside the /webhooks/sms handler. The important detail is that the API validator does not reconstruct the public URL from the backend request host. It derives the expected callback URL from the stored webhook base and validates against that derived string.
That is deliberate. Reverse proxies, TLS termination, and gateway forwarding can change the host the backend sees. Twilio signed the public URL it called, not necessarily the internal host the API received after forwarding. So the API holds the configured public webhook base, derives the public callback URL, and validates against that.
The consequence is simple: the stored base string, derived path, and Twilio-side URL have to line up exactly.
If the SMS tab stores https://webhooks-main.constructured.com, Strike gives Twilio https://webhooks-main.constructured.com/webhooks/sms as the outbound message StatusCallback, and the signatures line up. If someone stores https://main-gw.constructured.com, or pastes a different URL into Twilio’s Messaging Service, the Worker may still accept and forward a request, but the app’s second validation can reject it because the derived URL no longer matches the URL Twilio signed.
The Kubernetes overlays still carry explicit reply URL values for the deployed API, including the main-environment TWILIO_SMS_REPLY_URL. Treat those as deployment wiring that must stay aligned with the System Configuration base-host model. A configmap can make the runtime reachable, but the operator-facing source of truth remains the SMS tab’s webhook base and the two route-specific URLs derived from it.
The derived callback URL is both outbound configuration and inbound validation material. It is not just where Twilio sends callbacks; it is also part of the signature contract Strike uses to decide whether callbacks are authentic.
What happens during delivery tracking
When Strike sends a live SMS, the Twilio sender creates a message through Twilio’s Messages API. If a webhook base URL is configured, Strike derives the delivery callback and sets it as the message StatusCallback. If the base is empty, the send can still happen, but delivery status will not flow back into Strike.
After Twilio accepts the message, delivery updates arrive asynchronously. Twilio posts form-encoded status data to /webhooks/sms, including the message SID and message status. Strike parses those values and records the transition against the delivery store.
That gives the ECO workflow a more useful record than “we tried to send a text.” The system can tell whether Twilio accepted the message, whether delivery progressed, and whether Twilio reported failure or non-delivery later.
The callback path is delivery status only:
/webhooks/sms
Inbound replies are a different product path. STOP, HELP, opt-in, and other inbound reply behavior belong to the separate inbound SMS webhook. Current code derives that reply URL from the same base host as the delivery callback. Do not paste /webhooks/sms/inbound into the delivery callback slot in Twilio, and do not paste /webhooks/sms into the Messaging Service inbound request URL.
Where opt-out management lives
Twilio has a second operator-visible surface that affects the opt-in and opt-out features: Messaging Service Opt-out Management.
Console path:
- Open the target Twilio account.
- In the left navigation, open Messaging, then Services.
- Open the Messaging Service used by Strike. In the captured account, that service is Osprey Strike Customer Messaging.
- Open the Opt-out tab.
This page is not where you set the delivery-status callback URL. It is where Twilio manages carrier-standard consent keywords for the Messaging Service. In the captured service, the visible keyword shape matches the Strike inbound consent implementation:
| Twilio surface | Visible keyword behavior | Strike behavior |
|---|---|---|
| Opt-out | Stop |
Strike persists an opt-out by phone and returns the configured STOP confirmation. |
| Opt-in | Start, Unstop |
Strike treats START and UNSTOP as resubscribe/re-invite keywords. |
| Help | Managed in the same Twilio Opt-out Management feature family | Strike records HELP for audit, but sends no app reply because Twilio owns the reserved HELP response. |
That HELP split is deliberate. The inbound SMS handler records the HELP interaction so the consent audit trail still proves the webhook received it, but it returns empty TwiML. If Strike replied too, the contact could receive duplicate HELP responses because Twilio always auto-answers the reserved HELP keyword from the Messaging Service configuration.
Changing Messaging Service opt-out settings can change what subscribers see when they text STOP, START, UNSTOP, or HELP. Treat it as live consent-management configuration, not just console decoration.
How to determine the right value
Use this checklist when configuring a deployment:
- Open the Strike environment and read the UI hostname.
- Extract the environment slug from
osprey-<env>.constructured.ai. - Build
https://webhooks-<env>.constructured.com. - Confirm
webhooks-<env>.constructured.comappears in the Workerwebhook_hostnameslist. - Enter the base host in System Configuration -> SMS -> Webhook Base URL.
- Copy the derived delivery callback into Twilio delivery-status configuration and the derived inbound-reply URL into the Messaging Service inbound request URL.
- Save, then send or simulate a Twilio-signed delivery-status callback.
There are four common mistakes:
| Mistake | Symptom | Fix |
|---|---|---|
| Using the UI domain | Twilio reaches the wrong surface, or nothing useful handles the POST | Use webhooks-<env>.constructured.com. |
Using <env>-gw.constructured.com |
Request hits a protected backend path instead of the Worker front door | Use the Worker host; let it forward to gateway. |
| Storing a full callback URL as the base | Strike derives a doubled path such as /webhooks/sms/webhooks/sms |
Store only the base host. |
| Using the inbound reply path as the callback | Delivery callbacks do not hit the delivery-status handler | Use /webhooks/sms for delivery and /webhooks/sms/inbound for replies. |
For a live verification, send a properly Twilio-signed POST to the derived URL. If the message SID is fake, the app may return a retryable store miss, but the HTTP status still tells you which layer accepted the request:
| Response | Meaning |
|---|---|
503 |
The request passed both signature gates and reached the API; a fake or not-yet-recorded message SID missed the delivery row. |
401 |
The Worker/front-door layer rejected the request or the route is not live. |
403 |
The Worker accepted the request, but the API rejected the signature against the stored callback string or token. |
That 403 is the useful clue. It usually means the base in the SMS tab derives a URL that is not byte-identical to the public URL Twilio signed, Twilio is calling the wrong derived path, or the auth token used by the app does not match the one Twilio used to sign the request.
Where to check logs
When SMS behavior is unclear, check Twilio’s provider-side message log as well as Strike’s application-side audit trail.
Console path:
- Open the target Twilio account.
- In the left navigation, open Messaging, then Logs.
- Open Messages.
- Filter by time window, sender, recipient, direction, status, or message SID as needed.
The log page does not replace the Strike audit trail. It tells you what Twilio saw and accepted. Strike still needs its own delivery-status records and consent-audit rows to prove that the webhook reached the application and changed, or intentionally did not change, contact consent state.
The operator model
This configuration is a good example of why “global setting” does not have to mean “hard-coded deployment secret.” The value is deployment-wide because the public callback front door is deployment-wide. But the right operator should still be able to change it in the application, with a clear label and enough documentation to know what string belongs there.
That is the useful split:
- Infrastructure defines which Worker hostnames exist.
- System Configuration stores the public webhook base for this deployment.
- Strike derives the delivery callback and inbound-reply URLs from that base.
- Kubernetes overlays provide deployment wiring, but they must match the same base-host and reply-route contract.
- The sender attaches the delivery callback URL to outbound Twilio messages.
- Pager voice uses the NOC’s active Twilio instance, not the SMS tab’s global settings.
- Twilio’s Messaging Service owns reserved keyword settings and message logs.
- The Worker validates and forwards the callback.
- The API validates the same URL again and records delivery status or consent transitions.
Each piece has one job. The SMS tab is the place where those jobs meet.
- The SMS webhook base is global. It belongs in System Configuration because it is deployment-wide Twilio plumbing, not per-NOC policy.
- The base comes from the environment slug.
osprey-main.constructured.aimaps tohttps://webhooks-main.constructured.com, then Strike derives the two route-specific URLs. - The Worker is the public front door. Twilio calls
webhooks-<env>; the Worker forwards to<env>-gwafter validation. - The exact string matters. Strike validates incoming callbacks against the URL derived from the stored base, so near-misses can become signature failures.
- Delivery status is separate from replies.
/webhooks/smstracks Twilio delivery transitions; inbound STOP/HELP/START/UNSTOP behavior uses/webhooks/sms/inbound. - Twilio still has operational state. Messaging Service Opt-out Management controls reserved keyword responses, and Messaging -> Logs -> Messages is the provider-side evidence trail.
- Should the SMS tab derive and display a suggested webhook base from the current environment to reduce manual entry?
- What UI copy would make the Worker-versus-gateway distinction clear without turning the form into infrastructure documentation?
- Should the save flow perform a reachability or signed-callback verification before accepting a webhook base?
- Osprey Strike issue 1049 - The feature request and implementation decision thread for moving SMS configuration into System Configuration.
- Osprey Strike PR 1082 - The infrastructure documentation PR that records the `webhooks-
.constructured.com/webhooks/sms` derivation rule. - Twilio: Track outbound message status - Twilio's guide to delivery status callbacks for outbound messages.
- Twilio: Validating requests - Background on Twilio request signatures and why the exact public URL matters.
- NOC-Scoped Does Not Mean NOC-Owned - The related ownership model for Strike-owned telephony plumbing and NOC-scoped context.
Generated by Cairns · Agent-powered with Claude