# Gift people from a Clay table

GTM Swag helps go-to-market teams get meetings by sending personalized, thoughtful gifts. Each row that reaches a Clay HTTP API column adds one person to a GTM Swag playbook, for example when Clay finds a new role, a funding round or an event attendee. An owner or admin on your team approves each gift in GTM Swag, or turns on auto-approve so gifts within limits they set are approved right away. Clay can then fetch each gift link and record when the recipient opens it.

## Before you start

1. **A GTM Swag account.** Create one at https://gtmswag.com/login if you don't have one.
2. **A playbook that is on.** In GTM Swag, open **Integrations** and choose Clay. Pick a playbook (an always-on campaign) or create one there, then a teammate selects **Turn on playbook**. The page fills in the endpoint and body below with its campaign ID, which is also in the playbook URL: `https://gtmswag.com/campaigns/CAMPAIGN_ID`. If the note uses `{{company}}`, every row needs a company.
3. **Funds and an approver.** Playbook gifts are paid from the workspace balance. An owner or admin adds funds in **Settings → Billing & invoices**, and either approves each gift or turns on auto-approve on the playbook's page.
4. **A workspace key.** Create one in **Integrations → For developers → Create key** in GTM Swag. In Clay, add an **HTTP API (Headers)** account with header name `Authorization` and value `Bearer YOUR_GTM_SWAG_KEY`. Keep the key only in that account.
5. **Delivery status.** Until GTM Swag enables delivery for your workspace, everything works end to end, but no gift ships and no real payment is taken: try it with your own rows, not real prospects. To enable delivery, email eric@betterswag.com. Once delivery is enabled, each gift needs a US shipping address from your table (`addressMode: "known"`); rows without one wait in the queue with "Needs a shipping address". The balance is kept separately once delivery is enabled, so add funds again then.

## Add the HTTP API column

In your Clay table, choose **Add enrichment → HTTP API → Configure**:

- Method: `POST`
- Endpoint: `https://gtmswag.com/api/v1/playbooks/CAMPAIGN_ID/people`
- Account: the HTTP API (Headers) account from above
- Headers: `Content-Type: application/json`
- Body (insert Clay columns where shown, using `/` to pick a column):

```json
{
  "recipient": {
    "name": "{{Full Name}}",
    "company": "{{Company Name}}",
    "title": "{{Job Title}}",
    "sourceSystem": "clay",
    "externalId": "{{Row ID}}",
    "addressMode": "confirm"
  }
}
```

`sourceSystem: "clay"` with the Clay **Row ID** as `externalId` makes the call safe to rerun: the same row returns `alreadyQueued: true` instead of a second gift. Leave out fields you don't have; `email` and `company` are optional. When the row has a US shipping address, send `"addressMode": "known"` with `address`, `city`, `state` (a two-letter code like `TX` or the full name like `Texas`) and `zip`; `country` defaults to `US`. A `profileUrl` must be a personal `https://www.linkedin.com/in/…` profile, not a Sales Navigator or company link. Don't add other fields: unknown fields are refused with `400 VALIDATION_ERROR`. Keep the playbook's settings in GTM Swag: an edit made with a key that changes the playbook (its gift, note or limits) returns it to draft, and every row then fails with `409 PLAYBOOK_OFF` until a teammate turns it on again. Resending the same values changes nothing.

Run the column on one row first. A `200` response contains:

```json
{
  "data": {
    "queued": true,
    "recipientId": "recipient_0123456789abcdef0123456789abcdef",
    "alreadyQueued": false,
    "approvalUrl": "https://gtmswag.com/?queue=CAMPAIGN_ID",
    "approval": { "state": "waiting", "reason": "auto_approve_off" }
  }
}
```

Map `data.recipientId`, `data.approval.state` and `data.approvalUrl` to columns. `approval.state` is `waiting` (with a `reason`), `auto_approved` or `approved` (the gift exists; `sendId` names it), `scheduled` (auto-approve finishes it within minutes) or `skipped`. Without auto-approve, adding a person never charges or ships anything; the gift waits for approval. Rerunning a row never approves or charges twice.

**Large tables.** The workspace allows 240 requests per minute across all tools. A row that fails with `409 PLAYBOOK_BUSY` (other rows were being added to the same playbook at that moment) or `429 RATE_LIMIT` saved nothing: rerun those rows. Reruns are always safe.

## Approve

An owner or admin opens `approvalUrl` (Home in GTM Swag, filtered to the playbook) and approves or skips each person. Approval charges the workspace balance.

### Auto-approve (optional)

