TCPA
Pact's TCPA controls for voice and SMS — a pre-dial consent gate, STOP-keyword opt-out, DNC list checks, calling-window enforcement, and the current A2P 10DLC status.
Voice and SMS are where a CRM most easily creates legal exposure, so Pact enforces TCPA controls in code, on the send path, rather than leaving them to operator discipline. Every outbound dial routes through a single pre-dial gate, and every outbound text carries brand identification and an opt-out mechanism that writes straight to the suppression list.
The pre-dial gate
core/voice_compliance/enforcement.py exposes pre_dial_check(...), the single entry point every outbound dial path (campaign engine and click-to-call alike) passes through. It runs two hard checks in order and returns a machine-readable block reason so the UI can distinguish, e.g., "TCPA: missing consent" from "DNC: national registry":
- 1
Consent + hard suppression
Refuses the dial when the contact has withdrawn call/voice consent or sits in
suppression_entries(a DNC block), reusingconsent_block_reason. - 2
TCPA opt-in
When the tenant has
tcpa_required = True(the default within a saved config), the gate looks for agranted, non-withdrawn row inconsent_recordson thecall/voicechannel for that phone number. Absence blocks the dial with reasontcpa.
The result is a discriminated dataclass — allowed=True, or a ComplianceBlock carrying reason, detail, and the source table (suppression_entries vs consent_records).
The calling-window is enforced upstream
TCPA's time-of-day restriction is enforced by the campaign engine (core/voice_campaigns/engine.py), not by pre_dial_check. The default DEFAULT_WINDOW is a TCPA-shaped 09:00–20:00 with a per-campaign configurable timezone (UTC by default). A target reached outside its campaign's window is deferred to the next window open (next_window_open) rather than skipped — so no legitimate contact is dropped, they're just held until it's legal to dial.
Consent gating is mode-driven
A tenant that has never opened the compliance settings keeps the pre-V-9 posture: the
consent/suppression check always runs, but the TCPA opt-in gate only turns on once the tenant
saves a config on /admin/voice-mcp/compliance. Pick a mode — standard, soc2, hipaa, or
gdpr (core/voice_compliance/modes.py) — and the dependent toggles (TCPA required, data
residency, PHI redaction, audit retention) move together, with the deltas shown before you hit
Save.
Do-Not-Call lists
Tenants register DNC lists through /v1/admin/voice-compliance/dnc-lists (providers: national_ftc, state_registry, custom). Each list tracks its sync status and entry count; a number on an enabled, synced list is treated as a hard suppression and fails the first stage of the pre-dial gate.
SMS: brand ID, STOP, and suppression
core/sms/compliance.py encodes the CTIA/TCPA text rules:
- Brand prefix —
prepare_messageprepends the tenant name so recipients know who is texting. - Opt-out footer — an opt-out instruction is appended per CTIA guidelines.
- STOP keywords —
STOP,STOPALL,UNSUBSCRIBE,CANCEL,END,QUITare recognized. A received STOP writes a hardsuppression_entriesrow (channel='sms',reason='stop',source='twilio_stop'). - Pre-send check —
is_opted_outis consulted before every send, so a suppressed number is never texted again.
Consumer phone onboarding (core/phone_enrollment.py) captures un-pre-checked, versioned consent: Pact refuses to SMS a number without an explicit opt-in in the same request, and the durable consent record is written only after the code is verified — proving the user controls the number. The consent grant is append-only and survives phone removal for TCPA proof retention.
Partial: A2P 10DLC registration pending
The TCPA gating logic above is live. Message delivery over SMS is gated on carrier A2P
10DLC registration, which is still being completed — Twilio surfaces the not-yet-registered state
as error code 30034. Pact detects this distinctly (is_a2p_unregistered) and surfaces an
a2p_pending / "finishing SMS setup" state instead of a hard error, and paths that would promise
a text (e.g. team delegation notifications) intentionally do not, to avoid claiming a message that
can't yet reach the handset. Voice, and SMS once registration lands, run through the same consent
and suppression checks.