contacts scope — see Scopes. The scan covers your team’s saved contacts, Fuse and custom alike: everyone on your own lists and on your teammates’ public lists, not one list.
Start a scan
POST /business/contacts/dedupe-scan with the fields duplicates must match exactly on:
matchFields accepts any combination of linkedinUrl, email, phone, fullName, jobTitle, industry, location, companyName, companyDomain — with a floor: the selection must include at least one unique identifier (linkedinUrl, email or phone) or at least three profile fields. Anything weaker (say, jobTitle alone) would group unrelated people, and is rejected with 422 VALIDATION_FAILED.
Values are compared after trimming and lowercasing. email and phone compare each contact’s full set of addresses or numbers, so contacts that share only one of them are not grouped. A contact missing any selected field is left out of the scan.
The API responds 202 with a job id:
202 means accepted, not finished. Hold on to jobId — it is both your handle for polling and the id the merge step consumes.
Poll the scan
GET /business/contacts/dedupe-jobs/{jobId} returns the job’s current state:
status moves through the same lifecycle as every long-running job: pending → processing → completed or failed, and the terminal states are final. When the scan completes, scanResult carries one page of the duplicate groups — pageNum and limit page through them (default 50 per page, max 200, largest groups first), and scanResult.pagination says how many pages there are. Poll without paging params until status is completed, then page through the groups; a completed scan’s full result can run to a megabyte, so re-fetching every group on every poll is wasted transfer.
contactIdsis every member;contactspreviews the first 25 of them with their own fields, emails and phones.mergePreviewis what the surviving contact will look like if you merge the group: the winner’s identity (winnerContactId), empty fields filled from the other members, and the combined email/phone unions.emailsandphonescarry the first 10 entries each;emailCount/phoneCountare the full totals.listsnames the lists the members live in, up to 10 of them, andlistCountis the full total. Useful for judging blast radius before merging.groupIdis the stable id the merge step selects by.
truncated: true means more exist — merge what you have and scan again. Note the two totals side by side: totalGroups counts every group the scan found (it can exceed 200), while pagination.totalRecords counts the stored, pageable groups. Groups larger than 50 contacts are dropped and counted in skippedOversizedGroups; that many contacts matching exactly almost always means the field selection is too weak, not that one person has 50 records.
Scan results expire with the job after roughly 24 hours. Polling an unknown or expired jobId returns 404 JOB_NOT_FOUND; start a new scan.
Merge the groups
Review the groups, thenPOST /business/contacts/dedupe-merge with the scan’s job id. Omit groupIds to merge every group, or pass a subset of groupId values to merge only those:
202 with a new jobId — poll it at the same GET /business/contacts/dedupe-jobs/{jobId} endpoint. Within each group, the merge follows a fixed contract, the same one the in-app Merge Duplicates dialog applies:
- A Fuse contact beats a custom one as the survivor; among custom-only groups, the most recently created wins.
- The survivor’s empty fields are filled from the other members.
- All emails and phones across the group are combined onto the survivor.
- Custom-column values are carried over.
- The losing contacts are archived, and every list membership they held is repointed to the survivor — no list loses a row.
mergeResult carries the totals:
Groups that will not merge
Two safety rules can makemergedGroups come out lower than the number of groups you submitted:
- Conflicting Fuse identities. Two Fuse contacts with different PDL ids or different LinkedIn URLs are never combined, even when the scan grouped them (they matched on your selected fields, but Fuse knows they are different people). Such groups are split into their consistent parts or skipped.
- Stale groups. A group whose members were already merged or archived by the time the job ran — by a teammate, or an earlier merge — is counted in
skippedGroupsrather than failing the job.
scanJobId that is unknown or has expired answers 404 SCAN_NOT_FOUND. Starting a merge against a scan that is still running answers 409 SCAN_NOT_COMPLETED; poll the scan to completed first. A scan that found nothing answers 422 SCAN_HAS_NO_GROUPS — there is nothing to merge, which is the good outcome.
A compact end-to-end loop
Next steps
Clean and enrich your CRM data
Import a CRM export, merge its duplicates, enrich it and sync it back in one workflow
API reference
Full request and response schemas for the dedupe endpoints