# Make

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

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

BetterJobs has no native Make app. You use three built-in modules:

- **HTTP → Make a request** to call the API.
- **Webhooks → Custom webhook** to receive events.
- **Tools** and a **filter** 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 the HTTP module

1. Add **HTTP → Make a request**.

2. Set:

   | Setting                     | Value                                      |
   | --------------------------- | ------------------------------------------ |
   | URL                         | `https://api.betterjobs.cc/v1/jobs/search` |
   | Method                      | `POST`                                     |
   | Header `Authorization`      | `Bearer bj_live_...`                       |
   | Header `BetterJobs-Version` | `2026-10-01`                               |
   | Body type                   | Raw                                        |
   | Content type                | JSON (`application/json`)                  |
   | Parse response              | Yes                                        |

3. Paste the request content:

   ```json
   {
     "filters": {
       "title_or": ["Head of RevOps", "Head of Revenue Operations"],
       "country_code_or": ["DE", "AT", "CH"],
       "posted_within_days": 7
     },
     "waterfall": {
       "strategy": "cheapest_first",
       "max_credits": 100
     },
     "limit": 25
   }
   ```

   To use a value from an earlier module, place the cursor inside the quotes and pick the item from the mapping panel, for example a company domain inside `company_domain_or`.

4. Add an **Iterator** after it and map `data`. Each bundle is now one canonical job.

Map fields such as `title`, `company.domain`, `apply_url` and `id` into the next module. Store `id`. Fetching the same job later is free. All fields are in the [field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md).

> Keep the key out of shared blueprints
>
> A header typed into the module is saved in the scenario blueprint. If you export or share blueprints, store the key in a Make connection or custom variable instead, and map it into the header.

### Errors and rate limits

- Add a **Break** error handler to the HTTP module for `429` and `500`. It retries the bundle later. A `500` is safe to retry with the same `Idempotency-Key` header: you are never charged twice. See [Idempotency](https://docs.betterjobs.cc/platform/idempotency.md).
- `200` with `metadata.status` = `partial` is a success. One provider failed or timed out, and you pay only for the jobs returned. See [Provider status](https://docs.betterjobs.cc/resources/provider-status.md).
- A misspelled filter returns `400 unknown_filter`. Read `error.param` in the module output. See [Errors](https://docs.betterjobs.cc/platform/errors.md).

## Large searches: async and polling

One request returns at most 100 jobs. For backfills up to 10,000 jobs, use an async search. Make scenarios should not wait in a loop, so split the work in two scenarios.

**Scenario A: start the search.**

1. **HTTP → Make a request**: `POST https://api.betterjobs.cc/v1/searches` with the same headers. The body takes `filters`, `waterfall` and `limit` (up to 10,000). Add an `Idempotency-Key` header so a retried run does not start a second search.
2. Save the returned `id` (`srch_...`) in a **Data store** record with a `done` flag set to false.

**Scenario B: poll and collect.** Schedule it every few minutes.

1. **Data store → Search records** where `done` is false.

2. **HTTP → Make a request**: `GET https://api.betterjobs.cc/v1/searches/{id}` with the stored id.

3. Add a **Router** on `status`:

   - `queued` or `running`: do nothing. The next run checks again.
   - `on_hold`: you ran out of credits. The search resumes after a top-up.
   - `completed` or `partial`: read results, then set `done` to true.
   - `failed`: set `done` to true and alert someone.

4. To read results, page with `GET /v1/searches/{id}?limit=100&cursor=...`. Use a **Repeater** with *Repeats* set to `ceil(jobs_found / 100)`. Keep `next_cursor` in a **Set variable** with lifetime *One execution*, and pass it as `cursor` on the next repeat.

Branch on `status`, never on the HTTP code. `GET /v1/searches/{id}` returns `200` for every state. Reading results is free: jobs are charged when the search collects them. See [Async searches](https://docs.betterjobs.cc/platform/async-searches.md).

> On Pro or above?
>
> Skip the polling scenario. Set `webhook_url` on `POST /v1/searches` to a Make custom webhook. You receive `search.completed` when the search ends.

## Receive webhooks

BetterJobs POSTs one event per request to your endpoint and signs it with the `BetterJobs-Signature` header:

```text
BetterJobs-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
```

You compute the same HMAC with your endpoint secret and compare it to `v1`.

### 1. Create the custom webhook

1. Add **Webhooks → Custom webhook** as the first module and create a new hook.
2. Open **Advanced settings**. Turn on **Get request headers** and **JSON pass-through**. Pass-through keeps the raw body as one text value. The signature covers those exact bytes.
3. Copy the webhook URL. Use it as `webhook_url` in `POST /v1/watches` or `POST /v1/searches`.
4. Click **Redetermine data structure**, then send a test event, for example with `POST /v1/webhooks/replay` on an existing event id.

Make responds `200` as soon as the webhook accepts the request, which meets the 10-second deadline.

### 2. Check the signature

Add **Tools → Set multiple variables** after the webhook:

| Variable   | Value                                                                                                                                                                                        |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sig`      | The `BetterJobs-Signature` value from the headers array. Use `map()` on the headers with key `value`, filtered by `name`, then `first()`. Check the exact header name in the webhook output. |
| `t`        | `sig` split on `,`, first part, with `t=` removed                                                                                                                                            |
| `v1`       | `sig` split on `,`, last part, with `v1=` removed                                                                                                                                            |
| `expected` | `sha256()` of the text `t` + `.` + the raw body value, encoding `hex`, key = your endpoint secret                                                                                            |

Then set a **filter** on the link to the next module: `expected` *Equal to* `v1`. Bundles that fail the filter stop there.

> Limits of a filter
>
> A filter is not a constant-time comparison, and it does not check that `t` is recent. For high-value flows, also reject events whose `t` is more than 5 minutes old, and store the secret in a custom variable rather than typing it into the module.

### 3. Parse and route

Add **JSON → Parse JSON** on the raw body, then a **Router** on `type`:

- `job.opened`: start outbound. It is the only event that should.
- `job.reposted`: update your copy. Never re-trigger outbound.
- `job.closed`: stop sequences for that job.
- `search.completed`: fetch results with `GET /v1/searches/{id}`.

What each event means and costs 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, or after you fix a broken scenario, 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.
- [Pagination](https://docs.betterjobs.cc/platform/pagination.md) for how cursors work.
- [Troubleshooting](https://docs.betterjobs.cc/resources/troubleshooting.md) for signature mismatches and `402`.
