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

# Quickstart: send your first lead

> Check the key, post a lead as a dry run, read the result, then send it for real.

export const Screenshot = ({id, alt, caption, frame = 'browser', url, marks = [], lang = 'en', workspace = 'northwind-outbound'}) => {
  const src = `/images/screenshots/${lang}/${id}.png`;
  const chrome = url ? `app.pipelime.ai/${workspace}${url}` : 'app.pipelime.ai';
  return <figure className={`pl-shot pl-shot--${frame} not-prose`}>
      <div className="pl-shot__frame">
        {frame !== 'bare' && <div className="pl-shot__bar">
            <span className="pl-shot__dots">
              <span className="pl-shot__dot" />
              <span className="pl-shot__dot" />
              <span className="pl-shot__dot" />
            </span>
            <span className="pl-shot__url">{chrome}</span>
          </div>}
        <div className="pl-shot__media">
          <img src={src} alt={alt} loading="lazy" />
          {marks.map(mark => <span key={mark.n} className="pl-shot__mark" style={{
    left: `${mark.x}%`,
    top: `${mark.y}%`
  }} aria-hidden="true">
              {mark.n}
            </span>)}
        </div>
      </div>
      {caption && <figcaption className="pl-shot__caption">{caption}</figcaption>}
    </figure>;
};

By the end of this page your key is confirmed working, you have previewed a lead with a dry run, and you have sent a real one into this workspace: three requests in total.

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

## Before you begin

* An API key with the `leads:write` scope. It's ticked by default when you create one. See [Create, roll and revoke API keys](/en/developers/api-keys) if you don't have a key yet.
* At least one tag in this workspace. The endpoint doesn't require one, but a tag is what a campaign or a tag-triggered workflow picks a lead up by, so a lead sent without one is stored and then sits idle.

## Steps

<Steps>
  <Step title="Check your key">
    Call `GET /me` with your key in an `Authorization: Bearer` header. No scope is required for this one endpoint. It exists purely to answer "is my key working, and which workspace am I about to write to?" before you build anything else on it. Every request in this guide is relative to your base URL: `https://api.pipelime.ai/api/v1` in production.

    ```bash theme={"system"}
    curl https://api.pipelime.ai/api/v1/me \
      -H 'Authorization: Bearer YOUR_API_KEY'
    ```

    ```json theme={"system"}
    {
      "data": {
        "workspace": { "id": 42, "name": "Acme Freight" },
        "api_key": {
          "name": "Production integration",
          "prefix": "plk_live_a1B2c3D4",
          "scopes": ["leads:write", "tags:read"],
          "expires_at": null,
          "last_used_at": "2026-08-30T12:04:00Z"
        }
      }
    }
    ```

    `prefix` is the public half of the key: everything before the last underscore, the part the keys list keeps showing you after creation. The secret half is stored only as a one-way hash, so no endpoint can hand it back. `data` also carries a `rate_limit` block naming the requests-per-minute ceiling for this key and the most leads one batch call takes; see [Idempotency, deduplication and rate limits](/en/developers/idempotency-deduplication-and-rate-limits) for both figures and how to stay under them.
  </Step>

  <Step title="Build the request">
    A lead needs a `name`, plus at least one of `email`, `phone` or `linkedin_url`. That's the whole requirement. Everything else, including the company they work for and which tags to apply, is optional and rides along in the same call.

    ```json theme={"system"}
    {
      "name": "Jane Doe",
      "email": "jane.doe@acme-demo.test",
      "job_title": "Head of Growth",
      "company": { "website": "https://acme-demo.test" },
      "tags": ["Conference leads"]
    }
    ```

    Send a company website, or just a work email on a company domain, and we analyze the site in the background so the outreach is not generic. You don't have to fill in the industry and the description yourself. Until that analysis lands, the new company is named after its domain rather than the brand. Send a tag and it's applied to both the person and their company: one side triggers workflows, the other is what campaigns match on.
  </Step>

  <Step title="Send it as a dry run, and read what comes back">
    Add `"dry_run": true` and `POST` to `/leads`. The call checks the request and runs the same duplicate check as a real one, then rolls the transaction back. Nothing is saved, and no campaign or workflow fires.

    The app runs a request of the same shape for you, from the **Try it** panel: it posts the example built in the card above it, with your workspace's real tags to choose from and whichever key you paste into the field. **Dry run** is on by default (1): leave it there and select **Send test request**.

    <Screenshot id="api/quickstart-send-a-lead--try-it" url="/settings/api" alt="The Try it panel after a dry run: a 200 status chip, a Lead created outcome chip, a note that nothing was saved, and the response body in a dark code block below" caption="The lead id, the company id and the ids in tags_applied all come back null on a dry run, so a preview can never be mistaken for a saved record." marks={[{ n: 1, x: 20, y: 33 }]} />

    ```json theme={"system"}
    {
      "data": {
        "id": null,
        "name": "Jane Doe"
        // the rest of the lead's fields: see POST /leads
      },
      "meta": {
        "status": "created",
        "created": true,
        "duplicate": false,
        "dry_run": true,
        "lead_id": null,
        "company": { "id": null, "name": "acme-demo.test", "created": true, "enrichment_queued": true },
        "tags_applied": [{ "id": null, "name": "Conference leads" }],
        "tags_created": [],
        "warnings": []
      }
    }
    ```

    `meta` is the part worth reading closely. It exists to answer "I posted a lead and nothing happened" before you have to ask. `status` is `created`, `updated` or `skipped`; `duplicate` is true for the last two. `tags_applied` and `tags_created` show which tags actually landed versus which ones this call had to create on the fly. `warnings` carries anything worth knowing that isn't fatal on its own: a tag that didn't exist and was created, a lead that was merged into one you already had, a lead that arrived with no tag at all.
  </Step>

  <Step title="Send it for real">
    Drop `dry_run` from the body, or set it to `false`, and send the same request again. On the in-app panel, move the **Dry run** switch off instead: the button turns amber, reads **Send for real**, and a warning appears under it.

    <Warning>
      Turning dry run off creates a real lead in this workspace and can start a real campaign or workflow, exactly like any other lead that gets a matching tag.
    </Warning>

    A genuine create now returns `201`, with real ids in place of every `null`:

    ```json theme={"system"}
    {
      "meta": {
        "status": "created",
        "created": true,
        "dry_run": false,
        "lead_id": 4821,
        "company": { "id": 1189, "name": "acme-demo.test", "created": true, "enrichment_queued": true },
        "tags_applied": [{ "id": 6, "name": "Conference leads" }],
        "warnings": ["Created company \"acme-demo.test\" from company.website and queued a website analysis; its profile will fill in within a few minutes."]
      }
    }
    ```

    A dry run and a merge into an existing lead both still return `200`. Only a genuine create returns `201`. Read `meta.status` rather than the HTTP status if your integration needs to tell the three outcomes apart.
  </Step>
