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

Async searches

How do I run a search for up to 10,000 jobs and collect the results?

View .md

A synchronous search returns at most 100 jobs per page. For backfills and market sizing, start an async search instead. It runs in the background, collects up to 10,000 unique jobs, and tells you when it is done.

  1. POST /v1/searches with your filters. You get 202 and a search id (srch_...).
  2. Wait. Poll GET /v1/searches/{id}, or pass webhook_url and receive search.completed.
  3. Read the results page by page with GET /v1/searches/{id}?cursor=.... Reading is free.

The body takes the same filters and waterfall as POST /v1/jobs/search. limit is the total number of unique jobs to collect: 1 to 10000, default 1000.

Terminal window
curl https://api.betterjobs.cc/v1/searches \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01" \
-H "Idempotency-Key: 7b8f2c4e-1a3d-4f5b-9c6e-0d2a4b6c8e1f" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"title_or": ["Data Engineer", "Analytics Engineer"],
"country_code_or": ["DE", "FR", "NL", "ES", "PL"],
"posted_within_days": 30
},
"waterfall": { "strategy": "max_coverage", "max_credits": 5000 },
"limit": 5000,
"webhook_url": "https://hooks.northwind.example/betterjobs"
}'

The 202 body is the search itself, with status: queued, jobs_found: 0, empty data and metadata: null.

A search moves through six states. Three of them are final.

status Meaning What to do
queued Accepted, not started yet. Wait.
running Collecting jobs. jobs_found counts unique jobs so far. Wait. Show jobs_found as progress.
on_hold Out of credits. Collection is paused. Top up or upgrade. The search resumes on its own.
completed Finished. Every planned provider answered. Read results.
partial Finished, but at least one provider failed or timed out. Read results. metadata.providers.failed names the provider. You pay only for collected jobs.
failed Finished without results. Check metadata if present, then start a new search.

completed, partial and failed are final. data holds results only once the status is completed or partial. It is empty in every other state.

Polling is free, but it counts against your rate limit. Start with a short interval, back off, and give up at a deadline you choose. Treat on_hold as a signal to a human, not as a reason to poll forever.

Terminal window
curl https://api.betterjobs.cc/v1/searches/srch_2Vd9KqL4mN \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01"

The Python and TypeScript tabs reuse API and HEADERS from the create example above.

Pass webhook_url when you create the search. When the search reaches a final status, BetterJobs POSTs a search.completed event to that URL. watch_id is null, because no watch produced it.

{
"id": "evt_8Re5TyU1oP",
"type": "search.completed",
"created_at": "2026-10-11T09:06:45Z",
"watch_id": null,
"data": {
"search": { "id": "srch_2Vd9KqL4mN", "status": "completed", "jobs_found": 3184 }
}
}

The event fires for completed, partial and failed. Branch on data.search.status, then read results with GET /v1/searches/{id} as in collect() above. Verify the signature first. See Webhooks.

Use both if you can: the webhook for speed, and a slow poll as a safety net in case a delivery is missed.

  • Jobs are charged as the search collects them: 1 credit per unique job you have not paid for before. Duplicates and already-paid jobs are free.
  • Reading results with GET /v1/searches/{id} is free, however many times you read them.
  • waterfall.max_credits caps the whole search. Set it to the most you are willing to spend.
  • If credits run out, the search moves to on_hold instead of failing. It resumes after a top-up.

POST /v1/searches has no dry_run, and unknown body fields return 400 invalid_request. To size a search before you start, send the same filters and waterfall to POST /v1/jobs/search with "limit": 100 and "dry_run": true. It is free. expected_unique_jobs_range counts every matching job, across all pages; credits_range prices one page of 100 only. Expect the async search to cost at most the smallest of expected_unique_jobs_range.max, your limit and waterfall.max_credits. max_credits is the only hard bound. See Credits and billing.