Skip to main content

CRM sync

Keep your own CRM in step with the leads from your SalesLobe campaigns. The CRM API reads and updates the CRM part of a lead: phase, deal value, next action and notes. It works both ways and is built for incremental sync.

What it does not do: it never creates leads, never imports contacts and never sends email. A lead exists in SalesLobe once a prospect replied to one of your campaigns.

Requirements​

  • A key locked to one client (Settings → API Keys → pick the client).
  • The scopes crm.read and/or crm.write, granted explicitly. A full-access (*) key does not include them.
  • The CRM API switched on for your account. If it is not, every CRM call answers 403 feature_not_enabled.

The lead object​

{
"id": "5f0c…",
"email": "anna@acme.com",
"name": "Anna de Vries",
"company": { "name": null, "domain": "acme.com" },
"campaign": { "id": "b87f…", "name": "Q4 SaaS outreach" },
"classification": "hot",
"phase": "proposal",
"deal": { "value": 12000, "currency": "EUR" },
"next_action": { "date": "2026-10-07", "note": "Demo" },
"notes": [
{ "at": "2026-09-01T10:00:00Z", "by": "legacy", "text": "Called on Monday" },
{ "at": "2026-10-01T14:03:00Z", "by": "Mark's CRM", "text": "Demo booked" }
],
"last_activity_at": "2026-09-30T08:12:00Z",
"reply_count": 3,
"thread_url": "https://app.saleslobe.com/your-agency/portal/your-client?lead=…",
"updated_at": "2026-10-01T14:03:00Z",
"crm_updated_at": "2026-10-01T14:03:00Z"
}
  • id is stable: it does not change when new replies come in. thread_url always uses this id and opens the lead's current conversation in the portal.
  • name is null when the prospect's name is unknown (never the e-mail address).
  • company.domain is the domain of the e-mail address (null for Gmail, Outlook and other freemail); company.name is not stored yet.
  • phase: new, contacted, discovery, proposal, negotiation, won, lost.
  • classification is read only (SalesLobe classifies every reply).
  • updated_at changes when anything on the lead changes: use it for updated_since.
  • crm_updated_at changes only when a CRM field or note changes: use it for if_updated_at.
  • Notes written before the CRM API existed appear as the first note with author legacy.

Which leads​

Exactly the leads the client sees in its portal: never out-of-office, spam or not-yet-classified replies; hot, warm and cold unless the agency switched them off in the portal permissions; not interested only when the agency shows them. A lead that is not visible does not exist in the API: it is not listed and get, update and notes answer 404. When an out-of-office lead later sends a real reply, it appears automatically (with updated_since).

Filters​

GET /v1/crm/leads?classification=hot,warm&phase=proposal,negotiation&has_next_action=true. Filters apply per lead, so a page can hold fewer than limit leads: keep following next_cursor.

Incremental sync​

  1. First run: GET /v1/crm/leads and follow next_cursor until has_more is false.
  2. Store the time you started.
  3. Next runs: GET /v1/crm/leads?updated_since=<stored time>.

Conflicts​

Last write wins. Both sides are recorded in the Decision Trail. To avoid overwriting a newer change, send if_updated_at with the crm_updated_at you last read: if the lead changed since, you get 409 conflict with the current lead and nothing is written.

What a CRM update does in SalesLobe​

  • phase → won or lost: all planned follow-ups and check-ins for that lead are cancelled.
  • Any other CRM update (phase, next action, deal, note): Corty's planned follow-ups for that lead go to review instead of sending automatically, with the reason "lead bijgewerkt in CRM door <key name>".
  • Every write shows in the Decision Trail and in the client portal with the key name as actor.

Limits​

crm.write: at most 60 writes per minute per key (429 crm_write_rate_limited, Retry-After: 60). The key's normal rate limit applies to all calls.

Claude Desktop (MCP)​

The same endpoints are available as an MCP server for Claude Desktop: see MCP server.