# All employers jobs API (candidate surface)

Generated from the server's parameter registry on 2026-11-05T10:27:06Z. Base URL: `https://openrolesindex.com/orgs/all/v1`. Keyless: no API key, no account. Rate-limited per client address (HTTP 429 with `retry-after` when exceeded: wait that many seconds, then continue). Every response carries `x-org: all`.

Everything here is already scoped to this employer. These parameters are **rejected with 400** on this surface because they would only narrow or contradict that scope, or show what the career site does not: `organization`, `exclude_organization`, `source`, `exclude_source`, `domain`, `exclude_domain`, `include_unpublished`, `include_synthetic`.

## Endpoints

### Search jobs: `GET https://openrolesindex.com/orgs/all/v1/active-ats`

Active jobs at this employer. The one you will use most. Default order is `date_posted` descending; page with `offset` or `cursor`, stop when a page is shorter than `limit`.

Parameters: `time_frame`, `limit`, `offset`, `cursor`, `description_format`, `include_basic_organization_details`, `include_beta`, `include_duplicates`, `title`, `title_advanced`, `description`, `description_advanced`, `location`, `location_advanced`, `organization_advanced`, `source_category`, `ai_employment_type`, `ai_work_arrangement`, `ai_experience_level`, `ai_education`, `ai_language`, `ai_taxonomies_a`, `ai_taxonomies_a_primary`, `exclude_ai_taxonomies_a`, `ai_visa_sponsorship`, `has_salary`, `has_no_location`, `date_posted_gte`, `date_posted_lt`, `date_created_gte`, `date_created_lt`, `id`.

### Count jobs: `GET https://openrolesindex.com/orgs/all/v1/active-ats-count`

How many active jobs match a filter set. Use it to size a search or for *one-cert-away* counts before fetching anything.

Parameters: `time_frame`, `include_duplicates`, `title`, `title_advanced`, `description`, `description_advanced`, `location`, `location_advanced`, `organization_advanced`, `source_category`, `ai_employment_type`, `ai_work_arrangement`, `ai_experience_level`, `ai_education`, `ai_language`, `ai_taxonomies_a`, `ai_taxonomies_a_primary`, `exclude_ai_taxonomies_a`, `ai_visa_sponsorship`, `has_salary`, `has_no_location`, `date_posted_gte`, `date_posted_lt`, `date_created_gte`, `date_created_lt`.

`time_frame` on this endpoint: string · one of `1h`, `24h`, `7d`, `1m`, `6m` · default `1m`

`1h` / `24h` / `7d` / `6m` count **currently active** jobs, matching what the data endpoint returns for the same window. `1m` counts every job indexed in the last 31 days **including ones that have since expired** - monthly indexing volume, the number to size a plan with.

### Changed jobs: `GET https://openrolesindex.com/orgs/all/v1/modified-ats`

Active jobs whose tracked fields changed in the last 24 hours: re-check a shortlist without refetching it.

Parameters: `limit`, `offset`, `cursor`, `description_format`, `include_basic_organization_details`, `include_beta`, `include_duplicates`, `title`, `title_advanced`, `description`, `description_advanced`, `location`, `location_advanced`, `organization_advanced`, `source_category`, `ai_employment_type`, `ai_work_arrangement`, `ai_experience_level`, `ai_education`, `ai_language`, `ai_taxonomies_a`, `ai_taxonomies_a_primary`, `exclude_ai_taxonomies_a`, `ai_visa_sponsorship`, `has_salary`, `has_no_location`, `date_posted_gte`, `date_posted_lt`, `date_created_gte`, `date_created_lt`, `date_modified_gte`, `date_modified_lt`, `id`.

### Closed jobs: `GET https://openrolesindex.com/orgs/all/v1/expired-ats`

Ids of jobs that closed in the window, a bare JSON array of integers. Drop them from a saved shortlist.

Parameters: `time_frame`.

`time_frame` on this endpoint: string · one of `1h`, `1d`, `1m`, `6m` · default `1d`

- `1h` - rolling: expired in the last hour.
- `1d` - snapshot of the **previous UTC day** (not a rolling 24 hours). The usual choice for a daily sync.
- `1m` - the last 31 days.
- `6m` - the full retained history (183 days).

## Parameters

