Job Opportunities API

Check the data. Then trust it.

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.

Last verified 2026-08-22 · Assumes: Rate limits. · Markdown copy

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
CallRecords charged
GET /v1/jobs?limit=200up to 200 — one per row returned
GET /v1/jobs/{id}1
GET /v1/jobs/closed?limit=50up to 50
GET /v1/jobs/expired?limit=1000up to 1,000 — ids are rows too
GET /v1/changes?limit=500up to 500 — one per change
GET /v1/companies?limit=25up to 25
GET /v1/companies/{slug}1
GET /v1/exportone per row written, charged as the stream is produced
GET /v1/me0
GET /v1/meta/facets, /meta/freshness, /meta/providers0
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 limitsRecord allowance
PurposeProtect the origin from burstsThe licence — how much data you may take
Counted perKey, per minute and per dayKey, per calendar month
ResetsRolling minute; UTC midnightThe 1st of the month
Breach status429402
Retry?Yes, after Retry-AfterNo — until the 1st, or an upgrade
Visible inX-RateLimit-Limit / -Remaining / -Reset, and /v1/meX-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.

Watch the remaining allowance, from a cron job
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

4The 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
BehaviourOn a country-locked key
Job rowsOnly the locked country. Unmetered.
Company rowsMetered normally — the exemption is for job rows only.
X-JOA-Country-LockPresent on every response, carrying the ISO code.
X-RateLimit-Records-Limitunlimited on job-row responses.
Request limitsUnchanged — the per-minute and per-day ceilings still apply.
?country= for another codeReturns nothing. Not an error.
/v1/changesScoped 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.