Job Opportunities API

Check the data. Then trust it.

start

Overview

What the Job Opportunities API is, what it deliberately is not, and the ten minutes of reading that will save you the most time.

Last verified 2026-08-22 · Assumes: Nothing. This is the first page. · Markdown copy

This site documents one product completely: the public ledger at jobopportunitiesapi.org, the REST API behind it, and every control on the website. It is written so that a person can skim it and a program can read it, and there is a plain-Markdown copy of every page at /docs/ai/index.md for anything that is not a browser.

1What the Job Opportunities API is

A ledger of job openings taken from employers' own career pages, their applicant tracking systems, and government feeds — readable free, licensed as an API.

In depthWhy it exists, what it is not, what people get wrong

Most job data products are aggregators: they collect postings from wherever they appear, including from each other, and sell you the union. That produces a large catalogue with an unknowable provenance — you cannot tell whether a row came from the employer, from a staffing agency reposting it, or from another aggregator that had already lost track of where it came from.

This one is built the other way round. A row enters the ledger only from a source we have classified and are permitted to redistribute: the employer's own careers page, the employer's ATS (Greenhouse, Lever, Workday, SmartRecruiters and the rest), or a public employment agency. Every row carries the source it came from, and every field on that row carries whether the source stated it or we derived it. That per-field provenance is the distinctive thing here and it is why the catalogue is smaller than an aggregator's.

The second distinctive thing is that the ledger keeps what it removes. A role that closes is not deleted — it is marked closed, with the date and the reason, and stays queryable. So the data answers questions about hiring over time, not only about hiring right now.

