Attio duplicate detection with the API
Last checked September 1, 2026 · Deduptio is our product. Every claim about another tool comes from its public site or documentation and is linked in place — tell us if something is out of date.
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
| Job | Call | What to know |
|---|---|---|
| Prevent a duplicate on write | PUT /v2/objects/{object}/records with a matching attribute | Upsert. The matching attribute must be unique — email_address, domains. One call, no search-then-create race |
| Read the object to compare records | POST /v2/objects/{object}/records/query | Page through it. Rate limiting is score-based, so filter-free paging is cheaper than a clever filtered query on a large object |
| Resolve a duplicate | POST /v2/objects/{object}/records/merge | Two 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:
- Paging the object within a shared, score-based rate limit, while records change underneath you.
- Normalization.
Bob@Example.COMandbob@example.comare one value;https://www.acme.com/andacme.comare one domain;+1 (555) 010-9999and555-010-9999are one subscriber. Also: a null must never match another null, or every record missing an email becomes one enormous group. - 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.
- 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
- Two records per call. A group of four is three calls. Each call returns a
new_record_idthat must become the primary of the next hop. - Both inputs are consumed. After a merge, both originals are permanently unreadable. A retry that replays a completed hop passes a record that no longer exists — persist progress per hop, not per group.
- 202 means committed, not finished. The merge can be accepted and still be processing; reading the new record returns 404 until it completes. Poll with backoff. Treating a timeout as a failure and retrying is how you corrupt a chain.
- Uniqueness constraints order your writes.You cannot write the secondary record's email onto the survivor while the secondary still holds it. Field corrections belong after the merge.
- Rate limits. A 429 arrives with
Retry-Afterand the request was not processed, so retrying after the wait is safe. Budget for it in a bulk run. - List entries. If both records were in the same list, the survivor is now in it twice. Clean up afterwards — see duplicate list entries.
- Meetings. They survive a merge but lose their link to the deleted record, and no Attio API can re-link them. Record what was linked before you merge, or the information is gone.
- No undo. Nothing in the API restores a merged-away record. If you want a way back, snapshot every record in the group before the first hop — that is the only mechanism there is.
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 scanRelated
- Bulk merging duplicates in Attio— the same chain problem, from the operator's side.
- Deduptio vs Zapier or Make — why assert belongs in the automation and merging does not.
- When a merge fails · Rollback and safety · The Attio deduplication guide