# Push leads from Clay into a campaign

A Clay HTTP API column that adds each row to a campaign as it's enriched, carrying the enrichment columns as custom fields, with no code between.

Works with: Clay.

Endpoints used:

- [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) Create a lead
- [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) List leads

Clay is where many teams build and enrich a list; Victoria AI is where it gets worked. The join is one column. Clay's **HTTP API** enrichment calls [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) for each row, mapping the table's columns into the lead and the enrichment columns into `custom_fields`, and writes the lead's id back into the row. Rows flow into the campaign as they're enriched, a scoring column can gate them, and nothing runs outside Clay.

## Before you start

- An API key with `leads:write` and `leads:read`. See [Create an API key](https://docs.versionseven.ai/help/api-keys).
- The campaign's id, from its URL in the app or from `GET /v1/campaigns`.
- A Clay table with at least a first name, a last name, and a work email or a LinkedIn profile URL per row, plus whatever enrichment columns you want in the messages.

## 1. Save the key as an HTTP API account

In Clay, open **Settings → Connections** (or the account picker when you add the column) and create an **HTTP API** connection of the _Headers_ kind with one header:

| Header | Value |
| - | - |
| `Authorization` | `Bearer vk_…` |

Clay stores the header encrypted at the workspace level, so the key never sits in a column or a formula, and every HTTP API column in the workspace can reuse it.

## 2. Add a formula column for a stable request key

Add a **Formula** column named **Request key** that produces the campaign id, a colon, and the row's email in lowercase (for a LinkedIn-only row, the LinkedIn URL). Clay's formula editor writes the formula from a plain description such as _"the text 550e8400-e29b-41d4-a716-446655440000: followed by the Email column in lowercase"_.

The same row then always produces the same key, so re-running the column, which Clay does when an input changes, can't enrol the person twice: it's the request's `Idempotency-Key`, and the API accepts any unique string of up to 255 characters there. See [Idempotency](https://docs.versionseven.ai/guides/idempotency).

## 3. Add the HTTP API column

Add an enrichment, choose **HTTP API**, and switch to the **Configure** tab:

| Field | Value |
| - | - |
| Method | `POST` |
| Endpoint | `https://api.versionseven.ai/v1/leads` |
| Authentication | the HTTP API account from step 1 |
| Headers | `Content-Type` → `application/json`; `Idempotency-Key` → the **Request key** column |
| Body | the JSON below, with each `/Column` inserted from the table |

```json
{
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "lead": {
    "first_name": "/First Name",
    "last_name": "/Last Name",
    "email": "/Work Email",
    "linkedin_url": "/LinkedIn Profile",
    "company": "/Company Name",
    "title": "/Job Title",
    "company_website": "/Company Domain",
    "industry": "/Industry",
    "employees": "/Employee Count",
    "custom_fields": {
      "funding_stage": "/Funding Stage",
      "tech_stack": "/Tech Stack Summary",
      "recent_hire": "/Recent Sales Hire"
    }
  }
}
```

Each `/Column` is a reference inserted with Clay's `/` menu inside the body field; type the column's name to find it. Clay substitutes the row's value when the column runs, and an empty cell becomes an empty value, which the API ignores for optional fields. `employees` has to be a number: insert an integer column there, or leave it out.

Under **Run settings**, set the column to run **only if** the row has a work email or a LinkedIn profile, and, if you score rows, only if the score column passes your bar. That's the gate: Clay enriches everyone, Victoria AI gets the ones worth contacting.

## 4. Map the response into columns

Run the column on one row and open its output. Clay shows the JSON the API answered and lets you pick values into new columns with a JSON path:

| New column | Path | What it holds |
| - | - | - |
| Victoria lead id | `$.lead_id` | The lead's id in Victoria AI (on `201` and `200`). |
| Victoria created | `$.lead_created` | `true` for a new lead, `false` when an existing lead was enrolled. |
| Victoria error | `$.error` | The error code when the row was refused (below); empty on success. |

The first run is also the test: the lead appears in the campaign in the app, and [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) with `campaign_id` and `search` set to the row's email finds it with its `custom_fields`:

```bash
curl "https://api.versionseven.ai/v1/leads?campaign_id=550e8400-e29b-41d4-a716-446655440000&search=sarah.johnson%40acmecorp.com" \
  -H "Authorization: Bearer $VICTORIA_API_KEY"
```

Then run the column for the table.

## What each row's result means

| Status and `error` | Meaning |
| - | - |
| `201` | A new lead was created and enrolled. |
| `200` | The person was already a lead in your workspace (same email, or same LinkedIn URL) and was enrolled. Their stored name and company are kept; the custom fields travel with the enrolment. |
| `409 LEAD_ALREADY_IN_CAMPAIGN` | Already there. Nothing changed. |
| `409 LEAD_SUPPRESSED` | The person or their domain is on your [Do Not Contact](https://docs.versionseven.ai/help/do-not-contact) list. Nothing was created. |
| `400 VALIDATION_ERROR` | A required field is missing or malformed; the response's `details.errors` names each. A lead needs a first name, a last name, and an email or LinkedIn URL. |
| `403 TRIAL_LEAD_CAP_REACHED` | The workspace is on a free trial and has used its lead quota. |
| `429 RATE_LIMITED` | More than 100 requests a minute to the endpoint. Clay runs enrichment columns in parallel, so a large table can hit this; Clay retries, and the idempotency key makes a retry safe. Running the column in batches of a few hundred rows keeps it quiet. |

Clay treats a `4xx` as a failed enrichment for that row and leaves the other rows alone. The **Victoria error** column shows which rows to look at.

## Using the enrichment in messages

Every key under `custom_fields` becomes a variable in the campaign's messages: `{funding_stage}`, `{tech_stack}`, `{recent_hire}`. A step that reads `Saw you brought on {recent_hire}; congratulations` works for every row with that cell filled, and the campaign's [readiness check](https://docs.versionseven.ai/help/preflight-checks) counts the rows that lack it before activation. Clay's AI columns are good at writing one clean sentence per row for exactly this; keep each value short and free of line breaks.

Lead fields (`first_name`, `company`, `title`) are the lead's own and are reused across campaigns; `custom_fields` sent with a `campaign_id` belong to that enrolment, so the same person can carry different fields in different campaigns.

## What to expect

- **Clay reruns a column when its inputs change.** With the idempotency key derived from the campaign and the email, a rerun within 24 hours returns the first answer again; after that, the lead is already in the campaign and the row shows `LEAD_ALREADY_IN_CAMPAIGN`. Neither enrols twice.
- **Matching is by email, then LinkedIn URL.** Emails are compared without regard to case; LinkedIn URLs exactly, so send them in one form (Clay's LinkedIn columns are consistent).
- **Clay's own export action to sequencers** doesn't list Victoria AI; the HTTP API column is the general mechanism and does the same thing.
- **A campaign needs to be active to send.** Rows added to a draft wait in the backlog until it's activated.

## Next steps

- [Set up a campaign by API](https://docs.versionseven.ai/guides/set-up-a-campaign), to build the campaign the column feeds.
- [Add CRM contacts to a campaign](https://docs.versionseven.ai/cookbook/add-crm-contacts), the same `POST /leads` from a script, with every response handled.
- [Personalization fields](https://docs.versionseven.ai/help/personalization-fields) for how custom fields appear in messages and what happens when a row lacks one.
