Skip to Content
New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
API Reference (v2)CRM & Sequencer Sync
🔌

Audience: developers. API keys need a Pro or Enterprise plan (legacy Pro and Scale plans included) or an AppSumo Tier 5, 6 or 7 license, and they never work during a trial. Support agent: confirm the user’s plan grants API access before giving API advice.

TL;DR: POST /migrate/crm pushes every lead in a list to HubSpot or Salesforce; POST /migrate/sequencer enrolls a list in a Lemlist campaign. Both need the sync:write scope, a signed quote_id from POST /credits/estimate and approved: true. They cost 0.2 credits per lead written. Up to 500 leads the response has the final counts; a bigger list is queued (status: "pending") and GET /migrate/status/{workflow_id} returns its counts.

CRM & Sequencer Sync

API v2 can push a CluGTM lead list into a connected CRM or sequencer:

EndpointDestinationsScopeCost
POST /migrate/crmHubSpot (hubspot), Salesforce (salesforce)sync:write0.2 per lead
POST /migrate/sequencerLemlist (lemlist)sync:write0.2 per lead

No other destinations are supported. Outreach, Salesloft, Pipedrive and Close can’t be sync targets.

These endpoints (and GET /migrate/status/{workflow_id}) are live on /api/v2 but aren’t in the served OpenAPI spec, and they aren’t in the SDKs yet. Call them over HTTP. Older docs showed /sync/crm and /sync/sequencer; those paths don’t exist.

Before you start

  1. Connect the destination in the app. Open Integrations in the sidebar, find HubSpot, Salesforce or Lemlist under Available and click Link. HubSpot and Salesforce connect by signing in; Lemlist asks for your Lemlist API key in the Connect Lemlist dialog. See CRM integration.
  2. Have a list ready. Sync pushes every saved lead in the list. An empty list returns 400 validation_error.
  3. Know what each destination needs. HubSpot and Lemlist need an email on the lead; Salesforce needs a last name (taken from the full name when missing). Leads without them end up in skipped or failed. Enrich the list first if emails are missing.

The flow

  1. Quote. Call POST /credits/estimate with tool: "sync_to_crm" or "sync_to_sequencer", the list_id, the provider, and the same sub_action, campaign_id and field_mapping you’ll send to sync. You get a quote_id (5 minutes, single use) and the estimated_cost.
  2. Ask a person. Show the provider, list, lead count and cost, and wait for a clear yes.
  3. Sync. Call the sync endpoint with the same fields, the quote_id and approved: true. Up to 500 leads the push runs to completion and the response has the final counts.
  4. Poll, for big lists. Above 500 leads the push is queued and the response has status: "pending". Poll GET /migrate/status/{workflow_id} until status is completed or failed.
# 1. Quote curl -X POST https://api.cleanlist.ai/api/v2/credits/estimate \ -H "Authorization: Bearer clapi_your_api_key" \ -H "Content-Type: application/json" \ -d '{"tool": "sync_to_crm", "list_id": "LIST_ID", "provider": "hubspot", "sub_action": "contacts"}' # 2. After the user confirms, sync curl -X POST https://api.cleanlist.ai/api/v2/migrate/crm \ -H "Authorization: Bearer clapi_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "provider": "hubspot", "list_id": "LIST_ID", "sub_action": "contacts", "quote_id": "qte_v1_...", "approved": true }'

Sync a list to your CRM

POST /migrate/crm

Scope: sync:write · Cost: 0.2 credits per lead

Creates one CRM record per lead in the list.

Request body

FieldTypeNotes
providerstringRequired. hubspot or salesforce
list_idstringRequired. The list to push
sub_actionstringRecord type. HubSpot: contacts (default). Salesforce: leads (default) or contacts. Any other value returns 400 validation_error
field_mappingobjectOptional overrides: keys are CluGTM columns, values are CRM property names, for example {"first_name": "firstname", "company_name": "company"}. Omit to use CluGTM’s default mapping
quote_idstringRequired. From POST /credits/estimate with tool: "sync_to_crm" and the same provider, list_id, sub_action and field_mapping
approvedbooleanRequired to be true. Defaults to false, which returns 400 approval_required

Unknown fields are rejected with 422 validation_error; for example, sequence_id is no longer accepted.

Enroll a list in a Lemlist campaign

POST /migrate/sequencer

Scope: sync:write · Cost: 0.2 credits per lead

Adds every lead in the list as a contact in a Lemlist campaign.

Request body

