# Clay

> How do I call BetterJobs from a Clay HTTP API column?

Source: https://docs.betterjobs.cc/integrations/clay/

BetterJobs has no native Clay app. You call it from Clay’s generic **HTTP API** enrichment column. Each row sends one request. The response fields you pick become new columns.

This page has two recipes:

- **Is this company hiring?** One company profile per row. Fixed cost: 1 credit per row.
- **Which matching jobs does this company have open?** A job search per row, capped at a few jobs. Up to `limit` credits per row, and 0 when nothing matches.

> Before you start
>
> You need a live API key (`bj_live_...`). See [Authentication](https://docs.betterjobs.cc/getting-started/authentication.md). Check what your plan includes on [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md): partner providers start on Growth.

## Recipe 1: is this company hiring?

Use this when your table has a company domain column and you want a yes / no / unknown answer per row.

1. In your Clay table, add a column and choose **HTTP API** as the enrichment.

2. Set the request:

   | Setting                     | Value                                               |
   | --------------------------- | --------------------------------------------------- |
   | Method                      | `GET`                                               |
   | Endpoint                    | `https://api.betterjobs.cc/v1/companies/{{Domain}}` |
   | Header `Authorization`      | `Bearer bj_live_...`                                |
   | Header `BetterJobs-Version` | `2026-10-01`                                        |

   Replace `{{Domain}}` with your domain column. In Clay you insert a column reference by typing `/` in the field. Send the bare domain (`acme-robotics.example`), with no `https://` and no path.

3. Run the column on two or three rows first. Open a cell to see the full JSON response.

4. Pick the response paths to add as columns:

   | Response path                    | Column suggestion                            |
   | -------------------------------- | -------------------------------------------- |
   | `is_hiring.value`                | Hiring? (`true`, `false` or empty = unknown) |
   | `is_hiring.confidence`           | Hiring confidence (0-1)                      |
   | `is_hiring.basis`                | Why                                          |
   | `open_jobs_count`                | Open jobs                                    |
   | `hiring_pulse.direction`         | Trend (`up`, `flat`, `down`)                 |
   | `top_job_families[0].job_family` | Top job family                               |

5. Run the rest of the table.

> Empty does not mean 'not hiring'
>
> `is_hiring.value` is `null` when BetterJobs has no signal for the domain, for example no careers page or ATS found. Clay shows `null` as an empty cell. Do not filter empty cells into a “not hiring” segment. Only `false` means not hiring. See [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md).

> Re-running this column charges again
>
> A company profile costs 1 credit per request. Unlike jobs, profiles have no “already paid” rule. Re-running the column on the same rows charges again. Turn off auto-run on this column if your table refreshes often.

## Recipe 2: open jobs at this company

Use this when you want the actual job (title, apply link, date) to personalize outreach.

1. Add an **HTTP API** column.

2. Set the request:

   | Setting                     | Value                                      |
   | --------------------------- | ------------------------------------------ |
   | Method                      | `POST`                                     |
   | Endpoint                    | `https://api.betterjobs.cc/v1/jobs/search` |
   | Header `Authorization`      | `Bearer bj_live_...`                       |
   | Header `BetterJobs-Version` | `2026-10-01`                               |
   | Header `Content-Type`       | `application/json`                         |

3. Paste this body and replace `{{Domain}}` with your domain column:

   ```json
   {
     "filters": {
       "company_domain_or": ["{{Domain}}"],
       "title_or": ["Head of RevOps", "Revenue Operations"],
       "posted_within_days": 30
     },
     "waterfall": {
       "strategy": "cheapest_first",
       "max_credits": 3
     },
     "limit": 3
   }
   ```

   `limit: 3` returns at most three jobs. `max_credits: 3` is a hard cap on what one row can cost. Keep them equal.

4. Pick the response paths to add as columns:

   | Response path              | Column suggestion                             |
   | -------------------------- | --------------------------------------------- |
   | `data[0].title`            | Job title                                     |
   | `data[0].apply_url`        | Apply link                                    |
   | `data[0].posted_at`        | Posted (empty = unknown, use `first_seen_at`) |
   | `data[0].first_seen_at`    | First seen                                    |
   | `data[0].p_real`           | Real-job probability (0-1)                    |
   | `data[0].id`               | BetterJobs job id                             |
   | `metadata.credits_charged` | Credits this row cost                         |
   | `metadata.status`          | `complete` or `partial`                       |

   Store `data[0].id`. It is stable, and fetching that job again later is free.

5. Run a few rows, check `metadata.credits_charged`, then run the table.

A row with no matching jobs returns `data: []` and costs 0 credits. Re-running the column returns jobs you already paid for at no charge (`metadata.jobs_already_paid` counts them). See [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md).

> Only want confident jobs before outbound?
>
> Set `"strategy": "consensus"`. BetterJobs then returns only jobs seen by at least two sources. Fewer rows match, and the ones that do are corroborated. See [Choose a strategy](https://docs.betterjobs.cc/guides/choose-a-strategy.md).

## Build the body without typing JSON

Set the filters below and open the **Clay HTTP column** tab. It shows the method, endpoint, headers and body. Copy the body into Clay, then swap fixed values for `{{Column}}` references.

Every filter name must match the [filter list](https://docs.betterjobs.cc/platform/filters.md). A misspelled name returns `400 unknown_filter`. It is never ignored. Clay shows the error message in the cell, and `error.param` names the bad field.

## Test the request outside Clay

If a cell shows an error, run the same request from a terminal. It removes Clay from the picture.

**curl**

```bash
curl https://api.betterjobs.cc/v1/jobs/search \
  -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
  -H "BetterJobs-Version: 2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "company_domain_or": ["acme-robotics.example"],
      "title_or": ["Head of RevOps", "Revenue Operations"],
      "posted_within_days": 30
    },
    "waterfall": { "strategy": "cheapest_first", "max_credits": 3 },
    "limit": 3
  }'
```

**Python**

```python
import os
import requests


resp = requests.post(
    "https://api.betterjobs.cc/v1/jobs/search",
    headers={
        "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}",
        "BetterJobs-Version": "2026-10-01",
    },
    json={
        "filters": {
            "company_domain_or": ["acme-robotics.example"],
            "title_or": ["Head of RevOps", "Revenue Operations"],
            "posted_within_days": 30,
        },
        "waterfall": {"strategy": "cheapest_first", "max_credits": 3},
        "limit": 3,
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["metadata"]["credits_charged"], "credits charged")
```

**TypeScript**

```ts
const res = await fetch("https://api.betterjobs.cc/v1/jobs/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`,
    "BetterJobs-Version": "2026-10-01",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filters: {
      company_domain_or: ["acme-robotics.example"],
      title_or: ["Head of RevOps", "Revenue Operations"],
      posted_within_days: 30,
    },
    waterfall: { strategy: "cheapest_first", max_credits: 3 },
    limit: 3,
  }),
});
if (!res.ok) throw new Error(`BetterJobs ${res.status}: ${await res.text()}`);
const { metadata } = await res.json();
console.log(metadata.credits_charged, "credits charged");
```

`acme-robotics.example` is a fictional domain. Use one from your table.

## Cost per 1,000 rows

| Recipe                 | Credits per row                 | Worst case per 1,000 rows |
| ---------------------- | ------------------------------- | ------------------------- |
| Company profile        | 1, always                       | 1,000 credits             |
| Job search, `limit: 3` | 0 to 3 (0 when nothing matches) | 3,000 credits             |

The worst case assumes every row returns `limit` new jobs. Rows with no match, duplicates merged across providers and jobs you already paid for are free, so real spend is usually lower. Enter your own numbers below. “Unique jobs” is rows × jobs returned per row.

## Rate limits in Clay

Clay can send many rows at once. If cells fail with `429 rate_limited`, lower the column’s request rate in Clay and re-run the failed rows. Your limit per window is in `GET /v1/account` under `rate_limit`. See [Rate limits](https://docs.betterjobs.cc/platform/rate-limits.md).

## Related

- [Find companies hiring](https://docs.betterjobs.cc/guides/find-companies-hiring.md) for the same searches in code.
- [Field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md) for every response path you can map.
- [Troubleshooting](https://docs.betterjobs.cc/resources/troubleshooting.md) for empty cells, `402` and `partial` results.
