PPactDocs
CRM

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:

  • RunPOST /v1/activation/activations/{id}/run executes the full pipeline and reports included_count / suppressed_count.
  • Pause / ResumePOST …/pause and …/resume toggle the activation's lifecycle state.
  • Run historyGET …/runs pages through past runs; selecting one opens the member audit.

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:

ReasonMeaning
opted_outExplicit withdrawal recorded for the purpose.
purpose_missingNo consent record for the requested purpose.
jurisdiction_mismatchConsent doesn't cover the member's jurisdiction.
no_match_keyNo usable email/phone to match on.

An activation declares a purposeadvertising, 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.

text
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.