Skip to main content

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_id for anything post-resolve. A resolved contact — an identify call paired an anonymousId with an external_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_id for anything pre-resolve. Until an identify or alias fires, the only signal is the platform-minted anonymousId; 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.
The failure shape to catch: reading by-user on a session that has not identified returns a 404 comprehension error (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 nodesexternal_id, email, phone, and the platform-stashed anonymous_id attached by has_identifier edges.
  • Contact nodes — the surviving root contact plus every contact folded into it, connected by merge edges 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.
The 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_id fired 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_days lookback (1–90, default 30) so the unindexed context fan-out stays a bounded scan.
Edges classify as 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 from identify, 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 a cnt_ contact id) and read the stitched identifiers plus their fold provenance:
Identifier values in the response render masked on tenants in the default mask-on-return PII posture; the envelope still carries the full node/edge/provenance/stats structure either way.

2. By-anonymous device graph

Anchor on the pre-identify anonymousId and bound the hop-1 fan-out window:
Read 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:
The PUT upserts one rule keyed on its 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-minted anonymousId — 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