Endpoints / List jobs

List jobs

Search the job index. Every filter and how they combine, cursor paging, and how collapsing folds the same role posted to several boards into one row.

GET/v1/jobs
GEThttps://api.hyperjobs.io/v1/jobs

Search ~597k non-duplicate postings. Every parameter is optional; with none, you get the 50 most recent jobs. Different parameters combine with AND; within a multi-value parameter, comma-separated values are OR — with one exception, skill, where every listed value is required. Invalid parameters and values are rejected with a 400 that names the offending parameter, and title / description are Google-style searches rather than substring matches. See filtering for the conceptual tour; this page is the exact contract.

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" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=50"

Always applied#

Two conditions are on every query and can't be disabled:

  • Duplicates are excluded. One row per posting, however many boards syndicated it. See duplicates.
  • Expired postings are excluded. Anything past its expires_at is gone. /v1/jobs/feed excludes them too; /v1/jobs/expired is the purge feed that tells you which ids went away.

Results are always ordered newest-first by posted_at, falling back to created_at (when we first indexed the posting) when it carries no publish date. This is not configurable — no sort parameter exists. See pagination.

Query parameters#

Text search

title
string

Search job titles. Google-style: machine learning (AND), "machine learning" (phrase), rust OR go, -intern (exclude). No longer a plain substring match. Invalid search syntax is a 400, not an empty result.

description
string

Search title + description combined — not description-only. Same syntax as title. The slowest filter in the set; always combine it with something narrower.

title_advanced
string

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

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".

The advanced params need the full-text index, and return a 400 on a deployment where it isn't built — check the full_text_search flag on /v1/meta before relying on them. Plain title and description always work.

Company

exclude_organization
string (comma-separated)

Exclude companies by exact name, case-insensitive. Comma-separated — exclude_organization=Crossmark,TieTalent drops both.

company_industry
string (comma-separated)

Company LinkedIn industry (exact, case-insensitive), comma = OR — e.g. Software Development.

company_size
string (comma-separated)

Company size bucket, comma = OR — e.g. 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)

Company HQ country — full name or ISO-2, comma = OR.

domain
string (comma-separated)

Company website domain (exact), comma = OR — e.g. stripe.com.

The company-axis filters match against the verified company profile, so a posting whose employer never resolved to one can't match them — see the three company shapes.

Location

country
string (comma-separated)

Country — full name or ISO-2 code, comma = OR. country=US,Germany finds jobs in either. An unknown ISO-2 code is a 400. There is no location parameter (a pre-release draft had one) — sending it is a 400; use country.

city
string (comma-separated)

City name, comma = OR — city=Berlin,Amsterdam.

region
string (comma-separated)

State / province / region, comma = OR — region=California.

continent
string (comma-separated)

Continent, comma = OR. One of Africa, Antarctica, Asia, Europe, North America, Oceania, South America.

remote
boolean

true (or 1) → remote jobs only; false (or 0) → on-site/hybrid only. Anything else is a 400.

work_arrangement
string (comma-separated)

Work arrangement, comma = OR. One or more of On-site, Remote, Hybrid. More expressive than remote, which can't say "hybrid".

There's still no geo search — no radius, no coordinates, no bounding box. Location filtering matches names (country, city, region, continent), though postings do carry location.coords and location.timezones for client-side work — see the Job object.

Classification

skill
string (comma-separated)

Required skills — comma = AND (every listed skill must be present), the one filter where comma isn't OR. skill=python,aws means both.

Stored as lower-case single tokens; well-known aliases are translated on the way in — c++cpp, c#csharp, .netdotnet, node.jsnode, golanggo among them. Multi-word display values don't round-trip: the Machine Learning you read back is stored as machinelearning, so ?skill=Machine Learning finds nothing. See skills & taxonomies.

taxonomies
string (comma-separated)

Job family, comma = OR (overlap match), Title-Case — Software, Healthcare, Finance & Accounting. See enums for all 28.

employment_type
string (comma-separated)

Employment type, comma = OR. One or more of FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, PER_DIEM, VOLUNTEER, OTHER.

seniority
string (comma-separated)

Seniority level, comma = OR. One or more of Internship, Entry level, Associate, Mid-Senior level, Director, Management.

experience_level
string (comma-separated)

Years-of-experience bucket, comma = OR. One or more of 0-2, 2-5, 5-10, 10+.

education
string (comma-separated)

Education requirement, comma = OR (overlap match), lower-case. One or more of high school, professional certificate, associate degree, bachelor degree, postgraduate degree.

