Skip to main content
One POST creates a person, their company and the tag that puts them into a campaign or workflow. Or it merges into the lead you already have, if you send the same person twice.
Where
SettingsAPI & Integrations
Your role needs
Update access to Lead intake API update-companies. Admin have it by default.
Authenticated by a workspace API key, not by a signed-in session. Creating a key needs the same permission as the rest of the API settings tab.

Status codes

A dry run that would have created a lead still returns 200, not 201. The status code alone can’t tell you what happened. Read meta.status in the body instead; it’s the one field every code path sets.
POST /leads/batch doesn’t follow this table. Once the request itself validates it always returns 200, with a per-row outcome inside the body, so one bad row can’t fail the rest. See POST /leads/batch.

Request body

name is the only always-required field. Alongside it, send at least one way to reach the person. Everything else (job_title, description, gender, language, decision_maker, links, location, tags, tag_ids, a company block, and custom_fields) is optional and, when present, lands in the same request. custom_fields lands in the same place a workflow’s collect-data block writes to, so a value you send is readable inside a workflow immediately; see Custom fields and collected data. Each key is slugified before it is stored (Plan tier becomes plan_tier), and any value that is itself an object or an array is dropped rather than flattened. The full list, grouped and described field by field, is on the lead fields reference.

Matching and duplicates

Before the lead or its company is touched, we look for an existing lead in your workspace using the strongest identifier first: A name is never part of matching. Two people who happen to share one are two people, and merging them would be unrecoverable. Matching never reaches outside the workspace the key belongs to. on_duplicate decides what happens when a match is found: On update, a supplied field wins over what’s on file. But only if you actually sent it; anything you leave out is left alone. name only overwrites when it’s genuinely different, not just re-cased. The lead’s stage is never rolled back to Untouched, even if it had already moved further along your pipeline.
A lead that already has a company keeps it. A company block in this call fills that company’s description, industry and size only where they are blank, and adds a website, a LinkedIn page, contacts or a location alongside what is already there. But it never moves the lead to a different company, even one your payload names explicitly. That’s deliberate: re-posting the same person can’t orphan their campaign history. When the company.name you send differs from the name on file, meta.warnings says the payload’s company was ignored.
A rejected duplicate (on_duplicate: "error") gets its own response shape, not the usual envelope:
409 Conflict

How the company is resolved

A lead you already have keeps the company it already belongs to. For a person we haven’t seen before, the company is resolved in this order:
  1. company.id, when you send one and it belongs to your workspace. An id we can’t find fails with 422.
  2. A domain: company.website first, and only if you didn’t send a usable one, the domain of the person’s email, and then only when it isn’t a shared free-mail domain. The domain is matched against the websites of companies already on file; if nothing matches, a company is created from it. Once a domain is in play the steps below are never reached.
  3. company.name, matched case-insensitively against companies on file, and created if nothing matches.
  4. Nothing usable: a placeholder company named after the person, so the lead always has one.
Only a company created from a domain (step 2) is analyzed: creating one queues a background scan of its website, which is what keeps the outreach from reading as generic. A company created from a bare name, or the placeholder, has nothing to scan and is used as you sent it. enrich: false skips the scan; the company is still created. meta.company.enrichment_queued reports whether an analysis was queued.

Tags

tags (names) and tag_ids both work, and can be combined. Names are matched case-insensitively, so Inbound finds a tag already called inbound rather than creating a second one. An unrecognized name is created on the fly unless you send create_missing_tags: false, in which case the call fails with 422 and lists the tag names that do exist. An unknown tag_id always fails, regardless of that switch. Every tag is applied to both the lead and its company: tagging the lead is what lets a workflow enroll it, and tagging the company is what a campaign matches on when it decides who’s next. Send a tag and both happen in the same call. Tags are added to whatever a lead already has, never replacing them. An integration that syncs one tag can’t strip tags a person or another automation put there. Sending no tag at all is valid, but the lead is stored and never contacted until one is added. meta.warnings says so.
On on_duplicate: "skip", no tag is attached and meta.tags_applied comes back empty, because the existing lead is left exactly as it was. A brand-new name in tags is still created in your workspace for next time, and still appears in meta.tags_created, even though this call never uses it.

Dry run

dry_run: true runs the real thing inside a database transaction that always rolls back, so every check (column limits, matching, company resolution) behaves exactly as it would live. Nothing it writes survives the call.
A dry run never fires a queued job, even though everything else runs for real inside the rollback. Tag application, the website analysis and the trigger a new lead normally fires are all explicitly skipped. A queued job can’t be undone by rolling back a transaction, so a preview would otherwise have real side effects.
Every id in the response is null on a dry run (data.id, data.company.id, meta.lead_id, meta.company.id, and the id on each entry in tags_applied), so a preview response can never be mistaken for a saved record. One field to read carefully: meta.company.enrichment_queued still reads true on a preview. It tells you what a real call would queue, not what this one did.

Response envelope

The body is always { data, meta }. data is the lead in full, the same shape GET /leads/{id} returns: meta is the part worth reading closely. It’s the answer to “I posted a lead and nothing seems to have happened”:
A successful create
That example is trimmed to keep it readable. A real body carries every field in the two tables above, including the ones that came back null.

Automations

Creating a lead through this endpoint fires the same New Lead Ready trigger a workflow uses to react to a brand-new lead, even though, unlike a lead found through search, it’s born already analyzed rather than working through an enrichment pipeline first. Only a genuine created outcome fires it: an update or a skip does not. Tags are the other way in, and they work on an update too. Attaching a tag the lead didn’t already carry fires the Tag Added trigger, whatever put it there. So re-posting a person with a tag they don’t have yet can start a workflow, even though meta.status reads updated. Re-sending a tag they already carry changes nothing and fires nothing. Creating a lead through this endpoint doesn’t spend credits. You’re handing over data you already have, not asking us to find it.

Retrying safely

Add an Idempotency-Key header and a retried call with the exact same body returns the original response instead of creating a second lead: the difference between a network timeout being harmless and it silently duplicating your pipeline. Reusing the same key with a different body is rejected outright. Full mechanics are on Idempotency, deduplication and rate limits.

Lead fields reference

Every field this request accepts, grouped and described one by one.

POST /leads/batch

Send many leads in one call and read the outcome of each row on its own.

Idempotency, deduplication and rate limits

Retry safely, and see what happens when you send too fast.

Errors reference

Every status the API returns and what to change before retrying.