account
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.
1What counts as a record
One row delivered to you, on any endpoint that returns rows. Metadata endpoints and /v1/me cost nothing.
In depthWhy it exists, what it is not, what people get wrong
| 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.
Exact contractTypes, defaults, ranges, errors, edge cases
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. For every other key they share one monthly allowance.
2Records versus requests
Two independent counters. The request limits stop you overwhelming the service; the record allowance is what the plan actually sells.
In depthWhy it exists, what it is not, what people get wrong
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.
Exact contractTypes, defaults, ranges, errors, edge cases
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); 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.
3Running out
A 402 with record_quota_exhausted. It resets on the first of the month, and retrying before then will not help.
In depthWhy it exists, what it is not, what people get wrong
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.
Exact contractTypes, defaults, ranges, errors, edge cases
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.
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" >&24The country-locked plan
One country, no record limit on job rows. The restriction is what pays for the exemption, and the two are inseparable.
In depthWhy it exists, what it is not, what people get wrong
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.
Exact contractTypes, defaults, ranges, errors, edge cases
| 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.
This page was rendered 11 September 2026, 09:53 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.