Unknown parameters are a 400 with a did-you-mean; a filter is refused rather than silently ignored. List values are comma-separated; wrap a value containing a comma in double quotes.

### `time_frame`

**On `active-ats`:** string · one of `1h`, `24h`, `7d`, `6m` · default `24h` · accepted on `active-ats`

Window over `date_created` (when this API first indexed the job).

- `1h` - the last complete UTC hour. Poll hourly for a gap-free, duplicate-free firehose.
- `24h` - the 24 complete hours before the top of the current hour. Poll daily at a fixed time.
- `7d` - rolling seven days up to now.
- `6m` - everything active, up to now. Use this for the initial backfill. Also the only window that returns `date_modified` / `modified_fields`.

`1h` and `24h` are hour-aligned on purpose: consecutive polls tile the timeline exactly. The window actually applied is echoed in the `x-window-start` / `x-window-end` response headers.

**On `active-ats-count`:** string · one of `1h`, `24h`, `7d`, `1m`, `6m` · default `1m` · accepted on `active-ats-count`

`1h` / `24h` / `7d` / `6m` count **currently active** jobs, matching what the data endpoint returns for the same window. `1m` counts every job indexed in the last 31 days **including ones that have since expired** - monthly indexing volume, the number to size a plan with.

**On `expired-ats`:** string · one of `1h`, `1d`, `1m`, `6m` · default `1d` · accepted on `expired-ats`

- `1h` - rolling: expired in the last hour.
- `1d` - snapshot of the **previous UTC day** (not a rolling 24 hours). The usual choice for a daily sync.
- `1m` - the last 31 days.
- `6m` - the full retained history (183 days).

**On this surface: pass `time_frame=6m` for a search.** The default `24h` returns only jobs first indexed in the last 24 complete hours, which reads like "almost nothing is open".

### `limit`

integer · default `100` · 1-100 · accepted on `active-ats`, `modified-ats`

Rows to return (1-100). Page with `cursor` (pass the last `id` you received) or `offset`. You never need a count call to paginate: stop when a page comes back shorter than `limit`.

### `offset`

integer · default `0` · 0-unbounded · accepted on `active-ats`, `modified-ats`

Rows to skip. Gets slow at depth - prefer `cursor`.

### `cursor`

integer · 0-unbounded · accepted on `active-ats`, `modified-ats`

Keyset pagination: pass the `id` of the last row you received. Switches ordering to `id` ascending and overrides `offset`. Start a traversal with `cursor=0`.

### `description_format`

string · one of `text`, `html`, `hash` · accepted on `active-ats`, `modified-ats`

`text` returns `description_text`, `html` returns `description_html`. **Omit the parameter to leave descriptions out entirely** - they are ~10 KB per job and most syncs fetch them once.

`hash` returns `description_hash` instead of the body: 24 hex characters over the normalized **text** of the description, or `null` where a job has no description. It costs the same job credit as any other row but roughly 10 KB less bandwidth, so a poll can ask *did the wording change?* across a whole feed and re-fetch only the bodies whose hash moved. Because it is taken over the text, a markup-only edit does not move it; `modified_fields` containing `description_html` is the markup-level signal. The value is the same content key the enrichment cache is addressed by, so an unchanged hash also means the enrichment did not need to re-run.

Use `text` when you need to read or quote the posting; omit it for a wide search.

### `include_basic_organization_details`

boolean · accepted on `active-ats`, `modified-ats`

Accepted for client compatibility. This deployment has no organization enrichment source, so it adds no fields.

### `include_beta`

boolean · accepted on `active-ats`, `modified-ats`

