Job Opportunities API

Check the data. Then trust it.

API Access

Every field tagged with the source it came from, and every gap in the data published before you pay, including the ones that make us look bad. Collected employer-direct from applicant-tracking systems, employers' own careers pages and public agencies. 2,749,660 live openings across 141,331 employers (as of 2026-11-07).

Check our numbers right now: no key, no account
GET https://api.jobopportunitiesapi.org/public/facets

Loading live counts…

One job row, as a key returns it. A real row captured on 2026-08-03, shown as an example, not a live call. The counts above are what you can check without one.

{
  "data": [
    {
      "id": "8f2c…", "slug": "backend-engineer-8f2c1a4b",
      "title": "Backend Engineer", "company": "Nordbridge Labs",
      "company_slug": "nordbridge-labs",
      "category": "Engineering", "category_confidence": 0.82,
      "country": "DE", "city": "Berlin",
      "remote": "hybrid", "remote_inferred": false,
      "employment_type": "Full-time", "seniority": "Senior",
      "posted_at": "2026-07-12T09:00:00Z",
      "last_verified_at": "2026-08-03T04:00:00Z",
      "status": "live", "closed_at": null, "closed_reason": null,
      "source": "greenhouse",
      "source_type": "ats", "provider_type": "employer_ats",
      "has_description": true,
      "description": "We're looking for a backend engineer to join our platform team, "
        + "building the services that route millions of jobs a day. You'll work mostly in "
        + "Go, own a service end to end, and be on a small rotation for on-call. Remote "
        + "within the EU, or hybrid from our Berlin office two days a week.",
      "field_sources": {
        "remote": "published", "employment_type": "published",
        "category": "inferred", "seniority": "inferred",
        "salary": "absent", "location": "published",
        "posted_at": "published", "description": "published",
        "source_type": "inferred"
      }
    }
  ],
  "next_cursor": null,
  "has_more": true
}

Keyless requests return the ledger’s shape: counts, coverage and freshness. Job and company rows need a key. A free Primary key takes about a minute, needs no card, and gives you 1,000 records a month that never expire.

Get a free key →
  1. Enter your email.
  2. Click the link we send.
  3. Press “Create a free Primary key”. No card, and nothing to cancel.
Quick reference
Jobs
GET https://api.jobopportunitiesapi.org/v1/jobs
Companies
GET https://api.jobopportunitiesapi.org/v1/companies
Delta feed
GET https://api.jobopportunitiesapi.org/v1/changes?since=2026-07-01T00:00:00Z
Closed roles
GET https://api.jobopportunitiesapi.org/v1/jobs/closed?closed_after=2026-07-01
Freshness report
GET https://api.jobopportunitiesapi.org/v1/meta/freshness
Every source, keyless
GET https://api.jobopportunitiesapi.org/public/providers
Freshness, keyless
GET https://api.jobopportunitiesapi.org/public/freshness
How we compare
https://jobopportunitiesapi.org/compare, including where a rival wins
OpenAPI spec
GET https://api.jobopportunitiesapi.org/v1/openapi.yaml
Auth header
Authorization: Bearer YOUR_API_SECRET
Filters
?category=Engineering&country=DE
Confirmed only
?remote_confirmed=true&has_description=true&has_salary=true
Published fields only
?require_fields=salary,description
Without a key
statistics only: counts, coverage, freshness, providers
With the free key
job and company rows, keyset paging, 1,000 records a month, no card
Provenance
?source_type=ats,career_site,public_agency
Exact source
?provider=greenhouse,lever
Excluding a source
?exclude_provider=company_site
Title search
?title=platform+engineer&title_exclude=intern
Body search
?description_contains=kubernetes
Full text
?include_description=true&limit=50
By domain
?company_domain=stripe.com,figma.com
Salary in EUR
?min_salary=60000&max_salary=120000
Expiry feed
GET /v1/jobs/expired?since=2026-08-01T00:00:00Z
Exclusions
?exclude_category=Sales&exclude_country=US
/v1/jobs response
{
  "data": [
    {
      "id": "8f2c…", "slug": "backend-engineer-8f2c1a4b",
      "title": "Backend Engineer", "company": "Nordbridge Labs",
      "company_slug": "nordbridge-labs",
      "category": "Engineering", "category_confidence": 0.82,
      "country": "DE", "city": "Berlin",
      "remote": "hybrid", "remote_inferred": false,
      "employment_type": "Full-time", "seniority": "Senior",
      "posted_at": "2026-07-12T09:00:00Z",
      "last_verified_at": "2026-08-03T04:00:00Z",
      "status": "live", "closed_at": null, "closed_reason": null,
      "source": "greenhouse",
      "source_type": "ats", "provider_type": "employer_ats",
      "has_description": true,
      "description": "We're looking for a backend engineer to join our platform team, "
        + "building the services that route millions of jobs a day. You'll work mostly in "
        + "Go, own a service end to end, and be on a small rotation for on-call. Remote "
        + "within the EU, or hybrid from our Berlin office two days a week.",
      "field_sources": {
        "remote": "published", "employment_type": "published",
        "category": "inferred", "seniority": "inferred",
        "salary": "absent", "location": "published",
        "posted_at": "published", "description": "published",
        "source_type": "inferred"
      }
    }
  ],
  "next_cursor": "2026-07-12T09:00:00.481Z|8f2c…",
  "has_more": true
}

