Job Opportunities API

Check the data. Then trust it.

endpoints

Keyless endpoints

Everything under /public/*, what each returns, and which of them have no keyed equivalent at all.

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

1The complete list

Fourteen GET endpoints that need no key, plus a handful of POST routes that belong to the website rather than to the data API.

In depthWhy it exists, what it is not, what people get wrong
EndpointReturnsCacheKeyed twin
GET /public/jobsA page of listings, max 50 rows, no cursor.300s/v1/jobs
GET /public/jobs/{id}One listing, description always included.600s/v1/jobs/{id}
GET /public/companiesA page of employers.600s/v1/companies
GET /public/companies/{slug}One employer.600s/v1/companies/{slug}
GET /public/facetsEvery filter vocabulary with live counts.300s/v1/meta/facets
GET /public/providersEvery redistributable source with row counts.900s/v1/meta/providers
GET /public/freshnessVerification recency, field coverage, closures.900s/v1/meta/freshness
GET /public/coverageThe canonical coverage report.600s— none
GET /public/coverage/countriesPer-country coverage.900s— none
GET /public/coverage/employersPer-employer coverage.600s— none
GET /public/statsConvenience summary. Read this first.300s— none
GET /public/plansThe purchasable price list.300s— none
GET /public/openapi.json / .yamlThe machine contract, rendered live.3600s/v1/openapi.json
GET /public/ai-promptAn integration brief for a model, from the API host.600s— none
GET /public/statusService status.— none
GET /healthLiveness. Not under /public.— none
Exact contractTypes, defaults, ranges, errors, edge cases

There are also POST routes under /public/subscribe, unsubscribe, contact, submissions, optout/request, optout/verify, checkout, auth/request, auth/verify and the key-management routes. Those belong to the website: they are what its forms post to, most are protected by a Turnstile token or a signed session, and none of them is part of the data API. They are documented under The website.

2/public/jobs and /public/jobs/{id}

The same rows as /v1/jobs, capped at fifty per page, with no cursor and the paid-only parameters ignored rather than refused.

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

Same projection, same fields, same provenance block. What differs is the envelope: limit above 50 is clamped, cursor is a 402, and has_more reports false whether or not more rows exist — because the surface does not page, not because you have seen everything.

The parameters ignored here are the four marked paid endpoints only in the reference: status, quality, include_poster_type, and closed-row access generally. Sending them is not an error; they simply do nothing, so a keyless request always returns live, employer-direct rows.

Exact contractTypes, defaults, ranges, errors, edge cases

Access-Control-Allow-Origin: *, so this is callable from browser JavaScript with no proxy. Combined with the edge cache, a small public site can be built entirely on the client against these two endpoints.

A working list, client-side, no key, no backend
const params = new URLSearchParams({ country: 'NL', limit: '20' });
const res = await fetch(`https://api.jobopportunitiesapi.org/public/jobs?${params}`);
const { data } = await res.json();
document.querySelector('#jobs').innerHTML = data.map(j => `
  <li>
    <a href="${j.apply_url}" rel="nofollow noopener">${j.title}</a>
    — ${j.company}${j.city ? `, ${j.city}` : ''}
    ${j.remote_inferred && j.remote ? '<small>remote status inferred</small>' : ''}
  </li>`).join('');
A keyless page
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=NL&limit=2' \
  | jq '{rows: (.data|length), has_more, next_cursor}'
{
  "data": [
    {
      "id": "b594d5ef-5808-4f6b-9887-4966aa23926e",
      "slug": "pharmacy-technician-b594d5ef",
      "title": "Pharmacy Technician",
      "company": "cvshealth",
      "company_slug": "cvshealth",
      "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/cvshealth.png",
      "category": "Healthcare",
      "category_confidence": 0.65,
      "country": "NL",
… 70 more lines
Note `has_more: false` and `next_cursor: null` — the keyless surface does not page. Real response, fetched from /public/jobs?country=NL&limit=2 when this page was built (12 September 2026, 02:29 UTC). Run the command yourself and you will get today's rows, not these.

3The coverage family

Four keyless reports with no keyed equivalent: the canonical coverage report, per country, per employer, and a convenience summary.

In depthWhy it exists, what it is not, what people get wrong
/public/coverage
Canonical. The three populations, the reconciliation, per-field completeness, the source breakdown, the named excluded sources, and measured_at / age_seconds / stale. Everything on this site defers to it.
/public/coverage/countries
Per ISO code: live rows, how many carry a description, how many carry a published salary, how many employers.
/public/coverage/employers
The same idea per employer — how much data sits behind each company.
/public/stats
A convenience summary with different definitions, which is why it looks like it disagrees. The exact mapping.

These are keyless on purpose and there is no keyed version. A coverage report you need an account to read is a marketing claim; one anybody can fetch is an audit.

Exact contractTypes, defaults, ranges, errors, edge cases
FigureRowsWhat it counts
live_listings3,426,911Rows a caller can obtain from /v1/jobs: not delisted, not opted out, and not withheld by the quality gate. This is the number you can reproduce by paging the API.
withheld_listings223,949Rows present in the ledger and deliberately not served. quality_removed breaches the employer-direct guarantee or comes from a discovery-only source; quality_gated is reversible doubt; optout_hidden is a verified employer opt-out.
closed_listings5,707,085Roles that came off their source, retained with their closure date and reason.
ledger_rows9,357,945live_listings + withheld_listings + closed_listings. It reconciles exactly.
employers189,854Distinct employers with at least one row you can retrieve.
countries249Distinct ISO country codes present on live rows.
posted_last_7d188,764Live rows whose posted_at falls in the last seven days.

Reconciliation, computed from the same response: 3,426,911 + 223,949 + 5,707,085 = 9,357,945 and ledger_rows is 9,357,945 they match exactly, which is the guarantee.

The report also carries stale: false and age_seconds: 1,997. stale: true means the snapshot behind these figures is older than its refresh window; treat the numbers as indicative and re-read the endpoint before quoting them.

Measured 12 September 2026, 01:52 UTC · rendered from /public/coverage, which needs no key. These figures move; the endpoint is always right and this page is only as fresh as its last build.

4The spec, the AI brief, plans and status

Four small endpoints that describe the product rather than the data, all keyless and all safe to fetch on start-up.

In depthWhy it exists, what it is not, what people get wrong
/public/openapi.json and /public/openapi.yaml
The machine contract, rendered by the running API rather than checked in beside it — so its coverage figures are current and it describes the deployment you are talking to. Also served from the website host at /openapi.json, which is the URL people paste.
/public/ai-prompt
An integration brief written for a model, served from the API host with no website involved. Useful when a program has the API host and nothing else. The fuller version is /docs/ai/BUILD-A-SITE.md.
/public/plans
The purchasable price list: price, request limits, record allowance, and the delta_feed / bulk_export / single_country entitlements. Only active plans appear, so if a plan is not here you cannot buy it.
/public/status
Service status, as rendered on /status.
Exact contractTypes, defaults, ranges, errors, edge cases
PlanPriceRecords / monthRequests / dayRequests / minDelta feedBulk export
Explore
explore
Free1,0005,00030nono
Growth
growth
€80/month60,000100,000120yesno
Signal
signal
€299/month400,000400,000300yesyes
Scale
scale
€899/month2,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.

Everything a program needs to orient itself, keylessly
A=https://api.jobopportunitiesapi.org
curl -s "$A/public/openapi.json" | jq '{version: .info.version, paths: (.paths|keys|length)}'
curl -s "$A/public/plans"        | jq -r '.data[] | "\(.plan)\t\(.price_cents/100)EUR\t\(.records_per_month) records"'
curl -s "$A/public/coverage"     | jq '{live_listings, measured_at, stale}'

This page was rendered 12 September 2026, 02:29 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.