architectureoperationssecurity

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.

Osprey Strike System Configuration SMS tab showing Twilio live provider, account SID, masked auth token, and callback URL set to https://webhooks-main.constructured.com/webhooks/sms
The SMS tab stores the deployment-global Twilio settings. Current Strike code stores one public webhook base and derives both Twilio webhook endpoints from it.

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.

Key Takeaway

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.
Definition

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:

  1. Start with the Strike UI domain, such as https://osprey-main.constructured.ai/.
  2. Take the environment slug between osprey- and .constructured.ai; here, main.
  3. 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.

Warning

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.

Key Takeaway

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:

  1. Open the target Twilio account.
  2. In the left navigation, open Messaging, then Services.
  3. Open the Messaging Service used by Strike. In the captured account, that service is Osprey Strike Customer Messaging.
  4. Open the Opt-out tab.
Twilio Console Messaging Services page with the Osprey Strike Customer Messaging service listed
Messaging -> Services lists the Messaging Services in the account. The captured account shows Osprey Strike Customer Messaging; confirm the service before changing keyword behavior.
Twilio Console Opt-out management page showing STOP opt-out, START and UNSTOP opt-in, and HELP keyword confirmation messages for Constructured Inc emergency notifications
The Messaging Service Opt-out tab controls Twilio's reserved opt-out, opt-in, and help keyword behavior for this service. Current settings unsubscribe on STOP, resume on START or UNSTOP, and let Twilio send the configured HELP reply.

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.

Warning

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:

  1. Open the Strike environment and read the UI hostname.
  2. Extract the environment slug from osprey-<env>.constructured.ai.
  3. Build https://webhooks-<env>.constructured.com.
  4. Confirm webhooks-<env>.constructured.com appears in the Worker webhook_hostnames list.
  5. Enter the base host in System Configuration -> SMS -> Webhook Base URL.
  6. Copy the derived delivery callback into Twilio delivery-status configuration and the derived inbound-reply URL into the Messaging Service inbound request URL.
  7. 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:

  1. Open the target Twilio account.
  2. In the left navigation, open Messaging, then Logs.
  3. Open Messages.
  4. Filter by time window, sender, recipient, direction, status, or message SID as needed.
Twilio Console Messages log with Messaging, Logs, and Messages selected and a table of SMS records
Messaging -> Logs -> Messages is the Twilio-side evidence trail for SMS sends and inbound replies. Use it to find the provider message SID, status, direction, sender, recipient, and timestamp before correlating with Strike logs.

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.

  1. The SMS webhook base is global. It belongs in System Configuration because it is deployment-wide Twilio plumbing, not per-NOC policy.
  2. The base comes from the environment slug. osprey-main.constructured.ai maps to https://webhooks-main.constructured.com, then Strike derives the two route-specific URLs.
  3. The Worker is the public front door. Twilio calls webhooks-<env>; the Worker forwards to <env>-gw after validation.
  4. The exact string matters. Strike validates incoming callbacks against the URL derived from the stored base, so near-misses can become signature failures.
  5. Delivery status is separate from replies. /webhooks/sms tracks Twilio delivery transitions; inbound STOP/HELP/START/UNSTOP behavior uses /webhooks/sms/inbound.
  6. 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?
  1. Osprey Strike issue 1049 - The feature request and implementation decision thread for moving SMS configuration into System Configuration.
  2. Osprey Strike PR 1082 - The infrastructure documentation PR that records the `webhooks-.constructured.com/webhooks/sms` derivation rule.
  3. Twilio: Track outbound message status - Twilio's guide to delivery status callbacks for outbound messages.
  4. Twilio: Validating requests - Background on Twilio request signatures and why the exact public URL matters.
  5. NOC-Scoped Does Not Mean NOC-Owned - The related ownership model for Strike-owned telephony plumbing and NOC-scoped context.