You send one search. BetterJobs routes it to the BetterJobs index and the partner providers your plan enables. It merges what comes back into canonical jobs and returns one list. You pay 1 credit per unique job, not per provider asked.
What happens on one request
Section titled “What happens on one request”- Validate. Filters are checked against the filter grammar. An unknown field returns
400 unknown_filter. It is never silently ignored. - Plan. The
waterfall.strategypicks which sources to ask and in what order. Only sources enabled on your plan are eligible. - Fan out. Each source gets the query translated into its own grammar and field names. You never see six APIs.
- Merge. Records that describe the same opening collapse into one canonical job. Each source that saw it is listed in
sources[], with the fields it contributed. - Bill. You pay for unique jobs you have not paid for before. Duplicates, already-paid jobs and empty pages are free. See Credits and billing.
- Report.
metadatatells you what happened: which providers were tried, which hit, which failed, how many duplicates were merged and what you were charged.
The sources
Section titled “The sources”Seven sources can answer a search. The BetterJobs index is on every plan. The six partners are added by plan.
| Source | Slug | What it sells |
|---|---|---|
| BetterJobs index | betterjobs | Our own job sources: an owned crawl of employer career pages and ATSs. |
| Reqbeat | reqbeat | Deduplicated, normalized job postings and hiring events from ATSs, job boards and aggregators. |
| SignalsAPI | signalsapi | Recruiter-focused hiring signals plus the hiring owner's verified work email. |
| TheirStack | theirstack | Global job postings, technographics inferred from job text, buying intent and firmographics. |
| JobsPipe | jobspipe | Normalized job postings from 30+ sources with 12 months of history. |
| Coresignal | coresignal | Large historical job-posting dataset plus company and employee records. |
| Techmap | techmap | High-volume job feeds from ATSs, job boards and public employment offices (jobdatafeeds.com). |
GET /v1/providers returns every source, whether your plan enables it, and its live status. See Providers for each source’s facts and List providers for the endpoint.
Strategies
Section titled “Strategies”Set waterfall.strategy on the request. The default is cheapest_first.
| Strategy | What it does | Use when |
|---|---|---|
cheapest_first | Starts with the BetterJobs index and adds providers only until the page is full. | Default. Good for most searches and for keeping cost low. |
freshest_first | Tries the sources with the fastest refresh first. | You care about jobs posted in the last hours more than total coverage. |
max_coverage | Queries every provider enabled on your plan. | Market sizing, backfills, or any time a missed job costs more than a credit. |
consensus | Returns only jobs seen by at least min_sources sources (default 2). | You need high confidence the job is real, for example before outbound. |
own_only | Uses the BetterJobs index only. No partner providers. | Free and Starter plans, or when you need the simplest licensing. |
You can also pin the sources yourself with waterfall.providers, for example ["betterjobs", "theirstack"]. Every slug must be enabled on your plan, or the request returns 403 plan_required. For a decision guide, see Choose a strategy.
Budget caps
Section titled “Budget caps”Two fields bound every request. Set both in production.
| Field | Range | What it does |
|---|---|---|
waterfall.max_credits |
0 or more |
Hard cap on credits this request may charge. Results stop at the cap. |
waterfall.timeout_ms |
1000 to 30000, default 10000 |
Time budget. Providers that miss it are dropped and the result is partial. |
A dropped or failing provider never fails the request. You get 200 with metadata.status: partial, the provider listed in metadata.providers.failed with code provider_timeout or provider_error, and a bill for returned jobs only.
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": "max_coverage", "max_credits": 100, "timeout_ms": 10000 }, "limit": 25 }'import osimport requests
resp = requests.post( "https://api.betterjobs.cc/v1/jobs/search", headers={ "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}", "BetterJobs-Version": "2026-10-01", }, json={ "filters": { "title_or": ["Head of RevOps", "Head of Revenue Operations"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7, }, "waterfall": {"strategy": "max_coverage", "max_credits": 100, "timeout_ms": 10000}, "limit": 25, },)resp.raise_for_status()meta = resp.json()["metadata"]print(meta["status"], meta["providers"]["hit"], meta["providers"]["failed"])const resp = await fetch('https://api.betterjobs.cc/v1/jobs/search', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`, 'BetterJobs-Version': '2026-10-01', 'Content-Type': 'application/json', }, body: JSON.stringify({ filters: { title_or: ['Head of RevOps', 'Head of Revenue Operations'], country_code_or: ['DE', 'AT', 'CH'], posted_within_days: 7, }, waterfall: { strategy: 'max_coverage', max_credits: 100, timeout_ms: 10000 }, limit: 25, }),});if (!resp.ok) throw new Error(`BetterJobs ${resp.status}`);const { metadata } = await resp.json();console.log(metadata.status, metadata.providers.hit, metadata.providers.failed);Reading the trace
Section titled “Reading the trace”Every response carries a metadata.providers block. This is the illustrative example from the spec:
"providers": { "tried": ["betterjobs", "techmap", "theirstack", "coresignal"], "hit": ["betterjobs", "techmap", "theirstack"], "failed": [{ "provider": "coresignal", "code": "provider_timeout" }], "contributions": { "betterjobs": 1, "techmap": 0, "theirstack": 0 }}tried: sources the strategy asked.hit: sources that returned at least one match.failed: sources that errored or missedtimeout_ms.contributions: unique jobs each source contributed first. A source with0may still have corroborated jobs and added fields. Checksources[]on each job.
metadata.duplicates_merged counts provider records folded into jobs you already have in the response. They are free.
Why this is not a contact waterfall
Section titled “Why this is not a contact waterfall”Contact-enrichment waterfalls ask provider A for an email, then B if A has none, and stop at the first valid answer. One person has one right answer.
A job search has no single right answer. It is a set. Each provider sees a different slice of the market, and the same opening often shows up in several of them with different fields filled. So BetterJobs does not stop at the first hit:
- It keeps asking until the page is full (
cheapest_first) or until every source has answered (max_coverage). - It merges overlapping records instead of discarding them. A second source can add
salaryorsenioritythe first one lacked. - Overlap raises confidence. A job seen by several independent sources gets a higher
p_real. See Provenance and confidence.
Plan availability
Section titled “Plan availability”| Plan | Sources you can route to |
|---|---|
| Free, Starter | BetterJobs index only. Use own_only. |
| Growth | BetterJobs index + 2 partner providers |
| Pro, Scale, Enterprise | BetterJobs index + all six partner providers |
Prices and credits are in the plan table. GET /v1/providers shows exactly which sources your key can use today.