Developers

Push leads into CommOS from anywhere

One REST endpoint takes a lead from any platform — a form builder, an ad platform, Zapier, or your own backend — and drops it into the right pipeline, deduplicated and assigned.

OpenAPI spec
Quick start

Your first lead, in one request

Create a key under Settings → API keys in CommOS, then send this. The only thing a lead really needs is something to name it by: a name, a first and last name, or an email.

cURL
curl -X POST https://api.commos.io/api/v1/leads \
  -H "Authorization: Bearer $COMMOS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "phone": "+234 801 234 5678",
    "company": "Analytical Engines",
    "source": "website",
    "source_label": "Pricing page form",
    "utm_source": "linkedin"
  }'
201 Created
{
  "data": {
    "id": 4821,
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "phone": "+234 801 234 5678",
    "company": "Analytical Engines",
    "stage": "new",
    "pipeline_id": 3,
    "score": 0,
    "source": "website",
    "source_label": "Pricing page form",
    "owner": null,
    "custom_fields": { "utm_source": "linkedin" },
    "created_at": "2026-09-21T09:14:02+00:00"
  },
  "created": true,
  "deduplicated": false,
  "matched_on": null,
  "request_id": "req_01k5x9g8hb3q2wm7v0n4yq6d8t"
}
Safe to retrySend an Idempotency-Key and a retried request replays its original response instead of creating a second lead. Built for tools that retry on timeout.
Nothing is droppedSend fields we do not know about — UTM parameters, form answers, quiz scores — and they are kept as custom fields on the lead.
Scoped keysEach key carries only the scopes you grant, is shown once, and can be revoked the moment it leaks. Every call is logged for you to inspect.
Authentication

One key per integration

Keys are created in CommOS under Settings → API keys, shown once, and stored hashed — we cannot recover one for you. Give each integration its own key so you can revoke it without breaking the others.

Header
Authorization: Bearer cos_live_xxxxxxxxxxxxxxxxxxxx

# Platforms that cannot set Authorization:
X-API-Key: cos_live_xxxxxxxxxxxxxxxxxxxx
leads.writeCreate and update leads. Needed to push.
leads.readList and read leads, pipelines and custom fields.
contacts.writeCreate and update contacts.
contacts.readRead contacts and companies.

Keys are limited to 120 requests a minute. Every response carries an X-Request-Id, and the last 7 days of calls — with the payloads received — are visible in CommOS.

Behaviour

The same person, pushed twice

A lead that already exists is updated, not duplicated. CommOS matches on email first (case-insensitive), then on phone — comparing digits only, so the same number written four ways still finds one lead.

MatchedThe lead is updated and you get 200 with created: false and matched_on set to email or phone.
Not matchedA lead is created and you get 201 with created: true.
Never blankedAn update only touches the fields you send, and only moves the stage when you send stage explicitly.
Moved on requestSend pipeline_id on a lead that already exists and it moves to that pipeline, keeping its stage when the destination has one by that name.

Need every push to become its own record — a support ticket, say, rather than a person? Send "dedupe": "none".

Behaviour

Send what you know, not just who they are

A lead is more useful with the context behind it. Anything you send in notes becomes a note on the lead — the same notes a rep writes in the app, in the same Notes tab — so whoever picks the lead up sees why it matters.

cURL
curl -X POST https://api.commos.io/api/v1/leads \
  -H "Authorization: Bearer $COMMOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "notes": [
      "Downloaded the pricing guide twice this week",
      { "title": "Budget", "body": "Around $12k a year, signed off by the CFO" },
      { "title": "Timeline", "body": "Wants to be live before the Q4 board meeting" }
    ]
  }'
Plain or labelledA string becomes a note titled “Note”. Send {title, body} instead when the insight has a name — a form question, a call outcome, a budget.
They accumulateNotes on a later push are appended to the same lead, so a record builds up over time rather than being overwritten.
No duplicate pile-upA note identical to the one before it on that lead is skipped, so a sync that re-sends the same summary every run stays readable.
Reference

Endpoints

Base URL https://api.commos.io/api/v1