A workspace owner or admin can turn on **auto-approve** on the playbook's page in GTM Swag, with a per-gift limit and a monthly budget. Then each row whose gift fits both limits and the balance is approved right away and paid from the balance, and the response says `"approval": {"state": "auto_approved", "sendId": "send_..."}`. Anything else waits for a person with a reason, for example `over_gift_limit`, `monthly_cap`, `low_balance` (add funds in **Settings → Billing & invoices**) or `shipping_quote` (the gift's delivery isn't priced yet, so it always waits). Only a signed-in owner or admin can turn it on or raise its limits; a workspace key can't.

Adding funds, raising a limit or turning auto-approve on doesn't re-check rows already waiting. Rerun those rows after the change: with auto-approve on, the rerun evaluates the rule again and can approve and charge the gift, within its limits, and it never creates a second gift. Without auto-approve, an owner or admin approves them at `approvalUrl`.

## Get the gift link for your own outreach

With `addressMode: "confirm"`, the recipient confirms their own US shipping address through a private gift link. GTM Swag creates that link once the gift is approved and never emails it: you send it through your own outreach. People added with `addressMode: "known"` ship straight to that address and have no link.

1. Wait for the approved gift. Subscribe your webhook (below) to `send.created`, which fires once the gift exists and carries `attribution.externalId` (the Clay Row ID), or poll `GET https://gtmswag.com/api/v1/events?type=send.created`.
2. Fetch the link with a second HTTP API column (method `GET`, the same account):

   ```sh
   curl "https://gtmswag.com/api/v1/campaigns/CAMPAIGN_ID/distribution?recipientIds=RECIPIENT_ID" \
     -H "Authorization: Bearer YOUR_GTM_SWAG_KEY"
   ```

   Use the `data.recipientId` column as `RECIPIENT_ID`, or send `search=` with the Clay Row ID instead of `recipientIds`.

3. Read `data.records[0]`. When `availability` is `ready`, `gift_link` is the private link and `gift_expires_at` its expiry; map them to columns. `awaiting_approval` or `preparing_links` means there is no link yet: with an empty `gift_id`, the person still waits for approval; with a `gift_id`, the link is being created. Run the column again later. `unavailable` comes with an `availability_reason`, such as `known_address`, `claimed`, `revoked` or `expired`.

A gift link belongs to one recipient, expires 14 days after the gift is created (opened or not, so time approvals close to your outreach), and works for whoever holds it. An unused gift whose link expired can be regifted in GTM Swag for a fresh 14-day link. Keep it out of shared views, logs and webhooks. Reading it never buys, sends or renews a gift. For many rows at once, `POST https://gtmswag.com/api/v1/campaigns/CAMPAIGN_ID/distribution/exports` with `{"scope": "all"}` creates a CSV of ready links; download it from the returned `downloadUrl` with the same key. AI agents connected over MCP use `get_gift_link_records` and `export_gift_links`.

## Write link opens back to Clay

1. In Clay, create a table with a **Webhook** source and copy its URL.
2. In GTM Swag, a workspace owner or admin opens **Integrations → For developers → Signed webhooks**, adds that URL and selects `send.link_opened` (add `send.created` for new gifts and `send.status_changed` for delivery updates). Save the signing secret.
3. Each event body includes `type`, `sendId`, `campaignId` and `attribution.externalId`, which is the Clay Row ID you sent. Use a Clay lookup on `attribution.externalId` to update the original row.
4. Optionally select `playbook.person_waiting` too. It fires when a row's person waits for approval and carries `externalId` (the Clay Row ID), `reason` and `approvalUrl`, with no names or emails, so you can show who still needs a person.

`send.link_opened` fires once per gift link, the first time the recipient opens it. Opens by signed-in members of your workspace aren't counted. An open is not a reply or a meeting, and GTM Swag doesn't ask you to record either.

If you'd rather poll, call `GET https://gtmswag.com/api/v1/events?type=send.link_opened` with the same key and keep the returned `nextCursor` for the next call. `type` takes one event type; leave it out to get every type.

## Errors

- `409 PLAYBOOK_OFF`: the playbook is paused or a draft. A teammate turns it on at the returned `url`.
- `409 PLAYBOOK_BUSY`: other rows were being added at the same moment; rerun the row.
- `409 NOT_ALWAYS_ON`: the campaign ID is a batch campaign; use a playbook.
- `400 RECIPIENT_MESSAGE_REQUIRED`: the playbook note uses a placeholder, such as `{{company}}`, that the row lacks.
- `401 AUTH_REQUIRED`: check the key in the Clay account; the value must start with `Bearer `.
- `429 RATE_LIMIT`: wait a minute, or run fewer rows at a time, then rerun.

More: https://gtmswag.com/guides/connect.md and https://gtmswag.com/api-docs.md
