Identity graph and device graph
Three CDP tabs on Integrations → CDP answer the two questions an operator asks most often about resolution — “which identifiers ended up on this resolved profile?” and “which anonymous sessions are the same not-yet-identified visitor?”. The Identity graph tab answers the first, the Device graph tab answers the second, and the Identity rules tab authors the match contract both query through. This page defines what each graph returns, how reads route between them, and the privacy posture the API enforces. The resolution pipeline that feeds both views is Identity resolution and merge semantics.When to read by-user versus by-anonymous
Every lookup into a graph starts from exactly one anchor; pick the anchor by what you hold, not by what you want to see:- Anchor on
user_idfor anything post-resolve. A resolved contact — anidentifycall paired ananonymousIdwith anexternal_id, email, or enabled phone rule — answers through the identity graph: which identifiers stitched onto the survivor, and which contacts merged in, with the fold decision attached. - Anchor on
anonymous_idfor anything pre-resolve. Until anidentifyoraliasfires, the only signal is the platform-mintedanonymousId; a visitor who browses on a phone, then a laptop, then signs up on a third device lives as three anonymous sessions until one of them identifies. The device graph crosses those sessions on the shared device/cookie hints alone — no match rules invoked.
PROFILE_NOT_FOUND), and
reading by-anonymous on a session that has identified wraps the
reachable-but-thin part of the cross-session overlay. Check which time
slice of the visitor journey the session came from before you pick a
tab.
Section 1 — The by-user identity graph (post-resolve)
GET /api/v1/cdp/profiles/by-user-id/:user_id/identity-graph renders one
resolved profile as a node/edge graph. The user_id path param resolves
through the tenant’s identity-rules chain (default: external_id only;
phone and email resolve only when the tenant has enabled those rules)
— and the dashboard tab also accepts a raw cnt_-prefixed contact id,
so a contact-detail page can hand its own id over directly. Nothing
matching returns a 404 with PROFILE_NOT_FOUND.
Three node/edge kinds carry the answer:
- Identifier nodes —
external_id,email,phone, and the platform-stashedanonymous_idattached byhas_identifieredges. - Contact nodes — the surviving root contact plus every contact
folded into it, connected by
mergeedges that carry the fold’s decision metadata (action, actor, reason, timestamp). - Provenance — a chronological, oldest-first timeline of every identity decision the audit ledger recorded for this profile, rendered beside the graph so the merge payload stays readable even when the picture is dense.
max_events query caps how many audit rows the graph reads (1–1000,
default 200); a profile with a deep merge history sets truncated: true
when the tail was dropped, so a partial picture never reads as a
complete one. Merged-in contacts that transit an erasure or teardown
between resolve and render re-fail as not-found rather than returning
half a graph.
The graph is read-only. Splitting a bad fold back out goes through the
dashboard merge-history flow or the contacts unmerge surface (see
Identity resolution — the identity
graph never detaches identifiers itself.
Section 2 — The by-anonymous device graph (pre-resolve)
GET /api/v1/cdp/profiles/by-anonymous-id/:anonymous_id/device-graph
renders the cross-device overlay one anonymous session participates in.
Client SDKs stamp context.device_id (IDFV/GAID-class hint) and/or
context.cookie_id (a first-party cookie that survives an anonymousId
reset) on each event; the graph reads those hints off the event stream
and expands the seed session in two bounded hops:
- Hop 0 — the seed’s own signals. Every event the queried
anonymous_idfired carrying a device or cookie hint. - Hop 1 — sessions sharing those values. Every other anonymous
session whose events carry one of the same values, bounded by the
window_dayslookback (1–90, default 30) so the unindexed context fan-out stays a bounded scan.
seen_on_device / seen_with_cookie (an anonymous
session fired a hint), or identified_as (that session later paired
with a userId — the strongest cross-device confirmation a pre-resolve
overlay can carry). The response lists every shared-device session in
linked_anonymous_ids with device/cookie/linked counts in stats, and
flags truncated when either hop’s bound dropped a tail.
The graph is read-only: seeing two anonymous sessions on one device
does not stitch them — that stays an explicit identify or alias
call, two-hop only, so A→B→C transit walks stop at the first shared
hint. Fix the anchor on the seed session’s pre-identify anonymousId,
not on a later user id.
Section 3 — The identity-rules editor
GET / PUT /api/v1/cdp/identity-rules authors the match contract every
by-user graph read and every ingest lookup resolves through: one rule
row per identifier type (external_id, email, phone,
anonymous_id) with a numeric priority, enabled per type. The
dashboard tab renders and edits the same rows; the default chain is
external_id-only — phone and email rules resolve nothing until a
tenant enables them, so a shared-handset phone match cannot sneak into
a profile you did not opt in to match on. The
Identity resolution page specifies
how the simulator gates a rule change before the PUT saves it; a graph
read always uses the strongest enabled rule at query time, so a rule
change applies to the next lookup, not retroactively.
Section 4 — How merge candidates propagate
Merge candidates flow fromidentify, imports, and the
resolution-engine scans into the contacts merge surface; the merge
writes one audit-ledger row (the fold decision, the confidence, the
identifier corroboration), and both graph views re-derive from the same
ledger on their next read. The by-user graph’s provenance timeline is
where that row lands; the merge history and 30-minute undo window
described in
Identity resolution and merge semantics
govern the fold itself. Reads never queue candidates — every graph GET
is a projection over decisions that have already happened, so a graph
can only ever show a merge after it propagated, not as it is being
proposed.
Worked examples — script both graphs end-to-end
1. By-user identity graph
Anchor on the resolved user id (or acnt_ contact id) and read the
stitched identifiers plus their fold provenance:
2. By-anonymous device graph
Anchor on the pre-identifyanonymousId and bound the hop-1 fan-out
window:
identified_as links first: a shared device hint plus a later
identify on one of the sessions is the strongest confirmation that the
sessions belong to one visitor, short of an explicit alias.
3. Read and update the identity rules
Both graph reads resolve through the active rule chain; fetch the chain before you debug a missing match:match_type, so re-PUTting an
existing type updates it in place. An off-schema body — an unknown
match type or a missing field — returns a 422 field-level breakdown,
not an unchecked write.
Privacy guardrails
Both graph endpoints are read-only GETs that take an id in the URL path — a resolved user id / contact id, or a platform-mintedanonymousId — never an email, phone number, or name. PII you can
paste lives in request bodies, not paths; the URL stays in access logs
without leaking person identifiers. Response identifier values render
masked wherever the tenant’s PII posture defaults to mask-on-return,
and the device graph handles only platform-issued opaque hints. Access
to either graph requires a role scoped for contacts
(contacts:read — owner, admin, or developer on an API key), and
reads rate-limit at 60 per minute per tenant. The erase path from
CDP erasure propagation applies to
the underlying contacts and events the graphs read; a graph is a
projection, never a second copy to purge separately.
Cross-references
- Identity resolution and merge semantics — the deterministic/probabilistic pipeline both graphs project from.
- CDP data catalog and identity rules (guide) — the operator walkthrough of the rules editor surface.
- CDP API reference — per-endpoint schemas for every call named above.