# The waterfall

> How does one request fan out to seven sources and come back as one list?

Source: https://docs.betterjobs.cc/concepts/waterfall/

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.

Diagram: one search request goes to 7 sources: BetterJobs index (contributed fields), Reqbeat (duplicate, merged), SignalsAPI (no match), TheirStack (contributed fields), JobsPipe (duplicate, merged), Coresignal (no match), Techmap (contributed fields). Results are merged and deduplicated into one canonical job whose sources array lists betterjobs, theirstack, techmap.

Illustrative: one request, seven sources, one canonical job. Duplicates are merged and free.

## What happens on one request

1. **Validate.** Filters are checked against the [filter grammar](https://docs.betterjobs.cc/platform/filters.md). An unknown field returns `400 unknown_filter`. It is never silently ignored.
2. **Plan.** The `waterfall.strategy` picks which sources to ask and in what order. Only sources enabled on your plan are eligible.
3. **Fan out.** Each source gets the query translated into its own grammar and field names. You never see six APIs.
4. **Merge.** Records that describe the same opening collapse into one [canonical job](https://docs.betterjobs.cc/concepts/canonical-jobs.md). Each source that saw it is listed in `sources[]`, with the fields it contributed.
5. **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](https://docs.betterjobs.cc/concepts/credits-and-billing.md).
6. **Report.** `metadata` tells you what happened: which providers were tried, which hit, which failed, how many duplicates were merged and what you were charged.

## 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](https://docs.betterjobs.cc/providers/betterjobs-index.md) | `betterjobs` | Our own job sources: an owned crawl of employer career pages and ATSs.                         |
| [Reqbeat](https://docs.betterjobs.cc/providers/reqbeat.md)                   | `reqbeat`    | Deduplicated, normalized job postings and hiring events from ATSs, job boards and aggregators. |
| [SignalsAPI](https://docs.betterjobs.cc/providers/signalsapi.md)             | `signalsapi` | Recruiter-focused hiring signals plus the hiring owner's verified work email.                  |
| [TheirStack](https://docs.betterjobs.cc/providers/theirstack.md)             | `theirstack` | Global job postings, technographics inferred from job text, buying intent and firmographics.   |
| [JobsPipe](https://docs.betterjobs.cc/providers/jobspipe.md)                 | `jobspipe`   | Normalized job postings from 30+ sources with 12 months of history.                            |
| [Coresignal](https://docs.betterjobs.cc/providers/coresignal.md)             | `coresignal` | Large historical job-posting dataset plus company and employee records.                        |
| [Techmap](https://docs.betterjobs.cc/providers/techmap.md)                   | `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](https://docs.betterjobs.cc/providers.md) for each source’s facts and [List providers](/api/operations/listproviders/) for the endpoint.

## 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](https://docs.betterjobs.cc/guides/choose-a-strategy.md).

> consensus filters, it does not add
>
> `consensus` drops every job seen by fewer than `waterfall.min_sources` sources (default `2`, range `2` to `7`). It returns fewer jobs, each corroborated. It needs at least `min_sources` sources enabled on your plan.

## 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**

```bash
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
  }'
```

**Python**

```python
import os
import 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"])
```

**TypeScript**

```ts
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

Every response carries a `metadata.providers` block. This is the illustrative example from the spec:

```json
"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 missed `timeout_ms`.
- `contributions`: unique jobs each source contributed first. A source with `0` may still have corroborated jobs and added fields. Check `sources[]` 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

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 `salary` or `seniority` the first one lacked.
- Overlap raises confidence. A job seen by several independent sources gets a higher `p_real`. See [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md).

## 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](https://docs.betterjobs.cc/concepts/credits-and-billing.md#plans). `GET /v1/providers` shows exactly which sources your key can use today.

## Related

- [Choose a strategy](https://docs.betterjobs.cc/guides/choose-a-strategy.md)
- [Canonical jobs](https://docs.betterjobs.cc/concepts/canonical-jobs.md)
- [Errors and partial results](https://docs.betterjobs.cc/platform/errors.md#provider_timeout)
- [Search jobs API reference](/api/operations/searchjobs/)
