# Create a lead

`POST https://api.versionseven.ai/v1/leads`

Creates a lead, and enrols it in a campaign when you send `campaign_id`. If a lead with the same email or LinkedIn URL already exists in your organization, that lead is enrolled instead of a new one being created.

Send the lead's fields nested under `lead`, or spread across the top level of the body; both are accepted. `first_name` and `last_name` are required, along with at least one of `email` or `linkedin_url`. Emails are trimmed and matched without regard to case; LinkedIn URLs are trimmed and matched exactly.

| Situation | Response |
| - | - |
| No matching lead | `201`, with `lead_created: true`. The lead is created, and enrolled when you sent `campaign_id`. |
| A matching lead, not yet in `campaign_id` | `200`, with `lead_created: false`. The existing lead is enrolled, and `lead` is its stored record: the fields in your request don't update it. |
| A matching lead already in `campaign_id` | `409 LEAD_ALREADY_IN_CAMPAIGN` |
| A matching lead, and no `campaign_id` | `409 LEAD_ALREADY_EXISTS` |
| The contact is on your do-not-contact list | `409 LEAD_SUPPRESSED`. Nothing is created or enrolled; `details.suppressed_by` is `email`, `domain` or `linkedin_url`. |

Send an `Idempotency-Key` so a retry after a timeout returns the original response instead of a `409`.

> **Note:** An enrolment starts in the campaign's backlog. The lead is contacted once the campaign has sending capacity for it, not the moment this request returns.

- Required scope: `leads:write`
- Accepts an `Idempotency-Key` header, which makes retries safe. See [Idempotency](https://docs.versionseven.ai/guides/idempotency).

## Request

**cURL**

```bash
curl -X POST "https://api.versionseven.ai/v1/leads" \
  -H "Authorization: Bearer $VICTORIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "lead": {
    "first_name": "Sarah",
    "last_name": "Johnson",
    "annual_revenue": 5000000,
    "company": "Acme Corp",
    "company_website": "https://acmecorp.com",
    "custom_fields": {
      "priority": "high",
      "source": "conference"
    },
    "email": "sarah.johnson@acmecorp.com",
    "employees": 150,
    "industry": "Technology",
    "linkedin_url": "https://linkedin.com/in/sarahjohnson",
    "title": "VP of Sales"
  },
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "custom_fields": {
    "utm_source": "linkedin"
  }
}'
```

**Node.js**

```javascript
const response = await fetch("https://api.versionseven.ai/v1/leads", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    "lead": {
      "first_name": "Sarah",
      "last_name": "Johnson",
      "annual_revenue": 5000000,
      "company": "Acme Corp",
      "company_website": "https://acmecorp.com",
      "custom_fields": {
        "priority": "high",
        "source": "conference"
      },
      "email": "sarah.johnson@acmecorp.com",
      "employees": 150,
      "industry": "Technology",
      "linkedin_url": "https://linkedin.com/in/sarahjohnson",
      "title": "VP of Sales"
    },
    "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
    "custom_fields": {
      "utm_source": "linkedin"
    }
  }),
});

const data = await response.json();
```

**Python**

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.versionseven.ai/v1/leads",
    headers={
        "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "lead": {
            "first_name": "Sarah",
            "last_name": "Johnson",
            "annual_revenue": 5000000,
            "company": "Acme Corp",
            "company_website": "https://acmecorp.com",
            "custom_fields": {
                "priority": "high",
                "source": "conference",
            },
            "email": "sarah.johnson@acmecorp.com",
            "employees": 150,
            "industry": "Technology",
            "linkedin_url": "https://linkedin.com/in/sarahjohnson",
            "title": "VP of Sales",
        },
        "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
        "custom_fields": {
            "utm_source": "linkedin",
        },
    },
)
data = response.json()
```

## Headers

- `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`.
- `Idempotency-Key` (string, optional, at most 255 characters): Any unique string, such as a UUID, that makes retrying this request safe. A repeat with the same key for at least 24 hours returns the original response instead of doing the work again. Use a new key for each distinct request.

## Request body

- `lead` (object, required): Lead data. May also be supplied as top-level fields.
  - `first_name` (string, required, at most 2,000 characters, example `"Sarah"`): Lead's first name
  - `last_name` (string, required, at most 2,000 characters, example `"Johnson"`): Lead's last name
  - `annual_revenue` (integer or null, optional, ≥ 0, example `5000000`): Company annual revenue in dollars
  - `company` (string or null, optional, at most 2,000 characters, example `"Acme Corp"`): Company name
  - `company_website` (string or null, optional, at most 2,000 characters, example `"https://acmecorp.com"`): Lead's company website URL
  - `custom_fields` (object or null, optional): Custom fields as key-value pairs
  - `email` (string · email or null, optional, at most 2,000 characters, example `"sarah.johnson@acmecorp.com"`): Lead's email address (required if linkedin\_url not provided)
  - `employees` (integer or null, optional, ≥ 0, example `150`): Number of employees at the company
  - `industry` (string or null, optional, at most 2,000 characters, example `"Technology"`): Industry sector
  - `linkedin_url` (string or null, optional, at most 2,000 characters, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL (required if email not provided)
  - `title` (string or null, optional, at most 2,000 characters, example `"VP of Sales"`): Job title
- `campaign_id` (string · uuid or null, optional, at most 2,000 characters, example `"550e8400-e29b-41d4-a716-446655440000"`): UUID of the campaign to enrol the lead in. Omit to create the lead without enrolling it.
- `custom_fields` (object or null, optional): Custom fields for this campaign assignment (distinct from the lead's own custom\_fields)

## Response

### `200 / 201`

```json
{
  "message": "Lead successfully added to campaign",
  "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "enrollment": {
    "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "campaign_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "lead": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "created_at": "2024-01-15T10:30:00+00:00",
    "annual_revenue": 5000000,
    "company": "Acme Corp",
    "company_website": "https://acmecorp.com",
    "custom_fields": {},
    "delivered_at": "2026-09-01T09:00:00+00:00",
    "email": "sarah.johnson@acmecorp.com",
    "employees": 150,
    "first_name": "Sarah",
    "industry": "Technology",
    "last_name": "Johnson",
    "linkedin_url": "https://linkedin.com/in/sarahjohnson",
    "phone_number": "+1-555-123-4567",
    "qualification_score": 85.5,
    "source": "lead_database",
    "title": "VP of Sales"
  },
  "lead_created": true,
  "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "success": true
}
```

- `message` (string, required, example `"Lead successfully added to campaign"`)
- `lead_id` (string, required, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): ID of the created or existing lead
- `campaign_id` (string or null, optional): Campaign the lead was enrolled in. Also available as enrollment.campaign\_id
- `enrollment` (object or null, optional): Present when a campaign\_id was supplied; null otherwise
  - `sequence_lead_id` (string, required, example `"b2c3d4e5-f6a7-8901-bcde-f12345678901"`): ID of the campaign sequence entry
  - `campaign_id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign the lead was enrolled in
