# Keyless access

What /public/* gives you with no account: real rows, one page at a time, bounded so that evaluating is free and extracting is not.

**What this covers:** What you get without an account; The limits, exactly; When keyless stops being enough.

**Assumed knowledge:** Nothing.

**Canonical HTML:** https://jobopportunitiesapi.org/docs/api/keyless  
**Machine-readable index:** https://jobopportunitiesapi.org/docs/ai/index.md  
**Last verified:** 2026-08-22  
**Superseded by:** the live API at https://api.jobopportunitiesapi.org and its spec at https://jobopportunitiesapi.org/openapi.json — where this file and the API disagree, the API is right.

---

<a id="keyless-what-you-get"></a>

## 1. What you get without an account

Every read shape the API has, against the live ledger, with no key: rows, companies, facets, coverage, providers, freshness, plans and the spec.

### 1.1 In depth

The keyless surface is not a sandbox and it is not sample data. It reads from the same projection as `/v1`, so a row you see here is a row you would receive on a paid key, with the same fields and the same provenance block. That is the whole point: the product's claim is that its data is auditable, and a claim you have to sign up to check is not one.

What you cannot do without a key is **walk** the corpus. Keyless queries return one page; there is no cursor. You can change the filters and see different rows as many times as you like, which is what evaluation looks like, and you cannot page through two million rows, which is what extraction looks like.

> **You can build a real thing on this** — A city- or category-scoped board, a widget, a dashboard of coverage figures — all of that fits inside one page per query and a few requests a minute, from a browser, with no backend. The free Explore key exists for when you outgrow it.

### 1.2 Exact contract

| Keyless endpoint | Keyed equivalent | Difference |
| --- | --- | --- |
| `/public/jobs` | `/v1/jobs` | 50-row cap, no cursor, paid-only parameters ignored. |
| `/public/jobs/{slug}` | `/v1/jobs/{id}` | Slug or uuid on both. The keyless one always includes the description. |
| `/public/companies` | `/v1/companies` | Capped page, no deep paging. |
| `/public/companies/{slug}` | `/v1/companies/{slug}` | Same row. |
| `/public/facets` | `/v1/meta/facets` | Identical data. |
| `/public/providers` | `/v1/meta/providers` | Identical data. |
| `/public/freshness` | `/v1/meta/freshness` | Identical data. |
| `/public/coverage` | — (no keyed twin) | The canonical coverage report. |
| `/public/coverage/countries` | — | Per-country coverage. |
| `/public/coverage/employers` | — | Per-employer coverage. |
| `/public/stats` | — | Convenience summary; see [why it looks different](./ledger-coverage.md#stats-vs-coverage). |
| `/public/plans` | — | The price list. |
| `/public/openapi.json` | `/v1/openapi.json` | The same document. |
| `/public/ai-prompt` | — | An integration brief, from the API host, for a model. |
| — (no keyless twin) | `/v1/changes` | The delta feed is licensed. |
| — | `/v1/jobs/expired` | Same. |
| — | `/v1/jobs/closed` | Same. |
| — | `/v1/export` | Bulk export is licensed. |

### 1.3 Worked examples

A whole evaluation, keyless

```console
$ A=https://api.jobopportunitiesapi.org
$ curl -s "$A/public/coverage" | jq '{live_listings, employers, countries, measured_at, stale}'
{
  "age_seconds": 8077,
  "closed_listings": 5685266,
  "countries": 249,
  "definitions": {
    "closed_listings": "Roles that came off their source, retained with their closure date and reason.",
    "ledger_rows": "live_listings + withheld_listings + closed_listings. It reconciles exactly.",
    "live_listings": "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.",
    "measured_at": "When these figures were counted. The report is measured on a schedule, not per request, because counting it per request took 24 seconds and returned 503 instead.",
    "withheld_listings": "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."
  },
  "employers": 189201,
… 111 more lines
```

_Real response, fetched from `/public/coverage` when this file was built (11 September 2026, 13:14 UTC)._

**See also**

- [The limits, exactly](./api-keyless.md#keyless-limits)
- [The complete list](./endpoints-public.md#public-endpoints-list)
- [The plans](./account-plans.md#plans-table)

<a id="keyless-limits"></a>

## 2. The limits, exactly

About two requests a second sustained, a burst of ten, forty a minute, fifty rows a page, and a ten-minute block if you go through all of that.

### 2.1 In depth

These numbers were chosen against two measured profiles rather than picked. A person evaluating the product peaks at ten to fifteen requests in a minute — clicking the preset buttons on the API page, then a few queries of their own. An extractor does not stop. Forty a minute is comfortably above the first and pointless for the second.

The page cap matters more than the rate. Before it existed, `/public/jobs` served 294 rows a second to an unauthenticated caller with working keyset pagination: the entire live ledger in a bit over two hours single-threaded, or about twenty minutes across eight workers. The paid entry plan is €80 a month for sixty thousand records, and the keyless path was handing that over every three and a half minutes.

### 2.2 Exact contract

| Limit | Value | Applies to |
| --- | --- | --- |
| Sustained rate | 2 requests/second | Per client IP, refilled continuously. |
| Burst | 10 requests | The bucket's capacity. |
| Window ceiling | 40 requests/minute | Per client IP. |
| Block on breach | 10 minutes | `429` with `Retry-After`. Not permanent. |
| Page size | 50 rows | `limit` above 50 is clamped, not refused. |
| Paging | none | A `cursor` on a keyless request is a **402**, not a 401. |

> **`has_more` is false on keyless listing responses** — Because the keyless surface does not page, a keyless listing response reports `has_more: false` and `next_cursor: null` even when the ledger holds more rows matching your filters. Do not read that as “this is everything”. On `/v1` both fields mean what you expect.

What a keyless cursor gets you

```console
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?limit=2&cursor=anything'
{
  "error": "key_required_to_page",
  "message": "Keyless queries return one page. Change the filters to see different
               rows, or take a free Explore key at https://jobopportunitiesapi.org/login
               — 1,000 records a month, no card, and it pages.",
  "docs": "https://jobopportunitiesapi.org/api"
}
```

_Captured 2026-08-22. HTTP 402 — a licence boundary, not an authentication failure._

Three exemptions exist and none of them helps a third party: requests that did not arrive through the edge (the website's own server-side rendering, talking to the API over the internal network), our own egress addresses (so the QA harness does not rate-limit itself), and authenticated `/v1` callers, who already have a per-key limit and a record meter — two limiters on one request is how you get a support ticket nobody can reproduce.

**See also**

- [The per-plan limits](./api-rate-limits.md#limits-per-plan)
- [402 — the licence, not the throttle](./api-errors.md#error-402)
- [When keyless stops being enough](./api-keyless.md#keyless-when-to-upgrade)

<a id="keyless-when-to-upgrade"></a>

## 3. When keyless stops being enough

The moment you need a second page, a change feed, or more than about forty requests a minute. The free key removes the first and third of those.

### 3.1 In depth

The free **Explore** plan is not a trial — it does not expire, and it needs no card. It replaces the IP-based throttle with a per-key request limit and a monthly record allowance, and it turns on cursor paging. For a lot of small projects that is the end of the story.

- **You need page two.** Keyless returns one page; any key pages. This is the most common reason.
- **You need the delta feed.** `/v1/changes` and `/v1/jobs/expired` require a plan with `delta_feed`. Polling a listing endpoint and diffing is the alternative, and it costs far more records than the feed does.
- **You need bulk export.** `/v1/export` requires `bulk_export`.
- **You need throughput.** The per-minute ceiling on a paid plan is measured in hundreds rather than tens.
- **You need closed rows.** `status=closed` and `/v1/jobs/closed` are paid-only.

### 3.2 Exact contract

| plan | display_name | price | records_per_month | req_per_day | req_per_min | delta_feed | bulk_export | single_country |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| explore | Explore | Free | 1,000 | 5,000 | 30 | false | false | false |
| growth | Growth | €80/month | 60,000 | 100,000 | 120 | true | false | false |
| signal | Signal | €299/month | 400,000 | 400,000 | 300 | true | true | true |
| scale | Scale | €899/month | 2,000,000 | 1,000,000 | 600 | true | true | false |

_Generated from `GET https://api.jobopportunitiesapi.org/public/plans` (no key required). Only purchasable plans appear there._

Note that the ladder is about **records**, not requests, above the free tier. That is the single most commonly misunderstood thing about the pricing and it is explained properly in [the record meter](./account-record-meter.md) — read it before comparing the numbers above with another vendor's, because a request-priced API and a record-priced one are not comparable line by line.

**See also**

- [The plans](./account-plans.md#plans-table)
- [What counts as a record](./account-record-meter.md#meter-what-counts)
- [Step two — get a key](./quickstart.md#qs-get-key)

---

## Where to go next

This file is part of **API**. Others in the same group:

- [API overview](./api-overview.md) — Hosts, versioning, caching, CORS and the HTTP conventions every endpoint follows. Read the hosts section first — getting it wrong costs people hours.
- [Authentication](./api-authentication.md) — One header, three accepted spellings, and a deliberate refusal to tell an attacker which kind of wrong a wrong key is.
- [Rate limits](./api-rate-limits.md) — Two independent ceilings — requests per minute and per day — plus a monthly record allowance that is a licence rather than a throttle. They fail differently.
- [Pagination](./api-pagination.md) — Keyset cursors, not offsets. One rule matters more than the rest: hand next_cursor back exactly as you received it.
- [Errors](./api-errors.md) — Every status the API emits, its JSON shape, whether it is worth retrying, and what to do about it. Two of them are routinely confused and it is expensive.

Always useful:

- [index.md](./index.md) — the map of every file here
- [BUILD-A-SITE.md](./BUILD-A-SITE.md) — the paste-whole brief for building against this API
- [quickstart.md](./quickstart.md) — zero to a first authenticated response
- [api-errors.md](./api-errors.md) — every status code and whether to retry it
