api
Filtering cookbook
How the filters combine, how to exclude rather than include, which text filter to reach for, and which queries are expensive enough to time out.
1Twenty questions, and the query for each
The queries people actually write, as copyable requests. Every one of them is sent against the live API before this site is allowed to build.
In depthWhy it exists, what it is not, what people get wrong
| Question | Query |
|---|---|
| Engineering roles in Germany | ?country=DE&category=Engineering |
| …that are remote, as stated by the employer | ?country=DE&category=Engineering&remote=remote&remote_confirmed=true |
| …with a salary the employer published | ?country=DE&require_fields=salary |
| Anything in three countries at once | ?country=DE,AT,CH |
| Everywhere except the US | ?exclude_country=US |
| Everything except sales roles | ?exclude_category=Sales |
| Only roles from Greenhouse and Lever | ?provider=greenhouse,lever |
| Everything except one provider | ?exclude_provider=workday |
| Only employer career sites, no ATS | ?source_type=career_site |
| Roles at three companies I care about | ?company_domain=stripe.com,figma.com,linear.app |
| Roles at one company by slug | ?company=but |
| Senior roles only | ?seniority=senior |
| Roles where seniority was not stated | ?seniority=not_stated |
| Full-time only | ?employment_type=full_time |
| Posted in the last week | ?posted_after=2026-08-16 |
| Re-confirmed since yesterday | ?verified_after=2026-08-22T00:00:00Z |
| Anything mentioning Kubernetes in the advert | ?description_contains=kubernetes&country=NL |
| Titles containing “engineer”, excluding “sales engineer” | ?title=engineer&title_exclude=sales |
| Roles that have an advert body at all | ?has_description=true |
| Closed roles, so I can mark my mirror stale | ?status=closed — or better, /v1/jobs/expired |
Prefix any of these with the endpoint: https://api.jobopportunitiesapi.org/v1/jobs. On the keyless mirror (/public/jobs) they all work except the four marked paid-endpoints-only in the parameter reference, which are ignored rather than refused.
Exact contractTypes, defaults, ranges, errors, edge cases
$ A=https://api.jobopportunitiesapi.org/public/jobs $ curl -s "$A?country=DE&category=Engineering&limit=2" | jq -r '.data[].title' $ curl -s "$A?exclude_category=Sales&limit=2" | jq -r '.data[].category' $ curl -s "$A?provider=greenhouse&limit=2" | jq -r '.data[].source' { "data": [ { "id": "322858b6-3bc1-47a2-8007-839ea33039af", "slug": "flight-software-engineer-322858b6", "title": "Flight Software Engineer", "company": "The Exploration Company", "company_slug": "the-exploration-company", "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/the-exploration.png", "category": "Engineering", "category_confidence": 0.8, "country": "DE", "city": "Munich", "location": "Munich, Germany", "remote": "on_site", "remote_inferred": true, "posted_at": "2026-09-11T09:04:22Z", "first_seen_at": "2026-09-11T09:23:55Z", "last_verified_at": "2026-09-11T09:50:45Z", "status": "live", "closed_at": null, "closed_reason": null, "apply_url": "https://jobs.ashbyhq.com/the-exploration-company/446ca823-dbc2-4f82-8a31-169690b75cf2/application", "source": "ashby", "source_type": "ats", "provider_type": "employer_ats", "has_description": true, "field_sources": { … 54 more lines
2How filters combine
Different parameters AND together. A comma-separated list ORs within one parameter. There is no way to OR across two different parameters.
In depthWhy it exists, what it is not, what people get wrong
?country=DE&category=Engineering means German and engineering. ?country=DE,AT means German or Austrian. Those are the only two combinators, and knowing that is most of what you need to predict a result set.
There is no OR across parameters — you cannot ask for “engineering roles in Germany or any role in Austria” in one request. Send two requests and merge. That is a deliberate limit: an expression language over 34 parameters would be a query planner's problem and a support burden, and two requests cost two records more than one.
Exact contractTypes, defaults, ranges, errors, edge cases
Order of evaluation does not matter and neither does parameter order in the query string. Repeating a parameter (?country=DE&country=AT) is not the way to send a list — use the comma form; the repeated form takes the first occurrence and ignores the rest.
require_fields composes with everything and applies after the filters, not instead of them: ?require_fields=salary&country=DE is German rows that carry a published salary, and the completeness block reports the ledger-wide count for salary rather than the German one — the intersection is at most the smaller of the two.
3Excluding rather than including
Five parameters exclude: category, country, source_type, provider and company_domain. There is no general negation.
In depthWhy it exists, what it is not, what people get wrong
| Exclude | Parameter | Its include twin |
|---|---|---|
| A category | exclude_category | category |
| A country | exclude_country | country |
| A source bucket | exclude_source_type | source_type |
| A provider | exclude_provider | provider |
| A company by domain | exclude_company_domain | company_domain |
| Words in a title | title_exclude | title |
Everything else can only be included. If you need “not senior”, ask for the seniorities you do want, or fetch and filter your side — the vocabulary is small and enumerable.
Exact contractTypes, defaults, ranges, errors, edge cases
exclude_provider shares the provider vocabulary, so an unknown name is a 422 there too. That is the exact shape of the failure that produced the build gate described in controlled vocabularies.
4Which text filter to use
q for a quick search over title, company and location. title for titles only. description_contains for the advert body — and it is the expensive one.
In depthWhy it exists, what it is not, what people get wrong
- Looking for a role by name
title=engineer. Narrowest and cheapest of the three.- Looking for a company or a place
q=berlin—qcovers title, company name and location together.- Looking for a skill or a technology
description_contains=kubernetes. Only this one reads the advert body.- Excluding a kind of role
title_exclude=salesalongsidetitle=engineer.
Exact contractTypes, defaults, ranges, errors, edge cases
description_contains implies has_description=true, since it can only match rows that have a body — you do not need to send both. Roughly nine live rows in ten carry one; check the current figure on the coverage report.
curl -s -H "Authorization: Bearer $JOA_KEY" --get \
https://api.jobopportunitiesapi.org/v1/jobs \
--data-urlencode 'description_contains=kubernetes' \
--data-urlencode 'country=NL' \
--data-urlencode 'limit=5' \
| jq -r '.data[] | "\(.company)\t\(.title)"'5Queries that can time out, and how to make them fast
Two full-text filters together across the whole ledger is the most expensive request this API takes. Narrow it by country or category first.
In depthWhy it exists, what it is not, what people get wrong
There is a ten-second statement timeout. Cross it and you get a 503 with Retry-After, not a 500 — the failure is honest and retryable. But retrying an expensive query unchanged mostly produces another timeout, so the useful response is to narrow it.
- Add a country. The cheapest and most effective narrowing available.
- Add a category, or a provider. Both are indexed and both cut the candidate set hard.
- Drop the second full-text filter.
titleanddescription_containstogether are the pathological case: both indexes are GIN and the matching rows still have to be fetched to be ordered byposted_at. - Reduce `require_fields`. Each additional required field is another condition over the provenance block.
- Lower `limit`. Less to fetch and less to serialise — and fewer records charged.
Exact contractTypes, defaults, ranges, errors, edge cases
| Shape | Cost | Do this instead |
|---|---|---|
?limit=200 with no filters | Cheap | Fine. |
?country=DE&category=Engineering | Cheap | Fine. |
?description_contains=x over the whole ledger | Expensive | Add country or category. |
?title=x&description_contains=y | Most expensive | Narrow to one country, or run them as two queries and intersect. |
?require_fields=salary,location,description | Moderate | Ask for the fields you actually need. |
?include_description=true&limit=50 | Large response, not a slow query | Fetch descriptions for the rows you need, not for every page. |
/v1/export with wide filters | Streams; cannot time out the same way | Use after= to resume rather than restarting. |
If you routinely need a whole slice of the corpus, /v1/export is the right tool rather than a paged listing — it streams NDJSON ordered by id, resumes exactly, and does not re-run an ordering query per page.
This page was rendered 11 September 2026, 10:39 UTC. Every figure on it comes from the endpoint named beside it, and every published request is re-sent against the live API before this site is allowed to build. If something here is wrong, the thumbs-down above reaches a person.