EndpointWhat it doesScope
GET/v1/ping
Confirms the key works and reports which workspace and scopes it carries. The first call in any integration.200 with the workspace and the key's scopes.
Any key
POST/v1/leads
Creates a lead, or updates the one that already matches by email or phone.201 when a lead was created, 200 when an existing one was updated.
leads.write
POST/v1/leads/bulk
Each lead reports its own outcome, so one bad row never discards the batch.207 with a per-row result and a summary.
leads.write
GET/v1/leads
Filter by email, phone, stage, pipeline, source or updated_since. Paginated, newest first.200 with a page of leads.
leads.read
GET/v1/leads/{id}
Fetch a single lead by its CommOS id.200 with the lead, or 404 if it is not in this workspace.
leads.read
PATCH/v1/leads/{id}
Partial update. Fields you leave out are untouched, and the body can never redirect the write to a different lead.200 with the updated lead.
leads.write
GET/v1/deals
Each opportunity with a lead (a person can have several). Filter by lead_id, the lead's email, pipeline, stage, outcome or updated_since. Paginated, newest first.200 with a page of deals.
leads.read
GET/v1/deals/{id}
Fetch a single deal by its CommOS id, with its lead and owner.200 with the deal, or 404 if it is not in this workspace.
leads.read
POST/v1/deals
A new opportunity with a lead already in the workspace (lead_id or email): a renewal, an upsell. Defaults to the default pipeline's first stage, titled after their company, owned by their owner.201 with the new deal.
leads.write
PATCH/v1/deals/{id}
Partial update. Sending stage (and pipeline_id or pipeline) moves this deal only; a won or lost stage closes it. custom_fields merge, and null clears one.200 with the updated deal.
leads.write
GET/v1/pipelines
The values `stage` accepts, and the pipeline ids you can target.200 with each pipeline and its stage slugs.
leads.read
GET/v1/fields
The custom lead fields declared on the workspace, with their slugs, the deal fields (`deal_fields`), and the standard fields every lead has (`standard_fields`).200 with the field definitions.
leads.read
Reference

Lead fields

Everything you can send to POST /v1/leads. All optional — plus anything else you send, which is preserved as a custom field.

FieldTypeNotes
namestringFull name. On a new lead you may omit it — CommOS derives one from first_name / last_name, or from the email. On a lead that already exists, omitting it leaves the current name untouched.
first_namestringUsed with last_name when you do not have a single name field.
last_namestringUsed with first_name.
emailstringPrimary deduplication key, matched case-insensitively.
phonestringAny format. Matching compares digits only, so +234 801 234 5678 finds 08012345678.
companystringCompany name.
websitestringThe lead's website. acme.com is stored as https://acme.com; a value that is not a web address is kept under custom_fields.website.
industrystringIndustry label.
company_sizestringHeadcount band, e.g. 51-200.
company_keywordsstring[]Up to 12 descriptors used by the ICP and signal engines.
stagestringStage slug or display name from the target pipeline. Defaults to the pipeline's first stage.
pipeline_idintegerFrom GET /v1/pipelines. Chooses the pipeline for a new lead, and moves an existing one to it. Defaults to the workspace's default pipeline.
pipelinestringPipeline name, if you would rather not hard-code an id. Moves an existing lead just as pipeline_id does.
sourcestringWhere the lead came from, e.g. website, facebook_lead_ads. Set once, at creation.
source_labelstringHuman label shown on the lead, e.g. the campaign or form name. Defaults to the API key's name.
channelstringOne of phone, email, sms, whatsapp — the channel to reach this lead on.
scoreinteger0–100 lead score.
amountnumberExpected deal value.
currencystringThree-letter code, e.g. USD or NGN.
expected_close_datedateYYYY-MM-DD.
assigned_tointegerCommOS user id of the lead owner. They get a notification.
owner_emailstringAssign by the teammate's email instead of their id.
next_actionstringThe next step, shown on the lead card.
notestringA single note. Appears in the lead's Notes tab, same as one typed in the app.
notesstring[] | object[]Up to 20 notes — form answers, call notes, an AI summary. Plain strings, or {title, body} to label them. A note identical to the one before it is skipped, so a repeating sync does not pile up duplicates.
custom_fieldsobjectDeclared custom fields keep their slug; anything else is slugified and stored.
dedupestringauto (default) upserts on email then phone. none always creates a new lead.
Reference

Errors

Every failure answers in the same shape. Branch on error.type — it is stable. The message is for humans and may change.

422 Unprocessable
{
  "error": {
    "type": "validation_failed",
    "message": "The request payload is invalid.",
    "details": {
      "email": ["The email field must be a valid email address."]
    },
    "doc_url": "https://www.commos.io/developers#errors"
  },
  "request_id": "req_01k5x9g8hb3q2wm7v0n4yq6d8t"
}
TypeStatusMeaning
unauthorized401Missing, unrecognised, revoked or expired key. The message says which.
insufficient_scope403Valid key, but it lacks the scope this endpoint needs.
plan_upgrade_required403The workspace's plan does not include API access.
workspace_read_only403The subscription has lapsed. Reads still work; writes do not.
plan_limit_reached403A plan limit was hit mid-call. The details say which metric.
not_found404No such resource in this workspace.
conflict409An Idempotency-Key was reused for a different body, or that request is still in flight.
validation_failed422The payload did not validate. `details` maps each field to its messages.
rate_limited429Over 120 requests a minute. Back off for `Retry-After` seconds.
server_error500Our fault. Retry, and quote the `request_id` if it persists.

Start pushing leads

Create a workspace, generate a key under Settings → API keys, and send your first lead in under a minute.