A real row captured on 2026-08-03, shown as a static example: the counts above are live; a free key returns current rows in this shape from /v1/jobs.

What you can check that a feed will not tell you
Every job says which fields we guessed. field_sources marks each field published (carried by the source), inferred (derived by us) or absent. 81% of the ledger has a remote status we worked out from the listing text rather than one the employer stated; ?remote_confirmed=true narrows to the stated ones. category and seniority are always inferred: they are read off the job title, and category_confidence says how sure.
Every job says when we last checked it was still open. last_verified_at is the last positive confirmation at the source (the posting's own page or ATS API record found live, or the posting listed in a fresh read of the employer's job list; otherwise its first harvest), not a posting date; last_verified_via names which (an Ashby board read is labelled board_api: a read of Ashby's own job board, its system of record, rather than of the individual posting URL, and counted as page-verified for that reason). Filter with ?verified_after=, and see the coverage for yourself at /v1/meta/freshness.
We keep what closed. /v1/jobs/closed returns roles that have come off their source, with closed_at and a closed_reason: expired_upstream when we proved the vacancy dead, not_seen when it simply stopped appearing, apply_link_dead when its apply link kept failing at the source for two weeks. A req that opened and closed in nine days is a hiring signal, not a dead link.
Provenance is a filter, not a footnote. source_type is ats, career_site or public_agency on every row, and provider names the exact system underneath, so ?provider=greenhouse,lever is a query parameter, not a post-filter you write yourself.
Search says which field it searched. ?q= covers title, company and location. ?title= covers the title alone, ?description_contains= the advert body, because “engineer in the title” and “engineer mentioned somewhere” are different questions and one box cannot answer both without choosing for you. They combine as an AND. Ask for the body text itself with ?include_description=true.
Salary you can actually compare. Sources quote USD an hour, GBP a year, EUR a month. salary_min_annual_eur is one number in one unit, so ?min_salary=60000 means something, and it is null, never a guess, when the period or currency is unknown. Only 27% of the ledger carries employer-stated pay, so this is a narrow filter; the rates and the hours-per-year we assumed are printed in /v1/meta/freshness so you can check our arithmetic.
The full machine-readable contract is at /v1/openapi.yaml (OpenAPI 3.1, no key required to read it).
Building with an AI agent
Worked examples with tool definitions for Claude and GPT, an integration brief to paste into a model, and the honest MCP disclosure are all on their own page: /for/ai-agents →

Explore

The search, job, company and closed-job endpoints against the live ledger, the same rows the paid tiers return. The delta and closure feeds start on Growth. No card, no expiry.

Free
1,000records / month
  • not includedDelta feed
  • not includedClosure feed
  • not includedBulk export
  • not includedCountry lock option
Get a free key →

Starter

A small integration or a first product: more room than a free key, before you need Growth's volume.

€29 / month
excl. VAT
10,000records / month
  • not includedDelta feed
  • not includedClosure feed
  • not includedBulk export
  • not includedCountry lock option
Choose Starter →

Growth

For one product: a job board, a dashboard, a research project.

€80 / month
excl. VAT
60,000records / month
  • ✓Delta feed
  • ✓Closure feed
  • not includedBulk export
  • not includedCountry lock option
Choose Growth →
Recommended

Signal

400,000 records a month across every country, or uncapped job rows in one country with the optional country lock, plus the delta and closure feeds and bulk export.

€299 / month
excl. VAT
400,000records / month
  • ✓Delta feed
  • ✓Closure feed
  • ✓Bulk export
  • ✓Country lock option
Choose Signal →

Scale

Full-corpus refreshes, bulk export and the closure feed.

€899 / month
excl. VAT
2,000,000records / month
  • ✓Delta feed
  • ✓Closure feed
  • ✓Bulk export
  • not includedCountry lock option
Choose Scale →

Free access for students, nonprofits and research

Tell us what you are building and why. We grant free, time-limited keys by hand.

Email us your use case

Every listing carries its provenance and you can filter on it: an employer’s ATS, the company’s own careers page, or a public employment agency. There is no aggregator inventory here at all: those sources are not redistributable, so they never enter the ledger and every row you get is first-party. The aggregator and agency filter values are accepted for forward compatibility, but today they match nothing.