Job Opportunities API

Check the data. Then trust it.

api

Fields

Every field on a job row and a company row, with its type, whether it is always present, and whether it can ever be our inference rather than the source's statement.

Last verified 2026-08-22 · Assumes: Provenance — the published / inferred / absent trichotomy. · Markdown copy

1The job row

Identity, employer, classification, place, salary, timestamps, provenance and the apply link. Generated from the specification.

In depthWhy it exists, what it is not, what people get wrong

Fifteen fields are required and always present; the rest appear only when there is something to put in them. Two behaviours are worth internalising before you write a parser.

  • Optional fields are omitted, not nulled — with one deliberate exception. remote_inferred is always present, including when it is false, because it used to be omitted and that made a stated value and an absent one identical on the wire.
  • `closed_at` and `closed_reason` are present and `null` on a live row, rather than omitted, so a consumer can switch on them without a presence check.
  • `description` is present only when you asked for it with include_description=true, and only on rows that have one.
  • New fields appear without warning. Parse permissively.
Exact contractTypes, defaults, ranges, errors, edge cases
FieldTypeAlways presentMeaning
apply_urlstringno
categorystringno
category_confidencenumber · nullablenoClassifier confidence in category. Below 0.6 no category is published at all, so this is always null or >= 0.6.
citystringno
closed_atstring · nullableno
closed_reasonstring · nullableexpired_upstreamnot_seenno
companystringyes
company_logostringno
company_slugstringyes
countrystringno
descriptionstringnoThe full advert text. Present ONLY when the request set include_description=true, and only on the 80.9% of rows that have one (2,935,596 live rows).
employment_typestringno
field_sourcesFieldSourcesyesPer-field provenance. published means the value was carried by the source. inferred means WE produced it — it may be right, it is not a quotation. absent means no value, which is a different failure from a guess and is reported as such. category and seniority are ALWAYS inferred: both are read off the job title by a classifier, never from a field the employer filled in. salary is published when the source stated it in a field (salary_source: structured), and inferred when we read it out of the advert text (salary_source: parsed_description). It is never an estimate: the projection refuses to emit Erioun's AI salary predictions at all, so a modelled figure cannot reach this API by any route.
first_seen_atstringnoWhen the vacancy first entered this ledger. Populated on 100% of rows, unlike posted_at which the source often omits — so this is the field to sort or backfill by when you need every row to have a date.
has_descriptionbooleanyes
idstring · uuidyes
last_verified_atstringyesWhen we last confirmed this vacancy still exists at its source.
locationstringno
posted_atstringno
provider_typestringemployer_atsgovernmentdirectaggregatoryesThe provider-level class, in the original vocabulary. Kept so a query written against it still resolves.
remotestringremotehybridon_siteno
remote_inferredbooleanyesAlways present. True when we derived remote rather than read it. It used to be omitted when false, which made a stated value and an absent one identical on the wire.
salary_currencystringno
salary_maxnumberno
salary_minnumberno
salary_min_annual_eurnumbernosalary_min converted to an annual EUR figure, so a row quoting USD/hour and one quoting GBP/year are comparable. NULL when the period or the currency is unrecognised — never guessed, because reading an hourly rate as a salary is wrong by a factor of 2080. Converted with the indicative rates in /v1/meta/freshness, not a settlement rate.
salary_periodstringno
salary_sourcestringstructuredparsed_descriptionnoWhere the figure came from. structured is a field the source itself published. parsed_description is a real figure quoted in the advert text that WE read out and normalised — we chose the number and the period, so field_sources.salary reports it as inferred, never published.
senioritystringno
slugstringyes
sourcestringyesThe provider id, e.g. greenhouse, company_site, workday. The full published list is /public/providers; eures, arbeitsagentur and france_travail were named here until 2026-08-15, when they became discovery-only and left that list.
source_typestringatscareer_sitepublic_agencyaggregatoragencyunknownyesPer-row provenance where the source has been classified per row, otherwise the provider's class. Live distribution: ats 1,063,129, career_site 672,526, public_agency 320,727. RESERVED VALUES: aggregator and agency are accepted by the filter but currently match no rows. Every aggregator source is marked non-redistributable, so that inventory never enters the ledger at all, and no provider is classified agency yet. Filtering on either returns an empty page — that is the data, not a fault.
statusstringliveclosedyes
titlestringyes
upstream_expired_atstringnoWhen the radar proved the vacancy dead, as opposed to when we removed it.
One complete row
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?limit=1' | jq '.data[0]'
{
  "data": [
    {
      "id": "ffd24aa3-e155-4718-a6cc-4c9e20dede51",
      "slug": "assistant-manager-ii-ffd24aa3",
      "title": "Assistant Manager II",
      "company": "dollartree",
      "company_slug": "dollartree",
      "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/dollartree.png",
      "category": "Operations & Admin",
      "category_confidence": 0.8,
      "country": "US",
      "city": "GA Macon",
      "location": "GA-Macon",
      "remote": "on_site",
      "remote_inferred": true,
      "seniority": "Manager",
      "posted_at": "2026-09-11T01:04:43Z",
      "first_seen_at": "2026-09-11T01:11:47Z",
      "last_verified_at": "2026-09-11T01:50:06Z",
      "status": "live",
      "closed_at": null,
      "closed_reason": null,
      "apply_url": "https://dollartree.wd5.myworkdayjobs.com/dollartreeus/job/GA-Macon/Assistant-Manager-II_R-331907",
      "source": "workday",
      "source_type": "ats",
      "provider_type": "employer_ats",
      "has_description": false,
      "field_sources": {
        "remote": "inferred",
        "employment_type": "absent",
        "category": "inferred",
        "seniority": "inferred",
        "salary": "absent",
        "location": "inferred",
        "posted_at": "published",
        "description": "absent",
        "source_type": "inferred"
      }
    }
… 6 more lines
Real response, fetched from /public/jobs?limit=1 when this page was built (11 September 2026, 03:39 UTC). Run the command yourself and you will get today's rows, not these.

2field_sources

Nine fields, each marked published, inferred or absent. Present on every job row, always, with all nine keys.

In depthWhy it exists, what it is not, what people get wrong

This block is required and complete: all nine keys are present on every row, so you never have to test for their existence. Read it before the fields it describes.

Two of the nine can never be published, by construction — category and seniority are read off the job title. One of them is always inferred today for a different reason: source_type's per-row column is null on every live row, so the provider's class is the only path taken.

Exact contractTypes, defaults, ranges, errors, edge cases

Per-field provenance. published means the value was carried by the source. inferred means WE produced it — it may be right, it is not a quotation. absent means no value, which is a different failure from a guess and is reported as such.

category and seniority are ALWAYS inferred: both are read off the job title by a classifier, never from a field the employer filled in.

salary is published when the source stated it in a field (salary_source: structured), and inferred when we read it out of the advert text (salary_source: parsed_description). It is never an estimate: the projection refuses to emit Erioun's AI salary predictions at all, so a modelled figure cannot reach this API by any route.

FieldTypeAlways presentMeaning
categorystringpublishedinferredabsentyes
descriptionstringpublishedinferredabsentyes
employment_typestringpublishedinferredabsentyes
locationstringpublishedinferredabsentyes
posted_atstringpublishedinferredabsentyes
remotestringpublishedinferredabsentyes
salarystringpublishedinferredabsentyes
senioritystringpublishedinferredabsentyes
source_typestringpublishedinferredabsentyesinferred when the value is the provider's default class; published when the row itself was classified upstream. TODAY THIS IS ALWAYS inferred: the row-level column is null on every live listing, so the provider's class is the only path taken. published is reserved for when per-row classification lands. Do not build a filter that expects it to appear.
`field_sources` keyDescribesCan be `published`?
remoteremote, remote_inferredYes — a minority of sources state it.
employment_typeemployment_typeYes.
categorycategory, category_confidenceNo — always inferred.
seniorityseniorityNo — always inferred.
salarysalary_min, salary_max, salary_currency, salary_period, salary_sourceYes, when salary_source is structured.
locationcountry, city, locationYes.
posted_atposted_atYes.
descriptiondescription, has_descriptionYes.
source_typesource_typeReserved — always inferred today.

3The salary fields

Six fields describing money, one of which is a normalised comparison figure that is null whenever normalising would have required a guess.

In depthWhy it exists, what it is not, what people get wrong
salary_min, salary_max
The figures as stated, in the source's own currency and period. salary_max may equal salary_min for a single figure.
salary_currency
The currency as stated.
salary_period
The period as stated — annual, monthly, hourly, and so on.
salary_source
structured (a field the source published) or parsed_description (a real figure we read out of the advert text). Absent when there is no salary.
salary_min_annual_eur
salary_min converted to an annual EUR figure so rows can be compared. Null when the period or currency is unrecognised.

The null in that last row is the important part. Reading an hourly rate as an annual salary is wrong by a factor of about 2080, so an unrecognised period produces no figure rather than a wrong one. min_salary and max_salary filter on this column, which is why they select a narrow slice.

Exact contractTypes, defaults, ranges, errors, edge cases

Conversion uses indicative rates published on /v1/meta/freshness, not a settlement rate. For anything financial, take salary_min, salary_currency and salary_period and convert them yourself with a rate you control.

4The company row

Identity, website, sectors, and two different counts of open roles that mean different things.

In depthWhy it exists, what it is not, what people get wrong

open_roles is roles retrievable from this API — never a number the API cannot honour. own_site_roles is vacancies we watched on the company's own careers page under a source we do not redistribute. A company can have zero of the first and many of the second; those are the “discovered” companies, returned only with include_discovered=true, and they are evidence a company is hiring rather than inventory you can buy.

website is present only where we fetched the domain and found the company's own name on it — website_verified carries when. An absent website means we could not prove one, not that the company has none.

Exact contractTypes, defaults, ranges, errors, edge cases
FieldTypeAlways presentMeaning
careers_urlstringno
countrystringno
first_seenstringno
industrystringno
logostringno
namestringno
open_rolesintegernoRoles retrievable from THIS API. Never a number the API cannot honour.
org_typestringno
own_site_rolesintegernoVacancies seen on the company own careers page.
sectorsarrayno
slugstringno
source_typesarraynoaggregator and agency are reserved and match no rows today; see the source_type enum.
websitestringno
website_verifiedstringnoWhen we fetched this domain and found the company own name on it.

source_types is an array and can contain the reserved values aggregator and agency, which match no rows today — the same reservation as on the job row's source_type.

5Nullability, in one table

Which fields are guaranteed, which are omitted when empty, which are explicitly null, and which appear only on request.

In depthWhy it exists, what it is not, what people get wrong
BehaviourFieldsHow to handle it
Always presentid, slug, title, company, company_slug, remote_inferred, last_verified_at, status, source, source_type, provider_type, has_description, field_sourcesSafe to read without a guard.
Present but nullablecategory_confidence, closed_at, closed_reason, salary_min_annual_eurCheck for null, not for absence.
Omitted when emptycompany_logo, category, country, city, location, remote, employment_type, seniority, all salary_*, posted_at, apply_url, upstream_expired_at, first_seen_atCheck for absence. In most languages a missing key and a null read the same; if yours distinguishes them, treat both as “no value”.
Only on requestdescriptionNeeds include_description=true, and only appears on rows where has_description is true.
Only on some responsescompleteness (page level)Only when you passed require_fields.
Exact contractTypes, defaults, ranges, errors, edge cases

apply_url is in the “omitted when empty” list for completeness, but in practice it is on effectively every live row — a row without one is exactly the shape the quality gate holds back. Check the current figure on the coverage report rather than trusting this sentence.

For a language with strict types, the safe modelling is: required fields non-optional, everything else optional, and field_sources a required struct of nine required enums. Do not model the enums as closed — a new value is a compatible change and your parser should not fail on one.

This page was rendered 11 September 2026, 03:39 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.