Repository navigation
sync_audiences.uids: underspecified value shape per uid_type causes silent match failures #6872
Description
Summary
The audience-member schema used by sync_audiences has:
"uids": {
"type": "array",
"description": "Universal ID values (MAIDs, RampID, UID2, etc.) for user matching.",
"items": {
"type": "object",
"properties": {
"type": { \"\$ref\": \"/schemas/enums/uid-type.json\" },
"value": { \"type\": \"string\", \"description\": \"Universal ID value\" }
}
}
}type is one of 11 enumerated values (rampid, rampid_derived, id5, uid2, euid, pairid, maid, hashed_email, publisher_first_party, world_id_nullifier, other). But value is just "Universal ID value" — no per-type shape, encoding, normalization, or case-sensitivity guidance.
Contrast with the sibling hashed_email and hashed_phone fields on the same object: both get explicit normative guidance ("SHA-256 hash of lowercase, trimmed email address", "SHA-256 hash of E.164-formatted phone number"), plus a pattern ^[a-f0-9]{64}\$. UIDs get nothing.
Why it matters
Buyer sends {"type": "uid2", "value": "..."} on sync_audiences. Receiver's identity-match path decodes an inbound UID2 token to a raw 32-byte identifier and keys audience memberships on a canonical form of those bytes. If the buyer's value isn't the same shape the receiver hashes on, the audience is silently invisible.
Concrete example: UID2 has TWO on-the-wire forms — the advertising token (~200-char encrypted, rotates per mint) and the raw UID2 (44-char standard base64 of 32-byte SHA-256, case-sensitive). Only the raw form makes sense for audience membership; the token is per-request rotating. But the spec doesn't say which one belongs in value, so different implementations will disagree.
Same silent-failure pattern for every other uid_type — each has an unstated wire shape that receivers key on. When buyer and receiver disagree, matches drop to zero without any error signal.
Proposed fix
Extend either uid-type.json enumDescriptions or the audience-member.uids[].value description with per-type shape guidance. Preferred: enumDescriptions, since the same enum is used in other contexts (event logging, TMP identity match) and the shape guidance is universal.
Draft shape guidance per type (community review welcome — please push back on any that are wrong):
| `type` | Expected `value` shape | Case-sensitivity | Notes |
|---|---|---|---|
| `rampid` | LiveRamp maintained envelope, 32 bytes on the wire (per uid-type.json enumDescription) | Case-preserving | Envelope only LiveRamp can resolve; the value here is opaque to the receiver. See below for the mapped-form ambiguity. |
| `rampid_derived` | LiveRamp derived envelope, 48 bytes on the wire | Case-preserving | Same envelope-vs-mapped ambiguity as `rampid`. |
| `id5` | ID5 universal ID string, as issued by ID5 (buyer-decodable) | Case-preserving | Opaque bearer string. |
| `uid2` | Raw UID2: 44-char standard base64 (`+`/`/`, `=` padding) of a 32-byte SHA-256. NOT the encrypted UID2 advertising token. | Case-sensitive (per UID2 spec) | The encrypted advertising token rotates per mint and cannot be used for audience keying. |
| `euid` | Raw EUID: same shape as raw UID2, EU jurisdiction | Case-sensitive (per EUID spec) | |
| `pairid` | PAIR wire token, base64-encoded encrypted KsKp Publisher Identifier | Case-sensitive | Publisher-scoped — see #6684 / #6698. `audience-member` currently has nowhere to carry the `EID.matcher`; needs a separate design pass. |
| `maid` | 32-hex-char or 36-hex-char (dashed UUID) advertising identifier | Case-insensitive | Both dashed and undashed accepted; receiver normalizes. |
| `hashed_email` | 64-char lowercase hex SHA-256 of lowercase-trimmed email | Case: hex is unambiguous | Overlaps with the top-level `hashed_email` field — clarify precedence when both are present, or forbid appearing in both. |
| `publisher_first_party` | Publisher-defined string, opaque to buyer | Publisher-defined | The receiver's `uid.value` handling depends on which publisher context this is being asserted under; add guidance. |
| `world_id_nullifier` | Nullifier bytes | TBD — hex or base64? Please specify. | The uid-type.json description notes trust requires an accompanying attestation, but the wire encoding of the nullifier itself isn't stated. |
| `other` | Buyer-defined string, buyer-decodable | N/A | Fine as-is; documenting that no shape is guaranteed is enough. |
Two open ambiguities beyond wire shape
Even with per-type shape guidance, two structural questions remain that a doc-only fix can't resolve:
- RampID envelope vs mapped form. LiveRamp's usable-for-matching form is not the wire envelope — it's a mapping the receiver has to compute via a LiveRamp sidecar call. If the buyer uploads a raw envelope in `value`, the receiver's audience store won't match unless the receiver ALSO calls the sidecar at ingest, OR the buyer uploads a pre-mapped form. Neither is documented today. Options:
- (a) Spec mandates `value` = raw envelope; receiver runs sidecar at ingest.
- (b) Spec says `sync_audiences` doesn't support LiveRamp; use a separate batch import protocol.
- (c) Spec adds a new field like `value_form: "envelope" | "mapped"` so receiver knows what to do.
- PAIR matcher scope. `audience-member` has nowhere to carry the `ownerdomain` matcher scope PAIR requires per #6698. Any `pairid` upload today is publisher-ambiguous. Needs an additive schema field or explicit "not supported yet" note.
Both are worth their own dedicated design conversations; happy to open separate follow-up issues if the community agrees they need spec-level resolution rather than implementation-side workarounds.
Implementation note
This is not just a doc improvement — it's a source of real silent-failure bugs. Multiple implementations of `sync_audiences` today (buyer platforms, adapters, downstream identity stores) pass `uid.value` through opaquely because the spec gives them no shape to validate against. Any hardening on the receiver side without matching hardening on the buyer side just changes the failure mode from "silently wrong hash" to "silently rejected input." Getting the shape written down lets both sides converge.
Willing to draft a PR against uid-type.json enumDescriptions if there's alignment on the shapes above. Please push back on anything wrong before I do — I'd rather not codify inference-from-implementation as normative spec text.
Activity
bokelley commentedon Aug 25, 2026
Triage
Classification: Spec question — underspecified uid wire shapes causing silent interop failures
Bucket(s): spec / protocol, signals, schema
Status: ready-for-human (parts 2–4 need WG decisions); parts 1 and 5 ready to implement independently
Milestone: Spec Backlog
What the experts said:
hashed_email/hashed_phoneis real; uid2/euid raw-vs-token has clear external authority (UID2 Framework v3: advertising token MUST NOT appear in audience keying); maid and world_id_nullifier similarly clear; RampID encoding is blocked on LiveRamp input; pairid without ownerdomain is semantically unresolvable per PAIR spec §4.2; hashed_email overlap allows pattern bypass and should be structurally forbidden (minor-breaking).My take: The issue correctly identifies a real interop gap. Parts 1 and 5 have unambiguous external authority and can ship as patch-level enumDescription additions. Parts 2–4 are genuine WG decisions where the experts diverge or require external input before a path can be codified.
Parts 1 + 5 — ready to implement
Additive prose to
uid-type.jsonenumDescriptions only. Non-breaking (description-only). Patch-eligible on bothmainand3.1.x.uid2: "Raw UID2 only: 44-char URL-safe Base64 of a 32-byte SHA-256. Advertising token form (opaque, ~200 chars, rotates per mint) MUST NOT appear here — it is ephemeral, DSP-specific, and cannot be used for audience keying."euid: Same contract as uid2, EU jurisdiction.maid: "Lowercase dashed UUID:^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$. IDFA MUST be downcased before storage. Undashed hex form not accepted."world_id_nullifier: "Base64url-encoded 32-byte nullifier hash (Semaphore protocol output, no padding). Wire encoding is distinct from the hex form used in World ID client SDKs."static/schemas/source/enums/uid-type.json(enumDescriptions only)Part 2 — WG decision: RampID envelope vs. mapped form
Both experts agree this requires LiveRamp input before landing.
Option A — mandate raw envelope; receiver calls LiveRamp sidecar at ingest:
{ "type": "rampid", "value": "<raw LiveRamp XY envelope>" }Requires every receiver to have an active LiveRamp API relationship. AdCP would be implicitly mandating a commercial contract.
Option B — document rampid/rampid_derived as not currently supported in sync_audiences:
Honest about current state. Zero schema change.
Option C — add optional
value_formfield to uids[] item:{ "type": "rampid", "value": "<envelope>", "value_form": "envelope" }Most informative. Requires knowing the wire encoding first. Additive optional field — non-breaking once encoding is confirmed.
WG should invite LiveRamp to comment on encoding and sidecar expectations before choosing.
Part 3 — WG decision: pairid ownerdomain in audience-member
Option A — add optional
ownerdomainfield to uids[] item:{ "type": "pairid", "value": "<URL-safe Base64-encoded KsKp Publisher Identifier>", "ownerdomain": "publisher.example.com" }Non-breaking (
additionalProperties: truealready permits it; formal definition is additive). Required when type=pairid (if/then) would be minor. Protocol expert favors this.Option B — document pairid as not supported in audience-member until ownerdomain is added:
Zero schema change. Product expert favors this given early adoption stage.
Part 4 — WG decision: hashed_email in uids[] vs. top-level field
Experts disagree on the fix approach.
Option A — forbid
hashed_emailandhashed_phoneas uids[].type (protocol expert):+ "not": { "properties": { "type": { "enum": ["hashed_email", "hashed_phone"] } } }Closes the validator gap. Minor-breaking (invalidates docs using uids[{type:"hashed_email"}]) but real-world breakage is near-zero — any such sender is already failing at match time.
Option B — prose cross-validation only (product expert):
Zero schema change; doesn't close the validator gap but clarifies spec intent.
@bokelley, your call on A vs B — the breaking vs. non-breaking tradeoff is the deciding factor here.
Triaged by Claude Code. Session: https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID}
Generated by Claude Code
docs(audiences): scope uids[] not-hashed note to exclude hashed_email
docs(audiences): specify uids[].value wire form for sync_audiences (#…
bokelley commentedon Oct 31, 2026
Part of this shipped: #8059 (3.3.0-beta.0) specified the
uids[].valuewire form foruid2,euid,maidandhashed_email(parts 1 and 5 of the triage). Its description says the issue stays open for the rest.The rest was not decided for 3.3, so per the WG rule ('anything not decided moves to 3.4') only the remainder moves to 3.4: RampID envelope versus mapped form (needs LiveRamp input), PAIR
ownerdomain, theworld_id_nullifierencoding, and forbiddinghashed_email/hashed_phoneas auids[].type. Objections by Fri 2026-11-06. The milestone stays 3.3.0 for now; it will be moved after Nov 6.