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

Quickstart

How do I make my first BetterJobs request in under a minute?

View .md

Four steps: call the keyless sandbox, read the response, repeat the call with your own key, then build your own query. The first step needs no signup.

The sandbox has the same request and response shape as POST /v1/jobs/search. It needs no key, charges nothing and calls no provider. It returns fixed illustrative data.

Terminal window
curl https://api.betterjobs.cc/v1/sandbox/jobs/search \
-H "Content-Type: application/json" \
-d '{
"filters": {
"title_or": ["Head of RevOps"],
"country_code_or": ["DE", "AT", "CH"],
"posted_within_days": 7
},
"limit": 10
}'

This is the sandbox response. Data is illustrative.

{
"data": [
{
"id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB",
"title": "Head of Revenue Operations",
"company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" },
"location": { "city": "Berlin", "region": "Berlin", "country_code": "DE", "remote": false },
"employment_type": "full_time",
"seniority": "lead",
"job_family": "operations",
"salary": { "min": 110000, "max": 135000, "currency": "EUR", "period": "year", "origin": "declared" },
"description": "Acme Robotics is hiring a Head of Revenue Operations to own forecasting, CRM hygiene and the GTM tool stack across DACH.",
"apply_url": "https://jobs.acme-robotics.example/revops-lead/apply",
"posted_at": "2026-10-08T00:00:00Z",
"first_seen_at": "2026-10-08T06:40:00Z",
"last_seen_at": "2026-10-11T06:10:00Z",
"last_verified_at": "2026-10-11T06:10:00Z",
"status": "open",
"closed_reason": null,
"repost_count": 0,
"p_real": 0.94,
"sources": [
{
"provider": "betterjobs",
"provider_job_id": "bj_idx_5521907",
"url": "https://jobs.acme-robotics.example/revops-lead",
"first_seen_at": "2026-10-08T06:40:00Z",
"last_seen_at": "2026-10-11T06:10:00Z",
"fields": ["title", "description", "apply_url", "location", "employment_type", "posted_at"]
},
{
"provider": "theirstack",
"provider_job_id": "ts_88213377",
"url": "https://jobs.acme-robotics.example/revops-lead",
"first_seen_at": "2026-10-08T07:12:00Z",
"last_seen_at": "2026-10-11T04:02:00Z",
"fields": ["salary", "seniority"]
},
{
"provider": "techmap",
"provider_job_id": "tm_3f9a2c71",
"url": "https://boards.example/acme-robotics/revops-lead",
"first_seen_at": "2026-10-08T09:30:00Z",
"last_seen_at": "2026-10-10T23:40:00Z",
"fields": ["job_family"]
}
],
"license": { "display": true, "resale": false }
}
],
"next_cursor": null,
"metadata": {
"request_id": "req_sandbox_0001",
"status": "complete",
"credits_charged": 0,
"jobs_already_paid": 0,
"duplicates_merged": 2,
"credits_remaining": 0,
"providers": {
"tried": ["betterjobs", "techmap", "theirstack"],
"hit": ["betterjobs", "techmap", "theirstack"],
"failed": [],
"contributions": { "betterjobs": 1, "techmap": 0, "theirstack": 0 }
},
"field_coverage": { "salary": 1.0, "seniority": 1.0, "location.remote": 1.0, "description": 1.0 },
"latency_ms": 12
}
}

The response has three blocks.

Each item is one real opening, merged from every source that saw it. Three providers saw this job. You get it once.

  • id is stable across sources and requests. Store it. Re-reading a job you already paid for is free.
  • sources[] lists each provider that saw the job, its own id and URL, when it saw it, and which fields it contributed. Here the BetterJobs index gave the title and description, TheirStack gave salary and seniority, and Techmap gave job_family.
  • p_real is the probability (0 to 1) that this is a real open req, from cross-source corroboration. See Provenance and confidence.
  • salary.origin says whether pay was declared in the posting or inferred by a source.
  • license says whether you may show the job to end users (display) and resell it as data (resale). See Licensing.

null means this is the last page. Otherwise pass it back as cursor with the same filters to get the next page. See Pagination.

Field Meaning
request_id Unique id for this request. Also in the X-Request-Id header. Quote it to support.
status complete, or partial when at least one provider failed or timed out. You are billed only for returned jobs.
credits_charged Credits this request cost. 0 in the sandbox.
jobs_already_paid Returned jobs you had paid for before. Free.
duplicates_merged Provider records merged into a canonical job. Free. Here, 2.
credits_remaining Credits left on your account. 0 in the sandbox, which has no account.
providers.tried / hit / failed Which sources were asked, which matched, and which failed (provider_timeout or provider_error).
providers.contributions Unique jobs each source contributed first.
field_coverage Share (0 to 1) of returned jobs with a non-null value, per field. This is measured live on every response.
latency_ms Time the request took.
  1. Sign up at betterjobs.cc. The Free plan has 1,000 credits on the BetterJobs index. No card.

  2. Create an API key in your dashboard. Live keys start with bj_live_. Store it in an environment variable, never in client-side code. See Authentication.

    Terminal window
    export BETTERJOBS_API_KEY="bj_live_..."
  3. Ask for a free estimate first. dry_run: true returns the expected job count, the providers it would call and a credit range. Nothing is fetched or charged.

    Terminal window
    curl https://api.betterjobs.cc/v1/jobs/search \
    -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
    -H "BetterJobs-Version: 2026-10-01" \
    -H "Content-Type: application/json" \
    -d '{
    "filters": {
    "title_or": ["Head of RevOps"],
    "country_code_or": ["DE", "AT", "CH"],
    "posted_within_days": 7
    },
    "limit": 25,
    "dry_run": true
    }'
  4. Run the real search. Drop dry_run and add waterfall.max_credits as a hard cap. Results stop at the cap.

    Terminal window
    curl -i https://api.betterjobs.cc/v1/jobs/search \
    -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
    -H "BetterJobs-Version: 2026-10-01" \
    -H "Content-Type: application/json" \
    -d '{
    "filters": {
    "title_or": ["Head of RevOps"],
    "country_code_or": ["DE", "AT", "CH"],
    "posted_within_days": 7
    },
    "waterfall": { "strategy": "cheapest_first", "max_credits": 25 },
    "limit": 25
    }'

1 credit = 1 unique job returned. Duplicates and empty searches are free. Run the same search again and the jobs you already have come back free, counted in metadata.jobs_already_paid.

Change the fields. The request updates live in every format, including a Clay HTTP column body and an MCP tool call.

Seniority

Unknown filter fields return 400 unknown_filter, never a silent empty result. All filters are listed in Filters.