Skip to content

Atlas

Shipped

When the same real customer, vendor, or other entity shows up in more than one of your connected sources (or twice within one source), Atlas consolidates them into a single canonical entity. So a customer who exists in both Oracle NetSuite and Stripe resolves to one identity in your data model, not two.

As entity records flow in, Atlas reads their identity fields — email, phone, tax ID, an external key, a GTIN, or a domain — normalises each (lowercasing email, reducing phone numbers to E.164, stripping a web address down to a bare host, and so on), and checks them against the existing canonical entities. The outcome is one of:

  • Linked — exactly one existing entity matches. The incoming record is attached to it.
  • Created — no existing entity matches. A new canonical entity is created and linked.
  • Review — two or more existing entities match different identity fields of the same incoming record. Rather than guess, Atlas surfaces it for a human to decide.

Matching is deterministic and idempotent: re-running it on the same record produces the same canonical entity, and a record carrying none of those identity fields keeps the entity it was first given rather than gaining a new one each time it changes. It is also connection-agnostic, so the same customer seen under two different accounts of the same source still resolves to one identity.

A web address is only an identity when it belongs to the business. A social profile, a shortened link or a free mail host is listed by thousands of unrelated companies, so Atlas does not match on one at all — a record whose only identity field is a host like that keeps an entity of its own. A web address belongs to the business and never to a person, so two people at one company are two people: a lead is no longer matched on their employer’s website. For a person carrying no email and no phone, a name is matched only together with an employer; a name on its own is never enough.

A lead and a contact are the same person, so a prospect one connected system files as a lead and the human another files as a contact resolve to one identity when they share an email address or a phone number. Sales systems file the same person twice over — as a lead before they buy anything, and as a contact once somebody owns the relationship — and Atlas used to keep those two as unrelated identities for good.

Kinds of entity are otherwise compared only against their own kind: a customer is never consolidated with a lead, and an employee is never consolidated with a contact, however much they share. People on your own payroll stay separate from the people you sell to.

An identity holding both a lead and a contact is read one record at a time, under the capability each of those records needs: a user holding read:entity:contact sees the identity and the contact behind it, and not the lead.

Your apps can read the consolidated identity directly, so nothing has to fetch both copies of a company and merge them itself.

Ask for canonical entities with their source records embedded — one request, one row per identity:

GET /rest/v1/entity_canonical?select=*,entity_source_link(*)&entity_type=eq.customer&order=display_name&limit=50

The response nests every source record behind each identity under entity_source_link, and it pages by identity, so a page boundary never cuts one in half. Send Prefer: count=exact and the Content-Range header carries the exact number of identities.

Listing people takes a different filter. An identity’s entity_type is the kind of whichever record reached it first, so entity_type=eq.contact leaves out a person whose lead arrived first, and entity_type=eq.lead leaves out the ones whose contact did. Ask for match_scope=eq.person to list people in one pass. Every other kind is a scope of its own, so entity_type=eq.customer, eq.vendor and the rest read exactly as they did.

To go the other way — from a source record you already hold to the identity it belongs to, and to the other records behind it:

GET /rest/v1/entity_source_link?connection_id=eq.default&source_system=eq.netsuite&external_id=eq.1234&select=canonical_id,entity_canonical(*,entity_source_link(*))

The nested entity_source_link is what brings the other records back, so this answers the whole question in one request rather than two. Ask for entity_canonical(*) instead if the identity on its own is all you need.

Filter on connection_id as well as the source and the id: two workspaces of the same source can each hold the same upstream id.

Who can read it. Whoever can already read the underlying records. Each identity is visible to a user holding the read capability for one of the kinds it covers — read:entity:customer for customers, read:entity:vendor for vendors, either read:entity:lead or read:entity:contact for a person — and the source records listed under it are narrowed to the ones that user could read anyway. A user without the capability gets an empty result rather than an error, so check the capability before concluding your instance has nothing consolidated.

One thing to know if you use scopes as a confidentiality boundary. An identity is visible to anyone who can read at least one of the records behind it — that is what makes the cross-source lookup work, since the second system’s copy is often the one outside a user’s scope. The list of source records under it is narrowed to the ones that user can open. The identity’s own details are not: display_name, primary_email, tax_id and the rest are whichever record Atlas recorded them from, so they can come from a record that user cannot open. If a field must never cross a subsidiary, department or class boundary, do not read it from the identity row.

Both tables appear on your instance’s record catalogue, so turbobase project types generates types for them.

The same identities are a record type on your instance’s MCP server, one per bucket, named turbobase:identity_<bucket>:

Bucket MCP record type Capability
Customers turbobase:identity_customer read:entity:customer
Vendors turbobase:identity_vendor read:entity:vendor
Contacts turbobase:identity_contact read:entity:contact
Employees turbobase:identity_employee read:entity:employee
Partners turbobase:identity_partner read:entity:partner
Leads turbobase:identity_lead read:entity:lead
Items turbobase:identity_item read:item
list_records { "record_type": "turbobase:identity_customer", "page_size": 100 }

That is the one combined customer list across every connected system, and it pages like any other record type — pass page.next_cursor back to walk it. The bare form (identity_customer) works too. get_record does not: these rows are addressed by the identity’s own id rather than by a source-side id, so read them with list_records and match on id.

turbobase:identity_lead and turbobase:identity_contact are two lists over one scope: a person appears in whichever one matches the record that reached their identity first. Read both, or read entity_canonical with match_scope=eq.person over the app surface above, to get every person in one pass.

They are read-only on every instance, because Atlas derives them. To change what one says, change the source record it was derived from, or settle it in the review queue below.

Administrators work the cases Atlas will not decide alone at /data/atlas. Each row says which of two decisions it is asking for:

  • Merge into — two or more existing entities matched different identity fields of one incoming record. Confirm the entity it belongs to, or choose Keep separate if it is none of them.
  • Split off — an existing entity’s source records no longer share any identity field, so the merge may no longer hold. Confirming moves that one record onto an entity of its own and leaves the others where they are; Keep merged settles the question the other way.

Nothing is ever split automatically. An automatic un-merge would be the same kind of decision as an automatic merge, so Atlas only ever proposes one.

Every answer in the review queue is stored as a decision about two source records, and the automatic matcher obeys it on every sync from then on:

  • Apart — these two records are never the same entity, whatever they share. A website, an email or a name-and-employer in common no longer pulls them together.
  • Together — these two records are always the same entity, even if they share nothing. If one of them later moves, the other follows it.

Confirming a split, or choosing Keep separate, records apart. Confirming a merge, or choosing Keep merged, records together. A new record that genuinely matches both sides of an apart decision is still raised in the queue — a decision covers the records it names, not everyone who shares their details.

Administrators can make and undo the same decisions without waiting for the queue to ask:

  • API — GET /admin/atlas/decisions lists them (add source_system and external_id to see one record’s), POST /admin/atlas/decisions with { "kind": "apart" | "together", "a": {…}, "b": {…} } makes one and applies it straight away, and DELETE /admin/atlas/decisions/<id> forgets one. Each record is { "source_system", "external_id", "connection_id"? }. For together, a moves onto b’s entity; for apart, a moves onto an entity of its own.
  • MCP — an administrator’s agent has atlas_match_decisions to read them and, when the instance’s MCP server allows writes, atlas_decide to make or clear one.

Forgetting a decision moves nothing by itself: from the next sync, Atlas decides that pair on the evidence alone again.

  • Data model — the canonical entities Atlas consolidates into