language
string (comma-separated)

Job-description language — full name (English) or ISO-639-1 (en), comma = OR.

visa_sponsorship
"only" | "exclude"

only → jobs that mention visa sponsorship; exclude → hide them.

Comma means OR — except on skill

Every multi-value filter treats commas as OR: country=US,DE is either, employment_type=FULL_TIME,CONTRACTOR is either. The exception is skill, which stays ANDskill=python,aws requires both.

BASH
curl "…/v1/jobs?skill=python,aws"          # tagged BOTH — skill is AND
curl "…/v1/jobs?seniority=Director,Management"  # either — everything else is OR

Enum values are still matched exactly, case and all — but a wrong value is now a 400 whose hint lists the allowed values, instead of a silent empty result. seniority=mid-senior level tells you it wanted Mid-Senior level.

Salary

has_salary
boolean

true → only jobs with structured salary data. About 37% of the pool qualifies.

salary_gte
number

Annualized salary floor: matches jobs whose salary range reaches this value (annual_max >= value). Range-overlap semantics — see the warning below.

salary_lte
number

Annualized salary ceiling: matches jobs whose range starts at or below this value (annual_min <= value).

salary_currency
string (comma-separated)

ISO-4217 currency of the posted salary, comma = OR, case-insensitive — USD,EUR.

salary_gte means "could pay this much"

salary_gte is a range-overlap filter: it matches every job whose advertised band reaches the value. A role listed at $90k–$210k matches salary_gte=200000, because $200k is inside its band. That's the documented semantics — "the job could pay this much" — not "the floor is at least this much".

For a strict floor, filter with has_salary=true and compare salary.annual_min client-side. And note there is no salary_min parameter — sending it is a 400. Salary data explains the annualisation.

Source

source
string (comma-separated)

ATS / source platform, comma = OR. Valid values are the keys of sources in /v1/meta — public slugs like greenhouse,lever. See the sources reference.

source_type
string (comma-separated)

Provenance class, comma = OR. ats — a board the employer authors and manages applicants on, the canonical record (join.com included); jobboard — third-party boards (LinkedIn, Wellfound, …); aggregator — re-listing sites. Cross-source duplicates are already folded, so this filters provenance, not duplication — source_type=ats doesn't remove mirror copies (dedup did), it selects records the employer authored.

exclude_source
string (comma-separated)

Exclude source platforms, comma-separated. Same vocabulary as source.

