Before you start
Authentication. These endpoints take the signed-in user’s session token, not an API key:X-Tenant-Id header you send is replaced by
it.
To automate CRM Cleaning with an API key, use the MCP tools
instead. They cover the same actions and accept a key from your
API settings page.
Base URL. https://api.clustr-ai.com
Add-on. Every endpoint needs the CRM Cleaning add-on. Without it, calls return 403
with code FEATURE_NOT_ENABLED.
Responses. Every response is wrapped the same way:
400 BAD_REQUEST with
the message “invalid request body”. POST /apply, POST /revert-batch and
PUT /settings answer 400 INVALID_REQUEST instead. Calls that name a run or a change
that does not exist answer 404 NOT_FOUND.
Paging. List endpoints take limit (default 50, at most 200) and offset, and
return total so you can page.
Words used below
The queue
Get the queue
GET /api/crm/cleaning/queue
Every waiting change that can be applied, from every scan, grouped by kind, plus
counts. Findings are not in it.
Response
List waiting changes
GET /api/crm/cleaning/queue/changes
One page of the queue. Merge items carry both records side by side in merge: the
record that survives, the value each field keeps, certain, why the pair matched
(match_key and matched_value, shown in the app as “Same email: …”) and
name_conflict when the person names disagree.
Returns
{ items, total, limit, offset }.
Apply a group
POST /api/crm/cleaning/queue/apply
Accepts every waiting change of one kind. A background worker writes them to the CRM,
usually within minutes. action and object_type are required (400 INVALID_REQUEST
otherwise). For merges and company links, leave property empty to apply every waiting
change of that object type. An optional bucket (auto or confirm) narrows the
group; flag answers 409 FINDING_NOT_APPLICABLE.
Merges in bulk. Every bulk apply (this call, apply-similar, a run’s group or
bucket) takes only merges whose merge_tier is certain. A Likely merge stays waiting
until you accept it on its own with POST /changes/{change_id}/accept.
Request
Response
Dismiss a group
POST /api/crm/cleaning/queue/dismiss
Dismisses every waiting change of one kind. Nothing is written. suppress defaults to
true: Clustr also writes an exclusion rule so the kind is not suggested again. Send
"suppress": false to clear the group and keep the suggestions. (The app’s first option,
Dismiss these, sends false.)
Request
Response
One change
The accept, dismiss and similar calls take an optional body. On a merge,
accept also
takes field_overrides, a map of field to the value the surviving record should keep:
Request
Revert reads the field first. When the CRM already holds the old value, the change is
marked reverted without a write.
Retry answers
409 NOT_RETRYABLE when the change is not failed or conflict, and
404 NOT_FOUND when it does not exist. Retry reads
the live value again and writes the proposed value, so on a conflict it replaces the
edit someone made in the CRM. Format fixes are the exception: they still refuse when the
live value is no longer the same value. See History.
Findings
List findings
GET /api/crm/cleaning/findings
Problems cleaning cannot fix by itself, counted by kind and listed with a link to open
each record in the CRM (HubSpot only).
Response
Export findings
GET /api/crm/cleaning/findings/export
The open findings as a CSV file, with the same kind, detail_kind, object_type and
q filters as the list. Columns: Kind, Object type, Record ID, Record, Reason, First
seen, Last seen, CRM link, Clustr ID, Members, Member CRM links (a duplicate finding’s
records and their links, separated by semicolons). See Export the history for the file
format.
Dismiss findings
POST /api/crm/cleaning/findings/dismiss
Dismisses every open finding of one kind. Nothing changes in the CRM. suppress
defaults to false: the kind comes back on the next scan if the problem is still
there. Send "suppress": true to stop reporting it.
Request
Merge a duplicate group
POST /api/crm/cleaning/duplicates/{change_id}/merge
Takes the id of a duplicate finding whose mergeable is true. Calling it is the
confirmation: the merges are queued as accepted. Clustr reads the
records again from the CRM, your survivorship policy picks the record to keep, and one
merge per extra record is queued for the worker. HubSpot only.
A duplicate finding lists its records in members, each with its own crm_url.
For duplicate_deals, only deals that share a company can be merged. Clustr reads the
deals again and merges only while they are all still open, in one pipeline, for one
company, with the same name, amount and currency, and created within 90 days of each
other. The deal with more contacts, or else the older one, is kept. A deal merge cannot
be reverted.
Response
Scans and runs
Start a scan
POST /api/crm/cleaning/scan
Starts a scan of the connected CRM. The body is optional; its source labels the run
(default api). If a scan is already running, you get that run back with
already_running: true instead of a second scan.
Response
Run enrichment only
POST /api/crm/cleaning/enrich
Runs only the enrichment checks, inside the monthly enrichment budget: Enrich all.
It covers every allowed field on contacts and companies and spends at most about a third
of what is left this month. Paid lookups land on the next scan. Same response as a scan.
List runs
GET /api/crm/cleaning/runs?limit=20
The workspace’s scans, newest first, scheduled ones included. limit defaults to 20,
at most 100.
Get a run
The run reads answer
404 NOT_FOUND for a run that does not exist. A partial run
stopped early but kept everything it found. coverage says how much of
the CRM it examined.
History
GET /api/crm/cleaning/history
Every change across all runs, newest first.
Returns
{ changes, total, limit, offset, has_more }. Each change has revertable,
which is true only when a revert would be accepted right now. A filter Clustr cannot
read, such as a from that is not an RFC 3339 time or a to before from, answers
400 with code INVALID_REQUEST and the reason.
Export the history
GET /api/crm/cleaning/history/export
The history as a CSV file, with the same filters as the list.
Columns: Date applied, Rule, Object type, Record ID, Record name, Field, Before, After,
State, Decided by, CRM link, Clustr ID, Activity date (when the change was applied, else
last updated), Reason, Error.
- The body is the file, not JSON:
Content-Type: text/csvandContent-Disposition: attachment; filename="clustr-crm-cleaning-history-2026-10-08.csv". - At most 50,000 rows.
limitlowers that. X-Export-Totalis how many rows match,X-Export-Rowshow many are in the file, andX-Export-Truncatedistruewhen the file stopped at the limit.X-Total-Countcarries the same count asX-Export-Total, under its earlier name.- Rows are sent as they are read. If the export fails partway, the file’s last line is
# EXPORT FAILED: this file is incomplete. Download it again.and the connection is closed. SendAccept-Encoding: gzipto receive it compressed. - UTF-8 with a byte order mark, so Excel reads accents. A cell that starts with
=,+,-or@gets a leading'.
Revert a batch
POST /api/crm/cleaning/revert-batch
Reverts every applied change of one rule, through the same checks as a single revert.
This is Revert all in History.
Request
Response
- Merges and phone repairs are never reverted here; they count in
skipped_not_revertable. Revert a merge withPOST /changes/{change_id}/revert. - Changes past their 30 days count in
skipped_expired. - A field edited in the CRM since counts in
conflictand is left as it is. - The skipped counts are given on the first call only, so the numbers of a multi-call batch add up.
- One call reverts at most 1,000 changes, or stops starting new ones after about 20
seconds. When
has_moreistrue, call again withcursorset tonext_cursor.
400 INVALID_REQUEST when action is missing, a time is not RFC 3339,
run_id or cursor is not an id, or until is before since.
Settings
Get settings
GET /api/crm/cleaning/settings
The whole settings document. effective_modes is read-only: the mode each rule
resolves to, defaults included. effective_when_different is read-only too: what
enrichment does with a value your CRM already has, per object type, defaults included
({"contact": "suggest", "company": "keep"} unless you chose otherwise).
Replace settings
PUT /api/crm/cleaning/settings
Replaces the whole document. Read it first, change what you need, and send it all back:
a field you leave out is reset. Invalid values answer 400 INVALID_SETTINGS.
Request
alerts turns the
weekly summary highlights
off one by one; leaving it out keeps both on with a threshold of 25. A negative threshold answers
400 INVALID_SETTINGS. survivorship.primary_rules and match_rules are explained in
Duplicates and merges; an
unknown kind or field, a property_equals rule without a property and value, more than
10 primary rules, more than 5 match rules per object type or more than 6 fields in one
rule answers 400 INVALID_SETTINGS.
realtime_duplicates set to false stops the duplicate check
on new records; leaving it out keeps it on while
merges are on.
enrichment.when_different says what enrichment does when your CRM already has a
different value, per object type (contact, company): keep or suggest, a change
that always waits for a person. Unset, contacts suggest and companies keep. Another key or value answers
400 INVALID_SETTINGS. See Enrichment.
A suggestion in the queue carries reason, such as “Your CRM says X; LinkedIn now
says Y”.
Suggestions
GET /api/crm/cleaning/settings/suggestions
Rules your team always accepts and could switch from Ask me to Do it, from the last 90
days of decisions. Only Format & casing and LinkedIn URL are ever suggested. See
Suggestions to switch to Do it.
Response
auto in the settings and save.
List templates
GET /api/crm/cleaning/settings/templates
The ready-made recipes: { templates: [{ id, name, description, highlights, settings, show_flags }] }.
Apply one by merging its settings into yours and saving.
Health, completeness and summary
Completeness response
Older endpoints
GET /api/crm/cleaning/escalations and its POST /escalations/changes/{change_id}/accept
and dismiss calls serve the older email review page. New code should use the queue.
/api/crm/cleaning/escalations/act takes the signed one-click links in the summary
email; it needs no session, only the link’s token.
The /api/crm/cleaning/quality/* endpoints (GET /quality/pending,
POST /quality/samples/{sample_id}/verdict, GET /quality/precision) are for Clustr
staff and answer 403 STAFF_REQUIRED to everyone else.
Two more routes exist only on Clustr’s internal network, not at the base URL above:
sending the summary email now (POST /escalations/email/run), and one record’s likely
duplicates (POST /records/{object_type}/{object_id}/duplicates). Use the
find_crm_record_duplicates MCP tool for the second.