Skip to main content
Manage first-party CRM audiences on a seller account. Upload hashed customer lists, check matching status, and reference the resulting audiences in create_media_buy targeting overlays for explicit retargeting or suppression. Audiences are distinct from signals: signals are third-party data products you discover and activate; audiences are data you own and upload. Use audience_include to target only members of an uploaded list. audience_include is a hard constraint — only users on the list are eligible. To find new users similar to an audience (lookalike expansion), describe that intent in your campaign brief — the seller handles expansion strategy. Note: lookalike intent expressed in the brief cannot be verified through the protocol; confirm via seller-side reporting. Response Time: Upload accepted in ~1–2s. The task remains active until matching completes (1–48 hours depending on the seller). Configure push_notification_config to receive a webhook when the audience is ready. Request Schema: /schemas/latest/media-buy/sync-audiences-request.json Response Schema: /schemas/latest/media-buy/sync-audiences-response.json

Quick Start

Upload a customer list and check its status:
import { testAgent } from "@adcp/client/testing";
import { SyncAudiencesResponseSchema } from "@adcp/client";
import { createHash } from "crypto";

const hashEmail = (email) =>
  createHash("sha256").update(email.toLowerCase().trim()).digest("hex");

const hashPhone = (e164Phone) =>
  createHash("sha256").update(e164Phone).digest("hex");

