Job Opportunities API

Check the data. Then trust it.

api

Keyless access

What /public/* gives you with no account: real aggregate numbers against the live ledger (coverage, facets, freshness, providers, the price list, the spec). Job and company ROWS are not part of that anymore: they need a free key, full stop.

Last verified 2026-08-22 · Assumes: Nothing. · Markdown copy

1What you get without an account

Every STATISTICAL read shape the API has, against the live ledger, with no key: aggregate counts, coverage, provenance, providers, freshness, the price list and the spec. Rows (individual jobs and companies) are not part of the keyless surface: GET /public/jobs, /public/jobs/{id}, /public/companies and /public/companies/{slug} now require a key, exactly like their /v1 twins.

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

Until 2026, the four listing routes above answered keylessly too, capped at one 50-row page per query with no cursor. That was meant to make evaluating free and extracting expensive, but a page cap alone does not stop a patient caller: pacing itself just under the keyless rate limit, an anonymous script could still change the filters and walk the whole ledger, one bounded page at a time, forever. The rate limit made that slow. It did not make it impossible, and "slow" was never the offer. The four routes now return 401 to anyone who is not jobopportunitiesapi.org's own page rendering, with or without a key: rows have exactly one door now, /v1, metered on a plan.

What is unchanged, and stays open on purpose: every STATISTICAL route reads a precomputed aggregate or a small bounded audit table, never a row-by-row walk of the ledger, so none of them can be extracted the way /public/jobs could. Coverage, facets, freshness and the provider table carry the entire "check us before you buy" pitch, and none of that needed row-level access to make.

Exact contractTypes, defaults, ranges, errors, edge cases
Keyless endpointKeyed equivalentDifference
(needs a free key)/v1/jobsNo keyless tier anymore. Was capped at one 50-row page; now 401 without a key, whoever asks.
(needs a free key)/v1/jobs/{id}Same change. Slug or uuid on /v1.
(needs a free key)/v1/companiesSame change.
(needs a free key)/v1/companies/{slug}Same change.
/public/facets/v1/meta/facetsIdentical data. Unaffected (STATISTICAL, stays keyless).
/public/providers/v1/meta/providersIdentical data. Unaffected.
/public/freshness/v1/meta/freshnessIdentical data. Unaffected.
/public/coverage(no keyed twin)The canonical coverage report. Unaffected.
/public/coverage/countriesnot availablePer-country coverage. Unaffected.
/public/coverage/employersnot availablePer-employer coverage. Unaffected.
/public/statsnot availableConvenience summary; see why it looks different. Unaffected.
/public/plansnot availableThe price list. Unaffected.
/public/openapi.json/v1/openapi.jsonThe same document. Unaffected.
/public/ai-promptnot availableAn integration brief, from the API host, for a model. Unaffected.
(no keyless twin)/v1/changesThe delta feed is licensed.
not available/v1/jobs/expiredSame.
not available/v1/jobs/closedSame.
not available/v1/exportBulk export is licensed.
A whole evaluation, keyless: statistics only, no key, no rows
$ A=https://api.jobopportunitiesapi.org
$ curl -s "$A/public/coverage" | jq '{live_listings, employers, countries, measured_at, stale}'
{
  "age_seconds": 2142,
  "as_of": "2026-10-27",
  "closed_listings": 8653347,
  "closure_date_note": "JOA records as the closure date the day its daily verification confirmed that an advert had gone. Before late September 2026 that verification ran in large catch-up batches, so closure dates recorded before then show when JOA confirmed a closure, not when the employer closed the role; daily verification applies from late September 2026.",
  "countries": 236,
  "definitions": {
    "as_of_times": "/public/facets and /public/providers carry the latest snapshot (every 3 hours, generated_at). /public/coverage, /public/coverage/countries, /public/stats, /public/site-stats and the website carry the snapshot frozen once a day (as_of, measured_at = that snapshot's generated_at). Two surfaces agree exactly when their timestamps are equal; otherwise each states its own.",
    "closed_listings": "Roles that came off their source, retained with a closure date and reason. JOA records as the closure date the day its daily verification confirmed that an advert had gone. Before late September 2026 that verification ran in large catch-up batches, so closure dates recorded before then show when JOA confirmed a closure, not when the employer closed the role; daily verification applies from late September 2026.",
    "countries": "Distinct country codes with at least one served row. Only ISO 3166-1 alpha-2 codes (plus XK, Kosovo) are countries; any other value is published as country unknown and filtered by no country= value.",
    "employers": "Distinct employers with at least one served row.",
    "ledger_rows": "live_listings + withheld_listings + closed_listings. It reconciles exactly.",
… 178 more lines
Real response, fetched from /public/coverage when this page was built (27 October 2026, 11:28 UTC). Run the command yourself and you will get today's rows, not these.

2The limits, exactly

STATISTICAL routes carry no per-address limit at all: they are O(1) reads of a precomputed snapshot or a small bounded table, so there is nothing to walk. The four listing routes still sit behind the old IP throttle (about two requests a second, a burst of ten, forty a minute, a ten-minute block on breach) as a second layer, but it no longer matters much in practice: since the key requirement is checked first for every caller but jobopportunitiesapi.org's own rendering, an anonymous or wrongly-keyed request gets 401 regardless of how it is paced.

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

The throttle numbers below describe api/publiclimit.go, which still wraps the four listing routes and has not changed. What changed is what sits behind it: those four routes used to serve rows to anyone who respected the rate limit; now they serve rows to nobody but the website's own page rendering, whatever the rate limit would have allowed through. The throttle is defence in depth, not the boundary: the boundary is the key requirement below.

For history: the page cap mattered more than the rate, and neither was ever enough on its own. Before either existed, /public/jobs served 294 rows a second to an unauthenticated caller with working keyset pagination: the entire live ledger in a bit over two hours single-threaded. The paid entry plan is €80 a month for sixty thousand records, and the keyless path was handing that over every three and a half minutes. Capping the page size and rate-limiting the caller slowed that down; it took requiring a key on the route itself to actually stop it.

Exact contractTypes, defaults, ranges, errors, edge cases
LimitValueApplies to
Sustained rate2 requests/secondPer client IP, on the four listing routes only. STATISTICAL routes carry none.
Burst10 requestsThe bucket's capacity.
Window ceiling40 requests/minutePer client IP.
Block on breach10 minutes429 with Retry-After. Not permanent, and moot anyway once the key check below refuses the request first.
Key requiredalways, for rowsGET /public/jobs, /public/jobs/{id}, /public/companies, /public/companies/{slug} all answer 401 to anyone without a valid Explore-or-better key on /v1.
What a keyless request to a listing route gets you now
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?limit=2'
{
  "error": "key_required",
  "message": "Listings require an API key. Get a free Explore key (no card) at
               https://jobopportunitiesapi.org/register. Statistics under
               /public/stats, /public/facets and /public/coverage remain open.",
  "docs": "https://jobopportunitiesapi.org/api"
}
HTTP 401: a key requirement, not a rate limit. Confirmed 2026 (PUBLIC_LISTINGS_GATE).

One exemption exists on the four listing routes and it does not help a third party: the website's own server-side rendering, which authenticates with a header the browser never sees and no external caller can send. There is no Host-based or address-based exemption: a request that merely arrives from an internal-looking address is not, on its own, treated as trusted.

3Getting the rows the statistics describe

The statistics above tell you whether the data is worth having. Reading an actual job or company, even one, now needs the free key. There is no smaller step in between.

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

The free Explore plan is not a trial: it does not expire, and it needs no card. It is, as of PUBLIC_LISTINGS_GATE, the only way to read a job or company row from this API without being jobopportunitiesapi.org's own server. For a lot of small projects it is also the end of the story: it replaces nothing you had before, because there was no smaller keyless row tier for it to replace.

  • You want any row at all. /v1/jobs, /v1/jobs/{id}, /v1/companies, /v1/companies/{slug} all need a key now, Explore included.
  • You need to page. Any key pages; the keyless single-page tier no longer exists.
  • You need the delta feed. /v1/changes and /v1/jobs/expired require a plan with delta_feed. Polling a listing endpoint and diffing is the alternative, and it costs far more records than the feed does.
  • You need bulk export. /v1/export requires bulk_export.
  • You need throughput. The per-minute ceiling on a paid plan is measured in hundreds rather than tens.
  • You need closed rows. status=closed and /v1/jobs/closed are paid-only.
Exact contractTypes, defaults, ranges, errors, edge cases
PlanPriceRecords / monthRequests / dayRequests / minDelta feedBulk export
Explore
explore
Free1,0005,00030nono
Starter
starter
€29/month10,00020,00060nono
Growth
growth
€80/month60,000100,000120yesno
Signal
signal
€299/month400,000400,000300yesyes
Scale
scale
€899/month2,000,0001,000,000600yesyes
Starter (annual)
starter_annual
€26110,00020,00060nono
Growth (annual)
growth_annual
€72060,000100,000120yesno
Signal (annual)
signal_annual
€2,691400,000400,000300yesyes
Scale (annual)
scale_annual
€8,0912,000,0001,000,000600yesyes

Rendered from /public/plans when this page was built. Only purchasable plans appear there, so this table is the price list: if a plan is not here, you cannot buy it. Compare on /pricing.

Note that the ladder is about records, not requests, above the free tier. That is the single most commonly misunderstood thing about the pricing and it is explained properly in the record meter. Read it before comparing the numbers above with another vendor's, because a request-priced API and a record-priced one are not comparable line by line.

This page was rendered 27 October 2026, 11:28 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.