Skip to main content

Troubleshooting: TCPA voice-guard sentinel breadcrumbs

A blocked campaign send with 422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED is only half the story. The other half is proving the guard actually ran on the path that held the call — that the dial gate in question is wired through the TCPA window evaluation at all, and that the decision you saw came from the gate you expect. Every one of Orbit’s voice dial gates emits a Sentry breadcrumb under the category compliance.tcpa.federal-window on every evaluation, allowed or blocked. That breadcrumb stream is the audit trail this page works: if a gate is wired, the breadcrumb count is non-zero; if the count for a gate sits at zero for weeks, the wiring never took. This page supplements Troubleshooting: resolve dialing-window blocked voice calls, which maps each 422 code to its fix. This page works the observability layer: which gate fired, how to confirm it ran, and how to trace one blocked send through breadcrumbs until you reach the decision.

The four dial gates and their preconditions

Every outbound voice send path evaluates the federal window at its own gate. The gates share the window definition and the fail-closed posture — they differ in when they fire, who calls them, and whether a blocked evaluation throws, defers, or only logs. Two consequences of the table fall out immediately:
  • Deferral is not failure. Gates 2 and 3 never throw — they move the send to next_allowed_at. A “stuck” campaign with zero blocks in the API and a full contact list is usually contacts deferred to the next open window, not an error.
  • Only gate 4 can produce the 422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED on an actual dial attempt. Gates 1–3 don’t throw it. So when you see that code, you know which firewall fired — what remains is confirming the breadcrumb trail behind it (worked example below).
Every gate writes through the same category, so one query covers all four: Example Sentry filters (Discover → breadcrumbs, or the issue’s breadcrumb stream):
  • All gates, one category: breadcrumb.category:compliance.tcpa.federal-window
  • The gate that can throw (gateway pre-dial): narrow the above by environment = voice-gateway service; the breadcrumb immediately preceding a TCPA_FEDERAL_DIALING_WINDOW_BLOCKED event is the evaluation that threw.
  • A specific blocked send: add recipientPrefix for the number from the error payload and check that exactly one breadcrumb with that prefix was written in the seconds before the 422.
Do not filter by message text across gates — the message differs per emission point; the category is the join.

”Wiring didn’t take” triage — confirm the guard actually ran on a block

Work a 422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED the way the sentry story was designed to be worked:
  1. Confirm the error belongs to the throwing gate. Read the code and the send path: campaign launches and gateway-originated legs produce TCPA_FEDERAL_DIALING_WINDOW_BLOCKED; ad-hoc dashboard dials produce only advisory logs. If your blocked send came from the campaign surface but the error is TCPA_DIALING_WINDOW_BLOCKED (no FEDERAL), that’s your own opt-in quiet-hours gate — a different page (resolve dialing-window blocked calls) maps the fix.
  2. Find the breadcrumb for the block. Query Sentry for breadcrumb.category:compliance.tcpa.federal-window in the minutes around the 422, then narrow by the recipientPrefix you read off the error details. The breadcrumb must exist: it fires before the guard runs, so a thrown block without a preceding breadcrumb means the throw didn’t come from the instrumented gate — escalate with the error body.
  3. Check the sibling gates. A campaign send crosses gate 2 (pace/defer) before it ever reaches gate 4. If the pace gate’s breadcrumb for the same recipient shows the contact was deferred to next_allowed_at, the campaign surface behaved correctly and the 422 came from a send that skipped the pacer path; compare the received-at timestamps to confirm ordering.
  4. The zero-count signal. An empty result for the category over weeks — not hours — on a send path you know is dialing means the gate’s wiring never took on that path. Cross-check the gate’s preconditions first (send path 1 or 4? recipient +1?), then report the path with its tenant context. A silent gate on a path that should dial is a platform defect; the page exists precisely so the drift surfaces in the breadcrumb count rather than in an incident.
  5. Sanity the fail-closed fallback. A block whose reason is timezone_unresolved still produces its breadcrumb — fail-closed means the guard ran and couldn’t prove the window, not that it was skipped. An unresolved-timezone block with no breadcrumb is the same kind of defect as step 2.

Posture note — the one carve-out

Per your compliance boundary, every compliance control on the platform is yours — default-open, opt-in, fail-open on unresolvable input. The federal TCPA voice window is the single carve-out to that rule: platform-global, fail-closed, and with no tenant toggle. The statutory penalty (47 U.S.C. § 227(b)(1)(B), 500500–1,500 per call) is not yours to waive, so the control is not yours to disable. It accepts no skip flag on a real-customer path, no quiet-hours knob that widens the federal window, and no posture change from any settings page. The tenant opt-in voice gate layers on top — you can only be stricter than the federal floor. Treat the sentinel breadcrumbs as the observability contract that keeps this posture verifiable: the guard can’t be turned off, and the breadcrumb stream is how you confirm it never silently stopped running. Don’t try to weaken either property; that would reopen the exact class of incident the carve-out closed. (The dev/staging test-call bypass on gate 1 exists only for operator test numbers explicitly marked as such — it never applies to a customer dial.)

Worked reproduction: build-call → block → breadcrumb → resolution

A campaign launches at 7:40 AM Eastern toward a Florida recipient whose area code resolves America/New_York. Walk the whole chain:
  1. Build-call. The campaign pacer (gate 2) reaches the contact. The recipient is +1 NANP, so the gate evaluates. Recipient-local time: 7:40 AM — inside neither the federal 8–9 AM floor nor Florida’s overlay. The gate defers to next_allowed_at and writes a breadcrumb: category compliance.tcpa.federal-window, recipientPrefix matching the number, mode: preview (check variant — no-throw). The contact is held, not burned.
  2. Block. Your own scheduler bypasses the pacer’s deferral and pushes the same recipient straight to the gateway. Gate 4 evaluates — the breadcrumb fires first (same category, mode: enforce), then the guard throws TCPA_FEDERAL_DIALING_WINDOW_BLOCKED with next_allowed_at set to the next 8 AM recipient-local. You receive HTTP 422; no SIP is dispatched.
  3. Breadcrumb. Query breadcrumb.category:compliance.tcpa.federal-window + the number’s prefix. You find two breadcrumbs: the deferral at gate 2 a few minutes earlier, and the thrown evaluation at gate 4 immediately before your 422. Both are expected; the presence of the second one is the confirmation the throwing gate actually ran on this send. If it were missing, you’d be in step 2 of the triage section — that would matter.
  4. Resolution. Hand the recipient to the pacer’s deferral (next_allowed_at from either signal — the pacer already computed it) or re-run the send at the window open. The block doesn’t retry-safe; a retry inside the same blocked hour reproduces the same 422. If your own scheduler pushed the send, fix the scheduler to honor the deferral rather than hammer the gateway.
The whole episode, as the breadcrumb trail reads it, is: one defer at the pacer, one throw at the gateway, zero silent gaps. That is the sentinel doing exactly what it exists to do.

See also