Exact contractTypes, defaults, ranges, errors, edge cases
Transport
HTTPS only. HTTP/2 and HTTP/3 at the edge. No websockets, no gRPC, no GraphQL.
Format
JSON on every endpoint except /v1/export, which is NDJSON (one object per line).
Encoding
UTF-8 throughout. Job titles and company names are not transliterated.
Character of the API
Read-only. There is no endpoint on /v1 that writes anything.
Versioning
The path carries it: /v1. See Versioning and change policy.
CORS
Access-Control-Allow-Origin: * on the API, so browser code can call it directly.
Auth
A bearer key on /v1/*. Nothing at all on /public/*.

The machine contract is /openapi.json (and /openapi.yaml). Both are keyless, both are served by the running API rather than checked in beside it, and every parameter and field table on this site is generated from them at build time. Where this site and the spec disagree, the spec is right and the difference is a bug in this site.

The whole product, in one keyless request
# No key, no account. Three live rows from France.
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=FR&limit=1'
{
  "data": [
    {
      "id": "b8813094-5483-4d42-8749-c15e4c05c230",
      "slug": "fireman-b8813094",
      "title": "Fireman",
      "company": "disney",
      "company_slug": "disney",
      "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/disney.png",
      "category": "Safety & Environment",
      "category_confidence": 0.95,
      "country": "FR",
      "location": "Marne la Vallee Cedex 4, France",
      "remote": "on_site",
      "remote_inferred": true,
      "posted_at": "2026-09-11T22:06:08Z",
      "first_seen_at": "2026-09-11T22:09:29Z",
      "last_verified_at": "2026-09-12T00:42:56Z",
      "status": "live",
      "closed_at": null,
      "closed_reason": null,
      "apply_url": "https://disney.wd5.myworkdayjobs.com/disneycareerdc/job/Marne-la-Vallee-Cedex-4-France/Fireman_DLP-0000454599",
      "source": "workday",
      "source_type": "ats",
      "provider_type": "employer_ats",
      "has_description": false,
      "field_sources": {
        "remote": "inferred",
        "employment_type": "absent",
        "category": "inferred",
        "seniority": "absent",
        "salary": "absent",
        "location": "inferred",
        "posted_at": "published",
… 10 more lines
Real response, fetched from /public/jobs?country=FR&limit=1 when this page was built (12 September 2026, 00:51 UTC). Run the command yourself and you will get today's rows, not these.

2What it is not

Not an aggregator, not a job board, not a salary benchmark, and not the largest catalogue available. Each of those is a deliberate loss.

In depthWhy it exists, what it is not, what people get wrong

Stating the losses plainly is the point. If one of these is what you need, another product will serve you better and finding that out on this page costs you five minutes instead of a procurement cycle.

Not an aggregator
Rows from job aggregators are not merely deprioritised, they are excluded at the source level and never enter the served set. See the excluded sources — they are named.
Not a job board
There is no application flow, no candidate account, no CV store and no employer dashboard for managing a posting. apply_url points at the employer's own application page and that is where a candidate goes.
Not a compensation benchmark
Salary is published only where the source published it, or where a real figure was quoted in the advert text and we normalised it. Modelled or estimated salaries never reach this API by any route — see Salary.
Not the largest catalogue
An aggregator that redistributes everything will always have more rows. The trade is that you can tell where each of these came from.
Not a real-time feed
The ledger refreshes on a schedule, not on a webhook. See Freshness for what that means in hours.
Not a person-level dataset
There are no candidates, no applicants and no personal data in the ledger at all. A job row names a company, not a human.
Exact contractTypes, defaults, ranges, errors, edge cases

The exclusion is implemented as a join, not a filter, which is a stronger guarantee than it sounds. joa.providers.redistributable defaults to false and the projection that builds the served set inner-joins on it. A provider added upstream tomorrow therefore cannot leak into the public API before a human has classified it — the default is exclusion, and there is no query parameter that overrides it.

Similarly, rows whose poster is a staffing agency or a job board are excluded by default from every listing endpoint. They can be re-admitted deliberately with include_poster_type, on a paid key, and they arrive labelled. What can never be re-admitted is a row we have judged to breach the employer-direct guarantee: no parameter value opts back into that. See quality and withheld rows.

3Every endpoint, in one table

Twelve keyed endpoints under /v1 and ten keyless ones under /public. This table is generated from the live specification, so it is never behind the API.

In depthWhy it exists, what it is not, what people get wrong

The split is the licence, not the capability. /public/* exists so that you can evaluate the data — and build small things with it — without an account: it serves the same rows, from the same projection, with a page-size cap and a modest rate limit. /v1/* is the licensed surface: bigger pages, the delta feed, bulk export, and a record allowance instead of a request throttle.

There is no endpoint on /v1 whose data is unavailable on /public in some form; what /public does not give you is volume, the change feed, or an export. If you are deciding whether to buy, everything you need to decide is keyless.

Exact contractTypes, defaults, ranges, errors, edge cases
EndpointKey requiredWhat it returns
GET /v1/changesyesDelta feed — created, updated, withdrawn and delisted, in change order.
GET /v1/companiesyesEmployers whose live roles this API returns.
GET /v1/companies/{slug}yesOne employer.
GET /v1/exportyesBulk export of the full corpus, as a stream of NDJSON.
GET /v1/jobsyesList listings, newest first.
GET /v1/jobs/{id}yesOne listing, with its description.
GET /v1/jobs/closedyesRoles that have left their source, most recently closed first.
GET /v1/jobs/expiredyesIds of roles that have come off the ledger
GET /v1/meyesYour key, plan and quota.
GET /v1/meta/facetsyesEvery filter value with its live count.
GET /v1/meta/freshnessyesHow recently the ledger was verified, and how much of it is inferred.
GET /v1/meta/providersyesEvery source the ledger is built from, with its live row count.

The keyless mirrors are /public/jobs, /public/jobs/{slug}, /public/companies, /public/companies/{slug}, /public/facets, /public/providers, /public/freshness, /public/coverage, /public/coverage/countries, /public/coverage/employers, /public/stats, /public/plans and /public/openapi.json. They are documented in full on the keyless endpoints page.

4What to read, in what order

Developers should read the API group first. Buyers should read the ledger group first. Programs should fetch /docs/ai/BUILD-A-SITE.md and nothing else.

In depthWhy it exists, what it is not, what people get wrong
  1. If you are integrating todayQuickstart, then Authentication, Pagination, Errors. That is about fifteen minutes and it is the part that, skipped, costs the most later.
  2. If you are deciding whether to buyWhat the ledger is, Coverage and honesty, then Plans and the record meter. The meter is the most commonly misunderstood commercial mechanic here; read it before you compare prices.
  3. If you are a program/docs/ai/BUILD-A-SITE.md. It is self-contained and needs no other file.
  4. If you are looking for one specific thing — the search box at the top of the left-hand navigation searches every section title and summary on the site, and every section has a copyable deep link.
Exact contractTypes, defaults, ranges, errors, edge cases

Three habits will save you the most time, and each of them is a section on this site rather than a line here: never parse next_cursor; never hard-code a filter vocabulary; and always read field_sources before you trust a field.

This page was rendered 12 September 2026, 00:51 UTC. Every figure on it comes from the endpoint named beside it, and every published request is re-sent against the live API before this site is allowed to build. If something here is wrong, the thumbs-down above reaches a person.