Every search takes one filters object. The same object works in POST /v1/jobs/search, POST /v1/searches and in search watches (POST /v1/watches with type: search). BetterJobs translates it into each provider’s own query language, so you learn one grammar instead of six.
Build a query
Section titled “Build a query”Change the fields and copy the request. The output updates as you type.
Grammar
Section titled “Grammar”A filter name is a field name plus an optional suffix. The suffix says how to compare.
| Suffix | Meaning | Value type | Example |
|---|---|---|---|
_or |
Match any value in the list | array | "country_code_or": ["DE", "AT", "CH"] |
_not |
Exclude any value in the list | array | "title_not": ["Intern"] |
_gte |
Greater than or equal to | number | "salary_min_gte": 90000 |
| none | Exact value or special rule (see the table below) | boolean or integer | "remote": true |
Two rules hold for every request:
- Different filters combine with AND. A job must pass every filter you send.
- Values inside one
_orlist combine with OR.title_or: ["Head of RevOps", "Head of Revenue Operations"]matches either title.
All filters
Section titled “All filters”| Filter | Type | What it does |
|---|---|---|
title_or |
string[] | Match any of these title keywords. |
title_not |
string[] | Exclude titles containing any of these. |
country_code_or |
string[] | ISO 3166-1 alpha-2 codes, for example DE. |
posted_within_days |
integer, 1 to 365 | Jobs first seen within this many days. |
seniority_or |
Seniority[] | Any of intern, junior, mid, senior, lead, director, vp, c_level. |
employment_type_or |
EmploymentType[] | Any of full_time, part_time, contract, internship, temporary. |
remote |
boolean or null | true remote only, false non-remote only, null or omitted = any. |
salary_min_gte |
number | Keep jobs whose salary.min is at least this value (yearly, in the job’s currency). |
company_domain_or |
string[] | Company domains, for example acme-robotics.example. |
job_family_or |
string[] | Job families, for example operations, sales. |
include_closed |
boolean, default false |
Include jobs with status: closed. |
Enum values for seniority_or and employment_type_or are listed on Taxonomies. What each job field means is on the field dictionary.
Examples
Section titled “Examples”The landing-page query: Head of RevOps in Germany, Austria and Switzerland, first seen in the last 7 days.
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"], "title_not": ["Intern"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7 }, "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"], "title_not": ["Intern"], "country_code_or": ["DE", "AT", "CH"], "posted_within_days": 7, }, "limit": 25, }, timeout=30,)resp.raise_for_status()jobs = resp.json()["data"]const res = 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"], title_not: ["Intern"], country_code_or: ["DE", "AT", "CH"], posted_within_days: 7, }, limit: 25, }),});if (!res.ok) throw new Error(`BetterJobs ${res.status}: ${await res.text()}`);const { data: jobs } = await res.json();More filters objects you can drop into the same request:
{ "seniority_or": ["senior", "lead"], "remote": true, "salary_min_gte": 90000 }Senior or lead remote roles that state a minimum of at least 90,000 per year.
{ "company_domain_or": ["acme-robotics.example", "northwind.example"], "job_family_or": ["sales"] }Sales jobs at two named companies. Pair it with a watch to hear about new ones.
{ "company_domain_or": ["acme-robotics.example"], "include_closed": true, "posted_within_days": 90 }Every job, open or closed, a company listed in the last 90 days. Useful to measure hiring history.
Unknown filters are rejected
Section titled “Unknown filters are rejected”A filter name that does not exist returns 400 with code unknown_filter. BetterJobs never silently ignores a filter, because an ignored filter returns a wider, more expensive result than you asked for. error.param names the bad field:
{ "error": { "type": "invalid_request_error", "code": "unknown_filter", "message": "Unknown filter field 'job_title_or'. Did you mean 'title_or'?", "param": "filters.job_title_or", "doc_url": "https://docs.betterjobs.cc/platform/errors/#unknown_filter", "request_id": "req_4Bn8CxV2zA" }}A rejected request is never charged. See unknown_filter in the error catalog.
Check the cost before you fetch
Section titled “Check the cost before you fetch”Add "dry_run": true to any POST /v1/jobs/search body. You get a free estimate (expected_unique_jobs_range, providers_planned, credits_range) and nothing is fetched or charged. Set waterfall.max_credits to cap what a real request can spend. See Credits and billing.
Related
Section titled “Related”- Pagination: read more than one page of results.
- Async searches: run filters over up to 10,000 jobs.
- Choose a strategy: decide which providers a filter runs against.