Guides / Filtering jobs

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.

BASH
# 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_at is 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:

ModeFiltersBehaviour
Searchtitle, description (+ _advanced variants)Google-style full-text: words AND, "quoted phrases", OR, -exclusions.
SubstringorganizationCase-insensitive "contains".
Valuecountry, city, region, continent, seniority, work_arrangement, experience_level, language, source, salary_currency, the company_* familyMatches the whole value, comma = OR. Enum-backed ones 400 on a value outside the vocabulary.
Tokenskill, taxonomies, employment_type, educationMatches discrete values inside the job's list. Comma = OR — except skill, where it's AND.
title
string

Search job titles. Google-style: machine learning (AND), "machine learning" (phrase), rust OR go, -intern (exclude).

description
string

Search title + description combined. Same syntax as title. The slowest filter — combine it with others.

title_advanced
string

Boolean title search: & AND · | OR · ! NOT · <-> adjacency · term:* prefix · 'multi word phrase' · parentheses.

description_advanced
string

Boolean search on title + description. Same operators as title_advanced.

organization
string

Case-insensitive substring match on the hiring company name. organization=stripe matches "Stripe" and "Stripes Co".

BASH
# 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#

skill
string (comma-separated, AND)

Required 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 seeStored as?skill= with the displayed value
Machine Learningmachinelearning0 rows — use machinelearning
CI/CDcicd0 rows — use cicd
Quality Assurancequalityassurance0 rows
C++cppWorks — 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#

taxonomies
string (comma-separated, OR)

Job family, overlap match. 28 Title-Case values — Software, Healthcare, Finance & Accounting, Trades… See enums.

seniority
string (comma-separated, OR)

One of Internship, Entry level, Associate, Mid-Senior level, Director, Management.

experience_level
string (comma-separated, OR)

Years-of-experience bucket. One of 0-2, 2-5, 5-10, 10+.

employment_type
string (comma-separated, OR)

FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, PER_DIEM, VOLUNTEER, OTHER. Upper-case.

education
string (comma-separated, OR)

high 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.)

BASH
# 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:

BASH
# 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#

country
string (comma-separated, OR)

Country — full name or ISO-2 code. US,Germany both work. An unknown ISO-2 code is a 400.

city
string (comma-separated, OR)

City name.

region
string (comma-separated, OR)

State / province / region — California, Bavaria.

continent
string (comma-separated, OR)

One of the seven continents — Europe, North America… See enums.

remote
boolean

true → remote jobs only; false → on-site/hybrid only. Accepts true/false/1/0.

work_arrangement
string (comma-separated, OR)

Remote, 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.

BASH
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 parameter

There'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_salary
boolean

Only postings with a parsed salary. About 37% of the dataset qualifies.

salary_gte
number

Annualised salary floor with range-overlap semantics: matches jobs whose advertised band reaches the value (annual_max >= X).

salary_lte
number

Annualised ceiling: matches jobs whose band starts at or below the value (annual_min <= Y).

salary_currency
string (comma-separated, OR)

ISO-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_industry
string (comma-separated, OR)

Company LinkedIn industry, exact and case-insensitive — Software Development.

company_size
string (comma-separated, OR)

Size bucket — 51-200 employees,201-500 employees.

employee_count_gte
integer

Minimum company employee count (inclusive).

employee_count_lt
integer

Maximum company employee count (exclusive).

funding_gte
integer

Minimum company total funding in USD (Crunchbase, inclusive).

funding_lt
integer

Maximum company total funding in USD (exclusive).

company_country
string (comma-separated, OR)

Company HQ country — full name or ISO-2.

domain
string (comma-separated, OR)

Company website domain, exact — stripe.com.

exclude_organization
string (comma-separated)

Exclude companies by exact name, case-insensitive — the way to drop a staffing agency that floods your results.

BASH
# 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_frame
<number><m|h|d>

Only postings newer than the window. 15m, 2h, 24h, 7d, 30d.

posted_gte
ISO-8601 date

Jobs posted on/after this instant — 2026-07-01T00:00:00Z. The absolute form of time_frame.

posted_lt
ISO-8601 date

Jobs posted before this instant.

created_gte
ISO-8601 date

Jobs we first indexed on/after this instant — the checkpoint parameter for incremental sync.

created_lt
ISO-8601 date

Jobs we first indexed before this instant.

m means minutestime_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#

source
string (comma-separated, OR)

The platform the posting came from, by its public slug — greenhouse, ashby, lever. Valid values are the keys of /v1/metasources; see the sources reference.

exclude_source
string (comma-separated)

Exclude platforms — exclude_source=linkedin for direct-from-ATS rows only.

language
string (comma-separated, OR)

The detected language of the posting — full name (English) or ISO-639-1 (en).

visa_sponsorship
"only" | "exclude"

only → postings that mention sponsorship; exclude → hide them.

collapse
"true"

Fold multi-location repeats into one row per company + title, with an openings count. See collapsing.

description_format
"text" | "html"default: omitted

Include 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. skill ANDs its commas, and separate parameters always AND. Drop filters one at a time and watch total — or cheaper, point the same query at /v1/jobs/count.
  • A skill token that didn't round-trip. skill is free-text, so skill=Machine Learning returns zero rows without an error — the stored token is machinelearning. See the warning above.
Was this page helpful?