Key scopes
Every API key has one of two scopes, chosen when you create it.
Read only is the default, and it is the right choice for anything that only pulls data out — a reporting job, a warehouse sync, a dashboard. If that key ever leaks, the damage is disclosure rather than data loss.
Keys created before write access existed are read-only. Their behaviour has not changed. To write, create a new key with write access — you cannot upgrade an existing one.
403:
Creating a write key
- In the Cockpit, go to Settings → API Keys.
- Click Create key and give it a name that says where it will be used —
Zapier production,warehouse sync. - Under Access, choose Read and write.
- Copy the key. It is shown once.
How writes are attributed
An API key belongs to a workspace member — by default whoever created it. Every write made with that key is recorded as that person’s action:- Notes show their name as the author.
- Logged calls appear on their activity.
- Tasks are assigned to them unless you pass an
assigneeId. - Timeline entries carry their user id.
Creating leads
POST /leads needs a name plus at least one of email, phone or linkedinUrl. Without one of those there is no way to recognise the same person later, and every subsequent call would create another copy of them.
Retries are safe
Leads are deduplicated across the whole workspace by the person they resolve to. Post the same email twice and you get the same lead back, withcreated: false and a 200 instead of a 201:
created when it matters. A webhook handler that emails “welcome” on every POST /leads will email the same person twice; one that emails only when created is true will not.
Workflows still fire
A lead created with acampaignId fires the Lead enters campaign trigger, exactly as one added in the Cockpit does. Sequences run, so an API-created lead is never a lead that quietly sits still.
The same is true of PATCH /leads/{id}/status, which fires Status changed, and POST /campaigns/{id}/leads, which fires Lead enters campaign for each lead added.
Updating leads
PATCH /leads/{id} touches only the keys you send. Omitting a field leaves it alone; sending null clears it. This is what makes a partial sync from another system safe — you can push the two fields you own without flattening everything else.
POST /leads, so a lead can arrive with them already set rather than needing a second call.
A key that doesn’t exist yet is created automatically, which means a migration doesn’t have to pre-declare its columns in Settings first. That makes custom fields the right place to park identifiers from a system you’re moving off — the original record id, its created date — so you can find your rows again afterwards:
Legacy Record ID is stored and read back as legacy_record_id. The response echoes the canonical keys, and GET /leads and GET /leads/{id} return customFields in exactly the shape you wrote — so a value round-trips unchanged. Values are stored as text; numbers, dates and booleans are coerced rather than rejected.
Status is the one field this endpoint won’t take. Use PATCH /leads/{id}/status: it validates the key against your workspace’s list, records the transition on the timeline, and fires the Status changed trigger. Get the valid keys from GET /lead-statuses — an unknown one returns 400 rather than storing a status nothing in the product recognises.
Stopping contact
To stop contacting someone, flag them rather than deleting them:flaggedLeads tells you how many rows were affected. They stay in their campaigns, and their history stays intact.
DELETE /leads/{id} exists, but it is permanent and takes the history with it. Reach for it only when a record should never have existed.
Notes and calls
Notes are plain text and appear on the lead’s timeline immediately:POST /calls records a call that happened somewhere else — a mobile, another dialler, a conference line — so the lead’s history and your call metrics stay complete. Pass duration in seconds and it counts towards your talk time; notes becomes the call’s summary.
Outcome keys come from GET /call-outcomes.
Tasks
GET /tasks returns open tasks by default, soonest-due first. An integration polling for work to do wants the outstanding list, so completed tasks are excluded until you ask for them with done=true or done=all.
PATCH /tasks/{id} and { "done": true } rather than deleting it, so it stays on the record.
When writes are blocked
A workspace that cannot write in the Cockpit cannot write through the API either. If the trial has expired, an invoice is long overdue, or the subscription has been cancelled, writes return403 with a code you can branch on: