Deduptio

Attio duplicate detection with the API

The short answer

There is no duplicate-detection endpoint. Attio gives you three primitives — assert to prevent, query to find, merge to resolve — and everything between them is yours: paging, normalization, blocking, grouping, review and recovery.

The three endpoints that matter

JobCallWhat to know
Prevent a duplicate on writePUT /v2/objects/{object}/records with a matching attributeUpsert. The matching attribute must be unique — email_address, domains. One call, no search-then-create race
Read the object to compare recordsPOST /v2/objects/{object}/records/queryPage through it. Rate limiting is score-based, so filter-free paging is cheaper than a clever filtered query on a large object
Resolve a duplicatePOST /v2/objects/{object}/records/mergeTwo records per call: a primary and a secondary. Returns the new record id. Primary wins conflicts, secondary fills gaps

Endpoint behaviour per Attio's REST API documentation and our own production use, September 2026. The merge endpoint is in beta; check the docs before depending on it.

Prevention: assert, do not create

If you are writing into Attio from a form, a webhook, an enrichment run or a script, the single highest-value change is to stop calling create. Asserting with a matching attribute means Attio updates the record already holding that email or domain, or creates one if nothing matches — atomically, so two concurrent writes cannot both decide the record does not exist yet.

This is exact matching, and its blind spots are the obvious ones: a person whose email changed, a company on a second domain, a record with no identifier at all. Prevention keeps the number from growing. It never reduces it.

Detection: what you are actually building

"Find the duplicates" is four problems wearing a trench coat, and the API helps with none of them:

  1. Paging the object within a shared, score-based rate limit, while records change underneath you.
  2. Normalization. Bob@Example.COM and bob@example.com are one value; https://www.acme.com/ and acme.com are one domain; +1 (555) 010-9999 and 555-010-9999 are one subscriber. Also: a null must never match another null, or every record missing an email becomes one enormous group.
  3. Grouping without going quadratic. Comparing every record with every other is ten billion comparisons on a 100,000-record object. Real implementations block: emit coarse keys per value, compare only within a block. Without blocking, a fuzzy scan on a large object simply does not finish.
  4. Explaining the match. A group nobody can audit will not be merged, so you need to carry the reason — which key matched, and how closely — through to whoever approves.

Merging: the details that cost a weekend

Should you build it?

Build it if your matching rule is genuinely domain-specific — an internal identifier, a business rule nobody else would model — and the run is one-off. That is a day of work and it will be correct enough.

Do not build it as a permanent capability by accident. The endpoints are the easy part; the durable cost is everything around them: resumability, progress persistence, rate-limit behaviour, snapshots, a review surface for the fuzzy cases, and someone maintaining it when Attio changes an endpoint from beta to stable. Deduptio is that layer, and its documentation is explicit about the same edge cases listed here — if you are building this yourself, it doubles as a checklist.

Or skip the build

Connect Attio, define the rule, and run a read-only scan. If the output is not better than what you were about to build, you have lost ten minutes.

Start a free Attio duplicate scan

Related