Skip to main content

Account relations

A B2B account graph lives on three relation shapes: member contacts ↔ a named account (contact↔account), parent ↔ child account (hierarchy), and person ↔ company (the contact-level link). The surface at Audience → Accounts reads all of them back from the group ingest events you send — there is no link-row table to manage. This guide walks the surface’s layout, the ingest calls that create and update each shape, who can read against it, and what flows downstream into scoring and suppression. The CDP account entity concept defines the underlying model; this is the operator walkthrough it links against.

1. Which relations exist

The graph is a projection over group events with no separate join table, so a “relation” is one of these shapes:
  • Contact ↔ account membership. A contact belongs to an account when the ingest stream carries a group event where the resolved contact joined the account’s explicit groupId. The detail pane’s Members list and the profile-side relationship view in the account entity concept are both projections of these links.
  • Parent / child hierarchy (account → account). When an account’s golden record carries a parent_group_id (or one of the alternate spellings the ship checks in order: parentGroupId, parent_account_id, parentAccountId, parent_account, parentAccount), that value names its parent and the children of the parent list back. You wire this by sending the trait on the same group call that first identifies the account — no second endpoint.
  • Person-to-company. A member link between a resolved person profile and a named account. The contact’s company field is display-level; the actual person↔company edge lives on the group link above.
  • Contact ↔ account link objects. In the payload, each member edge is one JSON object: contact_id, display_name, company, joined_at, and last_seen_at — the per-contact row the Members table renders. The GET /api/v1/cdp/contacts/:contact_id/relationships endpoint answers the inverse question, “which other contacts share the same accounts as this one?”, with a per-edge shared_group_count weight.
All four shapes read live from every ingest event Orbit persists, so the graph needs no background recompute or cleanup step. A “relation edit” is one more group call with the right trait set.

2. Surface layout

The dashboard at Audience → Accounts puts the account list on one scrollable table and the graph context in a detail pane. Filter the list by members count, sort on any of the six fields (Members, Group activity, Last seen, First seen, Name, Account ID), and search by name or id. The detail pane opens when a row is selected and lays out:
  • Stat strip (Members / Group events / First seen / Last seen)
  • Badge row — account id, plus a parent chip that re-opens the parent’s pane
  • Golden record — every trait the survivorship rules merged
  • Members — the contact↔account link objects described above, with a returned-of-total count
  • Child accounts — navigable chips that re-open their pane
The timeline of relation events is the event stream itself: “Joined” and “Last seen” on each member row, plus First seen / Last seen in the header, come straight off the ingested group events, so the timeline rows you review in a contact’s profile are the same rows the account graph folds.

3. Create a relation

Relation writes go through the CDP ingest lane — the same HMAC-signed lane every ingest route uses. The exact endpoint is POST /cdp/v1/:ingest_id/group; the ingest-id lives on your CDP ingest secret and the route accepts the body listed in the CDP API reference.
One call creates the whole shape: membership (the resolved contact ↔ groupId), the account row (a distinct non-empty groupId), and hierarchy (the parent trait). Updating a relation is the same call again — a later event that omits a trait never erases the established value, but a non-empty new value overwrites it (recency survivorship, last-write-wins per trait). Graph side effects to plan for before posting:
  • Each call stamps a group event row, which is what powers the Members row’s “Joined”/“Last seen” and the account’s own First/Last seen — so the same call feeds both the relation edge and its timeline entry.
  • Because the golden record folds every group call for the account, a batch of correct-shape calls is idempotent under survivorship: replaying an old state just re-wins the older traits.
  • A call without a parent trait leaves the hierarchy root-level. A call carrying the parent trait moves the account into its parent’s child list immediately; a later call with a different parent re-parents without keeping the old one.

4. Read vs write gating

Two gates separate readers from editors and they differ along the whole surface:
  • Reads. The Accounts surface and the two read endpoints gate on owner, admin, or developer plus the contacts:read scope — the same gate that protects every /cdp surface that exposes PII-bearing traits. That means read is NOT open to the viewer role or the analyst/marketer seats: the surface is restricted on purpose.
  • Writes. Relation writes flow only through the signed ingest lane — your ingest secret holds the signing material, not any dashboard role. A member who can read the graph can still need the ingest secret in hand to change it.
For the full role model behind those gates see Roles, teams and permissions.

5. Where relations cascade

Account links feed several downstream surfaces, and keeping the graph clean matters as much as keeping the account list:
  • Account scoring. The ranked list you’re browsing is the same projection the Account Scores view reads against for aggregation — list shape and score viewers stay consistent.
  • Suppression and segments. The member link is the edge Segments and Suppression walk when an account-scoped filter evaluates “all contacts on this account”.
  • Golden traits. Every attribute the detail pane shows — including industry, region, parent — sits in the survivorship fold, so downstream reads consistently see the traits you care about rather than whatever a mis-sent call last wrote.
Nothing changes a contact’s suppressed-by-account status without the link edge itself, so treat each write as a graph mutation, not a signpost.

6. API walkthrough

The same relations the dashboard renders come over the REST endpoints protected by a session or API key with the contacts:read scope (plus the read role from section 4). Ranked account list:
Response (fields abbreviated):
Single account detail — golden record, members, parent, children:
Response (fields abbreviated):
Per-contact relationship graph — the inverse view that answers “who else shares this person’s accounts”:
Response (fields abbreviated):
Tuning tips by endpoint:
  • accounts/profiles accepts sort (members|events|recent|created|name|group_id), order (asc|desc), limit/offset (≤200), a min_members floor, and free-text q search.
  • contacts/:contact_id/relationships accepts min_shared_groups to raise the shared-tie threshold, limit (≤200), offset, and groups_limit for the memberships map. Return 404 when the contact id is not resolved in the workspace.
  • Callers hitting 429 should retry after the one-minute window — all three endpoints carry the same 60 per minute per tenant budget; stay idempotent and you stay safe.

7. Maintenance

The graph fixes itself or drifts itself — depending on how the ingest callers feed it. Mitigate drift at three points:
  • Naming. Trait keys priority-descend, so parent_group_id beats parentAccount — pick one convention per SDK and the survivorship fold stops flipping.
  • Split-brain ids. Each distinct non-empty groupId becomes an account row; trim whitespace/case sensitive id strings either at the SDK or with a transform function.
  • Hierarchy pull. Re-parenting an account moves its child list visibly at the next read, so check accounts/profile?group_id=parent after you wire a new parent trait.
If a read surface shows a link you never wanted to wire, send a new group call with a corrected traits bag — or for deletion-of-record cases use the CDP erasure propagation lane against the member link.