</Steps>

## What happens next

<Check>
  `meta.lead_id` is a real id, the lead has a row in this workspace, and both the person and their company carry the tag you sent: the tag is applied to each of them, because the two feed different automations. From there a campaign or workflow watching that tag can pick the lead up without you touching the product again.
</Check>

Send the same person a second time and, by default, the call merges into the first lead instead of creating a duplicate: we match on email, then LinkedIn, then phone, inside this workspace only. Add an `Idempotency-Key` header and a retried request replays the first response instead of risking a second lead.

## Troubleshooting

<AccordionGroup>
  <Accordion title="422: At least one contact detail is required: email, phone or linkedin_url">
    `name` alone isn't enough. A lead with no way to reach them would only ever produce a dead row, so the call is rejected before anything is stored. [Lead fields reference](/en/developers/lead-fields-reference) lists every field this endpoint accepts.
  </Accordion>

  <Accordion title="The call succeeds, but nothing seems to happen">
    Read `meta.warnings` and `meta.tags_applied` in the response you got back. A lead sent without a tag comes back with a warning saying exactly that: campaigns and tag-triggered workflows have nothing to match it on, so it sits idle until a tag is added. Send at least one tag name and try again. The one automation that runs regardless is a workflow starting from **New Lead Ready** with no tag filter set. Every lead this endpoint creates fires that trigger.
  </Accordion>

  <Accordion title="409: duplicate_lead">
    Only returned when the request sets `"on_duplicate": "error"` and the person already exists. The body names `matched_on` and the `lead_id` it collided with. Leave `on_duplicate` unset to merge into the existing lead instead, or set it to `"skip"` to leave that lead untouched. [POST /leads](/en/developers/post-leads) covers every option.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="POST /leads" icon="send" href="/en/developers/post-leads">
    Request body, duplicate handling, dry run, and every field of the response envelope.
  </Card>

  <Card title="Lead fields reference" icon="list" href="/en/developers/lead-fields-reference">
    Every accepted field by group, what it does, and the limits that apply to the repeating ones.
  </Card>

  <Card title="Errors reference" icon="octagon-alert" href="/en/developers/errors">
    Every status the API returns, the error code that comes with it, and what to change before retrying.
  </Card>
</CardGroup>
