---
name: upload-customer-list-audience
description: >-
  Turns CRM or spreadsheet contacts (via hubspot_search_contacts, salesforce_soql_query, or
  sheets_get_values) into a Meta customer-list audience: normalizes then SHA-256 hashes every identifier
  client-side so raw PII never transmits, stacks email, phone, name, location, external_id to lift match
  rate, screens out sensitive IDs (SSN, card, health) and under-13s, and gates on advertising-use consent
  (HUMAN STEP for EEA) before any mads_create_custom_audience or mads_add_audience_members call. Verifies
  matched size against the ~1,000-record lookalike floor, sets a refresh cadence, and routes deletions
  through mads_remove_audience_members. Reach for it whenever a CRM segment must become an audience, a list
  needs refreshing, or an opt-out enforced. It ends at a matched customer-list audience - it does not size
  the lookalike (build-lookalike-audiences), decide exclusions (set-exclusions-and-suppression), or
  configure Advantage+ caps (set-existing-customer-budget-cap).
---
# Upload and Hash a Customer-List Audience

## Purpose
The customer list is the highest-fidelity first-party audience Meta can build from, but it is also the
one built entirely from raw personal data, get the normalize-then-hash order wrong, or skip a consent or
identifier check, and the list either fails to match or creates real privacy exposure. This skill pulls
CRM contacts, prepares them correctly, and uploads them into a matched, policy-compliant customer-list
audience with a refresh loop that keeps it current.

## When to run
- No customer-list audience exists yet and one is needed as a lookalike seed, a suppression list, or a
  direct-targeting segment.
- An existing customer-list audience needs a refresh: new customers added, churned or opted-out members removed.
- A specific CRM segment (high-LTV, lapsed, single product line) needs its own dedicated list.
- A deletion or opt-out request must be enforced against an uploaded list.

## When NOT to run
- The audience should be built from pixel, app, or engagement data instead of a CRM pull →
  `build-custom-audiences`.
- The list already exists and the task is sizing a lookalike from it → `build-lookalike-audiences`.
- The task is deciding where this list gets excluded rather than building it →
  `set-exclusions-and-suppression`.

## Prerequisites
- A reachable contact source: HubSpot, Salesforce, or a spreadsheet export (any other CRM needs a manual
  export first, HUMAN STEP).
- At least one identifier per record (email, phone, or name plus location); more identifiers materially
  raise the match rate.
- Confirmed advertising-use consent for the record set, with explicit HUMAN STEP confirmation for any EEA
  records before those records are even pulled.
- A defined segment (all customers, high-LTV, lapsed, product line) agreed with the user before pulling data.

## Procedure
1. **HUMAN STEP.** Confirm consent coverage for the segment before pulling any records, especially EEA
   data, and confirm the source excludes anyone who opted out of advertising use. Do not proceed on an
   unconfirmed list.
2. **Pull the source records.** HubSpot: `hubspot_search_contacts` filtered to the segment (lifecycle
   stage, deal value, last-purchase date). Salesforce: `salesforce_soql_query` selecting email, phone,
   first/last name, city/state/zip/country, and external_id with a WHERE clause encoding the segment.
   Spreadsheet: `sheets_get_values` on the export tab.
3. **Screen out disallowed records.** Reject rows carrying sensitive identifiers (SSN, credit card, health
   information) and any record indicating an under-13 individual, these cannot be uploaded regardless of
   consent status.
4. **Normalize every identifier before hashing.** Lowercase and trim email; convert phone to E.164
   (+country code, digits only); lowercase and trim names; keep city/state/zip/country as issued. This
   order matters, hashing before normalizing silently breaks the match.
5. **Hash client-side.** Apply SHA-256 to each normalized identifier locally, before anything leaves the
   pulling environment. Raw PII is never transmitted, only hashed values go into the upload payload.
6. **Stack every identifier the source has.** Include email, phone, name, location, and external_id where
   available; each additional identifier field materially raises match rate over an email-only upload.