const result = await testAgent.syncAudiences({
  account: { account_id: "acct_12345" },
  audiences: [
    {
      audience_id: "existing_customers",
      name: "Existing customers",
      add: [
        { external_id: "crm_1001", hashed_email: hashEmail("alice@example.com") },
        { external_id: "crm_1002", hashed_email: hashEmail("bob@example.com"), hashed_phone: hashPhone("+12065551234") },
      ],
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAudiencesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("audiences" in validated) {
  for (const audience of validated.audiences) {
    console.log(`${audience.audience_id}: ${audience.action} (${audience.status ?? "n/a"})`);
    if (audience.status === "ready") {
      console.log(`  Matched ${audience.matched_count} of ${audience.uploaded_count} members (this sync)`);
    }
  }
}

Request Parameters

ParameterTypeRequiredDescription
accountaccount-refYesAccount reference. Pass { "account_id": "..." } or { "brand": {...}, "operator": "..." } if the seller supports implicit resolution.
audiencesAudience[]NoAudiences to sync. When omitted, the call is discovery-only and returns all existing audiences without modification.
delete_missingbooleanNoWhen true, buyer-managed audiences on the account not in this request are removed (default: false). Does not affect seller-managed audiences. Do not combine with an omitted audiences array or all buyer-managed audiences will be deleted.

Audience Object

FieldTypeRequiredDescription
audience_idstringYesBuyer’s identifier for this audience. Used to reference the audience in targeting overlays.
namestringNoHuman-readable name
deletebooleanNoWhen true, delete this audience from the account entirely. All other fields are ignored.
addAudienceMember[]NoMembers to add to this audience
removeAudienceMember[]NoMembers to remove from this audience. If the same identifier appears in both add and remove, remove takes precedence.
consent_basisstringNoGDPR lawful basis: consent, legitimate_interest, contract, or legal_obligation. Required by some sellers in regulated markets.

Audience Member

Every member requires an external_id (buyer-assigned stable identifier) plus at least one matchable identifier. Hash all values with SHA-256 before sending — normalize emails to lowercase+trim, phone numbers to E.164 format (e.g. +12065551234).
FieldTypeDescription
external_idstringRequired. Buyer-assigned stable identifier for this member (e.g. CRM record ID, loyalty ID). Used for deduplication, removal, and cross-referencing with buyer systems.
hashed_emailstringSHA-256 hash of lowercase, trimmed email (64-char hex)
hashed_phonestringSHA-256 hash of E.164-formatted phone number (64-char hex)
uidsUID[]Universal IDs: type (rampid, uid2, maid, etc.) + value
Providing multiple identifiers for the same person improves match rates. Composite identifiers (e.g. hashed first name + last name + zip) are not yet standardized — use ext for platform-specific extensions. Identifier support varies by seller: Check get_adcp_capabilitiesmedia_buy.audience_targeting.supported_identifier_types and media_buy.audience_targeting.supported_uid_types before sending. MAID support is not universal (LinkedIn does not accept MAIDs; iOS IDFA requires App Tracking Transparency consent). The media_buy.audience_targeting.matching_latency_hours range and media_buy.audience_targeting.minimum_audience_size in capabilities are also seller-specific. Size limit: Payloads are limited to 100,000 members per call across all audiences. For larger lists, chunk into sequential calls using add deltas. Concurrency: Ensure that calls made to sync_audience are independent of eachother. They may be processed out-of-order. If you need sequential execution, wait for the callback to your configured webhook before making another call.

Response

Success Response:
  • audiences — Results for each audience on the account, including audiences not in this request
Error Response:
  • errors — Array of operation-level errors (auth failure, account not found)
Note: Responses use discriminated unions — you get either success fields OR errors, never both. Each audience in success response includes:
FieldDescription
audience_idEchoed from request (buyer’s identifier)
seller_idSeller-assigned ID in their ad platform
actioncreated, updated, unchanged, deleted, or failed
statusprocessing, ready, or too_small. Present when action is created, updated, or unchanged; absent when action is deleted or failed.
uploaded_countMembers submitted in this sync operation (delta, not cumulative). 0 for discovery-only calls.
total_uploaded_countCumulative members uploaded across all syncs. Compare with matched_count to calculate match rate.
matched_countTotal members matched to platform users across all syncs (cumulative). Populated when status: "ready".
effective_match_rateDeduplicated match rate across all identifier types (0–1). A single number for reach estimation. Populated when status: "ready".
match_breakdownPer-identifier-type match results. Shows which ID types resolve and at what rate. See match breakdown.
last_synced_atISO 8601 timestamp of the most recent sync. Omitted if the seller does not track this.
minimum_sizeMinimum matched audience size for targeting on this platform. Populated when status: "too_small".
errorsPer-audience errors (only when action: "failed")

Match breakdown

When a seller supports per-identifier-type reporting, the response includes match_breakdown — an array showing which identity types are resolving and at what rate. This helps buyers decide which identifiers to prioritize in future uploads.
{
  "audience_id": "existing_customers",
  "action": "updated",
  "status": "ready",
  "uploaded_count": 5000,
  "total_uploaded_count": 25000,
  "matched_count": 18750,
  "effective_match_rate": 0.75,
  "match_breakdown": [
    { "id_type": "hashed_email", "submitted": 25000, "matched": 17500, "match_rate": 0.70 },
    { "id_type": "hashed_phone", "submitted": 15000, "matched": 12000, "match_rate": 0.80 },
    { "id_type": "rampid", "submitted": 8000, "matched": 7200, "match_rate": 0.90 }
  ]
}
Key semantics:
  • submitted and matched are cumulative across all syncs, matching total_uploaded_count semantics (not uploaded_count).
  • effective_match_rate is deduplicated — a member matched via both email and phone counts once. It will be less than or equal to the sum of per-type match rates.
  • match_rate is server-authoritative — consumers should prefer this value over computing their own from submitted/matched.
  • id_type values combine hashed PII types (hashed_email, hashed_phone) with universal ID types (rampid, uid2, id5, euid, pairid, maid).
Sellers that only support aggregate match counts omit match_breakdown entirely.

Common Scenarios

Discovery Only

Check status of all existing audiences without making changes. The response includes all audiences on the account — filter by audience_id to find the one you care about:
import { testAgent } from "@adcp/client/testing";
import { SyncAudiencesResponseSchema } from "@adcp/client";

const result = await testAgent.syncAudiences({
  account: { account_id: "acct_12345" },
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAudiencesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("audiences" in validated) {
  for (const audience of validated.audiences) {
    console.log(`${audience.audience_id}: ${audience.status ?? "n/a"}`);
  }
}

Suppression List

Upload a list of existing customers to suppress from acquisition campaigns:
import { testAgent } from "@adcp/client/testing";
import { SyncAudiencesResponseSchema } from "@adcp/client";
import { createHash } from "crypto";

const hashEmail = (email) =>
  createHash("sha256").update(email.toLowerCase().trim()).digest("hex");

// Hashed customer emails from CRM export
const existingCustomers = [
  { hashed_email: hashEmail("customer1@example.com") },
  { hashed_email: hashEmail("customer2@example.com") },
];

const result = await testAgent.syncAudiences({
  account: { account_id: "acct_12345" },
  audiences: [
    {
      audience_id: "existing_customers",
      name: "Existing customers — suppression",
      add: existingCustomers,
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAudiencesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("audiences" in validated) {
  const audience = validated.audiences[0];
  console.log(`Status: ${audience.status}`);
  // When ready, reference audience_id in create_media_buy targeting_overlay.audience_exclude
}

Removing Members

Update an audience incrementally — add new members and remove ones that no longer qualify:
import { testAgent } from "@adcp/client/testing";
import { SyncAudiencesResponseSchema } from "@adcp/client";
import { createHash } from "crypto";

const hashEmail = (email) =>
  createHash("sha256").update(email.toLowerCase().trim()).digest("hex");

const result = await testAgent.syncAudiences({
  account: { account_id: "acct_12345" },
  audiences: [
    {
      audience_id: "lapsed_subscribers",
      name: "Lapsed subscribers",
      add: [{ hashed_email: hashEmail("newlapse@example.com") }],
      remove: [{ hashed_email: hashEmail("reactivated@example.com") }],
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAudiencesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("audiences" in validated) {
  for (const audience of validated.audiences) {
    console.log(`${audience.audience_id}: ${audience.action}`);
  }
}

Deleting an Audience

Remove a specific audience from the account without affecting others. Set delete: true on the audience object:
import { testAgent } from "@adcp/client/testing";
import { SyncAudiencesResponseSchema } from "@adcp/client";

const result = await testAgent.syncAudiences({
  account: { account_id: "acct_12345" },
  audiences: [
    { audience_id: "old_campaign_list", delete: true },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAudiencesResponseSchema.parse(result.data);

if ("audiences" in validated) {
  const audience = validated.audiences.find(a => a.audience_id === "old_campaign_list");
  console.log(`${audience.audience_id}: ${audience.action}`); // "deleted"
}
To delete multiple audiences in one call, include each with delete: true. To delete all buyer-managed audiences at once, use delete_missing: true with an empty audiences array — but be careful, this removes everything.

Using Audiences in a Media Buy

Once an audience is ready, reference it by audience_id in create_media_buy targeting overlays. Audience IDs are scoped to the seller account — they cannot be used across sellers.
test=false
{
  "brand": { "house_domain": "acme.com", "brand_id": "main" },
  "start_time": "asap",
  "end_time": "2026-03-31T23:59:59Z",
  "packages": [
    {
      "product_id": "prod_sponsored_content",
      "pricing_option_id": "cpm_standard",
      "budget": 10000,
      "targeting_overlay": {
        "audience_include": ["high_value_prospects"],
        "audience_exclude": ["existing_customers"]
      }
    }
  ]
}

Audience Status

Platform matching is asynchronous. The status field reflects the current state:
StatusMeaning
processingPlatform is matching uploaded members against its user base. Poll again later — do not create campaigns yet.
readyAudience is available for targeting. matched_count is populated.
too_smallMatched audience is below the platform’s minimum size. minimum_size in the response tells you the threshold. Add more members and re-sync.
status is present when action is created, updated, or unchanged. It is absent when action is deleted or failed. Webhook (recommended): Configure push_notification_config at the protocol level before uploading. The task stays active while the seller’s platform matches members. When matching completes, the task completes and the webhook fires with the final result — status: "ready" or status: "too_small". Check get_adcp_capabilitiesaudience_targeting.matching_latency_hours to set realistic expectations (typically 1–48 hours). Polling fallback: If not using webhooks, poll with discovery-only calls (omit audiences) no more frequently than every 15 minutes. Use tasks/get with the task_id to check task status — the task will be submitted while matching is in progress and completed when the audience is ready or too small. Agent workflow: Upload with push_notification_config set. Externalize the audience_id and account_id before the session ends. When the webhook fires with status: "ready", resume and proceed to create_media_buy.

Hashing Requirements

Hash all identifiers with SHA-256 before sending. Normalize first:
IdentifierNormalizationExample
EmailLowercase, trim whitespacealice@example.com → hash
PhoneE.164 format+12065551234 → hash
MAIDNo normalization neededAs-is
test=false
import { createHash } from "crypto";

const hashEmail = (email) =>
  createHash("sha256").update(email.toLowerCase().trim()).digest("hex");

const hashPhone = (e164Phone) =>
  createHash("sha256").update(e164Phone).digest("hex");

Privacy Considerations

All PII is hashed by the buyer before transmission — the protocol never carries cleartext personal data. SHA-256 hashes are one-way and cannot be reversed to recover the original email or phone number. The seller matches by independently hashing its own user data with the same algorithm. Buyer obligations: The buyer is responsible for having a lawful basis to process and share audience data, regardless of jurisdiction. Include consent_basis on each audience to communicate the GDPR lawful basis to sellers operating in regulated markets — some sellers require this field for EU audiences. Data handling: Once uploaded, data processing and retention are governed by your agreement with the seller. Review the seller’s data processing terms before uploading audience data.

Error Handling

Error CodeDescriptionResolution
ACCOUNT_NOT_FOUNDAccount does not existVerify account_id
AUDIENCE_NOT_FOUNDAudience to remove from doesn’t existCheck audience_id or omit remove
INVALID_HASH_FORMATIdentifier doesn’t match expected hash formatVerify SHA-256 hex encoding (64 chars, lowercase)
RATE_LIMITEDToo many sync requestsRetry with exponential backoff; poll no more than every 15 minutes
CALL_TOO_LARGEToo many members in payloadPayloads are limited to 100,000 members across all audiences

Next Steps

  • Targeting — Reference audiences in targeting_overlay.audience_include and audience_exclude
  • create_media_buy — Apply audience targeting to packages
  • Conversion Tracking — Track outcomes from audience-targeted campaigns