Troubleshooting: TCPA voice-guard sentinel breadcrumbs
A blocked campaign send with422 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_BLOCKEDon 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).
Breadcrumb signal semantics
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_BLOCKEDevent is the evaluation that threw. - A specific blocked send: add
recipientPrefixfor the number from the error payload and check that exactly one breadcrumb with that prefix was written in the seconds before the 422.
”Wiring didn’t take” triage — confirm the guard actually ran on a block
Work a422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED the way the sentry story
was designed to be worked:
- 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 isTCPA_DIALING_WINDOW_BLOCKED(noFEDERAL), that’s your own opt-in quiet-hours gate — a different page (resolve dialing-window blocked calls) maps the fix. - Find the breadcrumb for the block. Query Sentry for
breadcrumb.category:compliance.tcpa.federal-windowin the minutes around the 422, then narrow by therecipientPrefixyou 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. - 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. - 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.
- Sanity the fail-closed fallback. A block whose
reasonistimezone_unresolvedstill 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), 1,500 per call) is not yours to waive, so the control is not yours to disable. It accepts noskip 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 resolvesAmerica/New_York. Walk the whole chain:
- 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_atand writes a breadcrumb: categorycompliance.tcpa.federal-window,recipientPrefixmatching the number,mode: preview(checkvariant — no-throw). The contact is held, not burned. - 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 throwsTCPA_FEDERAL_DIALING_WINDOW_BLOCKEDwithnext_allowed_atset to the next 8 AM recipient-local. You receive HTTP 422; no SIP is dispatched. - 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. - Resolution. Hand the recipient to the pacer’s deferral
(
next_allowed_atfrom 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.
See also
- TCPA federal voice guard — the concept page for the one gate this sentinel observes: the carve-out, fail-closed posture, and why no tenant toggle exists.
- US state calling windows — the mini-TCPA overlays the same evaluation composes with before the gate decides.
- Troubleshooting: resolve dialing-window blocked voice calls — the error-code → fix mapping for every code in the TCPA family.
- Quiet hours configuration — your
opt-in gate,
unknown_timezone_policy, and where to set a stricter window. - Send gates — the full chain of send-time controls the federal guard sits inside.