# Clay

> Source: https://developers.anymail.watch/clay/

Clay finds and enriches the people. anymail emails them. The link between the two is Clay's **HTTP API** column: for each row it calls the [REST API](/) and the person lands in an anymail campaign. There is nothing to install and nothing to connect on the anymail side. The campaign travels in the URL.

## What you need

- An API key with the `write` scope. In anymail open **Settings**, **Integrations**, **API keys**. It starts with `sgn_`.
- A campaign in `draft` or `active` status. Its id is in the address bar of the campaign page. **Settings**, **Integrations**, **Clay** shows the ready URL for each campaign.
- A Clay table with at least an email column.

## Column 1: create the prospect and enrol it

Add a column, choose **HTTP API**, and fill it in:

| Field | Value |
|---|---|
| Method | `POST` |
| Endpoint | `https://app.anymail.watch/api/v1/prospects?campaign=<campaign_id>` |
| Headers | `Authorization`: `Bearer sgn_…`, `Content-Type`: `application/json` |
| Body | the JSON below, with Clay columns inserted where the `/` menu puts them |

```json
{
  "email": "{{Email}}",
  "first_name": "{{First Name}}",
  "last_name": "{{Last Name}}",
  "company_name": "{{Company}}",
  "custom_fields": {
    "title": "{{Job Title}}",
    "icebreaker": "{{Icebreaker}}"
  }
}
```

Only `email` is required. `custom_fields` is a flat map of strings. Every key becomes a snippet in your sequence, so `{{icebreaker}}` in an anymail step prints that row's Clay column. Numbers and booleans are turned into text. Nested objects are rejected with `400`.

anymail answers:

| Status | Meaning |
|---|---|
| `201` | The prospect was created and enrolled. The body has `id`, `email` and `company_id`. |
| `409` | The address is already in your workspace (`"code": "duplicate"`). It was **not** enrolled. Column 2 handles it. |
| `400` | The address is not valid, the body is not JSON, or the campaign id is unknown or the campaign is not `draft`/`active`. An unknown campaign rejects the row before anything is created. |
| `401` / `403` | The key is wrong, deleted or read-only. |
| `429` | Too fast. See [Limits](#limits). |

## Column 2: enrol people who already exist

A `409` row needs one more call. Add a second **HTTP API** column:

| Field | Value |
|---|---|
| Method | `POST` |
| Endpoint | `https://app.anymail.watch/api/v1/campaigns/<campaign_id>/prospects` |
| Headers | the same as column 1 |
| Body | `{"emails": ["{{Email}}"]}` |

The answer is always `200` with a per-address `result`: `enrolled`, `already-enrolled`, `suppressed`, `missing-field`, `not-found` and the other outcomes of [enrolment](/campaigns/). Running it on a row that column 1 already enrolled is harmless and returns `already-enrolled`. To save requests, set the column's run condition to rows where column 1 did not return an `id`.

## Test it

Run both columns on one row first. Then check:

- **Prospects** in anymail lists the address with source `API`.
- The campaign's prospect list shows it as enrolled. `GET /api/v1/campaigns/<campaign_id>/prospects` returns the same.
- The integration history (**Settings**, **Integrations**, **History**) has a Clay line for every call that carried `?campaign=`, accepted or rejected, with the reason.

The same request from a terminal:

```bash
curl -X POST "https://app.anymail.watch/api/v1/prospects?campaign=<campaign_id>" \
  -H "Authorization: Bearer $ANYMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","first_name":"Ada","company_name":"Example Inc"}'
```

## Receiving signals back

Clay can receive anymail events through a webhook source. In Clay, create a table with a **Webhook** source and copy its URL. Then subscribe that URL to the events you want:

```bash
curl -X POST "https://app.anymail.watch/api/v1/hooks" \
  -H "Authorization: Bearer $ANYMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"offer_read","target_url":"<your Clay webhook URL>"}'
```

Each delivery becomes a row with `prospect_email`, `campaign_name` and `metadata`. The events that fit Clay best are `new_reply`, `link_clicked`, `offer_opened`, `offer_read` and `offer_temperature_changed`. A column can then act on `metadata.to`, for example only when an offer reaches `hot`. See [Webhooks](/webhooks/#events) for every event and payload.

## Limits

One key allows 60 requests per minute ([rate limits](/rate-limits/)). Column 1 and column 2 together take up to two requests per row. Set the HTTP API column's rate limit in Clay to 60 per minute, or 30 per minute if column 2 runs on every row. A `429` row can be rerun from Clay.

Adding a prospect does not send anything. A `draft` campaign sends nothing until you start it; an `active` one emails new people on its schedule and sending windows.

## Troubleshooting

| What you see | What to do |
|---|---|
| `Unknown or unusable campaign.` | The id in the URL is wrong, belongs to another workspace, or the campaign is paused or finished. Copy the URL again from **Settings**, **Integrations**, **Clay**. |
| `… is already in your workspace.` | Expected for known people. Make sure column 2 runs. |
| `` `custom_fields` … `` error | A value is an object or a list. Map a single text column instead. |
| Rows succeed but nobody is emailed | The campaign is still `draft`, or the person is on the [suppression list](/suppressions/) (column 2 returns `suppressed`). |
