How to Use the Job Opportunities API: The Developer's Guide (REST + MCP)
CodexFuse Team · 2026-11-04T03:03:07.260Z · 14 min read min read
How to Use the Job Opportunities API: The Developer's Guide (REST + MCP)
Learning how to use the Job Opportunities API is the fastest way to put a live employer-direct jobs API into a product, a dashboard, or an AI agent — because every row comes from the employer's own careers page, an applicant-tracking-system feed, or a government employment service, and nothing is resold from another job board. This guide is written for developers and AI-agent builders: people who will send HTTP requests and wire up tools, not people looking for career advice.
One disambiguation before we start, because the name is crowded: this API is not the PyPI opportunity-mcp package (a youth-scholarship index), not the Jobs in Rust agent API, not the 4-day-week jobs API, and not freehire, Jobgether, or the AJL employer API — all of those are different products with similar names. Everything below is this Job Opportunities API tutorial: it follows the arc from your first keyless call to an authenticated search to a wired-up MCP server, read from its vendor docs on 4 November 2026. If you have never called a REST API before, our what an API is primer will get you oriented in ten minutes; to fire off the example calls in this guide, a REST API tester works from the browser with no setup.
What Is the Job Opportunities API?
The Job Opportunities API is a jobs-data API and remote MCP server whose every listing is employer-direct: it comes from the employer's own careers page, their applicant tracking system, or a government employment feed, and the apply link points at the employer rather than an aggregator. The vendor's ledger page puts the definition mechanically: a row is employer-direct when it came from the employer's own careers page, their applicant tracking system, or a government employment feed, and applies at their URL — and "what it does not mean is that a human at the employer typed the row into this system." Nothing is resold from another board. In short, it is an employer direct jobs API: the dataset, the per-field provenance ledger, and the agent tools in one surface.
The dataset is large and freshly measured. The vendor's coverage page, read live on 4 November 2026, reports 2,531,638 live listings across 238 countries and 139,381 distinct employers (measured 3 November 2026, 04:52 UTC). The three source classes behind those rows are employer_ats (61 providers, 1,748,215 live rows), direct career sites (7 providers, 754,203 rows), and government feeds (2 providers, 29,220 rows). The full ledger holds 12,974,505 rows, which reconciles exactly against 2,531,638 live plus 9,579,366 closed plus 863,501 withheld listings. Treat every figure here as launch-era data — the API launched around November 2026 and these numbers move daily.
How Do I Use the Job Opportunities API Without a Key?
You can start with two endpoints that need no API key at all — your Job Opportunities API Explore key can wait until the keyed sections — and the vendor intends this as the first step, so an agent can inspect the dataset before anyone creates an account.
| Keyless endpoint | What it answers | REST route |
|---|---|---|
| coverage | How big the dataset is, per country and per source | /public/coverage and /public/stats |
| market_signals | Aggregate labour-market signals, e.g. salary percentiles and time-to-fill | /public/market-signals (plus /history) |
The public statistical routes carry no per-address rate limit at all, per the vendor's plans page. A first keyless call looks like this:
curl -s 'https://api.jobopportunitiesapi.org/public/coverage' | head -c 600
Note the host split the docs warn about, verbatim: "Two hosts, and getting them the wrong way round costs people hours. The API is api.jobopportunitiesapi.org. The website is jobopportunitiesapi.org." All API calls go to the api. host. The mcpmarket directory listing for this same product confirms the keyless pair independently: "market_signals and coverage need no key."
How Do I Get the Free Explore Key?
The free tier is called Explore, and the vendor is explicit about its terms: "The free Explore plan needs no card and does not expire." A second line on the plans page removes all doubt: "Explore is not a trial. It has no expiry date, it needs no payment details, and it gives you the search, job, company and closed-job endpoints against the live ledger." Here is the whole arc from zero to your first authenticated request:
- Create the key from the dashboard. Sign-in is passwordless via an email magic link; the key's secret is shown exactly once at creation, so copy it immediately. There is no OAuth app and no admin approval.
- Know your budget. Explore gives you 1,000 records a month, 5,000 requests a day, and 30 requests a minute. The licence is on records, not requests — a 402 response with
record_quota_exhaustedmeans stop, not retry. - Send the key in one Authorization header. The canonical form is
Authorization: Bearer <key>, on every/v1request. The key is never accepted in the query string ("There is no such parameter, deliberately") and never as a tool argument. - Make your first search. This is the docs' own quickstart example, and it works against the live ledger:
curl -s -H "Authorization: Bearer $JOA_KEY" \ 'https://api.jobopportunitiesapi.org/v1/jobs?country=IE&limit=2' - Page with cursors, not offsets. The API uses keyset cursors: hand
next_cursorback verbatim, and never parse or reconstruct it.limitaccepts 1–200 (default 25); anything else is a 422bad_limit.
If you write your requests by hand rather than in a tester, our curl API-testing guide walks through the same header, POST, and debugging flags step by step.
What Does the Authorization Header Look Like?
The authentication page is titled "One header, three accepted spellings," and it means exactly that: Authorization: Bearer <key> (canonical — "Write this"), Authorization: bearer <key> (the scheme token is case-insensitive per RFC 7235), and the bare Authorization: <key> with no scheme at all. The bare-key form exists deliberately, because many integration platforms give the user a single header field and send its contents verbatim. If you get it wrong, the 401 tells you the rule in plain language.
Two hard rules from the docs: X-Api-Key is not read, and a key in the URL or in a tool argument is never accepted. One credential exists — the key's secret, stored as a SHA-256 hash — with no expiry and no OAuth dance.
How Do I Read a Job Row's Provenance Fields?
Provenance is the Job Opportunities API's signature feature: the Job Opportunities API provenance model marks every job row with a field_sources object marking each field as published (the source said so), inferred (the vendor derived it), or absent (there is none). The docs stress the logic twice: "absent is not the same as false and it is not the same as a guess."
| Provenance value | What it means | Example |
|---|---|---|
published | The employer's source stated it in a structured field | A salary pulled from the posting's salary field |
inferred | Derived by the vendor — by a classifier or from advert text | Salary normalised out of the advert text; category and seniority, which are always read off the job title |
absent | The field has no value and none was claimed | No salary anywhere on the posting |
llm_inferred / unknown | Special cases for country only: a language model named the country from location text with no second signal (country_confidence: low), or nothing records where the country came from | A posting whose location text only implies a country |
The salary rule is the sharpest one: a published salary comes from a structured field, an inferred salary is read or normalised out of the advert text, and "it is never an estimate: modelled salaries are refused by the projection itself and cannot reach this API by any route." If you only trust employer-stated values, the require_fields=salary parameter returns only rows where salary is published (about 298,423 rows per the docs' listings page). And a small trap the docs call out: require_fields=category is a 422 error, because category can never be satisfied — it is always inferred.
What Happens When a Job Closes?
Rows are never deleted when a role closes. The docs' wording: "Rows are not deleted when a role closes. They are marked closed, with a date and a reason, and stay queryable." Each closed row carries closed_at and closed_reason, and closure verification has run daily since late September 2026. You read them through /v1/jobs/closed (full rows), /v1/jobs/expired (id-only), or the status=closed / status=any parameter on /v1/jobs.
The closed_reason filter takes five verbatim values, and the docs define the three crawled ones precisely:
expired_upstream— the source itself declared the vacancy over: an expiry date passed, or the ATS marked it closed.not_seen— the vacancy stopped appearing where it had been appearing, for long enough that its absence is a signal rather than a hiccup.apply_link_dead— the posting's own apply link kept failing at its source for two weeks (404, a closed page, the ATS saying closed, a host that no longer resolves), re-checked several times.employer_closed— the employer closed the role.deadline_passed— the stated application deadline passed.
For trend work, this is the difference between "fewer jobs" and "fewer jobs because the employer closed them" — the reason field is the whole point.
How Do the Six MCP Tools Work?
The Job Opportunities API ships as a remote MCP server alongside its REST routes — the Job Opportunities API MCP surface is six tools, each wrapping a REST endpoint, reachable at api.jobopportunitiesapi.org/mcp over Streamable HTTP. If the protocol is new to you, read what MCP is first: it is the open standard that lets AI assistants call tools like these in a uniform way.
| MCP tool | Wraps | Key needed? |
|---|---|---|
search_jobs | GET /v1/jobs | Yes — any valid key |
get_job | GET /v1/jobs/{id} | Yes — any valid key |
company_hiring | GET /v1/companies/{slug} plus a keyless open-roles trend | Hybrid: 30/90/365-day trend is keyless; the full roster needs a key |
market_signals | GET /public/market-signals | No — never |
coverage | GET /public/coverage + /public/stats | No — never |
changes_since | GET /v1/changes | Yes — and only on the Growth plan or above |
The docs summarise it in one line: "Two need no key at all: market_signals and coverage. The other four need a free API key, same as the REST routes they wrap." Metering is identical to REST — same key, same plan, same monthly allowance; there is no separate MCP quota. Connection details, verbatim from the MCP page: POST https://api.jobopportunitiesapi.org/mcp carries every JSON-RPC 2.0 message, GET on the same path answers 405, and the transport is stateless (no session id, no Mcp-Session-Id) at protocol version 2025-06-18. Exactly one door: the key travels as an Authorization HTTP header on the MCP connection — a key in the URL or passed as a tool argument is never accepted. It works in Claude Desktop, Claude.ai, Cursor, Windsurf, VS Code, and Cline, and the registry name is org.jobopportunitiesapi/mcp (also listed on glama.ai's MCP connector directory).
The changes_since gate is the one surprise: the delta change feed is a Growth-plan entitlement. A 403 carrying X-JOA-Required-Feature: delta_feed means the plan, not the key, is the problem.
What Do the Plans and Limits Allow?
Five plans, priced in euros, all read live from the vendor's /public/plans route when the docs page was rendered (verified 1 October 2026, rendered 4 November 2026):
| Plan | Price | Records / month | Requests / day | Requests / minute | Delta feed | Bulk export |
|---|---|---|---|---|---|---|
| Explore | Free, no card, no expiry | 1,000 | 5,000 | 30 | No | No |
| Starter | €29 / mo | 10,000 | 20,000 | 60 | No | No |
| Growth | €80 / mo | 60,000 | 100,000 | 120 | Yes | No |
| Signal | €299 / mo | 400,000 | 400,000 | 300 | Yes | Yes |
| Scale | €899 / mo | 2,000,000 | 1,000,000 | 600 | Yes | Yes |
The mental model that matters: the licence is on records, not requests, and there are three ceilings — monthly records, daily requests, per-minute requests. A 429 means slow down and retry; a 402 record_quota_exhausted means the month is spent — do not retry. Response headers carry X-RateLimit-Records-Remaining so you can watch the budget. Redistribution is a join, not a filter: the joa.providers.redistributable flag defaults false, so a provider added upstream tomorrow cannot appear before a human classifies it.
What Is the Job Opportunities API Not?
Three honest boundaries, each said once. First, staffing-agency and job-board poster rows are held out of the default results; they can be re-admitted on a paid key with include_poster_type=staffing,jobboard,all, and they arrive labelled — the default view is employer-direct only. Second, the coverage is deepest in the United States, then Germany, Britain, India, France, and the Netherlands; it is usable in Mexico and Brazil and thin in much of Africa (mcpmarket's plain-English reading of the coverage tool) — check /public/coverage for the country you care about before building on it. Third, this is a developer how-to for a data API, not career advice, and nothing in it promises a CodexFuse connector for the Job Opportunities API — the examples here run in any REST client.
Sources
- Job Opportunities API docs, "What JOA is" (ledger) — employer-direct definition,
apply_urlrule, source classes with live row counts (measured 3 November 2026, 04:52 UTC), closure-row semantics, redistribution join. (Fetched 4 November 2026.) - Job Opportunities API docs, "Provenance" (ledger) — the
field_sourcesvocabulary (published/inferred/absent, plusllm_inferred/unknownfor country), salary-never-estimated rule, always-inferred category/seniority,require_fieldsbehaviour. (Fetched 4 November 2026.) - Job Opportunities API docs, "Jobs" (endpoints) —
closed_reasonfilter values and closure routes. (Fetched 4 November 2026.) - Job Opportunities API docs, "MCP" — the six tools and what each wraps, keyless pair, remote server URL and transport, protocol version 2025-06-18, registry name, client list, keyless JSON-RPC example. (Fetched 4 November 2026.)
- Job Opportunities API docs, "Authentication" — one-header three-spelling rule, canonical
Authorization: Bearer <key>form, the host split, key-never-in-URL. (Fetched 4 November 2026.) - Job Opportunities API docs, "Quickstart" — free Explore key creation, passwordless sign-in, the verbatim first-request curl example. (Fetched 4 November 2026.)
- Job Opportunities API docs, "Plans" — Explore terms ("needs no card and does not expire"), full five-plan table rendered from
/public/plans, records-not-requests licence, 402/429/403 semantics. (Fetched 4 November 2026.) - mcpmarket.com/server/job-opportunities-api — independent directory listing: employer-direct description, provenance and closure summaries, six tools, keyless pair, Explore terms, FAQ on key usage. (Fetched 4 November 2026.)
- glama.ai MCP connector directory, namespace org.jobopportunitiesapi — third-party registry confirmation of the six tools. (Found 4 November 2026.)
- Homonym checks, all fetched 4 November 2026 and confirmed different products: PyPI
opportunity-mcpJSON API (youth-scholarship index, v0.1.1); jobsinrust.com/mcp (Jobs in Rust agent API); 4dayweek.io/developers (4-day-week jobs REST API plus its own MCP).
Frequently Asked Questions
Do I need an API key to use the Job Opportunities API?
Not for everything. The market_signals and coverage endpoints — and their MCP twins — are completely keyless, so you can audit the dataset before signing up. Job search, job details, company rosters, and the change feed need a key; the free Explore key covers everything except the delta feed.
How do I send the key if my client only takes a single header field?
Use the bare form: Authorization: <key> with no scheme. The docs accept it deliberately because many integration platforms send one field's contents verbatim. The canonical form remains Authorization: Bearer <key> — write that wherever you control the client.
Is this the same as the opportunity-mcp package on PyPI?
No. PyPI's opportunity-mcp is a different, local-indexed tool for youth opportunities (scholarships, fellowships, internships) from scholarship RSS sites — it has its own tools (search_opportunities, get_opportunity) and shares nothing with the Job Opportunities API. The same goes for the Jobs in Rust and 4-day-week APIs: similar names, different products.
How fresh is the data?
The coverage snapshot refreshes daily, provider and facet snapshots every 3 hours, and closure verification runs daily — so a role that disappears gets its closed_at and closed_reason within about a day. As of the November 2026 launch era, 188,360 listings were posted in the trailing 7 days.
What happens when I hit a limit?
Three different answers: a 429 means slow down and retry (watch Retry-After); a 402 record_quota_exhausted means your monthly record budget is spent — do not retry, upgrade the plan or wait; a 403 with X-JOA-Required-Feature: delta_feed means your plan lacks the delta-feed entitlement, which starts at Growth.

