Skip to main content
These are the endpoints the Clustr app itself calls for CRM Cleaning. Use them to build your own review screen, or to drive cleaning from a script.

Before you start

Authentication. These endpoints take the signed-in user’s session token, not an API key:
The workspace comes from the token. An 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:
An error carries a code and a message:
A body that is not valid JSON, or has the wrong shape, answers 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 answers: 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/csv and Content-Disposition: attachment; filename="clustr-crm-cleaning-history-2026-10-08.csv".
  • At most 50,000 rows. limit lowers that.
  • X-Export-Total is how many rows match, X-Export-Rows how many are in the file, and X-Export-Truncated is true when the file stopped at the limit. X-Total-Count carries the same count as X-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. Send Accept-Encoding: gzip to 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 with POST /changes/{change_id}/revert.
  • Changes past their 30 days count in skipped_expired.
  • A field edited in the CRM since counts in conflict and 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_more is true, call again with cursor set to next_cursor.
Errors: 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
The fields are explained in Settings. 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
To act on one, set the rule’s mode to 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.