BetterJobs has no native n8n node. You use two built-in nodes:
- HTTP Request to call the API.
- Webhook plus a Code node to receive events and check their signature.
Store the API key as a credential
Section titled “Store the API key as a credential”- In n8n, create a credential of type Header Auth.
- Set Name to
Authorization. - Set Value to
Bearer bj_live_...with your key. - Save it as
BetterJobs. Every HTTP Request node below uses it.
Keeping the key in a credential keeps it out of exported workflow JSON.
Search jobs with the HTTP Request node
Section titled “Search jobs with the HTTP Request node”-
Add an HTTP Request node.
-
Set:
Setting Value Method POSTURL https://api.betterjobs.cc/v1/jobs/searchAuthentication Generic Credential Type → Header Auth → BetterJobsSend Headers On. Name BetterJobs-Version, value2026-10-01Send Body On. Body Content Type JSON, Specify BodyUsing JSON -
Paste the body:
{"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 node, switch the field to expression mode and insert it, for example
"company_domain_or": ["{{ $json.domain }}"]. -
Add a Split Out node after it. Set Field To Split Out to
data. You now get one n8n item per canonical job.
Each item has the Job fields: id, title, company.domain, apply_url, sources and so on. Store id. Fetching the same job later is free.
Page through results
Section titled “Page through results”One request returns at most 100 jobs. For more, turn on pagination in the HTTP Request node under Options → Pagination:
| Setting | Value |
|---|---|
| Pagination Mode | Update a Parameter in Each Request |
| Type | Body |
| Name | cursor |
| Value | {{ $response.body.next_cursor }} |
| Pagination Complete When | Other |
| Complete Expression | {{ $response.body.next_cursor === null }} |
Also set Max Pages so a broad filter cannot page forever. waterfall.max_credits caps each request, not the whole run. See Pagination.
Handle errors and rate limits
Section titled “Handle errors and rate limits”- Under the node’s Settings, turn on Retry On Fail for
429and500. A500is safe to retry: send the sameIdempotency-Keyheader and you are never charged twice. See Idempotency. - If you call BetterJobs once per input item, set Options → Batching so items are sent in small batches with an interval. That keeps you under your rate limit. See Rate limits.
200withmetadata.status: "partial"is not an error. One provider failed or timed out. You still get the other results and pay only for those. See Provider status.
Receive webhooks
Section titled “Receive webhooks”BetterJobs POSTs one event per request to your endpoint. It signs each delivery with the BetterJobs-Signature header. Check the signature before you act on the event.
1. Add the Webhook node
Section titled “1. Add the Webhook node”- Add a Webhook node. Set HTTP Method to
POSTand pick a path, for examplebetterjobs. - Set Respond to
Immediately. BetterJobs needs a2xxwithin 10 seconds. A slow workflow would otherwise cause retries. - Under Options, turn on Raw Body. The signature covers the exact bytes BetterJobs sent. Parsed and re-serialized JSON will not match.
- Copy the Production URL. Use it as
webhook_urlwhen you create a watch (POST /v1/watches) or an async search.
2. Check the signature in a Code node
Section titled “2. Check the signature in a Code node”Add a Code node after the Webhook node. Set Mode to Run Once for All Items and Language to JavaScript.
// Verifies BetterJobs-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">const crypto = require('crypto');
const secret = $env.BETTERJOBS_WEBHOOK_SECRET;if (!secret) throw new Error('BETTERJOBS_WEBHOOK_SECRET is not set');
const TOLERANCE_SECONDS = 300;const out = [];
for (let i = 0; i < $input.all().length; i++) { const item = $input.all()[i]; const header = item.json.headers['betterjobs-signature']; if (!header) continue;
const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const raw = (await this.helpers.getBinaryDataBuffer(i, 'data')).toString('utf8');
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${raw}`).digest('hex'); const a = Buffer.from(expected, 'hex'); const b = Buffer.from(parts.v1 ?? '', 'hex'); const signatureOk = a.length === b.length && crypto.timingSafeEqual(a, b); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= TOLERANCE_SECONDS;
if (signatureOk && fresh) out.push({ json: JSON.parse(raw) });}
return out;The node passes on only events with a valid signature, as parsed JSON. Invalid deliveries are dropped. Check the Webhook node’s output once to confirm the binary property is named data. If not, change the second argument of getBinaryDataBuffer.
The 300-second tolerance on t rejects old deliveries replayed by someone else. Full details are on Webhooks.
3. Route by event type
Section titled “3. Route by event type”Add a Switch node on {{ $json.type }}. These three types matter most for outbound:
| Event | What happened | What to do | data | Cost |
|---|---|---|---|---|
job.opened | A new canonical job appeared for a watched company or saved search. | Trigger outbound. This is the only event that should start a sequence. | job (full Job) | 1 credit |
job.reposted | The same job was re-listed. repost_count went up. | Do not re-trigger outbound. Update your copy of the job only. | job (id, title, company, status, repost_count) | Free |
job.closed | The job is no longer live. closed_reason says why: filled, expired, removed or unknown. | Stop sequences tied to this job. Mark it closed in your CRM. | job (id, title, company, status, closed_reason) | Free |
job.opened is the only event that should start a sequence. Never re-trigger on job.reposted. All event types are in the event catalog.
Failed deliveries
Section titled “Failed deliveries”If your workflow is off or errors before it responds, BetterJobs retries with backoff for 24 hours. After you fix it, re-send any event with POST /v1/webhooks/replay. Replays are free. You can also read every event from GET /v1/events as a fallback.
Related
Section titled “Related”- Detect hiring changes for watches and events end to end.
- Sync patterns for daily upserts by job
id. - Troubleshooting for signature mismatches and empty results.