---
title: "Add a lead to a campaign"
method: POST
path: "/api/v1/leads"
tags: ["Leads"]
---

# Add a lead to a campaign

`POST /api/v1/leads`

## Request

- Base URL: `https://www.zen-mode.io`
- URL: `https://www.zen-mode.io/api/v1/leads`
- Auth: HTTP bearer

The URL must be a linkedin.com profile (`/in/<slug>`) or Sales Navigator lead (`/sales/lead/<id>`); one of those identities must be extractable or the request is refused. Suppressed contacts, people the campaign's own exclusion list matches, and campaigns whose messages come from CSV columns are rejected with 409 rather than silently skipped. The same rules decide every item of POST /api/v1/leads/bulk.

## Request body

- object
  - `campaign_id` integer, required
  - `linkedin_url` string, required
  - `name` string, nullable
  - `title` string, nullable
  - `company` string, nullable
  - `location` string, nullable
  - `messages` object
    - `connection_request` string
    - `follow_up_1` string
    - `follow_up_2` string
    - `follow_up_3` string

## Response `201`

Created. `delivery` says whether anything the server can see will work this lead, at the moment of the check. It is never a promise to send: the best state is `queued_running`, and even then the next pickup can be up to two hours away and `reasons` can still block it.

- object
  - `lead` Lead
    - `id` integer
    - `campaign_id` integer
    - `name` string, nullable
    - `title` string, nullable
    - `company` string, nullable
    - `location` string, nullable
    - `linkedin_url` string
    - `status` 'pending' | 'warming_up' | 'sent' | 'connected' | 'replied' | 'meeting_booked' | 'not_interested' | 'failed' | 'no_response' | 'stale_connection' | 'disconnected' | 'connection_removed' | 'messaging_closed' | 'calendar_clicked' | 'invitation_withdrawn'
    - `follow_up_number` integer
    - `calendar_clicked` boolean
    - `connection_sent_at` string, date-time, nullable
    - `connection_accepted_at` string, date-time, nullable
    - `replied_at` string, date-time, nullable
    - `created_at` string, date-time, nullable
    - `updated_at` string, date-time, nullable
  - `identity` object
    - `linkedin_slug` string, nullable
    - `sales_lead_id` string, nullable
  - `delivery` object
    - `state` 'paused_by_this_push' | 'campaign_not_active' | 'awaiting_approval' | 'unknown' | 'no_desktop_seen' | 'desktop_idle' | 'queued_running'
    - `reasons` string[]
    - `campaign` object
      - `status` string, nullable
      - `approval_state` string, nullable
      - `linkedin_account_id` integer, nullable
    - `desktop` object
      - `state` 'active' | 'went_quiet' | 'never_seen', nullable
      - `last_seen_at` string, date-time, nullable
      - `age_seconds` integer, nullable
      - `app_version` string, nullable
      - `account_running` boolean, nullable
      - `running_campaign_id` integer, nullable
      - `works_all_active_campaigns_on_account` boolean, nullable
    - `checked_at` string, date-time

## Other responses

- `400`: Malformed JSON, or `invalid_campaign_id` / `invalid_url` / `no_identity` / `inmail_requires_sn_url` (an InMail campaign accepts only Sales Navigator lead URLs, `/sales/lead/ACwAA...`).
- `401`: Missing, malformed or invalid Authorization header, or an unknown API key.
- `403`: The key is valid but the account's plan does not include API access.
- `404`: Campaign not found for this account.
- `409`: `suppressed`, `campaign_exclusion` (carries `matched_on`: `company`, `url` or `title`), `csv_message_campaign` (the campaign's messages come from CSV columns and the lead carries no `messages`), a `messages` refusal (`ai_sequence_campaign`, `unsupported_campaign_type`), or `already_exists`. A lead removed by hand from a different campaign does not block; any other existing row does. The already_exists body usually carries `lead` and `scope`, but the insert-race loser carries neither — treat both as optional. If this request's approval check paused or reset the campaign before losing the race, the 409 also carries `delivery`.
- `422`: `messages` failed a check (see PUT /api/v1/leads/{id}/messages); nothing was added.
- `429`: Rate limit exceeded (100 requests/minute). Carries `Retry-After`.
- `500`: Unhandled server error.
- `503`: `approval_check_failed`: the campaign's approval state could not be confirmed, so nothing was added. Retry with backoff.

---

[API](https://skmtc.dev/zen-mode/apis/zenmode-api.md) · [All operations](https://skmtc.dev/zen-mode/apis/zenmode-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zen-mode/apis/zenmode-api/revisions/2769372f1e3f?raw)
