- Documentation
- Guides
Filtering jobs
The full filter vocabulary for /v1/jobs, with the exact matching semantics behind each one.
Every filter on /v1/jobs is a query
parameter. Separate parameters combine with AND; within one parameter, a
comma means OR — with a single exception, skill, where a comma stays AND.
The same filter set drives /v1/jobs/count
when you want the number without the rows.
# Remote Python roles in the US or Canada, posted in the last week, with a salary.
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "skill=python" \
--data-urlencode "work_arrangement=Remote" \
--data-urlencode "country=US,Canada" \
--data-urlencode "has_salary=true" \
--data-urlencode "time_frame=7d"Bad input is a 400#
Every parameter name and value is validated. An unknown parameter, an empty
value, a misspelled enum member, an unrecognised ISO code — each returns
400 {"error": "invalid request", "param": "…", "hint": "…"} naming the
offending parameter — nothing is silently dropped.
The practical consequence: an empty result set means nothing matched. A typo'd filter can never masquerade as "no jobs found" — a request that only "works" by ignoring a bad parameter fails loudly instead. See errors.
Always applied#
Two conditions are on every /v1/jobs query and can't be turned off:
- Duplicates are excluded. The same posting syndicated to several boards is collapsed to one row. See duplicates.
- Expired postings are excluded. Anything past its
expires_atis gone.
/v1/jobs/feed applies the same two
exclusions. Deletions reach you separately through
/v1/jobs/expired — see
feed sync.
Results are always newest-first. That isn't configurable — see pagination.
The four matching modes#
Every filter behaves in one of four ways, and knowing which is which explains almost every surprising result:
| Mode | Filters | Behaviour |
|---|---|---|
| Search | title, description (+ _advanced variants) | Google-style full-text: words AND, "quoted phrases", OR, -exclusions. |
| Substring | organization | Case-insensitive "contains". |
| Value | country, city, region, continent, seniority, work_arrangement, experience_level, language, source, salary_currency, the company_* family | Matches the whole value, comma = OR. Enum-backed ones 400 on a value outside the vocabulary. |
| Token | skill, taxonomies, employment_type, education | Matches discrete values inside the job's list. Comma = OR — except skill, where it's AND. |
Text search#
titleSearch job titles. Google-style: machine learning (AND), "machine learning" (phrase), rust OR go, -intern (exclude).
descriptionSearch title + description combined. Same syntax as title. The slowest
filter — combine it with others.
title_advancedBoolean title search: & AND · | OR · ! NOT · <-> adjacency ·
term:* prefix · 'multi word phrase' · parentheses.
description_advancedBoolean search on title + description. Same operators as title_advanced.
organizationCase-insensitive substring match on the hiring company name.
organization=stripe matches "Stripe" and "Stripes Co".
# Rust or Go, no interns — the simple syntax covers most needs.
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "title=rust OR go -intern"
# The same query in boolean form, with a prefix wildcard.
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "title_advanced=(rust | go) & !intern"Malformed syntax — an unbalanced quote, a dangling operator — returns
400 {"error": "invalid search syntax", "param": "…", "hint": "…"} rather
than matching nothing.
Search matches words, but results still come back newest-first, not by
relevance — there is no relevance ranking. description= remains the one
filter that can make a request noticeably slow; always pair it with something
narrower. The _advanced variants require the full-text index and return a
400 on deployments without it — check the full_text_search flag on
/v1/meta.
Skills#
skillRequired skills — every listed skill must be present. Exact tokens:
skill=python, skill=c++, skill=react.
Skills are stored as canonical tokens, and the well-known aliases are
translated for you on the way in: c++, c#, .net, node.js, react.js,
vue.js, and golang all work even though they're stored as cpp, csharp,
dotnet, node, react, vue, and go.
Displayed skill values don't always round-trip
The skills array in a response is display-formatted. Feeding a displayed
value back as a filter doesn't always work:
| You see | Stored as | ?skill= with the displayed value |
|---|---|---|
Machine Learning | machinelearning | 0 rows — use machinelearning |
CI/CD | cicd | 0 rows — use cicd |
Quality Assurance | qualityassurance | 0 rows |
C++ | cpp | Works — aliased |
The rule: multi-word display names collapse to one lower-case token. If a
displayed skill has a space or a slash in it, strip them before filtering.
skill is a free-text filter, so this is the one remaining way a plausible
value returns zero rows instead of a 400.
Skills & taxonomies has the full list.
Category and level#
taxonomiesJob family, overlap match. 28 Title-Case values — Software, Healthcare,
Finance & Accounting, Trades… See enums.
seniorityOne of Internship, Entry level, Associate, Mid-Senior level,
Director, Management.
experience_levelYears-of-experience bucket. One of 0-2, 2-5, 5-10, 10+.
employment_typeFULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, PER_DIEM,
VOLUNTEER, OTHER. Upper-case.
educationhigh school, professional certificate, associate degree,
bachelor degree, postgraduate degree. Lower-case, overlap match.
These are still exact and case-sensitive — but a wrong value is now a 400
with the allowed list in the hint. seniority=mid-senior level errors
loudly instead of returning an empty set, so a casing mistake can no longer
masquerade as "no jobs match".
Comma means OR — except skill#
Every multi-value filter treats commas as OR — you get a union in one request. (An early draft of this API read commas as AND everywhere, which made a union cost one request per value merged client-side.)
# Software OR Healthcare — the union, in one request.
curl "…/v1/jobs?taxonomies=Software,Healthcare"
# Roles in Germany OR the Netherlands, at Director OR Management level.
curl "…/v1/jobs?country=DE,NL&seniority=Director,Management"The one exception is skill, which is deliberately AND — it expresses
required skills, and skill=react,typescript,graphql means all three:
# NOT "Python or AWS". This is "tagged BOTH Python AND AWS".
curl "…/v1/jobs?skill=python,aws"Different parameters still combine with AND, so country=DE,NL&remote=true
is "(Germany OR Netherlands) AND remote". There's no OR across parameters —
"remote OR posted this week" is still two requests merged by id.
A bad member inside a comma list — taxonomies=Software,Healthcareee —
returns a 400 naming the value, not a quietly smaller result set.
Locations#
countryCountry — full name or ISO-2 code. US,Germany both work. An unknown
ISO-2 code is a 400.
cityCity name.
regionState / province / region — California, Bavaria.
continentOne of the seven continents — Europe, North America…
See enums.
remotetrue → remote jobs only; false → on-site/hybrid only. Accepts
true/false/1/0.
work_arrangementRemote, Hybrid, or On-site. More precise than remote, which can't
express hybrid.
`location` is gone, and matching is whole-value now
The pre-release draft's location alias was removed — sending it is a 400.
The geo parameters are country, city, region, and continent.
Matching is against the whole value, not a substring: country=United no
longer matches "United Kingdom" and "United Arab Emirates". Say what you
mean — country=GB,US, or spell the names out.
curl "…/v1/jobs?country=US" # ✓ ISO-2 works now
curl "…/v1/jobs?country=United States,DE" # ✓ names and codes mix freely
curl "…/v1/jobs?location=United States" # ✗ — 400, unknown parameterThere's still no geo search — no radius, no bounding box. Coordinates are
exposed on the Job object (location.coords) so you can compute distance
client-side, but they aren't filterable server-side.
Salary#
has_salaryOnly postings with a parsed salary. About 37% of the dataset qualifies.
salary_gteAnnualised salary floor with range-overlap semantics: matches jobs
whose advertised band reaches the value (annual_max >= X).
salary_lteAnnualised ceiling: matches jobs whose band starts at or below the value
(annual_min <= Y).
salary_currencyISO-4217 currency of the posted salary — USD,EUR. Case-insensitive.
salary_gte means “could pay this much”
salary_gte=200000 matches a job advertised at $90k–$210k, because the
band reaches 200k. That's the documented range-overlap semantics — the
result set is "jobs that could pay X", not "jobs whose floor is at least X".
Postings with no upper bound (annual_max: null) don't match at all. For a
strict floor, compare salary.annual_min client-side —
Salary data has the full treatment.
Company#
Every job embeds its company; these parameters filter on that profile. A job whose company hasn't been verified won't match any of them.
company_industryCompany LinkedIn industry, exact and case-insensitive —
Software Development.
company_sizeSize bucket — 51-200 employees,201-500 employees.
employee_count_gteMinimum company employee count (inclusive).
employee_count_ltMaximum company employee count (exclusive).
funding_gteMinimum company total funding in USD (Crunchbase, inclusive).
funding_ltMaximum company total funding in USD (exclusive).
company_countryCompany HQ country — full name or ISO-2.
domainCompany website domain, exact — stripe.com.
exclude_organizationExclude companies by exact name, case-insensitive — the way to drop a staffing agency that floods your results.
# Python roles at big software companies — not the job's country, the employer's.
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "skill=python" \
--data-urlencode "company_industry=Software Development" \
--data-urlencode "employee_count_gte=1000"Time#
time_frameOnly postings newer than the window. 15m, 2h, 24h, 7d, 30d.
posted_gteJobs posted on/after this instant — 2026-07-01T00:00:00Z. The absolute
form of time_frame.
posted_ltJobs posted before this instant.
created_gteJobs we first indexed on/after this instant — the checkpoint parameter for incremental sync.
created_ltJobs we first indexed before this instant.
m means minutes — time_frame=30m is half an hour. The units are m
(minutes), h (hours), d (days); there is no week or month unit, and an
unparseable value like 1week is a 400, not silently all-time. There is no
posted_since alias (400) — use time_frame or posted_gte.
The window measures against the posting date, falling back to the ATS creation date and then to when we first indexed it.
Everything else#
sourceThe platform the posting came from, by its public slug — greenhouse,
ashby, lever. Valid values are the keys of
/v1/meta → sources; see the
sources reference.
exclude_sourceExclude platforms — exclude_source=linkedin for direct-from-ATS rows only.
languageThe detected language of the posting — full name (English) or ISO-639-1
(en).
visa_sponsorshiponly → postings that mention sponsorship; exclude → hide them.
collapseFold multi-location repeats into one row per company + title, with an
openings count. See collapsing.
description_formatInclude the description. Omitted by default — the field is absent, not null.
?source=lever works now — one vocabulary everywhere. Filter input, the
/v1/meta keys, and the source field on every Job payload all use the
public slug, so a value you read in a response feeds straight back as a
filter. (The old stored value lever.co is still accepted as input.)
Debugging an empty result set#
Most of the old debugging ritual is obsolete: a misspelled parameter, a bad enum member, or broken search syntax is now a 400 that names the problem, so an unexpectedly empty response almost always means the filters are valid but too tight. Two things to check:
- An accidental AND.
skillANDs its commas, and separate parameters always AND. Drop filters one at a time and watchtotal— or cheaper, point the same query at/v1/jobs/count. - A skill token that didn't round-trip.
skillis free-text, soskill=Machine Learningreturns zero rows without an error — the stored token ismachinelearning. See the warning above.