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
Everything under /public/* answers without a key. This is real data, not a sandbox: the same rows the paid endpoints return.
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. It serves from the same projection as /v1, so a row you see here is a row you would receive on a paid key — the differences are page size, throughput, and the endpoints that do not have a keyless mirror (the delta feed and bulk export).
It is bounded on purpose: about two requests a second sustained, forty a minute, and fifty rows a page. That is comfortable for evaluation and for a small site, and deliberately uncomfortable for extraction. Breaching it earns a ten-minute block, not a permanent one.
Exact contractTypes, defaults, ranges, errors, edge cases
Access-Control-Allow-Origin: * is set, so this works from a browser console and from client-side JavaScript with no proxy. Responses are edge-cached — /public/jobs for 300 seconds, /public/jobs/{slug} for 600 — so a repeated request may not reach the origin at all, and cf-cache-status in the response headers tells you which happened.
Two parameters behave differently here than on /v1: limit is capped at 50 rather than 200, and the parameters marked paid endpoints only in the parameter reference — status, quality, include_poster_type — are ignored rather than honoured.
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=FR&limit=3' { "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", "description": "absent", "source_type": "inferred" } }, { "id": "7d474303-35b6-4b59-8417-bbad81b629d8", … 77 more lines
curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=FR&limit=3' \
| jq '.data[] | {title, company, city, source, apply_url}'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 every REST endpoint against the live ledger, 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. 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-08-22", "key_prefix": "joadocs", "plan": "signal", "status": "active", "limits": { "per_day": 400000, "per_minute": 300 }, "renews_or_expires": null, "usage_today": 1 }
$ curl -s -H "Authorization: Bearer $JOA_KEY" \ 'https://api.jobopportunitiesapi.org/v1/jobs?country=IE&limit=2' { "data": [ { "id": "a4a22ef6-4dfc-4ebd-803d-186caedb90c9", "slug": "personal-trainer-a4a22ef6", "title": "Personal Trainer", "company": "lifetime", "company_slug": "lifetime", "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/lifetime.png", "category": "Healthcare", "category_confidence": 0.95, "country": "IE", "city": "Dublin", "location": "Dublin", "remote": "on_site", "remote_inferred": true, "seniority": "Entry", "posted_at": "2026-09-11T22:06:08Z", "first_seen_at": "2026-09-11T22:09:29Z", "last_verified_at": "2026-09-12T00:37:58Z", "status": "live", "closed_at": null, "closed_reason": null, "apply_url": "https://lifetime.wd1.myworkdayjobs.com/lifetime/job/Dublin/Personal-Trainer_R-172627", "source": "workday", "source_type": "ats", "provider_type": "employer_ats", "has_description": false, "field_sources": { "remote": "inferred", "employment_type": "absent", "category": "inferred", "seniority": "inferred", "salary": "absent", "location": "inferred", "posted_at": "published", "description": "absent", "source_type": "inferred" … 45 more lines
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 12 September 2026, 01:27 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.