- `lead` (object or null, optional): The lead record as stored
- `lead_created` (boolean or null, optional, example `true`): true when this request created the lead; false when a lead with this email or LinkedIn URL already existed and was enrolled
- `sequence_lead_id` (string or null, optional): ID of the campaign sequence entry. Also available as enrollment.sequence\_lead\_id
- `success` (boolean, optional, default `true`)

## Errors

Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example:

```json
{
  "success": false,
  "error": "UNAUTHORIZED",
  "message": "Invalid or inactive API key",
  "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4"
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. |
| 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. |
| 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. |
| 403 | `TRIAL_LEAD_CAP_REACHED` | The organization is on a free trial and has used up its lead quota, so the lead wasn't created. `details` carries the `cap`, the number `used` and the `remaining` count. Subscribe in the Victoria AI app to add more. |
| 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. |
| 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. |
| 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. |
| 409 | `LEAD_ALREADY_IN_CAMPAIGN` | The lead already exists in your organization and is already enrolled in the campaign given by `campaign_id`, so there's nothing to do. `details` carries the lead's `existing_lead_id`, the `campaign_id` and the enrolment's `sequence_lead_id`. |
| 409 | `LEAD_ALREADY_EXISTS` | A lead with the same email or LinkedIn URL already exists in your organization. From [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead), this means the request named no `campaign_id` to enrol the existing lead in, or raced an identical request. Changing a lead's email or LinkedIn URL to another lead's answers it too. When it's known, `details.existing_lead_id` identifies the existing lead. |
| 409 | `LEAD_SUPPRESSED` | The contact is on your organization's do-not-contact list, so the lead wasn't created, enrolled or updated. `details.suppressed_by` is `email`, `domain` or `linkedin_url`, and `details.value` is the entry that matched. |
| 409 | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was already used with a different request. Use a new key for each distinct request. |
| 409 | `IDEMPOTENCY_IN_PROGRESS` | The first request with this `Idempotency-Key` is still running. Retry after the `Retry-After` interval. |
| 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. |
| 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). |
| 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. |
| 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. |
| 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. |

## Response headers

| Header | Description |
| --- | --- |
| `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. |
| `RateLimit-Limit` | Requests allowed to this endpoint per minute. |
| `RateLimit-Remaining` | Requests you can still send to this endpoint right now. |
| `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. |
| `Retry-After` | On a `429`, and on `409 IDEMPOTENCY_IN_PROGRESS`: seconds to wait before retrying. |
| `Idempotent-Replay` | `true` when the response is a stored replay of an earlier request with the same `Idempotency-Key`. |