`true` adds `classification_onet`, `classification_soc`, `ai_requirements`, `ai_schedule` (shift types, shift length, weekends and nights, each with the posting's own words) and `enrichment` - the per-field provenance map saying which tier produced each `ai_*` value (`rules`, `llm:<model>`, `ontology:<method>`, `schedule:<tier>`).

**Always pass `include_beta=true` in this pack**: it adds `ai_requirements`, which the fit rubric runs on.

### `include_duplicates`

boolean · accepted on `active-ats`, `active-ats-count`, `modified-ats`

A posting reachable through two sources (an employer's own ATS board and a feed that republishes it) is served once, from the copy closest to the ATS. `true` returns every copy; the hidden ones are otherwise left out of lists, counts and the organization list.

### `title`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Search the job title. Google-style search: `software engineer` requires both words, `"software engineer"` is an exact phrase, `software OR engineer` matches either, `-engineer` excludes. Case-insensitive.

### `title_advanced`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Boolean search on the job title. Strict Boolean search. Operators: `&` AND, `|` OR, `!` NOT, `<->` followed-by, `<N>` within N words, `( )` grouping, `'quoted phrase'`, `word:*` prefix. There is no implicit AND - `software engineer` is a 400; write `software & engineer` or `'software engineer'`. Remember to URL-encode `&` as `%26`. Takes precedence over the plain parameter of the same name.

### `description`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Search title and description together. Google-style search: `software engineer` requires both words, `"software engineer"` is an exact phrase, `software OR engineer` matches either, `-engineer` excludes. Case-insensitive.

### `description_advanced`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Boolean search on title + description. Strict Boolean search. Operators: `&` AND, `|` OR, `!` NOT, `<->` followed-by, `<N>` within N words, `( )` grouping, `'quoted phrase'`, `word:*` prefix. There is no implicit AND - `software engineer` is a 400; write `software & engineer` or `'software engineer'`. Remember to URL-encode `&` as `%26`. Takes precedence over the plain parameter of the same name.

### `location`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Search `locations_derived`. Use FULL names - `United States` not `US`, `Pennsylvania, United States` not `PA`. Multi-location: `"New York" OR "Philadelphia"`. Google-style search: `software engineer` requires both words, `"software engineer"` is an exact phrase, `software OR engineer` matches either, `-engineer` excludes. Case-insensitive.

### `location_advanced`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Boolean search on `locations_derived` (full names). Strict Boolean search. Operators: `&` AND, `|` OR, `!` NOT, `<->` followed-by, `<N>` within N words, `( )` grouping, `'quoted phrase'`, `word:*` prefix. There is no implicit AND - `software engineer` is a 400; write `software & engineer` or `'software engineer'`. Remember to URL-encode `&` as `%26`. Takes precedence over the plain parameter of the same name.

### `organization_advanced`

string · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Boolean search on the organization name. Strict Boolean search. Operators: `&` AND, `|` OR, `!` NOT, `<->` followed-by, `<N>` within N words, `( )` grouping, `'quoted phrase'`, `word:*` prefix. There is no implicit AND - `software engineer` is a 400; write `software & engineer` or `'software engineer'`. Remember to URL-encode `&` as `%26`. Takes precedence over the plain parameter of the same name.

### `source_category`

list (comma-separated) · accepted on `active-ats`, `active-ats-count`, `modified-ats`

**Extension.** Exact match on `source_fields.category`, the source's own job category - the facet the career-site fleet routes on.

Values at this employer: `Engineering` (3828), `Other` (2828), `Clinical` (1860), `Sales` (1686), `Information Technology` (1319), `Quality Assurance` (711), `Consulting` (710), `Marketing` (611), `Production` (529), `Customer Service` (525), `Finance` (520), `Manufacturing` (512), `General Business` (494), `Analyst` (481), `Administrative` (448), `BevMo!` (347), `Field Operations` (293), `Supply Chain` (287), `Project Management` (273), `Product` (252), `Software Engineering` (243), `Research` (236), `Management` (228), `Professional Services` (221), `People` (219).

### `ai_employment_type`

list (comma-separated) · one of `FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `TEMPORARY`, `PER_DIEM`, `INTERN`, `VOLUNTEER`, `OTHER` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Overlap match on `ai_employment_type`.

### `ai_work_arrangement`

list (comma-separated) · one of `On-site`, `Hybrid`, `Remote OK`, `Remote Solely` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Match on `ai_work_arrangement`. Pass several to broaden: `Hybrid,Remote OK`.

### `ai_experience_level`

list (comma-separated) · one of `0-2`, `2-5`, `5-10`, `10+` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Match on `ai_experience_level`.

### `ai_education`

list (comma-separated) · one of `high school`, `associate degree`, `bachelor degree`, `professional certificate`, `postgraduate degree` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Overlap match on `ai_education`. `bachelor degree` does not broaden to lower levels.

### `ai_language`

list (comma-separated) · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Match on `ai_job_language` (ISO 639-1, e.g. `en`).

### `ai_taxonomies_a`

list (comma-separated) · one of `Technology`, `Healthcare`, `Management & Leadership`, `Finance & Accounting`, `Human Resources`, `Sales`, `Marketing`, `Customer Service & Support`, `Education`, `Legal`, `Engineering`, `Science & Research`, `Trades`, `Construction`, `Manufacturing`, `Logistics`, `Creative & Media`, `Hospitality`, `Environmental & Sustainability`, `Retail`, `Data & Analytics`, `Software`, `Energy`, `Agriculture`, `Social Services`, `Administrative`, `Government & Public Sector`, `Art & Design`, `Food & Beverage`, `Transportation`, `Consulting`, `Sports & Recreation`, `Security & Safety` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Overlap match on `ai_taxonomies_a` (up to five job families per job, most relevant first). Percent-encode `&`: `Management%20%26%20Leadership`. Only enriched jobs carry taxonomies.

### `ai_taxonomies_a_primary`

list (comma-separated) · one of `Technology`, `Healthcare`, `Management & Leadership`, `Finance & Accounting`, `Human Resources`, `Sales`, `Marketing`, `Customer Service & Support`, `Education`, `Legal`, `Engineering`, `Science & Research`, `Trades`, `Construction`, `Manufacturing`, `Logistics`, `Creative & Media`, `Hospitality`, `Environmental & Sustainability`, `Retail`, `Data & Analytics`, `Software`, `Energy`, `Agriculture`, `Social Services`, `Administrative`, `Government & Public Sector`, `Art & Design`, `Food & Beverage`, `Transportation`, `Consulting`, `Sports & Recreation`, `Security & Safety` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Match only the FIRST (primary) taxonomy - a tighter category match.

### `exclude_ai_taxonomies_a`

list (comma-separated) · one of `Technology`, `Healthcare`, `Management & Leadership`, `Finance & Accounting`, `Human Resources`, `Sales`, `Marketing`, `Customer Service & Support`, `Education`, `Legal`, `Engineering`, `Science & Research`, `Trades`, `Construction`, `Manufacturing`, `Logistics`, `Creative & Media`, `Hospitality`, `Environmental & Sustainability`, `Retail`, `Data & Analytics`, `Software`, `Energy`, `Agriculture`, `Social Services`, `Administrative`, `Government & Public Sector`, `Art & Design`, `Food & Beverage`, `Transportation`, `Consulting`, `Sports & Recreation`, `Security & Safety` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Exclude a job if ANY of its taxonomies is listed.

### `ai_visa_sponsorship`

string · one of `only`, `exclude` · accepted on `active-ats`, `active-ats-count`, `modified-ats`

`only` - postings that state they sponsor visas. `exclude` - everything else, including postings that do not mention it. Detection is from explicit statements in the posting.

### `has_salary`

boolean · accepted on `active-ats`, `active-ats-count`, `modified-ats`

`true` returns only jobs with a parsed pay range.

### `has_no_location`

boolean · accepted on `active-ats`, `active-ats-count`, `modified-ats`

`true` returns only jobs whose `locations_derived` is null.

### `date_posted_gte`

date-time · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Posted on or after (ISO 8601, UTC assumed). `date_posted` is the source's claim.

### `date_posted_lt`

date-time · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Posted before.

### `date_created_gte`

date-time · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Indexed on or after. `date_created` is assigned by this API, always present and monotonic - the safe field for incremental syncs.

### `date_created_lt`

date-time · accepted on `active-ats`, `active-ats-count`, `modified-ats`

Indexed before.

### `id`

list (comma-separated) · accepted on `active-ats`, `modified-ats`

One or more internal ids - the same `id` that `/v1/expired-ats` and `/v1/modified-ats` report. Use it to refresh rows you already hold.

### `date_modified_gte`

date-time · accepted on `modified-ats`

Modified on or after.

### `date_modified_lt`

date-time · accepted on `modified-ats`

Modified before.

## Search syntax

Plain parameters (`title`, `description`, `location`): Google-style search: `software engineer` requires both words, `"software engineer"` is an exact phrase, `software OR engineer` matches either, `-engineer` excludes. Case-insensitive.

`_advanced` parameters: Strict Boolean search. Operators: `&` AND, `|` OR, `!` NOT, `<->` followed-by, `<N>` within N words, `( )` grouping, `'quoted phrase'`, `word:*` prefix. There is no implicit AND - `software engineer` is a 400; write `software & engineer` or `'software engineer'`. Remember to URL-encode `&` as `%26`. Takes precedence over the plain parameter of the same name.

## Response fields

`active-ats` and `modified-ats` return a JSON array of job objects. `active-ats-count` returns `{"count": n}`. `expired-ats` returns a JSON array of ids. Fields of a job object:

- `id`: Internal id. Stable for the life of the listing; the id `/v1/expired-*` reports and `cursor` pages on.
- `title`: Job title.
- `organization`: Hiring organization, as the source names it.
- `organization_url`: Organization page URL. Not supplied by current sources.
- `date_posted`: When the source says the job was posted (UTC).
- `date_created`: When this API first indexed the job (UTC). Always present, monotonic - the field to sync on.
- `date_valid_through`: Source-supplied closing date, when there is one.
- `locations`: Source locations as schema.org `Place` objects (`address`, and `geo` when coordinates exist). Primary location first.
- `locations_alt`: Fallback location strings. Unused by current sources.
- `locations_derived`: Normalized `City, Region, Country` strings with codes expanded to full English names. This is what `location` searches.
- `location_type`: `TELECOMMUTE` for remote jobs, per the schema.org JobPosting vocabulary.
- `location_requirements`: Unused by current sources.
- `salary`: schema.org `MonetaryAmount`, present only when the source pay string parses to a real range.
- `employment_type`: The source's own employment-type label, verbatim.
- `published`: Always `true` here: the candidate surface serves exactly what the career site shows.
- `url`: Where a candidate applies.
- `source`: Source adapter, the public ATS board kind: `greenhouse`, `lever`, `ashby`, `smartrecruiters`, `workable`, `workday`, `oracle`.
- `source_type`: `ats` or `jb`.
- `synthetic`: `true` = a generated demonstration posting, not one from the employer's feed. Appears only when the employer serves a demonstration corpus; say so if you show one to a candidate.
- `source_domain`: Host of `url`.
- `source_slug`: The employer's identifier on the source: the board token.
- `domain_derived`: Employer's main website domain. Operator-supplied in `sources.json`; null when not configured - never guessed.
- `cities_derived`: Cities from `locations_derived`.
- `regions_derived`: States / provinces / regions, full names.
- `countries_derived`: Countries, full names.
- `lats_derived`: Latitudes, where the source supplies coordinates.
- `lngs_derived`: Longitudes.
- `ai_salary_currency`: ISO 4217. Pay comes from the source feed when it states it, otherwise from the posting text - see Enrichment.
- `ai_salary_value`: Pay when the source gives a single figure rather than a range.
- `ai_salary_min_value`: Bottom of the pay range.
- `ai_salary_max_value`: Top of the pay range.
- `ai_salary_unit_text`: `HOUR`, `DAY`, `WEEK`, `MONTH` or `YEAR`. A feed pay string yields `HOUR` or `YEAR`; the employer's page and the posting text can also state the others.
- `ai_experience_level`: `0-2`, `2-5`, `5-10`, `10+` - only when the feed or the posting states a number of years.
- `ai_work_arrangement`: `On-site`, `Hybrid`, `Remote OK` or `Remote Solely`. `null` when neither the feed nor the posting says.
- `ai_employment_type`: Normalized: `FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `TEMPORARY`, `PER_DIEM`, `INTERN`, `VOLUNTEER`, `OTHER`.
- `ai_job_language`: ISO 639-1 language of the posting, from the source's locale.
- `ai_education`: `high school`, `associate degree`, `bachelor degree`, `professional certificate`, `postgraduate degree`.
- `ai_benefits`: Non-salary benefits. The posting's own, plus the organization-wide benefits extracted once from its shared boilerplate.
- `ai_work_arrangement_office_days`: For Hybrid roles, in-office days per week, when stated.
- `ai_key_skills`: Skills the role needs (<= 12): the skill, tool and soft-skill labels of `ai_requirements`, required first.
- `ai_keywords`: Tools, technologies, certifications, licences, methods (<= 15).
- `ai_working_hours`: Weekly hours, only when the posting states them.
- `ai_visa_sponsorship`: `true`/`false` only when the posting mentions sponsorship; `null` when it does not.
- `ai_core_responsibilities`: Two-sentence summary of the role.
- `ai_requirements_summary`: Two-sentence summary of the requirements.
- `ai_taxonomies_a`: Up to five job families, most relevant first; the first is the primary taxonomy.
- `ai_hiring_manager_name`: When the posting text names a hiring manager.
- `ai_hiring_manager_email_address`: When the posting text gives one.
- `recruiter_name`: The recruiter the feed names for the posting (the feed's `recruiter` field). Not the hiring manager.
- `recruiter_email_address`: Recruiter or recruiting-team mailbox from the feed.
- `recruiter_url`: Recruiter profile URL. Not supplied by current sources.
- `classification_onet`: **`include_beta=true`.** O*NET-SOC occupation `{id, name, version}` (`version`: the O*NET database release the code comes from). Chosen by retrieval over O*NET titles; a model only breaks ties among retrieved candidates, so a code is never invented.
- `classification_soc`: **`include_beta=true`.** SOC occupation `{id, name}`, derived from the O*NET code.
- `ai_schedule`: **`include_beta=true`. Extension.** The shift the posting states: `{version, tier, shift_types: [{value, quote, src, by}], shift_length_hours, hours_per_week, days_pattern, weekends, nights, conflicts}`. `shift_types` values: `day`, `evening`, `night`, `rotating`, `weekend`, `on_call`, `variable`. `weekends` / `nights`: `{value: required|some|none, quote, src, by}`. Every value carries the exact words it came from (`quote`), where they live (`src`: `text`, `title` or `feed:<field>`) and the tier that read them (`by`: `feed`, `rules`, `jev`, `haiku`). A key that is absent means the posting does not say; where the feed and the text disagree the text's value is served and the disagreement is listed in `conflicts`. `null` until the posting has a stated schedule.
- `ai_role`: **`include_beta=true`. Extension.** Who the posting is, as fields: `{version, occupation: {id, name, method, confidence, title_agreement}, specialty: [{value, kind, quote, src, by}], seniority: {value, quote, src, by, own_name?, also?}, category_occupation: {id, name, source_category}}`. `seniority.value`: `assistant`, `associate`, `manager`, `supervisor`, `director`, `chief`, `lead`, `senior`, `student`, `intern`, `practitioner`, `staff` or `none`; `also` lists the other modifiers the title carries; `own_name` marks a modifier that is part of the coded occupation's name (Nursing Assistant). `category_occupation` is present when the client's own category names an occupation. A key that is absent means the posting does not say.
- `ai_requirements`: **`include_beta=true`. Extension.** What the posting asks for, one entry each, most important first (<= 20): `{label, kind, importance, min_years, level, evidence}`. `kind`: `skill`, `tool`, `certification`, `language`, `education`, `soft`. `importance`: `required` (stated as a requirement or minimum), `preferred` (preferred / a plus / nice to have - never promoted to required), `implied` (only visible from the responsibilities). `min_years` and `level` (1 awareness - 5 expert; CEFR C1 = 4) only when the posting states them for that item, else `null`. `evidence` is the posting's own phrase, verified verbatim against the text: a requirement whose phrase is not in the posting is dropped, never served. Credential identity and eligibility (certification / education items): `concept_id` is the reviewed canonical credential (or `null` when no reviewed mapping exists); a label naming several credentials carries `concept_ids` with `concepts_rule` (`all` | `any`); a state licence carries `concept_qualifier` `{region}`. On alternatives of a group, `eligible: false` with `exclusion {quote, span, src, rule}` means the posting itself rules that alternative out (absent = eligible); `eligibility_checked: true` marks alternatives that were checked. Read-time fields, derived from the text and never from a model's self-rating: `section` (the posting section the phrase sits in: `requirements`, `preferred`, `duties`, ... or `null`), `confidence` (`high` | `medium` | `low`: does that section, or the line's own required/preferred marker, agree with `importance`?), `group` (`{id, rule: "any"}` on alternatives quoted from one "or" phrase - meeting one meets the group), `extracted_by` (`<model>@p<prompt version>`), `extracted_at`. Labels are free text - no skills taxonomy is applied.
- `ai_salary_conflict`: **`include_beta=true`. Extension.** Present when the pay the feed or the employer's page states and the pay written in the posting text disagree by more than 2% at the same unit: `{served: {min, max, unit, source}, text: {min, max, unit, kind}, quote, also?}`. `served` is what the `ai_salary_*` fields carry (`source`: `feed`, or `scrape:json-ld` when the employer's page supplied the pay); `text` is ONE pay statement of the posting that disagrees with it, and `quote` (<= 160 characters) is that statement's own words: every figure of `text` appears in `quote`, and a range is never assembled from two statements. `kind`: `range` | `minimum` (`max` null) | `maximum` (`min` null) | `exact`. `also`: the posting's other pay statements, each `{min, max, unit, kind, quote, agrees}` (`agrees`: it matches `served`). `null` when there is none.
- `enrichment`: **`include_beta=true`. Extension.** `{version, fields: {name: origin}}` - which tier produced each value. `origin` is one of: `rules` (the feed's own value, normalized); `scrape:json-ld` (the employer's job page); `llm:<model>` (a model read the posting text); `llm:<model>+org` (the organization-wide benefits or sponsorship statement was used); `jev:<confidence>` (a Jev decision, confidence 0-1 to two decimals); `ontology:<method>` for `classification_onet` / `classification_soc` (`exact` title match, `lexical` match, `llm`, `jev` or `judge`); `schedule:<tier>` for `ai_schedule` (`feed`, `rules`, `jev`, `haiku`); `role:<tier>` for `ai_role` (`rules`, `jev`, `haiku`); `fixture:labels` (a generated demonstration posting's own labels). A field with no entry has no stored value.
- `source_fields`: **Extension. Nothing from the source is lost.** Every non-empty field of the source record, verbatim under its own name (the description bodies are served as `description_text` / `description_html` instead): identifiers (`id`, `ref`, `clientid`, `xc_id`), every URL the feed carries (`url`, `seo_url`, `is_internal`), the vendor's own `job_classifications`, `custom_fields`, portal and audience flags, plus friendly aliases `source_job_id`, `category`, `portal`, `date_updated`, `organization_raw`. Use it whenever an enriched field does not carry what you need.
- `date_modified`: Last tracked-field change. Returned on `/v1/modified-ats` and with `time_frame=6m`.
- `modified_fields`: Which tracked fields changed in the most recent modification.
- `description_text`: Plain-text description. Only with `description_format=text`.
- `description_html`: Source HTML description. Only with `description_format=html`.
- `description_hash`: 24 hex characters over the normalized description **text**, or `null` where there is no description. Only with `description_format=hash`. Poll with this to learn whether the wording changed without carrying ~10 KB per job; a markup-only edit does not move it, so use `modified_fields` containing `description_html` for that.

### `ai_requirements` items (with `include_beta=true`)

```
label       free text: "Python", "RN licence", "German (C1)", "Bachelor's degree"
kind        skill | tool | certification | language | education | soft
importance  required (stated as a requirement or minimum) | preferred (a plus; never promoted) | implied (only visible from the duties)
min_years   integer or null: only when the posting states years for THIS item
level       1-5 or null: awareness 1 ... expert/fluent 5 (CEFR C1 = 4). Indicative only; never gate on it alone
evidence    the posting's own phrase, verified verbatim against the text before it is served
section     where that phrase sits: requirements | preferred | duties | ... | null
confidence  high | medium | low: section and line marker agree with importance? low = read the evidence yourself
group       {id, rule: "any", source} on alternatives ("RN or LPN licence"): meeting one meets the group. source: extracted | derived | chain (an OR list) | model (a checked model reading)
concept_id  certification / education only: the reviewed canonical credential (e.g. bls, rn-license, bsn), or null when no reviewed mapping exists
concept_ids + concepts_rule   a label naming several credentials: all ("RT(R)(M)") or any ("CMA, CMAC or RMA")
concept_qualifier   {region} for a state licence ("Florida RN"): the candidate's licence must be for that state
eligible    false only when the posting itself rules this alternative out; absent = eligible. Never count a false one as met
exclusion   {quote, span, src: text | model, rule} with eligible=false: the posting's own sentence ("LPN applicants are not eligible ...")
eligibility_checked   true on alternatives whose eligibility was checked (absent on records enriched before this field existed)
extracted_by / extracted_at   which model and prompt version produced it, and when
```

A job without `ai_requirements` has not been enriched yet: read `description_text` instead and say the reading is yours, not the data's.

## Response headers

- `x-org`: the employer slug this response is scoped to.
- `x-window-start` / `x-window-end`: the `date_created` window that was applied. Consecutive `24h` (or `1h`) windows tile exactly, so a daily run at a fixed hour sees every new job once.
- `retry-after`: on 429, seconds to wait.

Errors are RFC 7807 problem documents (`application/problem+json`) whose `detail` says exactly what was wrong; fix the request rather than retrying it unchanged.

## Candidate keys and job alerts

The only part of this surface that needs a key, because the server does something for the candidate later: mail them new matching postings. Reads above stay keyless. The candidate's email address is the only personal data kept, is used only to send alerts, and is never returned by any endpoint. An alert is a saved search, not a saved fit: the profile is never sent. A key with no active alert for 12 months is deleted with its address. Keys look like `oric_...`, belong to this employer only, and go in `Authorization: Bearer oric_...`. Never put one in a document.

### Request a key: `POST https://openrolesindex.com/orgs/all/candidate-keys`

Body `{"email": "..."}` (JSON or form). Always answers 202 with the same body, whether or not the address works, so nothing about an address can be learned. A link is mailed; it works once and expires in 15 minutes. 429 (with `retry-after`) only when one client address asks more than 5 times an hour.

### Show the key once: `GET https://openrolesindex.com/orgs/all/candidate-keys/confirm?token=...`

Opened by the candidate from the mail (a page with the key and a copy button), or with `Accept: application/json` for `{"key": "oric_..."}`. 404 when the link is invalid, expired or used. The candidate pastes the key to their assistant: a chat assistant cannot read mail.

### Inspect or delete: `GET` and `DELETE https://openrolesindex.com/orgs/all/candidate-keys/me`

Bearer key. GET returns `{"created_at": ..., "alerts": n}`. DELETE (204) removes the key, its alerts and their address at once. 401 when the key is missing or not valid for this employer.

### Alerts: `POST`, `GET https://openrolesindex.com/orgs/all/alerts` and `DELETE https://openrolesindex.com/orgs/all/alerts/{id}`

Bearer key. POST `{"name": "...", "filters": {...}, "cadence": "daily" | "weekly"}` returns 201 with the alert; GET returns a list of `{id, name, filters, cadence, created_at, last_run_at, last_sent}`; DELETE returns 204 (404 for an unknown id). `filters` uses the same names and values as the MCP `search_jobs` tool (`title`, `query`, `category`, `location`, `work_arrangement`, `employment_type`, `experience_level`, `has_salary`) and is validated the same way (400 with the reason). At most 10 alerts per key.

Each digest lists up to 25 new postings (title, location, pay when stated, link, id) and the ids of the rest. Score them locally: `python3 fetch.py --ids 101,102,103` then `fit.py`. Nothing is sent when nothing matches.

### Unsubscribe: `GET https://openrolesindex.com/orgs/all/alerts/unsubscribe?token=...`

The link in every digest; no key needed. Deactivates that alert (HTML page, or JSON with `Accept: application/json`).

## Worked examples for this employer

Active Engineering in San Francisco, with requirements:

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats?time_frame=6m&include_beta=true&source_category=Engineering&location=%22San%20Francisco%22&limit=50'
```

How many Engineering postings are open:

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats-count?time_frame=6m&source_category=Engineering'
```

One job in full, with its description text (replace JOB_ID with an `id`):

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats?time_frame=6m&include_beta=true&id=JOB_ID&description_format=text'
```

Hybrid or remote roles only:

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats?time_frame=6m&include_beta=true&ai_work_arrangement=Hybrid,Remote%20OK,Remote%20Solely&limit=50'
```

Two categories or two cities at once, pay stated:

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats?time_frame=6m&include_beta=true&location=%22San%20Francisco%22%20OR%20%22United%20States%22&source_category=Engineering,Other&has_salary=true&limit=50'
```

Daily watch: jobs first indexed in the last 24 complete hours (run once a day at a fixed hour):

```
curl -s 'https://openrolesindex.com/orgs/all/v1/active-ats?time_frame=24h&include_beta=true&source_category=Engineering&limit=100'
```

Jobs that closed yesterday (drop them from a saved shortlist):

```
curl -s 'https://openrolesindex.com/orgs/all/v1/expired-ats?time_frame=1d'
```