7. **Create the audience.** Call `mads_create_custom_audience` with the customer-list subtype and a name
   encoding segment plus date (for example `high-ltv-customers-2026-08`). Check `mads_list_custom_audiences`
   first to avoid creating a near-duplicate, there is no idempotency key. Preview → explicit user approval
   → commit.
8. **Upload members.** Call `mads_add_audience_members` with the normalized-then-hashed records,
   batching large sets. Preview → user approval → commit. Report counts only, never echo raw or hashed
   identifiers back into chat or logs.
9. **Verify match and size.** Re-run `mads_list_custom_audiences` after processing and read matched size
   against the segment's source count. Under ~1,000 matched, the list is unusable as a lookalike seed
   (see `build-lookalike-audiences`) even though it is still fine for direct suppression use.
10. **Diagnose a weak match.** If matched size is far below the source count, add missing identifier
    fields (phone, location) from the same records and re-upload via `mads_add_audience_members`.
    Expect structurally lower match on B2B/corporate-email files and compensate with extra identifiers.
11. **Set the refresh loop.** Agree cadence with the user (see Decision rules); each cycle, re-pull the
    segment (step 2), diff against the prior pull, and call `mads_add_audience_members` for new
    records and `mads_remove_audience_members` for anyone who churned, opted out, or requested
    deletion, both previewed and approved before commit.
12. **Hand off.** Report segment name, matched size, and refresh owner. Route to `build-lookalike-audiences`
    (seeding) or `set-exclusions-and-suppression` (suppression use) depending on why the list was built.

## Decision rules
- **Identifier stacking:** email alone yields the weakest match; each added identifier (phone, name plus
  location, external_id) raises it, always upload every identifier the source has, not just email.
- **Size floor:** below ~1,000 matched, the list cannot serve as a lookalike seed; it remains usable for
  direct small-audience targeting or suppression at any size.
- **Consent gate:** no confirmed consent, especially for EEA records, means the record is excluded, full
  stop; this is a policy boundary, not a judgment call.
- **Rejected identifiers:** SSN, credit card, health information, and under-13 records are never uploaded,
  regardless of source or consent.
- **Hash order:** normalize first, SHA-256 second, always client-side before transmission, never send raw
  PII, and never hash before normalizing.
- **Refresh cadence:** high-value/all-customer lists monthly at minimum (weekly if automated); lapsed/win-back
  lists monthly; one-off campaign segments on the campaign's own calendar; deletions and opt-outs are
  removed immediately on request, never held for the next scheduled cycle.
- **Done means:** consent confirmed, disallowed identifiers screened out, records normalized then hashed
  client-side with every available identifier stacked, matched size verified against the list's intended
  use (seed vs. suppression vs. direct), and a refresh owner plus cadence written down.

## Common failure modes
- **Hashing before normalizing**, `SHA-256("John@X.com ")` matches nothing; the two steps must run in
  that exact order, every time.
- **Email-only uploads from a B2B source** producing a very low match rate that looks like a Meta problem
  but is actually an identifier-stacking problem.
- **Uploading without confirming EEA consent**, a policy violation with account-level consequences, not
  a formatting detail.
- **Treating a one-time upload as done**, without a named refresh owner, the list drifts stale, keeps
  serving to churned customers, and quietly loses value as a lookalike seed.
- **Using a hard delete to remove a handful of opted-out members** instead of
  `mads_remove_audience_members`, destroys the whole audience's history for what should be a
  targeted removal.
- **Echoing raw or hashed PII into chat or logs** when reporting upload results, report counts and match
  rates only.

## Related skills
- Run after this: `build-lookalike-audiences` (use this list as a seed), `set-exclusions-and-suppression`
  (use this list for suppression or exclusion), `set-existing-customer-budget-cap` (Advantage+ use of the
  customer list).
- Related: `build-custom-audiences` (non-CRM sources: pixel, app, engagement).
