Skip to content
API v1 preview — endpoints and fields may change before general availability.

n8n

How do I call BetterJobs and receive its webhooks in n8n?

View .md

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.
  1. In n8n, create a credential of type Header Auth.
  2. Set Name to Authorization.
  3. Set Value to Bearer bj_live_... with your key.
  4. Save it as BetterJobs. Every HTTP Request node below uses it.

Keeping the key in a credential keeps it out of exported workflow JSON.

  1. Add an HTTP Request node.

  2. Set:

    Setting Value
    Method POST
    URL https://api.betterjobs.cc/v1/jobs/search
    Authentication Generic Credential Type → Header Auth → BetterJobs
    Send Headers On. Name BetterJobs-Version, value 2026-10-01
    Send Body On. Body Content Type JSON, Specify Body Using JSON
  3. 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 }}"].

  4. 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.

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.

  • Under the node’s Settings, turn on Retry On Fail for 429 and 500. A 500 is safe to retry: send the same Idempotency-Key header 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.
  • 200 with metadata.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.

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 a Webhook node. Set HTTP Method to POST and pick a path, for example betterjobs.
  2. Set Respond to Immediately. BetterJobs needs a 2xx within 10 seconds. A slow workflow would otherwise cause retries.
  3. Under Options, turn on Raw Body. The signature covers the exact bytes BetterJobs sent. Parsed and re-serialized JSON will not match.
  4. Copy the Production URL. Use it as webhook_url when you create a watch (POST /v1/watches) or an async search.

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.

Add a Switch node on {{ $json.type }}. These three types matter most for outbound:

EventWhat happenedWhat to dodataCost
job.openedA 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.repostedThe 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.closedThe 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.

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.