endpoints
Keyless endpoints
Everything under /public/*, what each returns, and which of them have no keyed equivalent at all.
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
| Endpoint | Returns | Cache | Keyed twin |
|---|---|---|---|
GET /public/jobs | A 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/companies | A page of employers. | 600s | /v1/companies |
GET /public/companies/{slug} | One employer. | 600s | /v1/companies/{slug} |
GET /public/facets | Every filter vocabulary with live counts. | 300s | /v1/meta/facets |
GET /public/providers | Every redistributable source with row counts. | 900s | /v1/meta/providers |
GET /public/freshness | Verification recency, field coverage, closures. | 900s | /v1/meta/freshness |
GET /public/coverage | The canonical coverage report. | 600s | — none |
GET /public/coverage/countries | Per-country coverage. | 900s | — none |
GET /public/coverage/employers | Per-employer coverage. | 600s | — none |
GET /public/stats | Convenience summary. Read this first. | 300s | — none |
GET /public/plans | The purchasable price list. | 300s | — none |
GET /public/openapi.json / .yaml | The machine contract, rendered live. | 3600s | /v1/openapi.json |
GET /public/ai-prompt | An integration brief for a model, from the API host. | 600s | — none |
GET /public/status | Service status. | — | — none |
GET /health | Liveness. 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.
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('');$ 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
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
| Figure | Rows | What it counts |
|---|---|---|
live_listings | 3,426,911 | Rows 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_listings | 223,949 | Rows 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_listings | 5,707,085 | Roles that came off their source, retained with their closure date and reason. |
ledger_rows | 9,357,945 | live_listings + withheld_listings + closed_listings. It reconciles exactly. |
employers | 189,854 | Distinct employers with at least one row you can retrieve. |
countries | 249 | Distinct ISO country codes present on live rows. |
posted_last_7d | 188,764 | Live 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.jsonand/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_countryentitlements. 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
| Plan | Price | Records / month | Requests / day | Requests / min | Delta feed | Bulk export |
|---|---|---|---|---|---|---|
Exploreexplore | Free | 1,000 | 5,000 | 30 | no | no |
Growthgrowth | €80/month | 60,000 | 100,000 | 120 | yes | no |
Signalsignal | €299/month | 400,000 | 400,000 | 300 | yes | yes |
Scalescale | €899/month | 2,000,000 | 1,000,000 | 600 | yes | yes |
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.
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.