DEV Community

FlareCanary
FlareCanary

Posted on

Twilio's Transit CallerID Sunset on May 31 — Your Voice Flows Already Look Broken in Six Countries and You Probably Don't Know

If your product makes outbound voice calls through Twilio and you're presenting a CallerID that isn't a Twilio-owned or Verified CallerID number, May 31, 2026 is a date you need on the calendar. Twilio is sunsetting Transit CallerID. Six countries are already blocking these calls today.

The reason this one is dangerous isn't the cutoff itself — Twilio has been clear about the date for months. It's the failure mode before the cutoff, in the regions that have already started enforcing. The Twilio side of the call returns 200, the CallStatus webhook reports completed or in-progress, your aggregate latency dashboards stay green — and the call never reaches the called party because the regulated carrier drops it on the spec edge.

What Transit CallerID is, and what's changing

Twilio Voice lets you set a CallerID on outbound calls in three ways:

  1. A Twilio-owned number on your account.
  2. A Verified CallerID — a non-Twilio number you've proven you control via the verification call flow (OutgoingCallerIds resource).
  3. Transit CallerID — pass any arbitrary number through the From parameter (Programmable Voice) or the SIP From header (Elastic SIP Trunking), and Twilio carries it as-is to the destination carrier without proving you control it.

Option 3 is what's going away. Starting May 31, 2026, Twilio will reject outbound calls whose CallerID isn't a Twilio-owned number, a Verified CallerID, or a CallerID delivered through Immutable Call Forwarding (more on that below).

The regulatory pressure isn't from Twilio. It's from national regulators tightening on caller ID spoofing. The countries already blocking unverified Transit CallerID at the carrier layer:

  • Australia
  • Brazil
  • Germany
  • Norway
  • Spain
  • Sweden

If your traffic terminates in any of those countries today, the calls are already failing. The question is whether your monitoring is telling you.

The silent-fail mode that gets under everyone's monitoring

This is the part that bit teams in March and April when the early enforcement started, and it's the part that's going to bite the long tail when May 31 hits the rest of the regulated markets.