FieldTypeNotes
providerstringRequired. lemlist (the only supported sequencer)
list_idstringRequired. The list to enroll
campaign_idstringRequired. The Lemlist campaign id
field_mappingobjectOptional overrides: keys are CluGTM columns, values are Lemlist field names, for example {"first_name": "firstName", "company_name": "companyName"}
quote_idstringRequired. From POST /credits/estimate with tool: "sync_to_sequencer" and the same provider, list_id, campaign_id and field_mapping
approvedbooleanRequired to be true
curl -X POST https://api.cleanlist.ai/api/v2/migrate/sequencer \ -H "Authorization: Bearer clapi_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "provider": "lemlist", "list_id": "LIST_ID", "campaign_id": "cam_9d41ab2f", "quote_id": "qte_v1_...", "approved": true }'

The response

Both endpoints return the same shape. For a list of up to 500 leads the push runs inside the request and the counts are final:

{ "workflow_id": "cl-sync_9f2c1a4b7e3d0f28", "status": "completed", "provider": "hubspot", "list_id": "LIST_ID", "total_leads": 128, "successful": 121, "failed": 2, "skipped": 5, "estimated_cost": 26, "credits_charged": 25, "timestamp_ms": 1751404800000 }
FieldMeaning
workflow_idA reference for this push. On a queued push, pass it to GET /migrate/status/{workflow_id}
statuscompleted when the push ran (per-lead outcomes are in the counts), pending when it was queued (successful and failed are 0), failed on a run-level error
total_leadsLeads in the list that were attempted. Equals successful + skipped + failed
successfulLeads written to the destination
skippedLeads skipped for benign reasons (no writable fields, already present)
failedLeads the destination rejected
estimated_costCredits the quote reserved: 0.2 per lead in the list, rounded up
credits_chargedCredits charged: 0.2 per lead written (successful), rounded up. 0 on a queued push until it finishes

Lists over 500 leads: queued syncs

A list of more than 500 leads is queued instead of pushed inside the request. The sync endpoint answers at once with status: "pending", successful, failed and credits_charged at 0, and a workflow_id (it starts with crm-bulk-push-). skipped can already count CSV rows that are people already on the list, which are pushed once.

GET /migrate/status/{workflow_id}

Scope: sync:write · Cost: free

Returns the counts so far. Poll it every few seconds until status is completed or failed.

curl https://api.cleanlist.ai/api/v2/migrate/status/WORKFLOW_ID \ -H "Authorization: Bearer clapi_your_api_key"
{ "workflow_id": "crm-bulk-push-org_2xyz...-...", "status": "completed", "total_leads": 12000, "successful": 11640, "skipped": 210, "failed": 50, "unpaid": 100, "credits_charged": 2328, "timestamp_ms": 1751404800000 }
FieldMeaning
statuspending while the push runs, then completed or failed
total_leadsLeads in the push
successful, skipped, failedCounts so far, as in the sync response
unpaidLeads left unwritten because the credit balance ran out. They aren’t charged
credits_charged0.2 per lead written so far, rounded up

Once completed, successful + skipped + failed + unpaid equals total_leads. An unknown handle, a handle from another workspace, or one that finished and expired returns 404 not_found.

Cost

Sync costs 0.2 credits per lead, rounded up per call. The quote prices every lead in the list (128 leads quote 26 credits), and those credits are reserved when the push starts. When it finishes you’re charged only for the leads written to the destination, and skipped and failed leads are refunded: in the example above, 121 written leads cost 25 credits. If the push fails as a whole, the reservation is refunded. A queued push holds the quoted credits for its whole run and settles once at the end, so it never costs more than the quote.

Errors

CodeHTTPWhat to do
approval_required400Confirm with the user, then send approved: true
provider_not_connected400Connect the destination on the Integrations page (the API’s fix says “Settings → Integrations”), then quote again
validation_error400 or 422Empty list, invalid sub_action, missing campaign_id, or an unknown field such as sequence_id
quote_expired, quote_mismatch, quote_invalid400Quote again with exactly the fields you’ll send
spend_cap_exceeded400The list grew after the quote; quote again
quote_already_redeemed409Quotes are single use; quote again
insufficient_credits400Buy credits on the Plans page (app.clugtm.com/plans); the API’s fix text says app.clugtm.com/billing, which doesn’t exist
internal_error502The push failed as a whole and was refunded; retry later
not_found404GET /migrate/status doesn’t know that workflow_id in this workspace

See Errors & Rate Limits for every code.

Preview and discovery routes

Three free routes help an agent check a push before quoting it: POST /migrate/preview (what a push would do), GET /migrate/sequencer/campaigns (your Lemlist campaigns and their ids) and GET /migrate/crm/connections (which destinations are connected). They’re built for the CluGTM MCP, and aren’t available to AppSumo-only workspaces (they return 403 there). If they aren’t available to you, copy the campaign id from Lemlist.