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

Troubleshooting

Something looks wrong. What should I check first?

View .md

Find your symptom below. Each one lists what to check, in order. Every response carries an X-Request-Id header. Keep it: it is what support needs.

data is []. This costs 0 credits. Check, in order:

  1. Which sources were tried. Read metadata.providers.tried. On Free and Starter, or with strategy: own_only, only betterjobs (the BetterJobs index) is searched. Partner providers start on Growth. See Credits and billing.
  2. The time window. posted_within_days counts from when a job was first seen. A window of 1 or 2 days is narrow. Try 30.
  3. Title keywords. title_or matches title keywords. Add common variants: ["Head of RevOps", "Head of Revenue Operations", "RevOps Lead"]. Check that title_not is not excluding them.
  4. Country codes. country_code_or takes ISO 3166-1 alpha-2 codes: GB, not UK.
  5. Filters that drop unknowns. salary_min_gte excludes every job whose salary is null. Many postings do not state pay, so this filter removes a lot. remote: true keeps remote jobs only.
  6. The strategy. consensus returns only jobs seen by at least min_sources sources (default 2). Switch to cheapest_first or max_coverage to see single-source jobs.
  7. Closed jobs. Closed jobs are excluded unless you set include_closed: true.
  8. The status. If metadata.status is partial, a provider failed. See below.

Before running a wide search, send it with "dry_run": true. The free estimate returns expected_unique_jobs_range and providers_planned. A range of 0 to 0 means the filters are too tight.

metadata.status is partial, or an async search ended with status: partial. This is not an error.

  • One or more providers failed or missed waterfall.timeout_ms. They are listed in metadata.providers.failed with provider_timeout or provider_error.
  • You got everything the other sources found, and you paid only for that.
  • Check GET /v1/providers for the source’s status.

To recover: retry later (jobs you already paid for are free), raise timeout_ms up to 30,000, or leave the failing source out of waterfall.providers. See Provider status.

  • unknown_filter: a field in filters does not exist. BetterJobs rejects unknown fields instead of ignoring them, so a typo never silently widens your search. error.param names the field, and the message suggests a fix. A common cause is pasting another provider’s filter names, for example job_title_or instead of title_or. See Filters.
  • invalid_request: a value is out of range or malformed, for example limit above 100 on POST /v1/jobs/search. Read error.param and error.message.

Send the header exactly as Authorization: Bearer bj_live_.... Check for a missing Bearer , a trailing space or newline from copy-paste, or a revoked key. Sandbox keys start with bj_test_. See Authentication.

You do not have enough credits for this request.

  • The body has error.credits_needed and error.upgrade_url.
  • GET /v1/account shows credits_remaining and credits_reset_at.
  • Lower waterfall.max_credits or limit so the request fits what you have left.
  • An async search does not fail when credits run out. It moves to status: on_hold and resumes after a top-up.

See Errors.

You asked for a provider or feature your plan does not include, often by listing a provider in waterfall.providers. error.required_plan names the plan you need. Remove the provider, or check enabled_on_your_plan in GET /v1/providers first.

You sent too many requests in the current window.

  • Wait the number of seconds in the Retry-After header, then retry.
  • Watch RateLimit-Remaining and RateLimit-Reset on every response and slow down before you hit 0.
  • No-code tools often send one request per row at full speed. Throttle the step (Clay request rate, n8n batching, Make scheduling).
Terminal window
# -i prints the response headers, including RateLimit-* and Retry-After
curl -i https://api.betterjobs.cc/v1/account \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01"

See Rate limits.

Your computed HMAC differs from v1 in BetterJobs-Signature. The cause is almost always the input, not the algorithm. Check:

  1. Raw body. Sign the exact bytes you received. If your framework or tool parsed the JSON and you re-serialize it, key order and whitespace change and the HMAC breaks. Use the raw body option: n8n Raw Body, Make JSON pass-through, Zapier Catch Raw Hook, Express express.raw().
  2. Signed string. It is <t>.<raw body>: the t value from the header, a dot, then the body. Not the body alone.
  3. Header parsing. Split the header on ,, then each part on the first =. t is unix seconds. v1 is lowercase hex.
  4. Secret. Use the secret of the endpoint that received the event. A different endpoint has a different secret.
  5. Encoding. Compare hex to hex. Do not base64 the digest.
  6. Clock. If you reject old timestamps, check your server clock. A 5-minute tolerance is common.

Once your check is fixed, replay missed events with POST /v1/webhooks/replay. Replays are free and never charge again. See Webhooks.

BetterJobs charges a job once. Re-reading a job you already paid for is free, whichever endpoint returns it. To check a specific job, read the ledger:

Terminal window
curl "https://api.betterjobs.cc/v1/billing/ledger?job_id=job_01JC8X4M2Q7RV3T9KD5W6YH0AB" \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01"

Illustrative result: one charge, then two free re-reads.

{
"data": [
{ "id": "led_0Zc5XvB9nM", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_1Mn4BvC7xZ", "operation": "GET /v1/jobs/{id}", "credits": 0, "reason": "already_paid", "created_at": "2026-10-11T12:40:00Z" },
{ "id": "led_8Yb4WuA3mL", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_6Lk3AzX2wY", "operation": "POST /v1/jobs/search", "credits": 0, "reason": "already_paid", "created_at": "2026-10-11T11:05:00Z" },
{ "id": "led_2Xa3VtZ1kK", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_7Hc2LmQ9xT", "operation": "POST /v1/jobs/search", "credits": 1, "reason": "charged", "created_at": "2026-10-11T09:12:00Z" }
],
"next_cursor": null
}

Then check the usual causes of a real second charge:

  • Two different jobs. Two postings that look alike (same title, different city or team) are two canonical jobs with two id values. Compare the ids.
  • Company profiles. GET /v1/companies/{domain} costs 1 credit per request, every time. There is no already-paid rule for profiles.
  • Retried POSTs. Send an Idempotency-Key on every POST. A retry with the same key and body returns the first response and never charges twice. See Idempotency.

Each response’s X-Credits-Charged header and metadata.credits_charged show what that request cost. If the ledger still shows two charged entries for one job_id, contact support with both request_id values.