Goal: a list of companies hiring a Head of RevOps in Germany, Austria or Switzerland in the last 7 days, with the matching jobs under each company.
You search jobs, then group them by company.domain. One request covers every provider on your plan.
-
Estimate for free. Send the search with
dry_run: true. You getexpected_unique_jobs_rangeandcredits_range. Nothing is fetched or charged. -
Search with a cap. Send the same search without
dry_run. Setwaterfall.max_creditsso one page cannot charge more than you expect, and stop paging once the summedmetadata.credits_chargedreaches your run budget. -
Page through results. Pass
next_cursorback ascursoruntil it isnull. Each page holds up to 100 jobs. -
Group by company. Key each job on
company.domain. Fall back tocompany.idwhen the domain isnull. -
Optional: check each company. Call
GET /v1/companies/{domain}foris_hiringandhiring_pulse. That is 1 credit per profile.
Estimate first
Section titled “Estimate first”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", "Head of Revenue Operations"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7 }, "waterfall": { "strategy": "cheapest_first" }, "limit": 100, "dry_run": true }'import osimport requests
API = "https://api.betterjobs.cc/v1"HEADERS = { "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}", "BetterJobs-Version": "2026-10-01",}FILTERS = { "title_or": ["Head of RevOps", "Head of Revenue Operations"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7,}
resp = requests.post( f"{API}/jobs/search", headers=HEADERS, json={"filters": FILTERS, "waterfall": {"strategy": "cheapest_first"}, "limit": 100, "dry_run": True},)resp.raise_for_status()print(resp.json()["estimate"]) # freeconst API = 'https://api.betterjobs.cc/v1';const headers = { Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`, 'BetterJobs-Version': '2026-10-01', 'Content-Type': 'application/json',};const filters = { title_or: ['Head of RevOps', 'Head of Revenue Operations'], country_code_or: ['DE', 'AT', 'CH'], posted_within_days: 7,};
const res = await fetch(`${API}/jobs/search`, { method: 'POST', headers, body: JSON.stringify({ filters, waterfall: { strategy: 'cheapest_first' }, limit: 100, dry_run: true }),});if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);console.log((await res.json()).estimate); // freeThe estimate looks like this (illustrative):
{ "request_id": "req_3Kd8PwZ1uY", "estimate": { "expected_unique_jobs_range": { "min": 40, "max": 75 }, "providers_planned": ["betterjobs", "theirstack", "techmap"], "credits_range": { "min": 40, "max": 75 } }}Search, page and group
Section titled “Search, page and group”# First page. Repeat with "cursor": "<next_cursor>" until next_cursor is null.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" \ -H "Idempotency-Key: 7b8f2c4e-1a3d-4f5b-9c6e-0d2a4b6c8e1f" \ -d '{ "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": 100 }'Group the saved pages with jq:
jq -s '[.[].data[]] | group_by(.company.domain // .company.id) | map({company: .[0].company, jobs: map({id, title, location})})' page-*.jsonimport uuidfrom collections import defaultdict
def search_all(filters: dict, budget: int) -> list[dict]: """Stops when the run has spent `budget` credits. max_credits caps each page only.""" jobs, cursor, spent = [], None, 0 while spent < budget: body = { "filters": filters, "waterfall": {"strategy": "cheapest_first", "max_credits": min(100, budget - spent)}, "limit": 100, } if cursor: body["cursor"] = cursor resp = requests.post( f"{API}/jobs/search", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())}, json=body, ) resp.raise_for_status() page = resp.json() if page["metadata"]["status"] == "partial": print("partial page, failed providers:", page["metadata"]["providers"]["failed"]) jobs += page["data"] spent += page["metadata"]["credits_charged"] cursor = page["next_cursor"] if cursor is None: break return jobs
companies: dict[str, dict] = defaultdict(lambda: {"company": None, "jobs": []})for job in search_all(FILTERS, budget=100): key = job["company"]["domain"] or job["company"]["id"] companies[key]["company"] = job["company"] companies[key]["jobs"].append({"id": job["id"], "title": job["title"], "p_real": job["p_real"]})
for key, entry in sorted(companies.items(), key=lambda kv: -len(kv[1]["jobs"])): print(entry["company"]["name"], key, len(entry["jobs"]))type Job = { id: string; title: string; p_real: number; company: { id: string; name: string; domain: string | null };};
async function searchAll(budget: number): Promise<Job[]> { // Stops when the run has spent `budget` credits. max_credits caps each page only. const jobs: Job[] = []; let cursor: string | null = null; let spent = 0; do { const res = await fetch(`${API}/jobs/search`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ filters, waterfall: { strategy: 'cheapest_first', max_credits: Math.min(100, budget - spent) }, limit: 100, ...(cursor ? { cursor } : {}), }), }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); const page = await res.json(); if (page.metadata.status === 'partial') console.warn('partial page', page.metadata.providers.failed); jobs.push(...page.data); spent += page.metadata.credits_charged; cursor = page.next_cursor; } while (cursor && spent < budget); return jobs;}
const companies = new Map<string, { company: Job['company']; jobs: Pick<Job, 'id' | 'title' | 'p_real'>[] }>();for (const job of await searchAll(100)) { const key = job.company.domain ?? job.company.id; const entry = companies.get(key) ?? { company: job.company, jobs: [] }; entry.jobs.push({ id: job.id, title: job.title, p_real: job.p_real }); companies.set(key, entry);}console.log([...companies.values()].sort((a, b) => b.jobs.length - a.jobs.length));Example response excerpt
Section titled “Example response excerpt”One canonical job from the search (illustrative, trimmed). Three sources saw it; you pay for it once.
{ "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 }, "seniority": "lead", "salary": { "min": 110000, "max": 135000, "currency": "EUR", "period": "year", "origin": "declared" }, "status": "open", "p_real": 0.94, "sources": [ { "provider": "betterjobs", "provider_job_id": "bj_idx_5521907", "fields": ["title", "description", "apply_url", "location", "employment_type", "posted_at"] }, { "provider": "theirstack", "provider_job_id": "ts_88213377", "fields": ["salary", "seniority"] }, { "provider": "techmap", "provider_job_id": "tm_3f9a2c71", "fields": ["job_family"] } ] } ], "next_cursor": "cur_8fJ2kQ", "metadata": { "status": "complete", "credits_charged": 1, "jobs_already_paid": 0, "duplicates_merged": 2, "providers": { "tried": ["betterjobs", "techmap", "theirstack"], "hit": ["betterjobs", "techmap", "theirstack"], "failed": [] } }}After grouping (illustrative):
[ { "company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" }, "jobs": [{ "id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "title": "Head of Revenue Operations", "p_real": 0.94 }] }]Check a company (optional)
Section titled “Check a company (optional)”A search tells you who posted matching jobs. A company profile tells you whether the company is hiring overall and in which direction.
curl https://api.betterjobs.cc/v1/companies/acme-robotics.example \ -H "Authorization: Bearer $BETTERJOBS_API_KEY" \ -H "BetterJobs-Version: 2026-10-01"profile = requests.get(f"{API}/companies/acme-robotics.example", headers=HEADERS)profile.raise_for_status()p = profile.json()print(p["is_hiring"]["value"], p["hiring_pulse"]["direction"], p["open_jobs_count"])const profile = await fetch(`${API}/companies/acme-robotics.example`, { headers });if (!profile.ok) throw new Error(`${profile.status} ${await profile.text()}`);const p = await profile.json();console.log(p.is_hiring.value, p.hiring_pulse.direction, p.open_jobs_count);{ "company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" }, "open_jobs_count": 42, "is_hiring": { "value": true, "confidence": 0.96, "basis": "42 open jobs corroborated by 4 sources in the last 30 days" }, "hiring_pulse": { "direction": "up", "open_jobs_30d_change": 9 }}Credit cost
Section titled “Credit cost”| Call | Cost |
|---|---|
POST /v1/jobs/search with dry_run: true |
Free |
POST /v1/jobs/search |
1 credit per unique job returned. Duplicates, jobs you already paid for and empty pages are free. |
GET /v1/companies/{domain} |
1 credit per profile |
A company with 30 matching jobs costs 30 credits through search. If you already have an account list and only need yes or no, GET /v1/companies/{domain} at 1 credit each is cheaper. See Credits and billing.
Pitfalls
Section titled “Pitfalls”posted_within_dayscounts from first seen. It matches jobs first seen within that many days. A job posted earlier but discovered late still matches. Checkposted_atif you need the employer’s date.- Partial pages are normal. A provider timeout gives
200withmetadata.status: partial. You pay only for returned jobs. Re-run later to fill gaps; jobs you already paid for come back free. max_creditsstops results. When the cap is hit, the page stops early. Raise the cap or narrow the filters.- Before outbound, prefer corroborated jobs. Use
p_real, or theconsensusstrategy. See Choose a strategy. - Unknown filter fields fail. A typo returns
400 unknown_filterwith the field inparam. See Filters.
- Detect hiring changes: get told when these companies open or close jobs.
- Pagination and Companies.
- API reference: Search jobs, Get a company.