Here's the call flow when a Transit CallerID call hits a country that's already enforcing:

  1. Your code POSTs to /2010-04-01/Accounts/{AccountSid}/Calls.json with From=+15551234567 (your customer's CallerID, not yours), To=+49xxxxxxxxx, and a TwiML URL.
  2. Twilio's API returns HTTP 201 with a CallSid.
  3. Your statusCallback webhook fires initiated, then ringing.
  4. The call traverses Twilio's network, hits the German carrier's edge (in this example), and the carrier drops it because the CallerID doesn't pass STIR/SHAKEN attestation or the local equivalent. Some regulators just block; others return a 503 Service Unavailable or a Q.850 cause code (16 — Normal call clearing) that looks identical to a normal hangup.
  5. Your statusCallback fires completed with Duration=0 and CallStatus=completed.

The string completed is doing a lot of work there. In Twilio's status taxonomy, completed means "the call was set up and torn down without an explicit failure on the signaling layer." It does not mean "the called party picked up." It does not mean "audio flowed in either direction." A call that was administratively dropped by a regulated carrier with a clean 503 looks identical to a call that connected, played a 30-second message, and hung up — except for Duration and the absence of any RTP statistics.

If your app tracks CallStatus == 'completed' as success — and most do — your dashboard is lying to you. Every call to AU/BR/DE/ES/NO/SE has been silently failing for some teams since March.

Where to look for the truth

Three signals expose the silent fail. None of them are on the default Twilio monitoring page.

1. The Insights API call summary.

GET https://insights.twilio.com/v1/Voice/{CallSid}/Summary
Enter fullscreen mode Exit fullscreen mode

Specifically the call_state and attributes.disposition fields. A regulator-blocked call surfaces as call_state=completed, disposition=no-answer or failed-attestation. The disposition lives in Insights, not in the Calls resource.

2. Per-event breakdown.

GET https://insights.twilio.com/v1/Voice/{CallSid}/Events
Enter fullscreen mode Exit fullscreen mode

Look for an event with name=last_sip_response_code greater than 400, or name=carrier_attestation with a value of C or null. STIR/SHAKEN attestation C (gateway attestation) is what unverified Transit CallerID resolves to, and the regulated carriers in the six countries above reject it.

3. The hangup cause code on the completed callback.

Even on the basic statusCallback, Twilio includes the Q.850 cause when it's available: HangupCauseCode and HangupSource. A normal completion shows HangupSource=caller or callee. A regulator block frequently shows HangupSource=carrier with a code in the 21–27 range (call rejected, network out of order, requested facility not subscribed). Most apps don't read those two fields.

If you do nothing else before May 31, log HangupSource and HangupCauseCode on every status callback and graph the per-country rate of HangupSource=carrier. That single dashboard surfaces the silent fail today, before the global cutoff lands.

The migration paths

There are three migration targets, and they map to different use cases. Picking the wrong one means writing code that compiles but doesn't satisfy the regulator.

1. Static CallerID — use Verified CallerID

If your app always presents the same CallerID on every outbound call (e.g., your business's main line), the answer is to add it as a Verified CallerID on your account.

curl -X POST https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/OutgoingCallerIds.json \
  --data-urlencode "PhoneNumber=+15551234567" \
  --data-urlencode "FriendlyName=Main Business Line" \
  -u $ACCOUNT_SID:$AUTH_TOKEN
Enter fullscreen mode Exit fullscreen mode

Twilio places a verification call to the number. Whoever answers reads back a 6-digit code, and the number is then registered as a Verified CallerID. Once verified, calls using that number as From are treated as authenticated through May 31 and beyond. Verification needs to be re-run if the underlying number changes carrier or ownership — there isn't a long-term challenge mechanism, just a one-time call.

Static use case is the simplest migration. Most static-use teams will be fine if they verify their numbers before the deadline.

2. Dynamic CallerID where you control all the lines — port them to Twilio

If your app rotates between a small pool of CallerIDs (regional presence numbers, vanity lines), the cleanest path is porting those numbers into your Twilio account. Twilio-owned numbers don't need verification — they're the gold-standard CallerID and carry full STIR/SHAKEN attestation A.

This is the highest-effort migration if you have many numbers, because porting is a per-number process with carrier paperwork. Start the port at least 30 days before May 31 if you want to use this path.

3. Per-call dynamic CallerID where you don't own the numbers — Immutable Call Forwarding

This is the one that breaks the most assumptions. If your app's whole business model is "customer A calls in, we forward to customer B's mobile, and customer B sees customer A's CallerID" — call masking, virtual receptionists, two-sided marketplace voice, customer-support outdial showing your customer's branded number — none of the above two options works. You don't own customer A's number, and you can't verify a different number per call.

Twilio's answer is Immutable Call Forwarding, marketed as PV Immutable Call Forwarding (PV-ICF) for Programmable Voice and ESIPT Immutable Call Forwarding for Elastic SIP Trunking.

The mechanism is fundamentally different from Transit CallerID. Instead of a fresh outbound call where you assert a CallerID, the call leg is forwarded through Twilio as a B2BUA (back-to-back user agent) bridge — the inbound leg is a real call from customer A, and the outbound leg preserves customer A's identity through the bridge under STIR/SHAKEN's call diversion mechanism.

For Programmable Voice TwiML, this is a <Dial> invocation against a leg you've received, not a fresh outbound <Dial>:

<Response>
  <Dial callerId="{{the inbound caller's number, immutably forwarded}}">
    <Number>+49xxxxxxxxx</Number>
  </Dial>
</Response>
Enter fullscreen mode Exit fullscreen mode

The constraint: the call must originate from an inbound leg you received. You can't synthesize an outbound call with PV-ICF — you need a real inbound to forward. For most call-masking products this is exactly what's happening today; the change is in how Twilio attests it on the way out.

For Elastic SIP Trunking the equivalent is configured at the trunk level under Call Transfer settings, with the "Caller ID for Transfer Target" knob set to Transferor (preserving the original caller's identity) instead of the default Transferee. SIP REFER carries the diversion header that the destination carrier honors as a forwarded leg, not a Transit CallerID.

If your product is in the marketplace/masking/virtual-receptionist category and you don't have ICF wired up before May 31, the migration is not a small change — you need to refactor your call paths so every dynamic CallerID call originates from an inbound leg, and your TwiML needs to flip from outbound <Dial> to inbound-then-<Dial>.

Why your tests probably don't catch it

This one shares the shape of the prior Twilio regional-domains intercept, but the silent-fail surface is wider. Five reasons it slips through:

Sandboxes don't enforce. Twilio's test credentials don't run STIR/SHAKEN attestation. Calls placed in test mode complete cleanly regardless of CallerID provenance. The validation only kicks in when the call hits a real PSTN destination.

Unit tests don't dial. They mock the SDK, assert that client.calls.create was called with the right From, and move on. The string +15551234567 is just as valid in a mock as a verified number.

Integration tests don't terminate internationally. Most teams' integration tests dial the team's own numbers — almost always US/CA. The six countries already enforcing are not on the default test list. Integration tests pass; production calls to AU/BR/DE/ES/NO/SE quietly drop.

CallStatus is a misleading signal. As covered above, completed doesn't mean delivered. Apps that key on CallStatus for success monitoring are systematically reporting blocked calls as successes.

The Twilio Console call log shows them as completed too. This one trips up support teams. The default call log view shows duration and status; a regulator-blocked call has duration 0 and status completed. It looks identical to a hung-up-on-ringback call, which happens for legitimate reasons all the time. Until you click into the call detail page and read the Insights events, there's no flag.

What to do, in order, before May 31

  1. Today: log HangupSource and HangupCauseCode on every status callback. Graph the rate of HangupSource=carrier per destination country code. If the AU/BR/DE/ES/NO/SE rate jumps relative to baseline, you have Transit CallerID exposure live.

  2. This week: audit your From numbers. Pull the last 30 days of Calls and group by the From field. Anything that isn't a Twilio-owned number (incoming_phone_numbers resource) or a verified OutgoingCallerIds entry is exposed.

  3. Next week: classify by use case. Static lines → Verified CallerID. Owned but external → port to Twilio. Customer-presented per-call → ICF refactor.

  4. Before mid-May: complete the Verified CallerID flow for static numbers. The verification call requires someone to answer and read a code, so this is a coordination problem, not a technical one. Don't leave it for the last week.

  5. Before May 31: migrate the ICF path. This is the largest change for marketplace and call-masking products. Don't skip it because the cutoff date moves through your release schedule.

  6. After May 31: keep the per-country HangupSource=carrier dashboard. The list of regulated countries will grow. Saudi Arabia, France, and Japan are all on the watch list of the same regulator coalition that drove the AU/BR/DE/ES/NO/SE rollouts. Today's six are not the last six.

How drift monitoring catches this class of change

The pattern in this Twilio change is identical to the one in the regional-domains deprecation we covered last month: a long pre-announced cutoff, an authoritative changelog post, and a per-region silent enforcement window before the global flag day. It also matches the GitHub merge_commit_sha removal, the OpenAI Responses input_text deprecation, and the Stripe Basil current_period_end move — all changes where the API surface kept returning 200 long after the underlying contract had drifted out from under the caller.

The fix at the architecture level is to stop treating "200 OK" as a success signal at the boundary. The API tells you the request was accepted; it doesn't tell you the contract still holds.

That's the gap FlareCanary was built to close. Point it at the Twilio Calls and Insights endpoints you depend on, and it polls on a schedule, learns the expected response shape, and flags when a field's nullability changes, when a new attestation field appears, when an enum tightens. Severity-classified so noise stays low.

You don't need a dedicated tool. You can cron a script that hits the relevant endpoints, hashes the field set, and diffs. The point is that some layer needs to be watching response shape and per-country dispositions, because the regulator-driven changes don't bump API versions when they tighten — they just tighten.

The harder pattern

This is the third Twilio incident we've covered in the series. Regional domains in April. A2P 10DLC required fields in June. Transit CallerID in May. Three different teams inside Twilio, three different changelog posts, three different failure surfaces — and the same silent-success pattern across all of them.

Most teams have a list of which Twilio products they use. Almost none of them have a list of which Twilio contracts they assume — which fields exist, which CallerIDs work in which countries, which API versions their SDK pin resolves to. That gap, between "what we use" and "what we'd find out about a schema or policy change in advance of the cutoff," is the entire reason silent-fail incidents land in production instead of in CI.

A 200 response on a call placement tells you the request was accepted. It doesn't tell you the call rang in Berlin.

If you'd been diffing Twilio's STIR/SHAKEN attestation field on calls to your top destination countries since the AU/BR rollout, you'd have caught the silent failure six weeks before the global cutoff — long enough to verify your numbers, port what needs porting, and refactor your ICF paths without scrambling.

That's the habit, and it generalizes to every voice provider, every regulator coalition, every API you don't control.


If your product runs voice through Twilio and you've already seen the regional silent-fail pattern, drop a reply with the country and the timeline. We're collecting these and the regulator coalition behind the rollouts looks bigger than the published list.

Top comments (0)