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

# Zapier, Make, n8n and your own code

> Add people to B2B Signals from any tool that can send a web request, with worked examples for one person, many people, tests and campaigns.

Use this when a tool you already use finds people you want in B2B Signals: a form, a CRM, a spreadsheet, Zapier, Make, n8n or your own code. Each request adds one person, or up to 100, with the reason you picked them.

Data moves one way: from your tool to B2B Signals. To get events back out, use [Webhooks](https://docs.b2bsignals.ai/developers/webhooks.md).

## Before you start

* An API key for your workspace. "API keys come with a paid plan (Software or Custom)." Only the owner or an admin can make one. See [API keys](https://docs.b2bsignals.ai/developers/api-keys.md).
* Your address. Open **Sources**, then **Integrations**, and press **Connect** on the **API** row. Open **Developer details** and copy the **Address**.

![Send people with the API drawer showing the address, the masked key and a sample body](https://mintcdn.com/b2-b-signals-llc/u5yYe4FZtFc5vtD_/images/integrations/api-drawer.png?fit=max\&auto=format\&n=u5yYe4FZtFc5vtD_\&q=85\&s=9317dda2735d26ae844dba9d869de030)

The API drawer with Developer details open. Your own address appears where this picture shows \$B2B\_API\_BASE, and your key is masked.

In the examples, `$B2B_API_BASE` is the address without `/intake/people` at the end, and `$B2B_API_KEY` is your key. See [API keys](https://docs.b2bsignals.ai/developers/api-keys.md) for where to find the base URL.

## Set it up in Zapier, Make or n8n

**1. Add a web request step**

In your tool, add a step that sends an HTTP request. Set the method to `POST` and the URL to your address.

**2. Add two headers**

`X-API-Key` with your key, and `Content-Type` with `application/json`.

**3. Build the body**

Map your tool's fields to ours. See [Fields you can send](#fields-you-can-send).

**4. Test first**

Add `?test=true` to the URL and run the step once. The answer says what a real send would do, and nobody is added.

**5. Go live**

Remove `?test=true`. From now on every run adds or updates a person.

> [!NOTE]
>
> Clay uses the same address with `?source=clay`, which keeps its rows as tests until you turn Clay on. Other tools call the address without `source`, so their rows are saved at once. See [Clay](https://docs.b2bsignals.ai/integrations/clay.md).

## Fields you can send

Every request needs a person: a `linkedin_url`, or a `name` with a `company_name` or `company_domain`. A row with only a company is refused.

| Field | What it is | Limit |
| - | - | - |
| `linkedin_url` | Their LinkedIn profile link. It must contain `/in/` | |
| `name` | Their full name. It must read as a person, not the company | |
| `title` | Their job title | |
| `company_name` | Their company | |
| `company_domain` | Their company website, such as `example.com` | |
| `reason` | Why you picked them. Shows as their [reason](https://docs.b2bsignals.ai/help/glossary.md#reason) in People | 500 characters |
| `signal` | A short label for what happened, such as `Website visit` | 120 characters |
| `evidence_url` | A link that backs up the reason. `https://` is added if missing | 1000 characters |
| `custom` | Your own fields, as `{"key": "value"}`. Use them in messages as `{{custom.key}}` | |
| `workflow_id` | A campaign's id, to start the person in that campaign | |
| `force` | `true` starts them even if they're active in another campaign. Do not contact still applies | |
| `test` | `true` checks the row and saves nothing | |

## Add one person

```bash theme={null}
curl -X POST "$B2B_API_BASE/intake/people" \
  -H "X-API-Key: $B2B_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "linkedin_url": "https://www.linkedin.com/in/jordan-example",
    "name": "Jordan Example",
    "title": "Head of Operations",
    "company_name": "Example Logistics",
    "reason": "Asked for a demo on our website",
    "signal": "Demo request",
    "evidence_url": "https://example.com/demo-requests/123",
    "custom": { "deal_size": "50 seats" }
  }'
```

A new person answers `201`:

```json theme={null}
{
  "lead_id": "6f0c2d3e-0000-4000-8000-000000000001",
  "action": "created",
  "enrolled": false,
  "enroll_status": null,
  "test": false,
  "would": null,
  "reason_code": null,
  "do_not_contact": null,
  "message": null,
  "person": null
}
```

Send the same person again and `action` is `updated`. We match on the LinkedIn profile, so the same person is never added twice. A person on [Do not contact](https://docs.b2bsignals.ai/people/do-not-contact.md) is saved, but never contacted.

Adding a person sends nothing to anyone.

## Add a person without a LinkedIn URL

Send a full name with a company name or domain. We look up their LinkedIn profile.

```bash theme={null}
curl -X POST "$B2B_API_BASE/intake/people" \
  -H "X-API-Key: $B2B_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jordan Example",
    "company_domain": "example.com",
    "reason": "Downloaded the pricing guide"
  }'
```

These lookups have limits:

| Limit | Answer past it |
| - | - |
| 150 lookups per workspace per day, counted in UTC | `429` with code `lookup_limit_reached`. The `Retry-After` header holds the seconds until midnight UTC |
| 20 lookups in one request | `429` with code `lookup_limit_reached`. Send the rest in another request |
| One request has about 25 seconds for lookups | `429` with code `lookup_deferred` and `Retry-After: 30`. Send the row again |

A row with a LinkedIn URL never uses a lookup.

## Test a row

Add `?test=true`, or put `"test": true` in the body. Nothing is saved, and a test never runs a lookup by name.

```bash theme={null}
curl -X POST "$B2B_API_BASE/intake/people?test=true" \
  -H "X-API-Key: $B2B_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "linkedin_url": "https://www.linkedin.com/in/jordan-example", "reason": "Asked for a demo" }'
```

```json theme={null}
{
  "lead_id": null,
  "action": "test",
  "enrolled": false,
  "enroll_status": null,
  "test": true,
  "would": "create",
  "reason_code": null,
  "do_not_contact": false,
  "message": "Test received. Nobody was added.",
  "person": {
    "name": null,
    "title": null,
    "company": null,
    "linkedin_url": "https://www.linkedin.com/in/jordan-example",
    "found_by": "linkedin_url"
  }
}
```

| `would` | What a real send would do |
| - | - |
| `create` | Add a new person |
| `update` | Update a person already in People |
| `reject` | Nothing. `reason_code` and `message` say why |

A test that would be refused still answers `200`, with `would: "reject"`.

## Add people and start them in a campaign

Add `workflow_id` to start the person in a campaign. To find the id, list your campaigns:

```bash theme={null}
curl "$B2B_API_BASE/workflows/" -H "X-API-Key: $B2B_API_KEY"
```

Each campaign has an `id` and a `name`. Then:

```bash theme={null}
curl -X POST "$B2B_API_BASE/intake/people" \
  -H "X-API-Key: $B2B_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "linkedin_url": "https://www.linkedin.com/in/jordan-example",
    "reason": "Asked for a demo",
    "workflow_id": "9b2e7a10-0000-4000-8000-000000000002"
  }'
```

```json theme={null}
{ "lead_id": "6f0c2d3e-0000-4000-8000-000000000001", "action": "created", "enrolled": true, "enroll_status": "enrolled", "test": false, "would": null, "reason_code": null, "do_not_contact": null, "message": null, "person": null }
```

Starting someone in a campaign doesn't send a message right away. The campaign's schedule, [sender limits](https://docs.b2bsignals.ai/campaigns/senders-and-limits.md) and [approvals](https://docs.b2bsignals.ai/campaigns/approvals.md) still decide when anything goes.

If the person can't start, they are still saved. `enrolled` is `false` and `enroll_status` says why:

| `enroll_status` | Why they didn't start |
| - | - |
| `skipped_suppressed` | They are on Do not contact |
| `skipped_active` | They're already running in another campaign. Send `force: true` to start them anyway |
| `skipped_rejected` | They were rejected earlier. Send `force: true` to start them anyway |
| `skipped_name_unresolved` | We don't have their real name yet |
| `skipped_in_other_tool` | They were sent to another outreach tool in the last 90 days |
| `skipped_already_connected` | The campaign sends invites, and they're already a connection |
| `skipped_campaign_ended` | The campaign's end date has passed |
| `skipped_competitor_domain` | Their company is on a [Smart Agent](https://docs.b2bsignals.ai/sources/smart-agents.md)'s competitor list |
| `skipped_teammate_domain`, `skipped_own_team` | They work at your own company |
| `workflow_not_found` | No campaign with that id in this workspace |
| `workflow_archived` | The campaign is archived |
| `error`, `commit_failed` | Something went wrong. Send the row again |

`force` never overrides Do not contact, a missing name, or a recent send to another tool.

## Add up to 100 people at once

```bash theme={null}
curl -X POST "$B2B_API_BASE/intake/people/bulk" \
  -H "X-API-Key: $B2B_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_id": "9b2e7a10-0000-4000-8000-000000000002",
    "leads": [
      { "linkedin_url": "https://www.linkedin.com/in/jordan-example", "reason": "Asked for a demo" },
      { "name": "Sam Example", "company_name": "Example Labs", "reason": "Visited pricing twice" },
      { "company_name": "Example Corp" }
    ]
  }'
```

The answer is `200`, with one result per person, in the order you sent them, and a summary:

```json theme={null}
{
  "results": [
    { "index": 0, "linkedin_url": "https://www.linkedin.com/in/jordan-example", "lead_id": "6f0c2d3e-0000-4000-8000-000000000001", "action": "updated", "enrolled": false, "enroll_status": "skipped_active", "error": null, "reason_code": null, "would": null, "do_not_contact": null, "person": null },
    { "index": 1, "linkedin_url": null, "lead_id": "6f0c2d3e-0000-4000-8000-000000000003", "action": "created", "enrolled": true, "enroll_status": "enrolled", "error": null, "reason_code": null, "would": null, "do_not_contact": null, "person": null },
    { "index": 2, "linkedin_url": null, "lead_id": null, "action": "rejected", "enrolled": false, "enroll_status": null, "error": "No person could be identified. Send a LinkedIn profile URL, or a full name and a company. Rows with only a company are skipped.", "reason_code": "no_person_found", "would": null, "do_not_contact": null, "person": null }
  ],
  "summary": { "created": 1, "updated": 1, "enrolled": 1, "rejected": 1, "tested": 0, "errors": 0 }
}
```

* The top-level `workflow_id` applies to everyone who doesn't name their own. A person's own `workflow_id` wins.
* The top-level `force` applies to everyone. A person's own `force: true` also counts.
* One bad row doesn't fail the others. Check each result's `action`, `reason_code` and `error`.
* People are handled one after another, so a full request of 100 can take a while.

## Errors

| Status | When | What to do |
| - | - | - |
| `401` | "Missing authentication credentials", or "Invalid API key" | Check the `X-API-Key` header. A new key replaces the old one |
| `403` | You sent `X-Workspace-Id` for another workspace. A key works only in its own | Leave the header out |
| `422` with `detail.code: "no_person_found"` | We couldn't identify a person | Add a LinkedIn URL, or a full name with a company |
| `422` with a list in `detail` | A field is wrong, such as a LinkedIn URL without `/in/` | Fix the field named in `loc` |
| `429` with `lookup_limit_reached` or `lookup_deferred` | A lookup limit was reached | Wait for `Retry-After`, or add the LinkedIn URL |
| `503` with `lookup_unavailable` | The lookup isn't available right now | Send the row again later, or add the LinkedIn URL |

An error in one person's row is answered like this:

```json theme={null}
{
  "detail": {
    "code": "no_person_found",
    "message": "'Example Corp' is the company, not a person. No person could be identified. Send a LinkedIn profile URL, or a full name and a company. Rows with only a company are skipped."
  }
}
```

## Check that it works

`GET /intake/status` tells you when your key was last used and the last test row received:

```bash theme={null}
curl "$B2B_API_BASE/intake/status" -H "X-API-Key: $B2B_API_KEY"
```

In the app, the **API** row under **Sources** › **Integrations** shows **Connected** once people arrive, with how many came this week and when the last one did.

> [card] [API quickstart](https://docs.b2bsignals.ai/developers/quickstart.md)
>
> List people, add them to campaigns and read results.

> [card] [Clay](https://docs.b2bsignals.ai/integrations/clay.md)
>
> Send rows from a Clay table.

> [card] [API keys](https://docs.b2bsignals.ai/developers/api-keys.md)
>
> Make and replace your key.

> [card] [Webhooks](https://docs.b2bsignals.ai/developers/webhooks.md)
>
> Get events back out.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.