# Zapier

> How do I call BetterJobs and receive its webhooks in Zapier?

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

BetterJobs has no native Zapier app. You use two built-in apps:

- **Webhooks by Zapier** to call the API and to catch events.
- **Code by Zapier** to check the event signature.

> Before you start
>
> You need a live API key (`bj_live_...`). See [Authentication](https://docs.betterjobs.cc/getting-started/authentication.md). Webhook deliveries are listed on the Pro plan and above. Check yours on [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md).

## Search jobs with a Custom Request

1. Add an action step: **Webhooks by Zapier → Custom Request**.

2. Set:

   | Field   | Value                                                                                                            |
   | ------- | ---------------------------------------------------------------------------------------------------------------- |
   | Method  | `POST`                                                                                                           |
   | URL     | `https://api.betterjobs.cc/v1/jobs/search`                                                                       |
   | Headers | `Authorization` = `Bearer bj_live_...`, `BetterJobs-Version` = `2026-10-01`, `Content-Type` = `application/json` |

3. Paste the **Data** field:

   ```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": 5
     },
     "limit": 5
   }
   ```

   Replace `acme-robotics.example` (a fictional domain) with a field mapped from the trigger, for example a company domain from your CRM.

4. Test the step. Zapier parses the JSON response into fields.

Zapier flattens the `data` array: each job field becomes a list, such as all titles joined. For one job per row, keep `limit` small and use the first value. For one action per job, add **Looping by Zapier** on the job `id` list.

Map `metadata.credits_charged` into a log column so you can see what each run cost. A run with no matching jobs returns `data: []` and costs 0 credits.

> Zaps re-run
>
> Replays and Zap retries send the request again. Jobs you already paid for are free on a repeat, so search requests are safe. `GET /v1/companies/{domain}` is not: each profile request costs 1 credit, every time. See [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md).

Zapier is a poor fit for paging through hundreds of jobs. For large pulls, use an [async search](https://docs.betterjobs.cc/platform/async-searches.md) with `webhook_url` set to a Zapier catch hook, or use [n8n](https://docs.betterjobs.cc/integrations/n8n.md) or code.

## Receive webhooks

BetterJobs POSTs one event per request and signs it with the `BetterJobs-Signature` header (`t=<unix>,v1=<hex>`). `v1` is the HMAC-SHA256 of `<t>.<raw body>` with your endpoint secret. Check it before the Zap acts.

### 1. Catch the raw hook

1. Create a Zap with the trigger **Webhooks by Zapier → Catch Raw Hook**. Use *Raw*, not the plain *Catch Hook*: the signature covers the exact body bytes, and the plain trigger parses them first.
2. Copy the webhook URL. Use it as `webhook_url` in `POST /v1/watches` or `POST /v1/searches`.
3. Send a test event. `POST /v1/webhooks/replay` re-sends an existing event to its endpoint.
4. In the test data, find the raw body field and the `BetterJobs-Signature` header field.

Zapier responds `200` as soon as it catches the request, which meets BetterJobs’ 10-second deadline.

### 2. Verify in a Code step

Add **Code by Zapier → Run Python** (or **Run JavaScript**). Set the **Input Data**:

| Key         | Value                                                    |
| ----------- | -------------------------------------------------------- |
| `raw_body`  | The raw body field from the trigger                      |
| `signature` | The `BetterJobs-Signature` header field from the trigger |
| `secret`    | Your endpoint secret                                     |

The secret lives in the step’s input field, not in the code.

**Python**

```python
import hashlib
import hmac
import json
import time


TOLERANCE_SECONDS = 300


parts = dict(p.split("=", 1) for p in input_data["signature"].split(","))
signed = f'{parts["t"]}.{input_data["raw_body"]}'.encode()
expected = hmac.new(input_data["secret"].encode(), signed, hashlib.sha256).hexdigest()


fresh = abs(time.time() - int(parts["t"])) <= TOLERANCE_SECONDS
valid = fresh and hmac.compare_digest(expected, parts["v1"])
event = json.loads(input_data["raw_body"]) if valid else {}


output = {
    "valid": valid,
    "type": event.get("type"),
    "event_id": event.get("id"),
    "job_id": event.get("data", {}).get("job", {}).get("id"),
}
```

**JavaScript**

```js
const crypto = require('crypto');


const TOLERANCE_SECONDS = 300;


const parts = Object.fromEntries(inputData.signature.split(',').map((p) => p.split('=')));
const expected = crypto
  .createHmac('sha256', inputData.secret)
  .update(`${parts.t}.${inputData.raw_body}`)
  .digest('hex');


const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 ?? '', 'hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= TOLERANCE_SECONDS;
const valid = fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
const event = valid ? JSON.parse(inputData.raw_body) : {};


output = {
  valid,
  type: event.type ?? null,
  event_id: event.id ?? null,
  job_id: event.data?.job?.id ?? null,
};
```

### 3. Filter and route

1. Add **Filter by Zapier**: only continue if `valid` is true.

2. Add **Paths by Zapier** on `type`:

   - `job.opened`: start outbound. It is the only event that should.
   - `job.reposted`: update the record. Never re-trigger outbound.
   - `job.closed`: stop sequences tied to `job_id`.

Return more fields from the Code step if a later step needs them. The full payload of each event type is in the [event catalog](https://docs.betterjobs.cc/data/events.md).

### Failed deliveries

If a delivery does not get a `2xx` within 10 seconds, BetterJobs retries for 24 hours with backoff. After that, replay any event with `POST /v1/webhooks/replay`. Replays are free. See [Webhooks](https://docs.betterjobs.cc/platform/webhooks.md).

## Related

- [Detect hiring changes](https://docs.betterjobs.cc/guides/detect-hiring-changes.md) for watches end to end.
- [Clay](https://docs.betterjobs.cc/integrations/clay.md) for per-row enrichment in a table.
- [Troubleshooting](https://docs.betterjobs.cc/resources/troubleshooting.md) for signature mismatches and empty results.
