ledger
Sourcing and refusals
The three source classes, the providers inside each, the redistribution flag that gates them, and the sources we fetch for ourselves and never republish.
1The three source classes
Every published row comes from an employer's own site, an employer's applicant tracking system, or a public employment agency. There is no fourth class.
In depthWhy it exists, what it is not, what people get wrong
direct— the employer's own careers page- Read straight from the employer's site. The most first-party source there is: nobody stands between the vacancy and the row. This is the largest class by a wide margin.
employer_ats— the employer's applicant tracking system- Greenhouse, Lever, Workday, SmartRecruiters, Personio, Teamtailor, Ashby, Workable, Recruitee, BambooHR, Oracle HCM and the rest. The employer publishes into the ATS; we read the ATS's own public feed. The apply link goes to the employer's ATS-hosted application, which is where a candidate would apply anyway.
government— public employment services- National employment agencies publishing on employers' behalf, where the licence permits redistribution. Currently a small share of rows.
A row carries the class twice, in two vocabularies, and this trips people up. provider_type holds the original vocabulary (employer_ats, government, direct, aggregator) and is kept so that a query written against it still resolves. source_type holds the newer, finer per-row vocabulary (ats, career_site, public_agency, aggregator, agency, unknown). The source_type filter accepts the legacy spellings too and maps them across.
Exact contractTypes, defaults, ranges, errors, edge cases
| Source class | Providers | Live rows |
|---|---|---|
direct | 3 | 1,733,131 |
employer_ats | 17 | 1,662,793 |
government | 3 | 10,227 |
Measured 11 September 2026, 10: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.
| Legacy value (`provider_type`) | Maps to (`source_type`) | Still accepted by the filter? |
|---|---|---|
employer_ats | ats | Yes |
government | public_agency | Yes |
direct | career_site | Yes |
aggregator | aggregator | Yes — but it matches no rows; see below |
2Every provider, with its live row count
The provider field names the exact system a row came from. This list is the vocabulary the provider filter accepts — read it at run time, not from here.
In depthWhy it exists, what it is not, what people get wrong
provider is finer than source_type: it names Greenhouse rather than “an ATS”. That matters when you care about the shape of the data rather than its origin — description coverage, for instance, varies from about 60% to nearly 100% depending on which system published the row, and the table below shows it per provider.
Exact contractTypes, defaults, ranges, errors, edge cases
| provider | Label | Live rows | With a description | source_class | source_type |
|---|---|---|---|---|---|
company_site | Company career site | 1,886,354 | 1,585,677 (84%) | direct | career_site |
workday | Workday | 629,719 | 275,792 (43.7%) | employer_ats | ats |
oracle | Oracle HCM | 234,539 | 203,066 (86.5%) | employer_ats | ats |
greenhouse | Greenhouse | 158,958 | 158,940 (99.9%) | employer_ats | ats |
smartrecruiters | SmartRecruiters | 100,055 | 98,333 (98.2%) | employer_ats | ats |
paylocity | Paylocity | 85,094 | 79,647 (93.5%) | employer_ats | ats |
teamtailor | Teamtailor | 69,607 | 69,546 (99.9%) | employer_ats | ats |
ashby | Ashby | 68,084 | 68,084 (100%) | employer_ats | ats |
workable | Workable | 63,792 | 63,790 (99.9%) | employer_ats | ats |
lever | Lever | 49,904 | 49,180 (98.5%) | employer_ats | ats |
personio | Personio | 48,387 | 44,039 (91%) | employer_ats | ats |
recruitee | Recruitee | 35,824 | 35,804 (99.9%) | employer_ats | ats |
breezy | Breezy HR | 35,023 | 34,342 (98%) | employer_ats | ats |
bamboohr | BambooHR | 25,921 | 25,327 (97.7%) | employer_ats | ats |
join | join.com | 16,372 | 16,320 (99.6%) | employer_ats | ats |
rippling | Rippling | 15,909 | 15,573 (97.8%) | employer_ats | ats |
pinpoint | Pinpoint | 14,639 | 14,639 (100%) | employer_ats | ats |
ukg | UKG Pro | 11,058 | 11,058 (100%) | employer_ats | ats |
jobtech_sweden | JobTech Sweden | 10,329 | 10,329 (100%) | government | public_agency |
af_employer_direct | Employer career site (Sweden) | 902 | 900 (99.7%) | direct | career_site |
nav_norway | NAV Norge | 145 | 0 (0%) | government | public_agency |
extension_submission | Direct submission | 6 | 5 (83.3%) | direct | career_site |
22 providers. This list is the vocabulary the provider and exclude_provider filters accept: a name that is not here is a 422, never a silently empty page. Rendered from /public/providers. Read it at request time rather than pinning this list — on 2026-08-15 three names left it and a documented example became a 422 the next morning.
provider and exclude_provider take up to twelve comma-separated values each, and an unrecognised one is a 422 with the name echoed back — never a silently empty page. That is deliberate: an empty page from a typo is indistinguishable from an empty page from a narrow filter, and only one of them is your bug.
$ curl -s https://api.jobopportunitiesapi.org/public/providers \ | jq -r '.data[] | "\(.provider)\t\(.live_listings)\t\(.source_type)"' { "data": [ { "provider": "company_site", "label": "Company career site", "live_listings": 1886354, "with_description": 1585677, "with_description_pct": 84, "source_class": "direct", "source_type": "career_site" }, { "provider": "workday", "label": "Workday", "live_listings": 629719, "with_description": 275792, "with_description_pct": 43.7, "source_class": "employer_ats", "source_type": "ats" }, { "provider": "oracle", "label": "Oracle HCM", "live_listings": 234539, "with_description": 203066, "with_description_pct": 86.5, … 178 more lines
3The redistribution flag — a join, not a filter
A source is published only if it is explicitly marked redistributable. The default is false, so anything new is excluded until a human classifies it.
In depthWhy it exists, what it is not, what people get wrong
This is the single most important implementation detail in the sourcing story, and it is worth understanding the difference it makes. A blacklist — “publish everything except these names” — fails open: a source added upstream on Tuesday is published on Tuesday, and nobody finds out until someone notices. An allowlist enforced as an inner join fails closed: the same source produces zero rows in the served set until it is classified.
That is why the excluded list can be printed rather than promised. It is not a statement about intent; it is a description of a column whose default is exclusion.
Exact contractTypes, defaults, ranges, errors, edge cases
joa.providers.redistributable is a boolean defaulting to false. The projection that builds the served set inner-joins listings to providers on that flag. A provider row also carries source_class, source_type, a licence URL, when the licence was last checked and by whom, any required attribution text, and a structured restrictions object — so the classification is a record, not a memory.
One more guarantee sits alongside it, in the same query: every salary column is wrapped in a condition that refuses to emit an AI-estimated figure. Estimated salaries exist elsewhere in the group and cannot reach this API by any route. See Salary provenance.
4The sources we fetch and never republish
Named, not merely counted. These are fetched for our own products and are excluded from this API because we are not licensed to redistribute them.
In depthWhy it exists, what it is not, what people get wrong
adzunaapify_all_jobsapify_fantastic_jobsapify_job_listingsapify_xingarbeitnowarbeitsagenturcareerjeteuresfrance_travailhimalayasjobicyjooblelanding_jobsremoteokremotivethe_museweworkremotelyworking_nomads
19 sources excluded, listed by name in /public/coverage under excluded_sources. Measured 11 September 2026, 10:52 UTC.
There are two reasons on that list and they are not the same reason.
- Aggregators — the large majority
- Job aggregators and marketplaces whose terms do not permit us to redistribute their inventory. We read them for our own products; nothing from them enters this ledger's served set. Republishing them would also defeat the point of the product: an aggregator's row cannot tell you where it originally came from.
- Three public agencies, discovery-only since 2026-08-15
eures,arbeitsagenturandfrance_travail. These were published until that date and are now used for discovery only — they tell us an employer is hiring, and we then look for the vacancy at the employer. The apply link on those rows resolves to the agency's portal rather than to the employer, which does not meet the employer-direct promise this API is sold on.
Exact contractTypes, defaults, ranges, errors, edge cases
The list on this page is rendered from excluded_sources on /public/coverage, which reads it from the provider table at request time — so it is the actual contents of the column, not a copy of it. The per-source licence detail (the URL, when it was last checked, any attribution required) is recorded internally and is not currently exposed on a public endpoint; if you need it for a procurement review, ask at /contact and it will be sent to you.
A row from an excluded source is not merely filtered out of responses — it never enters the served set, so it is counted as quality_removed in `withheld_listings` rather than as a live row you are not allowed to see.
5Staffing agencies and job boards
Real vacancies whose poster is not the employer. Excluded by default, re-admittable deliberately on a paid key, and always labelled.
In depthWhy it exists, what it is not, what people get wrong
This is a different category from an excluded source. The posting is genuine and the apply link is the poster's own — it is not a stale copy of somebody else's row. What it is not is the employer. A staffing agency advertising a role it is recruiting for is doing something legitimate, and a row saying so is useful; it simply is not what “employer-direct” means.
The practical reason for the default is volume. One agency with eighteen thousand listings can flood a category until search stops being useful, and the flooding is invisible unless you already know to look for it. Excluding by default and re-admitting on request puts the choice in the caller's hands rather than in the shape of the data.
Exact contractTypes, defaults, ranges, errors, edge cases
include_poster_typestringcomma-separatedstaffingjobboardallRe-admits vacancies whose poster is a staffing agency or a job board. Excluded by DEFAULT: the posting is real and the apply link is the poster's own, but the poster is not the employer, and one agency with 18,000 listings can flood a category until search stops being useful. Comma-separated. Paid endpoints only. An unrecognised value is a 422.
qualitystringallall re-admits rows we have GATED -- reversible doubt, such as a
future posted_at or a missing apply URL. It never re-admits rows we
have REMOVED: those breach the employer-direct guarantee and no query
parameter may opt back into it. Each returned row carries its verdict,
its rule id and the matched host. Paid endpoints only.
2 parameters, generated from /openapi.json when this page was built. The spec is served from the running API and is the contract; if this table and the spec ever disagree, the spec is right and this is a bug — please say so with the thumbs-down below.
include_poster_type is a paid-endpoints-only parameter: on /public/* it is ignored. An unrecognised value is a 422. It is comma-separated, so include_poster_type=staffing,jobboard and include_poster_type=all are both valid.
A=https://api.jobopportunitiesapi.org
H="Authorization: Bearer $JOA_KEY"
curl -s -H "$H" "$A/v1/jobs?country=GB&category=Engineering&limit=1" | jq '.data|length'
curl -s -H "$H" "$A/v1/jobs?country=GB&category=Engineering&include_poster_type=all&limit=1" | jq '.data|length'This page was rendered 11 September 2026, 13:40 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.