# Choose a strategy

> Which waterfall strategy should my request use, and when should I pin providers?

Source: https://docs.betterjobs.cc/guides/choose-a-strategy/

`waterfall.strategy` decides which sources a request asks and when it stops. The default, `cheapest_first`, is right for most searches. This page tells you when it is not. What each strategy does is defined on [The waterfall](https://docs.betterjobs.cc/concepts/waterfall.md#strategies).

## Decide in five questions

Stop at the first “yes”.

1. **Are you on Free or Starter?** Use `own_only`. These plans route to the BetterJobs index only, so `cheapest_first`, `freshest_first`, `max_coverage` and `own_only` ask the same single source, and `consensus` cannot run (it needs at least 2 sources). `own_only` says so explicitly.

2. **Do you need every matching job?** Backfills, daily sync, market sizing, territory counts. Use `max_coverage`. It asks every provider your plan enables. A missed job costs you more than a credit.

3. **Will a person or a sequence act on each job?** Outbound, recruiter alerts, CRM tasks. Use `consensus` with `min_sources: 2`. You get fewer jobs, each seen by at least two independent sources.

4. **Do the last few hours matter more than completeness?** Alerting on fresh postings, “posted today” feeds. Use `freshest_first`. It asks the fastest-refreshing sources first.

5. **None of the above?** Keep `cheapest_first`. It starts with the BetterJobs index and adds providers only until the page is full.

## By job to be done

| You want to                                | Strategy                         | Settings                                    | Why                                                                                                                                  |
| ------------------------------------------ | -------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Look up who is hiring for a role this week | `cheapest_first`                 | `max_credits`                               | Fills the page at the lowest cost. See [Find companies hiring](https://docs.betterjobs.cc/guides/find-companies-hiring.md).          |
| Build or refresh a local copy              | `max_coverage`                   | `max_credits`, async for more than 100 jobs | Same sources every run, so gaps do not appear between runs. See [Sync patterns](https://docs.betterjobs.cc/guides/sync-patterns.md). |
| Size a market or a territory               | `max_coverage`                   | `dry_run: true` first                       | The free estimate gives `expected_unique_jobs_range` before you spend.                                                               |
| Trigger outbound on a job                  | `consensus`                      | `min_sources: 2`, filter on `p_real`        | Corroborated jobs only. Fewer wasted touches on stale or ghost postings.                                                             |
| Alert on brand-new postings                | `freshest_first`                 | short `posted_within_days`, `timeout_ms`    | Fast sources answer first. Pair with a [watch](https://docs.betterjobs.cc/guides/detect-hiring-changes.md) for push delivery.        |
| Show jobs to your own end users            | `own_only` or pinned `providers` | check `license.display`                     | Simplest licensing. See [Licensing](https://docs.betterjobs.cc/concepts/licensing.md).                                               |
| Keep using a provider you already trust    | any                              | `providers: [...]`                          | Pin the sources. See [Pin providers](#pin-providers).                                                                                |

## Compare strategies for free

Run the same filters with `dry_run: true` once per strategy. Nothing is fetched or charged. Compare `providers_planned` and `credits_range`.

**curl**

```bash
for strategy in cheapest_first max_coverage consensus; do
  curl -s 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": "'"$strategy"'" },
      "limit": 100,
      "dry_run": true
    }'
  echo
done
```

**Python**

```python
import os


import 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"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7}


for strategy in ["cheapest_first", "max_coverage", "consensus"]:
    resp = requests.post(
        f"{API}/jobs/search",
        headers=HEADERS,
        json={"filters": FILTERS, "waterfall": {"strategy": strategy}, "limit": 100, "dry_run": True},
        timeout=30,
    )
    resp.raise_for_status()
    est = resp.json()["estimate"]
    print(strategy, est["providers_planned"], est["credits_range"])
```

**TypeScript**

```ts
const 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'], country_code_or: ['DE', 'AT', 'CH'], posted_within_days: 7 };


for (const strategy of ['cheapest_first', 'max_coverage', 'consensus']) {
  const res = await fetch(`${API}/jobs/search`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ filters, waterfall: { strategy }, limit: 100, dry_run: true }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  const { estimate } = await res.json();
  console.log(strategy, estimate.providers_planned, estimate.credits_range);
}
```

One estimate (illustrative):

```json
{
  "request_id": "req_3Kd8PwZ1uY",
  "estimate": {
    "expected_unique_jobs_range": { "min": 40, "max": 75 },
    "providers_planned": ["betterjobs", "reqbeat", "signalsapi", "theirstack", "jobspipe", "coresignal", "techmap"],
    "credits_range": { "min": 40, "max": 75 }
  }
}
```

After a real request, `metadata.providers.contributions` shows how many unique jobs each source added first, and `metadata.field_coverage` shows how complete each field was. Those two numbers, on your own queries, are the best guide to which strategy pays off.

## Pin providers

`waterfall.providers` overrides the strategy’s choice of sources. Use it to keep a provider you already know, or to leave one out.

```json
{
  "filters": { "title_or": ["Head of RevOps"], "country_code_or": ["DE", "AT", "CH"] },
  "waterfall": { "strategy": "max_coverage", "providers": ["betterjobs", "theirstack", "techmap"] }
}
```

Every slug must be enabled on your plan. `GET /v1/providers` shows `enabled_on_your_plan` per source, and `GET /v1/account` lists `providers_enabled`. Asking for a source outside your plan returns `403` [`plan_required`](https://docs.betterjobs.cc/platform/errors.md#plan_required) with `required_plan`.

## Build one

Pick a strategy and copy the request.

## Budget knobs

These work with every strategy.

| Field                   | What it does                                                                                             | Set it when                             |
| ----------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `waterfall.max_credits` | Hard cap on credits for this request. Results stop at the cap.                                           | Always, in production.                  |
| `waterfall.timeout_ms`  | Time budget, `1000` to `30000`, default `10000`. Slow providers are dropped and the result is `partial`. | A user is waiting on the response.      |
| `waterfall.min_sources` | Sources that must agree for `consensus`, `2` to `7`, default `2`.                                        | Raising it trades volume for certainty. |
| `dry_run`               | Free estimate, nothing fetched.                                                                          | Before any large or new query.          |

## Pitfalls

> max\_coverage means your plan's coverage
>
> `max_coverage` asks every provider **your plan enables**. On Growth that is the BetterJobs index plus two partner providers; Pro and above include all six. Check `metadata.providers.tried` to see who was asked.

> consensus drops jobs, it does not find more
>
> A job seen by one source only is left out, even if it is real. Use `consensus` to act on jobs, not to count them. It also needs at least `min_sources` sources enabled on your plan.

- **`cheapest_first` is not for sync.** It stops when the page is full, so the set of sources can change between runs. Use `max_coverage` for copies you keep.
- **`freshest_first` changes order, not data.** It asks fast-refreshing sources first. Judge freshness on each job with `first_seen_at`, `last_seen_at` and `last_verified_at`. See [Freshness and lifecycle](https://docs.betterjobs.cc/concepts/freshness-and-lifecycle.md).
- **Strategies cost the same per job.** Every source costs 1 credit per unique job. A strategy changes how many jobs you get, not the price of one.
- **Partial is not failure.** Any strategy can return `metadata.status: partial` when a provider times out. You pay only for returned jobs.

## Next

- [The waterfall](https://docs.betterjobs.cc/concepts/waterfall.md): how fan-out, merge and billing work.
- [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md): `p_real` and `sources[]`.
- [Providers](https://docs.betterjobs.cc/providers.md): what each source sells.
