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. 3,314,163 live openings across 178,393 employers.

Try it right now — no key, no account
GET https://api.jobopportunitiesapi.org/public/jobs?country=FR&require_fields=description&limit=5
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
one page per filter, up to 50 rows, ~2 req/s
With the free key
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,
      "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
}
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. 89% 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 a re-confirmation at the source, not a posting date. 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_reasonexpired_upstream when we proved the vacancy dead, not_seen when it simply stopped appearing. 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 6.1% of the ledger publishes structured salary, 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 you can clone and run. github.com/lucagiftzek/joa-examples — a daily country pull, watching an employer by the domain in your CRM, a salary benchmark with no estimates in it, a CSV for a spreadsheet, embedding chunks that keep their provenance, a tool definition for Claude and for GPT, and a SQLite mirror off the delta feed. Python, standard library only, no dependencies. Every example is run against this API before it ships, and CI re-runs the keyless half daily.
An integration brief to paste into a model. PROMPT.md gives an LLM or a coding agent the base URL, the auth header, every endpoint, every parameter that actually exists, the plan gating so it does not propose an endpoint your key will 403 on, the field_sources object, the rate-limit headers and all three of this API’s cursors.
Start without a key. The first nine requests in the examples’ curl cookbook need no account at all — /public/ is a real, bounded mirror of the same rows, capped at one page per filter and 50 rows.

Explore

Free

Every REST endpoint, against the live ledger — the same rows the paid tiers return. No card, no expiry.

  • 1,000 records / month
  • No delta feed
  • REST only, no bulk export
Get a free key →

Growth

€80 / month
excl. VAT

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

  • 60,000 records / month
  • Delta feed for incremental sync
  • REST only, no bulk export
Choose Growth →
RECOMMENDED

Signal

€299 / month
excl. VAT

Hiring signal at scale, or one country with no record limit.

  • 400,000 records / month
  • Delta feed for incremental sync
  • Bulk export of the full corpus
  • …or one country with no record limit
Choose Signal →

Scale

€899 / month
excl. VAT

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

  • 2,000,000 records / month
  • Delta feed for incremental sync
  • Bulk export of the full corpus
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.