ledger
Coverage and honesty
How to read /public/coverage, why the weak numbers are published as prominently as the strong ones, and what measured_at and stale actually mean.
1The coverage report
One keyless endpoint returns the whole picture: the three populations, per-field completeness, the source breakdown, and the sources we refuse.
In depthWhy it exists, what it is not, what people get wrong
/public/coverage is the canonical report and everything else on this site defers to it. It is deliberately keyless and deliberately complete: the same response carries the figures that flatter the product and the figures that do not, so there is no version of it that could be quoted selectively without the reader noticing.
It is measured on a schedule rather than counted per request. Counting it per request took twenty-four seconds and returned a 503 instead — a report that times out is not a more honest report. So every response carries measured_at, age_seconds and a stale boolean, and you should read those before you read anything else in it.
Exact contractTypes, defaults, ranges, errors, edge cases
| Figure | Rows | What it counts |
|---|---|---|
live_listings | 3,406,509 | 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 | 226,450 | 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,679,106 | Roles that came off their source, retained with their closure date and reason. |
ledger_rows | 9,312,065 | live_listings + withheld_listings + closed_listings. It reconciles exactly. |
employers | 189,037 | Distinct employers with at least one row you can retrieve. |
countries | 249 | Distinct ISO country codes present on live rows. |
posted_last_7d | 198,363 | Live rows whose posted_at falls in the last seven days. |
Reconciliation, computed from the same response: 3,406,509 + 226,450 + 5,679,106 = 9,312,065 and ledger_rows is 9,312,065 — they match exactly, which is the guarantee.
The report also carries stale: false and age_seconds: 5,871. 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 11 September 2026, 07: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.
$ curl -s https://api.jobopportunitiesapi.org/public/coverage | jq 'del(.fields, .definitions, .excluded_sources)' { "age_seconds": 5871, "closed_listings": 5679106, "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": 189037, "excluded_sources": [ "adzuna", "apify_all_jobs", "apify_fantastic_jobs", "apify_job_listings", "apify_xing", "arbeitnow", "arbeitsagentur", "careerjet", "eures", "france_travail", "himalayas", "jobicy", "jooble", "landing_jobs", "remoteok", "remotive", "the_muse", … 93 more lines
2Per-field completeness
How many live rows carry each field, as a count and a share, with a sentence saying what the gap is rather than leaving you to guess.
In depthWhy it exists, what it is not, what people get wrong
The point of publishing this is that a percentage on its own is not information. “City: 71%” could mean the pipeline is broken or it could mean that a lot of sources state only a free-text location — those are different problems with different consequences for you. So every row carries a note saying which it is.
Read the weak rows first. salary_eur and seniority are the two fields people most often plan around and are the two with the thinnest coverage; if your product needs either of them on most rows, that is worth knowing on day one rather than in week three.
Exact contractTypes, defaults, ranges, errors, edge cases
| Field | Live rows carrying it | Share | What the gap is |
|---|---|---|---|
apply_url | 3,406,509 | Every row links to the employer's own application page. | |
country | 2,511,291 | ISO country. Absent where the source states only a free-text location. | |
category | 3,216,600 | Inferred by us and published only above 0.6 confidence. | |
remote | 3,399,117 | Stated by the employer, or inferred — field_sources tells you which. | |
city | 2,450,804 | Absent where the source gives a region or a remote-only role. | |
description | 2,710,063 | Full advert text. The largest gap in the ledger, and the honest number. | |
seniority | 1,533,597 | Inferred from the title. Most titles do not state one. | |
salary_eur | 222,712 | Structured salary only. We never publish an estimate as a fact. |
Measured 11 September 2026, 07: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.
These counts are ledger-wide over live rows. They are not conditioned on your filters, and the intersection with your filters is at most the smallest of them. For the per-market view — which is usually the one that decides anything — use the per-country table. To retrieve only rows that carry a field as published, use `require_fields`.
3Per-country coverage
How much data sits behind each country code: live rows, how many carry a description, how many carry a published salary, and how many employers.
In depthWhy it exists, what it is not, what people get wrong
This is the answer to “is there enough of this in my market”, and it is the table to read before planning around any ledger-wide percentage. The variation between markets is enormous — description coverage runs from the high sixties to the high nineties, and published salary from a fraction of a per cent to a double-digit share — so a single global figure will mislead you in one direction or the other.
live- Rows you can retrieve for that country right now.
described/described_pct- How many of them carry an advert body, as a count and a share.
with_salary- How many carry a salary the employer published, not one we read out of the text.
employers- Distinct companies behind those rows. A high
livewith a lowemployersmeans a few large hirers dominate.
Exact contractTypes, defaults, ranges, errors, edge cases
Every country in the ledger — 170 rows, sorted by live listings
| ISO | Live | With a description | Described | With a published salary | Employers |
|---|---|---|---|---|---|
US | 1,573,203 | 1,195,876 | 76% | 182,267 | 104,136 |
DE | 168,538 | 158,034 | 93.8% | 7,866 | 29,231 |
GB | 110,483 | 95,694 | 86.6% | 5,772 | 11,758 |
IN | 72,174 | 57,951 | 80.3% | 985 | 5,273 |
FR | 63,256 | 59,347 | 93.8% | 2,801 | 4,071 |
CA | 59,607 | 54,321 | 91.1% | 4,651 | 6,115 |
NL | 50,469 | 46,555 | 92.2% | 5,006 | 7,119 |
SE | 31,591 | 30,799 | 97.5% | 51 | 5,532 |
AU | 23,130 | 20,636 | 89.2% | 286 | 3,218 |
ES | 21,735 | 18,524 | 85.2% | 508 | 2,966 |
BE | 16,531 | 14,565 | 88.1% | 146 | 2,343 |
IT | 14,239 | 12,951 | 91% | 319 | 1,825 |
CH | 14,207 | 13,085 | 92.1% | 108 | 2,265 |
HU | 14,122 | 2,900 | 20.5% | 21 | 480 |
MX | 14,042 | 11,451 | 81.5% | 198 | 1,748 |
PL | 12,887 | 10,919 | 84.7% | 279 | 1,906 |
BR | 12,837 | 9,865 | 76.8% | 296 | 1,552 |
SG | 12,642 | 9,999 | 79.1% | 111 | 1,635 |
PH | 11,455 | 9,842 | 85.9% | 277 | 1,289 |
MY | 10,240 | 8,842 | 86.3% | 747 | 1,512 |
AT | 10,083 | 9,460 | 93.8% | 3,071 | 2,094 |
AE | 10,081 | 8,997 | 89.2% | 772 | 1,172 |
JP | 9,974 | 8,542 | 85.6% | 107 | 1,841 |
CN | 9,832 | 7,408 | 75.3% | 65 | 1,187 |
HR | 8,829 | 8,591 | 97.3% | 21 | 1,170 |
IE | 8,550 | 7,419 | 86.8% | 313 | 1,442 |
NO | 8,349 | 7,702 | 92.3% | 144 | 1,296 |
ZA | 7,066 | 6,490 | 91.8% | 667 | 982 |
PT | 6,956 | 6,189 | 89% | 115 | 902 |
DK | 5,572 | 5,135 | 92.2% | 45 | 830 |
GR | 5,560 | 5,219 | 93.9% | 57 | 654 |
FI | 5,120 | 4,424 | 86.4% | 73 | 939 |
ID | 5,013 | 4,349 | 86.8% | 12 | 906 |
TH | 4,749 | 3,895 | 82% | 61 | 639 |
CO | 4,703 | 4,017 | 85.4% | 84 | 896 |
SA | 4,496 | 4,010 | 89.2% | 27 | 618 |
RO | 4,489 | 3,963 | 88.3% | 36 | 762 |
NZ | 4,120 | 3,776 | 91.7% | 31 | 576 |
HK | 3,739 | 2,931 | 78.4% | 164 | 737 |
EG | 3,720 | 3,249 | 87.3% | 44 | 512 |
TW | 3,599 | 2,755 | 76.5% | 29 | 520 |
VN | 3,535 | 2,907 | 82.2% | 21 | 547 |
CZ | 3,309 | 2,696 | 81.5% | 41 | 606 |
LT | 3,040 | 2,798 | 92% | 85 | 261 |
AR | 3,020 | 2,557 | 84.7% | 91 | 680 |
GE | 3,004 | 2,059 | 68.5% | 24 | 612 |
KR | 2,840 | 2,331 | 82.1% | 17 | 600 |
UA | 2,764 | 2,549 | 92.2% | 31 | 366 |
BG | 2,707 | 2,407 | 88.9% | 75 | 409 |
PE | 2,459 | 1,859 | 75.6% | 15 | 321 |
IL | 2,340 | 1,992 | 85.1% | 22 | 398 |
CL | 2,138 | 1,787 | 83.6% | 9 | 340 |
TR | 1,839 | 1,494 | 81.2% | 8 | 430 |
MA | 1,767 | 1,588 | 89.9% | 4 | 242 |
UN | 1,652 | 1,632 | 98.8% | 485 | 352 |
LU | 1,628 | 1,417 | 87% | 22 | 358 |
NG | 1,513 | 1,476 | 97.6% | 5 | 269 |
PK | 1,461 | 1,402 | 96% | 46 | 253 |
QA | 1,374 | 1,151 | 83.8% | 13 | 169 |
CR | 1,202 | 876 | 72.9% | 8 | 237 |
RS | 1,138 | 1,053 | 92.5% | 11 | 335 |
SK | 1,136 | 937 | 82.5% | 26 | 217 |
MT | 1,130 | 1,063 | 94.1% | 35 | 231 |
CY | 1,102 | 1,055 | 95.7% | 28 | 238 |
EE | 924 | 879 | 95.1% | 17 | 200 |
LK | 915 | 847 | 92.6% | 4 | 106 |
GT | 818 | 721 | 88.1% | 16 | 109 |
LV | 773 | 713 | 92.2% | 34 | 165 |
KE | 749 | 691 | 92.3% | 7 | 214 |
EC | 673 | 599 | 89% | 5 | 87 |
JO | 632 | 496 | 78.5% | 0 | 127 |
TN | 621 | 521 | 83.9% | 5 | 115 |
PA | 569 | 446 | 78.4% | 2 | 145 |
KZ | 525 | 479 | 91.2% | 9 | 116 |
LB | 453 | 338 | 74.6% | 5 | 145 |
DO | 440 | 383 | 87% | 0 | 103 |
SI | 417 | 330 | 79.1% | 5 | 117 |
PR | 404 | 271 | 67.1% | 8 | 75 |
KW | 391 | 357 | 91.3% | 0 | 86 |
AZ | 384 | 368 | 95.8% | 167 | 226 |
MV | 359 | 340 | 94.7% | 0 | 17 |
UY | 348 | 287 | 82.5% | 0 | 107 |
BD | 339 | 308 | 90.9% | 5 | 125 |
BH | 326 | 269 | 82.5% | 3 | 86 |
LI | 324 | 322 | 99.4% | 0 | 36 |
MU | 322 | 319 | 99.1% | 0 | 35 |
RU | 309 | 300 | 97.1% | 3 | 93 |
OM | 289 | 214 | 74% | 1 | 82 |
SV | 283 | 265 | 93.6% | 72 | 67 |
TZ | 269 | 265 | 98.5% | 3 | 46 |
AM | 268 | 244 | 91% | 1 | 76 |
AL | 252 | 235 | 93.3% | 36 | 102 |
HN | 230 | 209 | 90.9% | 17 | 71 |
GH | 223 | 207 | 92.8% | 8 | 91 |
SR | 222 | 180 | 81.1% | 1 | 25 |
VE | 214 | 202 | 94.4% | 4 | 65 |
BM | 199 | 186 | 93.5% | 0 | 12 |
SW | 197 | 197 | 100% | 0 | 99 |
MD | 189 | 168 | 88.9% | 0 | 51 |
IQ | 184 | 173 | 94% | 0 | 41 |
NI | 183 | 178 | 97.3% | 0 | 47 |
NP | 155 | 116 | 74.8% | 1 | 34 |
AD | 151 | 145 | 96% | 21 | 135 |
MK | 143 | 129 | 90.2% | 2 | 67 |
DZ | 141 | 115 | 81.6% | 0 | 39 |
FJ | 140 | 108 | 77.1% | 0 | 21 |
ET | 137 | 111 | 81% | 0 | 46 |
JM | 127 | 111 | 87.4% | 5 | 49 |
SO | 125 | 125 | 100% | 1 | 20 |
BA | 121 | 116 | 95.9% | 0 | 53 |
AO | 117 | 84 | 71.8% | 0 | 20 |
XK | 115 | 114 | 99.1% | 0 | 27 |
KH | 111 | 87 | 78.4% | 4 | 46 |
MZ | 110 | 105 | 95.5% | 0 | 25 |
BO | 110 | 102 | 92.7% | 0 | 35 |
BY | 107 | 107 | 100% | 0 | 22 |
GI | 105 | 104 | 99% | 0 | 27 |
CI | 102 | 102 | 100% | 0 | 36 |
UG | 102 | 102 | 100% | 0 | 35 |
CD | 101 | 100 | 99% | 1 | 30 |
MM | 98 | 68 | 69.4% | 1 | 31 |
JE | 89 | 88 | 98.9% | 1 | 19 |
TT | 89 | 85 | 95.5% | 0 | 19 |
MO | 88 | 61 | 69.3% | 0 | 17 |
ME | 88 | 67 | 76.1% | 0 | 18 |
IM | 85 | 85 | 100% | 2 | 18 |
BS | 84 | 74 | 88.1% | 1 | 24 |
SN | 84 | 79 | 94% | 0 | 30 |
SP | 81 | 81 | 100% | 0 | 14 |
UZ | 80 | 76 | 95% | 0 | 32 |
CM | 78 | 78 | 100% | 0 | 26 |
AW | 77 | 63 | 81.8% | 0 | 6 |
GG | 76 | 76 | 100% | 0 | 15 |
MC | 72 | 59 | 81.9% | 1 | 30 |
IS | 72 | 59 | 81.9% | 0 | 33 |
KY | 70 | 59 | 84.3% | 1 | 17 |
LA | 68 | 50 | 73.5% | 2 | 34 |
PG | 68 | 57 | 83.8% | 0 | 20 |
ZM | 68 | 64 | 94.1% | 0 | 27 |
PF | 67 | 61 | 91% | 2 | 9 |
MQ | 65 | 64 | 98.5% | 1 | 19 |
IR | 62 | 55 | 88.7% | 1 | 16 |
HO | 61 | 61 | 100% | 0 | 3 |
PY | 61 | 54 | 88.5% | 0 | 32 |
NA | 60 | 56 | 93.3% | 0 | 18 |
RW | 58 | 48 | 82.8% | 0 | 24 |
GY | 58 | 53 | 91.4% | 0 | 16 |
SC | 52 | 52 | 100% | 0 | 7 |
BB | 50 | 45 | 90% | 0 | 13 |
RE | 49 | 48 | 98% | 1 | 25 |
MG | 46 | 44 | 95.7% | 0 | 14 |
SZ | 46 | 46 | 100% | 0 | 6 |
SL | 44 | 41 | 93.2% | 0 | 12 |
JA | 41 | 41 | 100% | 0 | 16 |
GA | 40 | 40 | 100% | 0 | 12 |
BW | 39 | 39 | 100% | 0 | 18 |
NE | 37 | 34 | 91.9% | 4 | 24 |
LY | 36 | 35 | 97.2% | 0 | 13 |
TD | 33 | 33 | 100% | 0 | 11 |
SD | 31 | 27 | 87.1% | 1 | 13 |
WS | 31 | 27 | 87.1% | 0 | 6 |
AF | 30 | 24 | 80% | 0 | 14 |
PO | 30 | 30 | 100% | 0 | 7 |
GP | 28 | 28 | 100% | 0 | 13 |
ZW | 27 | 27 | 100% | 0 | 13 |
GU | 27 | 26 | 96.3% | 1 | 15 |
LR | 26 | 26 | 100% | 0 | 7 |
HT | 26 | 25 | 96.2% | 1 | 12 |
SM | 25 | 15 | 60% | 0 | 6 |
SY | 25 | 24 | 96% | 0 | 16 |
170 countries, rendered from /public/coverage/countries, which needs no key. The count is of ISO codes we have live rows for — it is not a list of the world’s countries and it moves as employers post and roles close. Measured 11 September 2026, 07:52 UTC.
curl -s https://api.jobopportunitiesapi.org/public/coverage/countries \
| jq '.countries[] | select(.country=="DE")'4Freshness — measured_at, stale, and the refresh cycle
The ledger is refreshed on a schedule, not on a webhook. Every report says when it was measured and whether that is older than it should be.
In depthWhy it exists, what it is not, what people get wrong
There are two different clocks and confusing them is easy. last_refreshed is when the ledger itself was last rebuilt from its sources. measured_at is when the coverage snapshot was counted. The second is always at or after the first, and it is the one the figures on a report belong to.
stale: true means the snapshot is older than its refresh window — usually because a refresh is running long or has failed. It does not mean the data is wrong; it means the counts are indicative and you should re-read the endpoint before quoting them anywhere that matters.
Per-row freshness is a separate thing again. Every listing carries last_verified_at: the last time we confirmed that vacancy still existed at its source. verified_after filters on it, which is the parameter you want if you are asking “what has been re-confirmed since yesterday” rather than “what is new”.
Exact contractTypes, defaults, ranges, errors, edge cases
| Figure | Value | Meaning |
|---|---|---|
measured_at | 11 September 2026, 07:52 UTC | When the snapshot behind this report was taken. |
generated_at | 11 September 2026, 09:30 UTC | When this response was assembled. |
live_listings | 3,406,509 | Rows /v1/jobs will serve. |
ledger_rows | 9,312,065 | Live + withheld + closed. |
closures.last_24h | 90,740 | Roles that left their source in the last day. |
closures.last_7d | 1,631,534 | …and in the last week. |
closures.total | 5,679,106 | Retained: kept indefinitely. |
Measured 11 September 2026, 07:52 UTC · rendered from /public/freshness, which needs no key. These figures move; the endpoint is always right and this page is only as fresh as its last build.
| Field | Where | What it times |
|---|---|---|
measured_at | /public/coverage, /public/freshness | When the counts were taken. |
age_seconds | /public/coverage | Seconds since measured_at. |
stale | /public/coverage | measured_at is older than the refresh window. |
last_refreshed | /public/coverage, /public/stats | When the ledger was last rebuilt from sources. |
generated_at | /public/freshness | When that response was assembled. |
last_verified_at | every job row | When we last confirmed this vacancy exists at its source. |
first_seen_at | every job row | When the vacancy first entered this ledger. Populated on 100% of rows. |
posted_at | job rows, where stated | The source's own posting date. Often absent — sort or backfill on first_seen_at instead. |
5/public/stats and why it looks like it disagrees
It does not disagree — it publishes a broader live_listings that includes gated rows, and names the narrower one employer_direct_live. Coverage is canonical.
In depthWhy it exists, what it is not, what people get wrong
This is the single most confusing thing on the keyless surface, so it is worth stating precisely. /public/stats carries two counts where /public/coverage carries one:
stats.employer_direct_live- The rows you can actually retrieve. Identical to
coverage.live_listings. This is the number the website quotes and the one to use. stats.live_listings- A broader population: not delisted and not opted out, but including rows held back by the quality gate. Larger than what you can fetch.
stats.not_employer_direct_live- The difference between the two — the quality-gated rows.
live_listings − not_employer_direct_live = employer_direct_live. stats.companies- Employers with at least one row in the broader population, so it is larger than
coverage.employers, which counts employers with at least one retrievable row.
Exact contractTypes, defaults, ranges, errors, edge cases
$ curl -s https://api.jobopportunitiesapi.org/public/stats | jq 'del(.definitions)' { "companies": 234711, "definitions": { "authority": "For a reconciling report with per-field completeness, use /public/coverage: live + withheld + closed = ledger_rows, exactly.", "companies": "Employers with at least one live listing. Like live_listings, this INCLUDES employers whose every live listing is withheld by the quality gate, so it is larger than the number of employers you can reach. Use employer_direct_companies for that.", "employer_direct_companies": "Employers a caller can actually obtain from /v1/jobs: at least one live listing that is not opted out and not withheld. This is the number you can reproduce by paging the API, and the one /public/coverage publishes as employers.", "employer_direct_live": "Rows a caller can actually obtain from /v1/jobs: live, not opted out, and not withheld. This is the number you can reproduce by paging the API, and the one the website quotes.", "live_listings": "Rows in the ledger that are not delisted and not opted out. This INCLUDES rows withheld by the quality gate, so it is larger than what you can retrieve. Use employer_direct_live for that.", "not_employer_direct_live": "Rows held back by the quality gate — aggregator or agency destinations, and discovery-only sources. live_listings minus this equals employer_direct_live.", "posted_last_7d": "Listings whose posted_at falls in the last seven days." }, "employer_direct_companies": 189037, "employer_direct_live": 3352026, "last_refreshed": "2026-09-11T08:54:30Z", … 4 more lines
There is a third figure in the same neighbourhood: posted_last_7d appears on both endpoints and is computed over the respective population, so it too differs. Use coverage's.
This page was rendered 11 September 2026, 09:41 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.