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 and return final counts in the response.

CRM & Sequencer Sync

API v2 can push a Cleanlist 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 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. The push runs to completion and the response has the final counts.
# 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 Cleanlist columns, values are CRM property names, for example {"first_name": "firstname", "company_name": "company"}. Omit to use Cleanlist'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 Cleanlist 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 when the push finishes:

{
  "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": 26,
  "agent_instructions": "Synced 121 of 128 leads to HubSpot as contacts. 5 were skipped (no writable fields) and 2 failed.",
  "timestamp_ms": 1751404800000
}
FieldMeaning
workflow_idA reference for this push. The counts are already final; there's nothing to poll
statuscompleted when the push ran (per-lead outcomes are in the counts), 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_cost, credits_chargedCredits for this push (0.2 per lead, rounded up)

Cost

Sync costs 0.2 credits per lead in the list, rounded up per call: 128 leads cost 26 credits. The credits are reserved when the push starts and charged in full once it completes, including leads that were skipped or failed. If the push fails as a whole, the reservation is refunded.

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.cleanlist.ai/plans); the API's fix text says app.cleanlist.ai/billing, which doesn't exist
internal_error502The push failed as a whole and was refunded; retry later

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 Cleanlist 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.

Related