# The record meter

The licence is on rows delivered, not on requests made. This is the most commonly misunderstood mechanic here, and it changes how you write your client.

**What this covers:** What counts as a record; Records versus requests; Running out; The country-locked plan.

**Assumed knowledge:** [Rate limits](./api-rate-limits.md).

**Canonical HTML:** https://jobopportunitiesapi.org/docs/account/record-meter  
**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.

---

> **One sentence, if you read nothing else** — `limit=200` costs the same one request and two hundred times the records that `limit=1` does. Ask for what you will use.

---

<a id="meter-what-counts"></a>

## 1. What counts as a record

One row delivered to you, on any endpoint that returns rows. Metadata endpoints and /v1/me cost nothing.

### 1.1 In depth

| Call | Records charged |
| --- | --- |
| `GET /v1/jobs?limit=200` | up to 200 — one per row returned |
| `GET /v1/jobs/{id}` | 1 |
| `GET /v1/jobs/closed?limit=50` | up to 50 |
| `GET /v1/jobs/expired?limit=1000` | up to 1,000 — ids are rows too |
| `GET /v1/changes?limit=500` | up to 500 — one per change |
| `GET /v1/companies?limit=25` | up to 25 |
| `GET /v1/companies/{slug}` | 1 |
| `GET /v1/export` | one per row **written**, charged as the stream is produced |
| `GET /v1/me` | **0** |
| `GET /v1/meta/facets`, `/meta/freshness`, `/meta/providers` | **0** |
| Anything under `/public/*` | **0** — keyless and unmetered, but IP-rate-limited |

An empty page costs nothing, because no rows were delivered. A page that returns fewer rows than you asked for charges for what arrived, not for what you asked.

### 1.2 Exact contract

Metering is applied at a single gate that every row-returning endpoint passes through, rather than per handler. It used to be on `/v1/jobs` alone, which meant `/v1/changes`, `/v1/jobs/closed`, `/v1/jobs/expired`, `/v1/companies` and `/v1/jobs/{id}` served rows and charged nothing — the allowance could be sidestepped by reading the same ledger through a different door.

The charge is fire-and-forget: the metering write happens after the response and never fails a request the customer has already received. Losing a charge is a rounding error; losing the response is not. In practice this means the counters can lag a moment behind reality under load.

