Twilio SMS Global Settings
How Strike sysadmins configure SMS delivery callbacks 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 callback URL. The trap is that the callback URL is not the Strike UI domain and not the backend gateway. It is the public Cloudflare Worker URL Twilio signs and posts to before the request reaches Strike.
The SMS tab is global configuration
The System Configuration SMS tab holds deployment-global SMS settings: provider, account SID, auth token, and the delivery StatusCallback 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 or live Twilio.
The callback URL is not the UI domain
The callback URL is not the UI URL. From https://osprey-<env>.constructured.ai/, take <env> and build https://webhooks-<env>.constructured.com/webhooks/sms. For osprey-main.constructured.ai, the value is https://webhooks-main.constructured.com/webhooks/sms.
Why exactness matters
Twilio signatures bind to the exact URL. Strike sets that configured string as the outbound message StatusCallback, then validates incoming callbacks against the same string. 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.
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.
The operator model
The SMS tab is where the layers meet: infrastructure provides Worker hostnames, System Configuration stores the chosen callback, the sender gives it 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 four fields: provider, account SID, auth token, and callback URL. 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 callback value in the screenshot is:
https://webhooks-main.constructured.com/webhooks/sms
That value is correct for the main Strike environment. It is not derived by copying the browser address bar. It is not the API gateway hostname. It is the public Cloudflare Worker hostname plus the SMS delivery-status path.
For a Strike UI domain https://osprey-<env>.constructured.ai/, the SMS delivery callback is https://webhooks-<env>.constructured.com/webhooks/sms.
The SMS tab is global configuration
Issue 1049 moved SMS setup out of process startup configuration and into System Configuration. That change matters operationally: a sysadmin can switch the provider, set the Twilio account, replace the encrypted auth token, and update the delivery callback 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 local or test behavior, live Twilio for real delivery. The callback URL is also global because Twilio needs one public delivery-status endpoint 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.
In the SMS tab, the fields mean:
| Field | What it controls | Operational note |
|---|---|---|
| Provider | Whether SMS uses mock behavior or live Twilio | 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. |
| Callback URL | The public delivery-status URL sent to Twilio as StatusCallback |
Must match the Worker URL exactly. |
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 callback URL 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 callback host directly.
The callback host uses a different subdomain and a different top-level domain:
https://webhooks-main.constructured.com/webhooks/sms
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/webhooks/sms.
That gives the common environments:
| Strike UI domain | Environment slug | SMS callback URL |
|---|---|---|
https://osprey-main.constructured.ai/ |
main |
https://webhooks-main.constructured.com/webhooks/sms |
https://osprey-demo.constructured.ai/ |
demo |
https://webhooks-demo.constructured.com/webhooks/sms |
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 the SMS callback at osprey-<env>.constructured.ai, and do not point it 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 validates against the configured callback URL 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 callback URL and validates against that.
The consequence is simple: the stored callback string has to be exact.
If the SMS tab stores https://webhooks-main.constructured.com/webhooks/sms, and Twilio was given that same URL as the outbound message StatusCallback, the signatures line up. If someone stores https://main-gw.constructured.com/webhooks/sms, the Worker may still accept and forward a request, but the app’s second validation can reject it because the configured URL no longer matches the URL Twilio signed.
The 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 callback URL is configured, Strike sets it as the message StatusCallback. If it 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 work, not to this delivery-status callback. Do not use /webhooks/sms/inbound in the delivery callback field.
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/webhooks/sms. - Confirm
webhooks-<env>.constructured.comappears in the Workerwebhook_hostnameslist. - Enter that exact URL in System Configuration -> SMS -> Callback URL.
- Save, then send or simulate a Twilio-signed delivery-status callback.
There are three 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. |
| Using the inbound reply path | Delivery callbacks do not hit the delivery-status handler | Use /webhooks/sms, not /webhooks/sms/inbound. |
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 URL in the SMS tab is not byte-identical to the public URL Twilio signed, or the auth token used by the app does not match the one Twilio used to sign the request.
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 which callback URL this deployment tells Twilio to use.
- The sender attaches that callback URL to outbound Twilio messages.
- The Worker validates and forwards the callback.
- The API validates the same URL again and records delivery status.
Each piece has one job. The SMS tab is the place where those jobs meet.
- The SMS callback is global. It belongs in System Configuration because it is deployment-wide Twilio plumbing, not per-NOC policy.
- The URL comes from the environment slug.
osprey-main.constructured.aimaps tohttps://webhooks-main.constructured.com/webhooks/sms. - 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 stored callback URL, so near-misses can become signature failures.
- Delivery status is separate from replies.
/webhooks/smstracks Twilio delivery transitions; inbound STOP/HELP behavior uses a different endpoint.
- Should the SMS tab derive and display a suggested callback URL 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 callback URL?
- 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