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.
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 -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"
}'{
"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"
}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.
Authorization: Bearer cos_live_xxxxxxxxxxxxxxxxxxxx
# Platforms that cannot set Authorization:
X-API-Key: cos_live_xxxxxxxxxxxxxxxxxxxxleads.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.
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.
Need every push to become its own record — a support ticket, say, rather than a person? Send "dedupe": "none".
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 -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" }
]
}'Endpoints
Base URL https://api.commos.io/api/v1
| Endpoint | What it does | Scope |
|---|---|---|
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 |
Lead fields
Everything you can send to POST /v1/leads. All optional — plus anything else you send, which is preserved as a custom field.
| Field | Type | Notes |
|---|---|---|
name | string | Full 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_name | string | Used with last_name when you do not have a single name field. |
last_name | string | Used with first_name. |
email | string | Primary deduplication key, matched case-insensitively. |
phone | string | Any format. Matching compares digits only, so +234 801 234 5678 finds 08012345678. |
company | string | Company name. |
website | string | The 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. |
industry | string | Industry label. |
company_size | string | Headcount band, e.g. 51-200. |
company_keywords | string[] | Up to 12 descriptors used by the ICP and signal engines. |
stage | string | Stage slug or display name from the target pipeline. Defaults to the pipeline's first stage. |
pipeline_id | integer | From GET /v1/pipelines. Chooses the pipeline for a new lead, and moves an existing one to it. Defaults to the workspace's default pipeline. |
pipeline | string | Pipeline name, if you would rather not hard-code an id. Moves an existing lead just as pipeline_id does. |
source | string | Where the lead came from, e.g. website, facebook_lead_ads. Set once, at creation. |
source_label | string | Human label shown on the lead, e.g. the campaign or form name. Defaults to the API key's name. |
channel | string | One of phone, email, sms, whatsapp — the channel to reach this lead on. |
score | integer | 0–100 lead score. |
amount | number | Expected deal value. |
currency | string | Three-letter code, e.g. USD or NGN. |
expected_close_date | date | YYYY-MM-DD. |
assigned_to | integer | CommOS user id of the lead owner. They get a notification. |
owner_email | string | Assign by the teammate's email instead of their id. |
next_action | string | The next step, shown on the lead card. |
note | string | A single note. Appears in the lead's Notes tab, same as one typed in the app. |
notes | string[] | 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_fields | object | Declared custom fields keep their slug; anything else is slugified and stored. |
dedupe | string | auto (default) upserts on email then phone. none always creates a new lead. |
Errors
Every failure answers in the same shape. Branch on error.type — it is stable. The message is for humans and may change.
{
"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"
}| Type | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing, unrecognised, revoked or expired key. The message says which. |
insufficient_scope | 403 | Valid key, but it lacks the scope this endpoint needs. |
plan_upgrade_required | 403 | The workspace's plan does not include API access. |
workspace_read_only | 403 | The subscription has lapsed. Reads still work; writes do not. |
plan_limit_reached | 403 | A plan limit was hit mid-call. The details say which metric. |
not_found | 404 | No such resource in this workspace. |
conflict | 409 | An Idempotency-Key was reused for a different body, or that request is still in flight. |
validation_failed | 422 | The payload did not validate. `details` maps each field to its messages. |
rate_limited | 429 | Over 120 requests a minute. Back off for `Retry-After` seconds. |
server_error | 500 | Our 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.