start
Quickstart
From nothing to a real response in one command, and to an authenticated one in about a minute. No card at any point.
1Step one: a real response, with no account (statistics)
Everything STATISTICAL under /public/* answers without a key: coverage, facets, freshness, providers, the price list. This is real data, not a sandbox: the same ledger the paid endpoints read. Job and company ROWS are a step two thing now: see below.
In depthWhy it exists, what it is not, what people get wrong
The keyless surface exists so that evaluating this product costs you nothing and requires no conversation. /public/coverage, /public/facets, /public/providers and /public/freshness read live off the same projection as /v1, so a figure you see here is the same figure a paid key would compute. What is NOT keyless anymore is actual rows: /public/jobs and /public/companies used to answer one capped page without a key; a patient anonymous caller could still walk the whole ledger through that page cap, so both now require the free key from step two.
The STATISTICAL routes carry no per-address rate limit at all: they are O(1) reads of a precomputed snapshot, not a walk of the ledger, so there is nothing to bound. See Keyless access for the complete list and for what changed.
Exact contractTypes, defaults, ranges, errors, edge cases
Access-Control-Allow-Origin: * is set on these too, so this works from a browser console and from client-side JavaScript with no proxy. Responses are edge-cached (/public/coverage for 600 seconds, /public/facets for 300), so a repeated request may not reach the origin at all, and cf-cache-status in the response headers tells you which happened.
$ curl -s 'https://api.jobopportunitiesapi.org/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
2Step two: get a key
Sign in with your email at /login, click through the link it sends, and create a free key on the dashboard. No password, no card.
In depthWhy it exists, what it is not, what people get wrong
Sign-in is passwordless. You give an email address, the site emails you a one-time link, and following that link signs you in. There is no password to choose, to forget or to leak, and there is nothing in the database that could be stolen and replayed as one.
The free plan (Explore) is created from the dashboard with one click and needs no payment details. It gives you the search, job, company and closed-job endpoints against the live ledger (the delta feed starts on Growth), with a monthly record allowance rather than a trial period. It does not expire. The exact allowance is in the plans table, rendered from the live price list rather than typed here.
Exact contractTypes, defaults, ranges, errors, edge cases
- Open /login and enter an email address.
- The site POSTs to its own
/api/auth/request, which asks the API for a magic link and mails it. Nothing is stored against the address until you follow the link. - Following the link hits
/auth/verify, which exchanges the one-time token for a session cookie and redirects to/dashboard. - On the dashboard, Create a free key calls
POST /public/auth/keyswith that session. The response carries the secret; the page shows it once. - Send it as
Authorization: Bearer <secret>from then on.
The session is a signed value in a cookie, not a row in a session table, and it lasts 30 days. Signing out clears the cookie. Neither the login flow nor the dashboard is part of the API contract (they are the website), but they are documented in full under Sign-in and the dashboard.
3Step three: your first authenticated call
Send the key as a bearer token to api.jobopportunitiesapi.org. Everything under /v1 uses exactly this header.
In depthWhy it exists, what it is not, what people get wrong
The first call worth making is /v1/me, because it answers the two questions you will ask again later: which plan is this key on, and how much of it have I used. The answers are records_used_month against records_limit_month (the billed unit), and usage_today (requests today, the burst limit). It costs no records.
After that, /v1/jobs is the endpoint you will spend most of your time in. It takes about thirty-five parameters; you do not need to learn them, you need to know that the reference exists and is generated from the specification rather than written by hand.
Exact contractTypes, defaults, ranges, errors, edge cases
Three forms of the header are accepted: Authorization: Bearer <key>, Authorization: bearer <key> (the scheme token is case-insensitive) and the bare Authorization: <key> with no scheme at all. Bearer is the canonical form and the one to write; the tolerance exists so that a client library that strips the scheme does not fail confusingly. See Authentication.
Every metered response carries X-RateLimit-Records-Remaining. Watch it rather than waiting for a 402. The 402 is not something to retry: it means the month's licence is spent.
# Replace $JOA_KEY with the secret from the dashboard. $ export JOA_KEY='your-api-key-here' $ curl -s -H "Authorization: Bearer $JOA_KEY" https://api.jobopportunitiesapi.org/v1/me { "created": "2026-10-17", "key_id": "00000000-0000-4000-8000-000000000000", "key_prefix": "joadocs", "limits": { "max_page_size": 200, "per_day": 100000, "per_minute": 120, "ranked_per_day": 0 }, "plan": "growth", "records_country_lock": null, "records_limit_month": 60000, "records_reset_at": "2026-11-01T00:00:00Z", "records_used_month": 125, "renews_or_expires": "2026-11-17", "status": "active", "usage_today": 3 }
$ curl -s -H "Authorization: Bearer $JOA_KEY" \
'https://api.jobopportunitiesapi.org/v1/jobs?country=IE&limit=2'
4Step four: the three things to get right before you ship
Treat next_cursor as opaque, read field_sources before trusting a field, and handle 402 differently from 429. Everything else is detail.
In depthWhy it exists, what it is not, what people get wrong
- next_cursor is opaque
- It looks like a timestamp and a uuid joined by a pipe, and it is not: it is a token to hand back verbatim. Parsing it, reconstructing it, or storing its halves separately will work until the day the ordering changes, and then silently skip or repeat rows. Pagination.
- field_sources before the field
- Every row carries, per field, whether the source published it, whether we inferred it, or whether it is absent. A remote flag we inferred from the location text is not the same fact as one the employer ticked, and the row tells you which it is. Provenance.
- 402 is not 429
- 429 means slow down and retry. 402 means this month's record allowance is spent and retrying will not help until the first of the month. A client that treats them the same will hammer a wall for three weeks. Errors.
Exact contractTypes, defaults, ranges, errors, edge cases
Two more that cost less but still cost. First, do not hard-code a filter vocabulary: providers, categories and source types are enumerable at run time from /public/facets and /public/providers, and a value that has left the list is a 422 rather than an empty page, which is a feature, but only if you read it. Second, keep company identity on company_domain where you have it: slugs are not currently guaranteed stable across refreshes. Company slugs.
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.