> ## Documentation Index
> Fetch the complete documentation index at: https://help.pipelime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /leads

> Request body, duplicate handling, dry run, and every field of the response envelope.

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.

<div className="pl-availability">
  <div className="pl-availability__row">
    <div className="pl-availability__label">Where</div>
    <div className="pl-availability__value"><span className="pl-path">Settings<span className="pl-path__sep">→</span>API & Integrations</span></div>
  </div>

  <div className="pl-availability__row">
    <div className="pl-availability__label">Your role needs</div>
    <div className="pl-availability__value">Update access to Lead intake API <code>update-companies</code>. Admin have it by default.</div>
  </div>

  <div className="pl-availability__note">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.</div>
</div>

## Status codes

| Status                     | When                                                                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `201 Created`              | A brand-new lead was created, and this wasn't a dry run                                                                  |
| `200 OK`                   | Everything else that succeeds: an update, a skip, or any dry run, including one that would have created                  |
| `400 Bad Request`          | The `Idempotency-Key` header is longer than the API accepts                                                              |
| `401 Unauthorized`         | Missing, invalid, revoked or expired key: see [Authentication](/en/developers/authentication)                            |
| `403 Forbidden`            | The key is valid but wasn't granted `leads:write`                                                                        |
| `409 Conflict`             | `on_duplicate: "error"` and a matching lead already exists, or the same `Idempotency-Key` was sent with a different body |
| `422 Unprocessable Entity` | The payload fails validation: no `name`, no contact detail, an unknown tag name, a company id from another workspace     |
| `429 Too Many Requests`    | Too many calls on this key inside one minute                                                                             |

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.

<Note>
  `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](/en/developers/post-leads-batch).
</Note>

## Request body

`name` is the only always-required field. Alongside it, send at least one way to reach the person.

| Field                            | Rule                                                                                      |
| -------------------------------- | ----------------------------------------------------------------------------------------- |
| `name`                           | Always required                                                                           |
| `email`, `phone`, `linkedin_url` | Send at least one of the three, or in their place, a non-empty `emails` or `phones` array |

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](/en/leads/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](/en/developers/lead-fields-reference).

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST 'https://api.pipelime.ai/api/v1/leads' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "name": "Jane Doe",
      "email": "jane.doe@acme-demo.test",
      "tags": ["Inbound"]
    }'
  ```

  ```json Smallest valid request theme={"system"}
  {
    "name": "Jane Doe",
    "email": "jane.doe@acme-demo.test",
    "tags": ["Inbound"]
  }
  ```

  ```json With a company and a job title theme={"system"}
  {
    "name": "Jane Doe",
    "email": "jane.doe@acme-demo.test",
    "job_title": "Head of Growth",
    "company": {
      "website": "https://acme-demo.test"
    },
    "tags": ["Inbound"]
  }
  ```
</CodeGroup>

## 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:

| Order | Matched on                                                                                                       | `matched_on` reads |
| ----- | ---------------------------------------------------------------------------------------------------------------- | ------------------ |
| 1     | Any email address in the payload                                                                                 | `email address`    |
| 2     | The profile handle inside `linkedin_url`                                                                         | `LinkedIn profile` |
| 3     | A phone number, compared on its trailing digits so the same number with and without a country code still matches | `phone number`     |

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_duplicate`     | What happens                                                                         |
| ------------------ | ------------------------------------------------------------------------------------ |
| `update` (default) | Merges the payload into the existing lead. `meta.status` is `updated`.               |
| `skip`             | Leaves the existing lead untouched and returns it as-is. `meta.status` is `skipped`. |
| `error`            | Rejects the call with `409 Conflict` instead of touching anything.                   |

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.

<Warning>
  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.
</Warning>

A rejected duplicate (`on_duplicate: "error"`) gets its own response shape, not the usual envelope:

```json 409 Conflict theme={"system"}
{
  "message": "A lead with this email address already exists in this workspace.",
  "error": "duplicate_lead",
  "matched_on": "email address",
  "lead_id": 4821
}
```

## 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.

<Note>
  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.
</Note>

## 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.

<Note>
  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.
</Note>

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:

| Field                                            | Type                     | Notes                                                                                        |
| ------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------- |
| `id`                                             | integer or `null`        | `null` on every dry run                                                                      |
| `name`                                           | string                   |                                                                                              |
| `job_title`, `description`, `gender`, `language` | string or `null`         |                                                                                              |
| `decision_maker`                                 | boolean or `null`        |                                                                                              |
| `stage`                                          | string                   | `untouched` on a new lead: see [pipeline stages](/en/reference/status-vocabularies)          |
| `source`                                         | string                   | `api` for a lead created here, which is how you tell these apart from search or manual leads |
| `emails`, `phones`                               | array of strings         |                                                                                              |
| `links`                                          | array of `{ type, url }` |                                                                                              |
| `location`                                       | object or `null`         |                                                                                              |
| `tags`                                           | array                    | This lead's full tag list after the call                                                     |
| `company`                                        | object or `null`         | The company this lead belongs to                                                             |
| `created_at`, `updated_at`                       | timestamps               |                                                                                              |

`meta` is the part worth reading closely. It's the answer to "I posted a lead and nothing seems to have happened":

| Field          | Type                              | What it means                                                                                                 |
| -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `status`       | `created`, `updated` or `skipped` | What this call actually did                                                                                   |
| `created`      | boolean                           | Shorthand for `status === "created"`                                                                          |
| `duplicate`    | boolean                           | True whenever `status` isn't `created`                                                                        |
| `dry_run`      | boolean                           | Echoes whether this call was a preview                                                                        |
| `lead_id`      | integer or `null`                 | `null` on every dry run                                                                                       |
| `company`      | object or `null`                  | `id`, `name`, `created` and `enrichment_queued`                                                               |
| `tags_applied` | array of `{ id, name }`           | The tags this call resolved from `tags` and `tag_ids`, not the lead's full list, which is `data.tags`         |
| `tags_created` | array of strings                  | Names of tags that didn't exist yet and were created by this call                                             |
| `warnings`     | array of strings                  | Notices worth reading: an unset tag, a merged duplicate, a company preserved instead of overwritten, and more |

```json A successful create theme={"system"}
{
  "data": {
    "id": 4821,
    "name": "Jane Doe",
    "job_title": null,
    "stage": "untouched",
    "source": "api",
    "emails": ["jane.doe@acme-demo.test"],
    "phones": [],
    "tags": [{ "id": 12, "name": "Inbound" }],
    "company": { "id": 930, "name": "acme-demo.test" }
  },
  "meta": {
    "status": "created",
    "created": true,
    "duplicate": false,
    "dry_run": false,
    "lead_id": 4821,
    "company": { "id": 930, "name": "acme-demo.test", "created": true, "enrichment_queued": true },
    "tags_applied": [{ "id": 12, "name": "Inbound" }],
    "tags_created": [],
    "warnings": ["Created company \"acme-demo.test\" from the email domain and queued a website analysis; its profile will fill in within a few minutes."]
  }
}
```

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](/en/developers/idempotency-deduplication-and-rate-limits).

## Related

<CardGroup cols={2}>
  <Card title="Lead fields reference" icon="list" href="/en/developers/lead-fields-reference">
    Every field this request accepts, grouped and described one by one.
  </Card>

  <Card title="POST /leads/batch" icon="layers" href="/en/developers/post-leads-batch">
    Send many leads in one call and read the outcome of each row on its own.
  </Card>

  <Card title="Idempotency, deduplication and rate limits" icon="repeat" href="/en/developers/idempotency-deduplication-and-rate-limits">
    Retry safely, and see what happens when you send too fast.
  </Card>

  <Card title="Errors reference" icon="circle-alert" href="/en/developers/errors">
    Every status the API returns and what to change before retrying.
  </Card>
</CardGroup>