Job rows and company rows are metered as separate scopes internally, which matters only for the country-locked SKU — see [below](#country-locked). For every other key they share one monthly allowance.

**See also**

- [Records versus requests](./account-record-meter.md#meter-vs-rate-limit)
- [The response headers](./api-rate-limits.md#limit-headers)
- [Choosing one](./account-plans.md#plan-choose)

<a id="meter-vs-rate-limit"></a>

## 2. Records versus requests

Two independent counters. The request limits stop you overwhelming the service; the record allowance is what the plan actually sells.

### 2.1 In depth

Selling requests alone does not work for a dataset. Ten thousand requests a day at two hundred rows a page is two million rows a day — the whole ledger, daily — which is not a boundary at all. So the licence is on rows and the request ceilings exist for a different purpose.

|  | Request limits | Record allowance |
| --- | --- | --- |
| Purpose | Protect the origin from bursts | The licence — how much data you may take |
| Counted per | Key, per minute and per day | Key, per calendar month |
| Resets | Rolling minute; UTC midnight | The 1st of the month |
| Breach status | 429 | 402 |
| Retry? | Yes, after `Retry-After` | **No** — until the 1st, or an upgrade |
| Visible in | `X-RateLimit-Limit` / `-Remaining` / `-Reset`, and `/v1/me` | `X-RateLimit-Records-*`, and the dashboard |

On most plans you will meet the record allowance long before the request ceiling. If you are meeting the request ceiling instead, you are almost certainly making many small requests where fewer larger ones would do.

### 2.2 Exact contract

The practical design rules that follow: page at the largest `limit` you will actually consume; do not re-read rows you already have (use [the delta feed](./endpoints-changes.md)); do not fetch descriptions for rows you will not display; and put staging on its own key so a runaway test job cannot spend production's month.

**See also**

- [Requests versus records](./api-rate-limits.md#limits-vs-meter)
- [An incremental sync](./recipes-sync.md#recipe-incremental-sync)
- [Grant keys and the free tier](./account-plans.md#grant-keys)

<a id="meter-exhausted"></a>

## 3. Running out

A 402 with record_quota_exhausted. It resets on the first of the month, and retrying before then will not help.

### 3.1 In depth

It should never be a surprise. Every metered response carries `X-RateLimit-Records-Remaining`, so the wall is visible from a long way off — alert on that header crossing a threshold rather than on the 402 arriving.

When it does arrive there are three responses: wait for the first of the month, upgrade the plan (which takes effect immediately), or reduce what you are asking for. The third is usually the right one if a daily full re-read is what emptied it.

> **Do not treat it as a rate limit** — A client that backs off and retries a 402 will do so for up to three weeks, generating load and getting nothing — and the logs will look like ordinary throttling, so nobody will notice.

### 3.2 Exact contract

The one place it does not arrive as a status is `/v1/export`. The stream has already returned 200, so the error is the **last line of the body**, carrying the `after` cursor to resume from. [Export detail](./endpoints-export.md#export-quota).

Watch the remaining allowance, from a cron job

```bash
remaining=$(curl -s -D - -o /dev/null -H "Authorization: Bearer $JOA_KEY" \
  'https://api.jobopportunitiesapi.org/v1/jobs?limit=1' \
  | awk -F': ' 'tolower($1)=="x-ratelimit-records-remaining"{print $2+0}')

# One record spent to learn where you stand — cheaper than discovering it
# by being cut off mid-sync.
[ "${remaining:-0}" -lt 5000 ] && echo "JOA records low: $remaining" >&2
```

**See also**

- [402 — the licence, not the throttle](./api-errors.md#error-402)
- [The response headers](./api-rate-limits.md#limit-headers)
- [The failure mode that catches people](./endpoints-export.md#export-quota)

<a id="country-locked"></a>

## 4. The country-locked plan

One country, no record limit on job rows. The restriction is what pays for the exemption, and the two are inseparable.

### 4.1 In depth

A country-locked key sees only its country. In exchange, job rows are exempt from the record meter entirely — `X-RateLimit-Records-Limit` reports `unlimited` and there is no monthly ceiling to run into. It suits a product that serves one market and needs volume within it.

The lock is not a filter you can lift. A request for another country returns nothing, and `/v1/changes` is scoped the same way. Every response carries `X-JOA-Country-Lock` with the ISO code, on purpose: the only thing worse than the restriction is a customer discovering it by wondering where France went.

### 4.2 Exact contract

| Behaviour | On a country-locked key |
| --- | --- |
| Job rows | Only the locked country. **Unmetered.** |
| Company rows | Metered normally — the exemption is for job rows only. |
| `X-JOA-Country-Lock` | Present on every response, carrying the ISO code. |
| `X-RateLimit-Records-Limit` | `unlimited` on job-row responses. |
| Request limits | Unchanged — the per-minute and per-day ceilings still apply. |
| `?country=` for another code | Returns nothing. Not an error. |
| `/v1/changes` | Scoped to the locked country. |

Availability is a plan property (`single_country` on `/public/plans`) rather than a separate product. If your use is one market and the record ceiling is what is stopping you, this is the shape to ask about at [/contact](https://jobopportunitiesapi.org/contact).

**See also**

- [The plans](./account-plans.md#plans-table)
- [Country codes](./api-parameters.md#country-codes)
- [The response headers](./api-rate-limits.md#limit-headers)

---

## Where to go next

This file is part of **Account & billing**. Others in the same group:

- [Plans](./account-plans.md) — What each plan includes, rendered from the live price list, and what actually distinguishes them — which is records and entitlements, not features.
- [Sign-in and the dashboard](./account-sign-in.md) — Passwordless sign-in, every control on the account page, and what rotating or cancelling a key actually does.
- [Billing, invoices and status](./account-billing.md) — What happens between clicking buy and the key switching on, where the invoice comes from, and how to tell whether the data behind the API is current.

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