One vocabulary everywhere: the filter input, the keys of /v1/meta.sources, and the source field on every job payload all use the same public slug (the stored value lever.co is still accepted as filter input, but you'll never see it in a response).

Time

time_frame
<number><m|h|d>

Posting-age shortcut: 15m, 2h, 24h, 7d, 30d. m is minutes — there's no week or month unit. Measured against the posting date. A value that doesn't parse is a 400, and there is no posted_since alias.

posted_gte
ISO 8601 date

Jobs posted on/after this instant — 2026-07-01T00:00:00Z. The absolute alternative to 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. Use for incremental sync — or better, use the feed.

created_lt
ISO 8601 date

Jobs we first indexed before this instant.

Watch the unit: m is minutes, not months — time_frame=30m is half an hour, not two and a half years. (An early draft of this API used m for months.) And an unparseable window is a 400 rather than a silently ignored filter, so time_frame=1week can't return all of history with a straight face.

Output

description_format
"text" | "html"default: omitted

Include the job description, as clean plain text or sanitized html. Omit the parameter and the description key is absent from the response entirely, not null — smaller payloads. Any other value is a 400.

collapse
"true" | "title,organization"

Fold repeats of the same role into one row per company + title, adding an openings count to each job. The representative row is the newest in its group. total becomes the number of groups, not the number of postings, and the response carries no next_cursor — page collapsed results with offset. See collapsing.

Descriptions are omitted by default — and that's usually what you want

Descriptions are multi-kilobyte. Requesting 200 of them when you wanted titles is the most common way to make this API feel slow. Ask for them on the single-job endpoint once you know which job you care about.

Paging

limit
integerdefault: 50

Results per page — minimum 1, maximum 200. Out-of-range values are a 400, including limit=0 and negative numbers — there is no negative-limit-returns-everything backdoor.

offset
integerdefault: 0

Rows to skip — minimum 0, maximum 100000. Fine for shallow pages; for deep pagination prefer cursor. Ignored when cursor is passed.

cursor
string

Opaque keyset cursor from the previous page's next_cursor — stable at any depth, unlike offset. Page until next_cursor comes back null. A malformed cursor is a 400.

Counting cheaply: /v1/jobs/count takes the same filter set and returns just { count } — no need for the old limit=1-and-read-total trick, though that still works.

Response#

data
Job[]

The page of matching jobs, newest first. Every field is documented in the Job object; company is embedded, so there's no second request. With collapse, each job also carries openings.

total
integer

Exact count of all matching rows, not the page size. It's a real COUNT(*) over the same filters rather than an estimate, so a very broad query pays to count rows it will never return — use /v1/jobs/count when the count is all you want. With collapse, it counts groups.

limit
integer

The effective page size.

offset
integer

The effective offset. Omitted when you paginate with cursor.

next_cursor
string | null

Opaque cursor for the next page; null when you've reached the end. Pass it back as ?cursor=. Absent on collapse responses — page those with offset. See pagination.

collapsed
true

Present only when collapse was applied. Absent otherwise, not false.

Response
{
  "data": [
    {
      "id": "ashby:7c1f2a94-3e17-4d8b-9c05-1f6ab2e40d33",
      "title": "Staff Backend Engineer",
      "company": {
        "slug": "linear",
        "name": "Linear",
        "domain": "linear.app",
        "website": "https://linear.app",
        "logo": "https://logo.hyperjobs.io/linear.app",
        "industry": "Software Development",
        "employee_count": 120
      },
      "location": {
        "remote": true,
        "countries": ["United States"],
        "cities": null,
        "regions": null,
        "coords": null,
        "timezones": ["America/New_York"],
        "continent": "North America",
        "raw": ["Remote (US)"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "Remote",
      "office_days": null,
      "remote_location": ["United States"],
      "department": "Engineering",
      "team": "Sync Engine",
      "requisition_id": "ENG-142",
      "seniority": "Mid-Senior level",
      "experience_level": "5-10",
      "salary": {
        "min": 190000,
        "max": 240000,
        "currency": "USD",
        "unit": "YEAR",
        "annual_min": 190000,
        "annual_max": 240000,
        "summary": "$190,000 – $240,000 a year"
      },
      "working_hours": 40,
      "skills": ["typescript", "graphql", "postgresql"],
      "keywords": ["distributed systems", "real-time sync"],
      "taxonomies": ["Software"],
      "education_requirements": ["bachelor degree"],
      "visa_sponsorship": null,
      "job_language": "en",
      "requirements_summary": "8+ years building distributed backend systems…",
      "core_responsibilities": "Own the sync engine's server-side…",
      "benefits": ["Equity", "Health, dental & vision", "401(k) matching"],
      "hiring_manager": null,
      "apply_url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
      "url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
      "source": "ashby",
      "source_type": "ats",
      "source_domain": "jobs.ashbyhq.com",
      "posted_at": "2026-07-08T14:02:11.000Z",
      "expires_at": null,
      "modified_at": "2026-07-12T08:30:05.000Z",
      "modified_fields": ["salary_max"],
      "created_at": "2026-07-08T14:10:22.000Z",
      "updated_at": "2026-07-14T02:15:40.000Z"
    }
  ],
  "total": 21497,
  "limit": 50,
  "offset": 0,
  "next_cursor": "eyJwIjoiMjAyNi0wNy0wOCAxNDowMjoxMSIsImlkIjoiYXNoYnk6N2MxZjJhOTQuLi4ifQ"
}

employment_type, education_requirements, and benefits are real JSON arrays — no JSON.parse needed (a pre-release draft returned them as raw JSON strings). description is absent above because it wasn't requested. apply_url falls back to url, posted_at falls back to the ATS creation date, and updated_at falls back to created_at — so none of the three are null in practice. location is never null either: remote defaults to false, so the object is always there even when every list inside it is empty.

Errors#

StatusCause
400Invalid parameter or value. The body names the parameter and hints the fix.
401Missing, unknown, or revoked key.
429Rate limit exceeded, or monthly quota exhausted.
500Server error. Retry with backoff.
400
{
  "error": "invalid request",
  "param": "seniority",
  "hint": "invalid value \"mid-senior level\" — allowed: Internship, Entry level, Associate, Mid-Senior level, Director, Management"
}

Validation is strict: unknown parameters, empty values, bad enum members, and malformed numbers, dates, or cursors are all 400s — nothing is silently coerced or dropped, so a typo'd filter can never masquerade as an empty result. Hints point at /openapi.json, the live machine-readable spec (served without a key). See errors.

Was this page helpful?