Activation detail
Run a single audience activation: consent-gated resolve, SHA-256 hashing, ad-platform upload, run history, and per-member audit.
Activation detail
The activation detail view at /activation/{id} is the control surface for one audience activation — the pipe that pushes a consent-cleared audience to an ad platform (Meta, Google, LinkedIn, TikTok, Reddit, Snap, X) as a hashed match list. It is served by the /v1/activation API (api/routes/activation.py), which orchestrates resolve → consent gate → hash → upload → audit.
Live, consent-gated surface
The runner, the consent gate, SHA-256 hashing, run history, and the per-member audit are all real code. Uploads go through httpx-backed adapters that call the actual platform APIs — so a live upload needs valid destination credentials configured; without them the adapter has nothing to push to.
What the page controls
The header shows the activation's status and destination. Primary actions map directly to endpoints:
- Run —
POST /v1/activation/activations/{id}/runexecutes the full pipeline and reportsincluded_count/suppressed_count. - Pause / Resume —
POST …/pauseand…/resumetoggle the activation's lifecycle state. - Run history —
GET …/runspages through past runs; selecting one opens the member audit.
The consent gate is the point
Before any identifier is hashed or uploaded, every candidate passes apply_consent_gate (core/activation/consent_gate.py), which classifies each member as included or suppressed with one of four reason codes rendered in the suppression donut:
| Reason | Meaning |
|---|---|
opted_out | Explicit withdrawal recorded for the purpose. |
purpose_missing | No consent record for the requested purpose. |
jurisdiction_mismatch | Consent doesn't cover the member's jurisdiction. |
no_match_key | No usable email/phone to match on. |
An activation declares a purpose — advertising, retargeting, lookalike_seed, or suppression_list — and the gate maps that to the consent ledger's action type and purpose string (PURPOSE_MAP). This is why a raw CRM segment and the audience that actually reaches Meta are different sizes: the suppressed members never leave the building.
Hard-stop above 50% suppression
When suppression reaches SUPPRESSION_HARD_STOP_PCT (50%) of the audience, the run is halted rather than pushing a heavily-degraded list, and a Slack notification fires if ACTIVATION_SLACK_WEBHOOK_URL is configured. This is a guardrail against uploading an audience that consent has largely gutted.
Preview before you run
POST /v1/activation/activations/{id}/preview runs resolve + gate + hash without uploading or persisting — a dry run that returns the included/suppressed counts and breakdown so marketing-ops can see exactly what would ship. Preview deliberately never exposes the hashes themselves.
Hashing and matching
Included members are hashed with the canonicalization every ad platform expects: sha256(email.strip().lower()) for email and sha256(digits_only(phone)) for phone (core/activation/hashing.py). The platform hashes its own user data the same way and matches on equal hashes — the raw email or phone never leaves Pact. Because upload is keyed by the same hashed identifiers, re-running an activation is idempotent.
Member audit
Selecting a run opens the per-member audit (GET /v1/activation/runs/{id}/audit), a paged drilldown of every candidate with its included / suppressed action and reason — the compliance record proving why each person was or wasn't uploaded.
segment ──▶ consent gate ──▶ included ──▶ sha256 ──▶ platform upload
│
└──▶ suppressed (opted_out | purpose_missing |
jurisdiction_mismatch | no_match_key)
│
└──▶ activation_member_audit (per-member reason)
Environment guard for safe testing
Setting ACTIVATION_DRY_RUN (1/true/yes) makes runs go through resolve, gate, and hash but skip the actual platform upload — useful for verifying suppression math against a destination without touching a real ad account.