# Enums & vocabularies

> Every fixed value the API accepts and returns, with live counts and the filter each one belongs to.

Source: https://hyperjobs.io/docs/reference/enums

The complete vocabularies behind the enum filters on
[`/v1/jobs`](/docs/api-reference/jobs-list). If a filter just returned a `400`,
the `hint` in the body names the values on this page.

<Warning title="A wrong value is a 400 now, not an empty list">
  Enum filters — `seniority`, `work_arrangement`, `experience_level`,
  `employment_type`, `education`, `taxonomies`, `continent` — are still
  **case-sensitive** equality, but a typo, a wrong case, or a value
  from the wrong vocabulary is rejected with a [`400`](/docs/errors) whose
  `hint` lists the allowed values. A typo'd filter can never masquerade as
  an empty result.

  ```bash
  curl "…/v1/jobs?seniority=Mid-Senior level"   # ✓ 198,375 jobs
  curl "…/v1/jobs?seniority=mid-senior level"   # ✗ 400 — wrong case
  curl "…/v1/jobs?seniority=Senior"             # ✗ 400 — not a value
  curl "…/v1/jobs?seniority=5-10"               # ✗ 400 — that's experience_level
  ```

  The exceptions are the open vocabularies — `skill`, `source`, `language`, and
  `city` aren't enums, so an unknown value there still matches nothing rather
  than erroring.
</Warning>

Counts below are from a recent snapshot. They're here to show you the shape of
each distribution — don't hardcode them. For a live count of any value, send the
filter with `limit=1` and read `total`:

```bash
# The live number of Director-level jobs, without transferring any of them.
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "seniority=Director" --data-urlencode "limit=1"
# → { "data": [ … ], "total": 26465, "limit": 1, "offset": 0 }
```

[`/v1/meta`](/docs/api-reference/meta) gives live totals for jobs, companies,
the source breakdown, and per-field fill rates (`coverage_pct`) — but no
per-enum counts for the vocabularies below.

## Quick reference

| Filter | Vocabulary | Matching |
| --- | --- | --- |
| `seniority` | [6 values](#seniority) | Exact, case-sensitive · comma = **OR** |
| `experience_level` | [4 values](#experience_level) | Exact, case-sensitive · comma = **OR** |
| `work_arrangement` | [3 values](#work_arrangement) | Exact, case-sensitive · comma = **OR** |
| `employment_type` | [8 values](#employment_type) | Token in a list, comma = **OR** |
| `education` | [5 values](#education_requirements) | Token in a list, comma = **OR** (overlap) |
| `taxonomies` | [28 values](#taxonomies) | Token in a list, comma = **OR** (overlap) |
| `continent` | [7 values](#continent) | Exact, case-sensitive · comma = **OR** |
| `visa_sponsorship` | [2 values](#visa_sponsorship) | Single value |
| `source` | [64 live slugs](/docs/reference/sources#the-live-values), live via `/v1/meta` | Exact, case-sensitive · comma = **OR** |
| `language` | [Full name or ISO-639-1](#language) | Exact · comma = **OR** |
| `skill` | [Open](/docs/guides/skills-and-taxonomies) | Token in a list, comma = **AND** — the exception |

Comma = **OR** on every multi-value filter, with one exception: `skill`, where
every listed value is required. That's the one inversion to keep straight — [see
below](#commas-mean-or--except-skill). `country` and `company_country` aren't
enums but take a full name or an ISO-2 code, comma = OR — `?country=US,Germany`
works, and an unrecognised ISO-2 code is a `400`.

## `seniority`

Six values. Filter: `?seniority=`. Field:
[`job.seniority`](/docs/objects/job#classification).

| Value | Jobs |
| --- | --- |
| `Mid-Senior level` | 198,375 |
| `Associate` | 99,023 |
| `Entry level` | 78,095 |
| `Management` | 28,749 |
| `Director` | 26,465 |
| `Internship` | 11,603 |

Note the shape: `Mid-Senior level` is nearly half of everything classified, so
it's a weak filter on its own. `Management` and `Director` are distinct values,
not synonyms — a query for leadership roles needs both, which is one
request: `?seniority=Director,Management` (comma = OR).

## `experience_level`

Four bands, in years. Filter: `?experience_level=`. Field:
[`job.experience_level`](/docs/objects/job#classification).

| Value | Jobs |
| --- | --- |
| `5-10` | 170,565 |
| `2-5` | 112,024 |
| `0-2` | 100,549 |
| `10+` | 42,798 |

The values are literal strings — `?experience_level=5-10`, not `5` and not
`5–10` with an en-dash. There's no range syntax and no numeric comparison.

<Note title="`experience_level` and `seniority` are separate fields">
  They're related but derived independently, and a posting can carry a
  combination that looks inconsistent. The rough correspondence is:

  | `experience_level` | Aligns with |
  | --- | --- |
  | `0-2` | `Entry level` |
  | `2-5` | `Associate` |
  | `5-10` | `Mid-Senior level` |
  | `10+` | `Mid-Senior level` |

  Both bands above 5 years map to the same seniority, so `seniority` cannot
  distinguish a 6-year role from a 15-year one — use `experience_level` when that
  difference matters. And because neither field is computed from the other at
  read time, **don't filter on both** expecting a narrower result: you'll get the
  intersection of two independent judgements, which is often much smaller than
  either alone.
</Note>

## `work_arrangement`

Three values. Filter: `?work_arrangement=`. Field:
[`job.work_arrangement`](/docs/objects/job#classification).

| Value | Jobs |
| --- | --- |
| `On-site` | 333,870 |
| `Remote` | 127,895 |
| `Hybrid` | 73,706 |

`On-site` carries a hyphen and a lower-case `s`. `Onsite`, `On-Site`, and
`onsite` all return zero rows.

<Warning title="`work_arrangement=Remote` and `remote=true` are different filters">
  They read different fields and return different sets — 127,895 postings have
  `work_arrangement=Remote`, while 85,069 have
  [`location.remote`](/docs/objects/job#location) true. Neither number is wrong;
  they're separate signals derived from separate evidence.

  - **`?work_arrangement=Remote`** is the classification. It's the more precise
    of the two, and the only one that can express `Hybrid`.
  - **`?remote=`** filters on `location.remote`. It takes `true`/`false`/`1`/`0`
    — `false` means on-site/hybrid only — and anything else is a
    `400` (a pre-release draft silently ignored `remote=true`; that trap never
    shipped).

  Prefer `work_arrangement`. Use both only if you want the stricter
  intersection.
</Warning>

## `continent`

Seven values. Filter: `?continent=`, comma = OR. Field:
[`job.location.continent`](/docs/objects/job#location).

`Africa` · `Antarctica` · `Asia` · `Europe` · `North America` · `Oceania` ·
`South America`

Title Case, with the space in the two-word names — `North America`, not
`north-america`. A value outside the seven is a `400` naming the list.

## `employment_type`

Filter: `?employment_type=`. Field:
[`job.employment_type`](/docs/objects/job#classification).

| Value | Jobs |
| --- | --- |
| `FULL_TIME` | 429,457 |
| `PART_TIME` | Query `total` |
| `CONTRACTOR` | Query `total` |
| `INTERN` | Query `total` |
| `TEMPORARY` | Query `total` |
| `PER_DIEM` | Query `total` |
| `VOLUNTEER` | Query `total` |
| `OTHER` | Query `total` |

`FULL_TIME` dominates so heavily that the rest are a rounding error by
comparison; for a current count of any one of them, send it with `limit=1` and
read `total`.

`SCREAMING_SNAKE_CASE`, always. `Full-time`, `full_time`, and `FULLTIME` all
return zero rows.

### It's an array, not a value

A posting can carry several types at once, so the field holds a **list** —
`["FULL_TIME","TEMPORARY"]` for a full-time contract role, and 20+ combinations
occur in the data. The filter matches a value **inside** that list, so
`?employment_type=FULL_TIME` finds both `["FULL_TIME"]` and
`["FULL_TIME","TEMPORARY"]` — and `?employment_type=FULL_TIME,CONTRACTOR`
matches either type (comma = OR).

<Note title="A real array">
  A pre-release draft of this API returned this field as a raw JSON string —
  `"[\"FULL_TIME\"]"` — and you had to `JSON.parse` it. It's a real array, like
  `skills` and `taxonomies` beside it:

  ```js
  job.employment_type.includes("FULL_TIME");     // ✓ — just an array
  ```

  If you shipped a defensive parse, delete it. The same applies to
  `education_requirements` and `benefits`. See
  [the Job object](/docs/objects/job#classification).
</Note>

## `education_requirements`

Filter: **`?education=`** — note the parameter is `education`, not
`education_requirements`, though the field it filters is
[`job.education_requirements`](/docs/objects/job#classification).

| Value | Jobs |
| --- | --- |
| `bachelor degree` | 49,367 |
| `high school` | 30,156 |
| `postgraduate degree` | 22,749 |
| `professional certificate` | 5,318 |
| `associate degree` | 4,053 |

All lower-case, all with a space, none pluralised. `Bachelor degree`,
`bachelors degree`, and `Bachelor's` are a `400` naming the five values. Comma =
OR (overlap match): `?education=bachelor degree,postgraduate degree` matches
postings listing either.

Also an array field — a real array, [not a raw JSON
string](/docs/objects/job#classification) — and also sparse: the five values
together cover well under a third of the dataset, because most postings state no
education requirement at all. A `null` means the posting was silent, **not**
that no degree is required, so filtering on education excludes the large
majority of jobs that simply didn't say.

## `taxonomies`

28 job families. Filter: `?taxonomies=`. Field:
[`job.taxonomies`](/docs/objects/job#classification). Title Case, with spaced
ampersands in the compound names.

`Administrative` · `Agriculture` · `Art & Design` · `Construction` ·
`Consulting` · `Creative & Media` · `Customer Service & Support` ·
`Data & Analytics` · `Education` · `Energy` · `Engineering` ·
`Finance & Accounting` · `Healthcare` · `Hospitality` · `Human Resources` ·
`Legal` · `Logistics` · `Management & Leadership` · `Manufacturing` ·
`Marketing` · `Sales` · `Science & Research` · `Security & Safety` ·
`Social Services` · `Software` · `Technology` · `Trades` · `Transportation`

Per-taxonomy counts aren't published anywhere. For the distribution, query each
value with `limit=1` and read `total` — 28 cheap requests, well within a minute's
[rate limit](/docs/rate-limits).

Present on about 64% of postings — a job with no taxonomy isn't uncategorisable,
it just wasn't classified.

The `&` needs URL-encoding as `%26`, or the query string ends at the ampersand
and the rest of your filters vanish:

```bash
# The shell and the URL both need to leave `&` alone. --data-urlencode does both.
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "taxonomies=Finance & Accounting"
```

<Note title="`Software`, `Technology`, and `Engineering` are three separate values">
  They overlap in ordinary usage but not here, and none implies another. A
  backend role might carry `["Software"]`, `["Software","Technology"]`, or
  `["Engineering"]` depending on how the posting reads.

  Since [commas mean OR](#commas-mean-or--except-skill),
  `?taxonomies=Software,Technology,Engineering` covers all three in one request
  — an overlap match: any listed value qualifies. And a value outside the 28 is
  a `400` naming the list, so a typo can't silently empty the result. [Skills &
  taxonomies](/docs/guides/skills-and-taxonomies) works through it.
</Note>

## `visa_sponsorship`

Two values. Filter: `?visa_sponsorship=`. A single value, not a comma list.

| Value | Meaning |
| --- | --- |
| `only` | Only jobs that mention visa sponsorship |
| `exclude` | Hide jobs that mention it |

Anything else — including `true` — is a `400`.

## `language`

Filter: `?language=`, comma = OR. Field:
[`job.job_language`](/docs/objects/job#classification).

The detected language of the posting. The field stores the short lower-case
ISO-639-1 code — `en` is by far the most common — but the filter
takes either the full name or the code: `?language=English` and `?language=en`
are the same query.

33 codes are supported:

`en` · `fr` · `de` · `nl` · `es` · `it` · `pt` · `pl` · `sv` · `zh` · `ja` ·
`hu` · `id` · `no` · `cs` · `el` · `da` · `fi` · `uk` · `ro` · `tr` · `bg` ·
`sk` · `ko` · `ru` · `hr` · `ar` · `vi` · `sl` · `sr` · `th` · `he` · `ms`

Not a validated enum — a value outside the list matches nothing rather than
erroring.

## `source`

64 public platform slugs return rows, documented in full on the [sources
page](/docs/reference/sources#the-live-values). There is no second tier: every
platform we list is a value you can filter on. The filter takes the slug —
`lever`, not the stored `lever.co` (the stored value is still accepted as input) —
and comma = OR. `exclude_source` takes the same values to filter platforms out.

The keys of [`/v1/meta`](/docs/api-reference/meta)`.sources` are this list,
live, with counts. It's the only vocabulary the API will enumerate for you —
and worth reading, because `source` is a csv filter rather than an enum: an
unknown value matches nothing instead of returning a `400`.

## `source_type`

| Value | Means |
| --- | --- |
| `ats` | A board the employer authors and manages applicants on — the canonical record. Includes join.com. |
| `jobboard` | A third-party board the employer posted to — LinkedIn, Wellfound, … |
| `aggregator` | A re-listing site that copied the posting from somewhere else. |

Comma = OR, and this one **is** a checked enum — a value off the list is a
`400`. It filters provenance, not duplication: cross-source mirrors are
[already folded](/docs/guides/deduplication), so `source_type=ats` selects
employer-authored records rather than removing copies. The same value appears
as `source_type` on every [Job object](/docs/objects/job).

## `skill`

An open vocabulary of canonical tokens, not an enum — far too many values to
list, and it grows.

The trap is that displayed skills don't always round-trip: you see
`Machine Learning`, but the filter wants `machinelearning`. A few aliases
(`c++`→`cpp`, `c#`→`csharp`, `.net`→`dotnet`, `node.js`/`nodejs`→`node`,
`react.js`→`react`, `vue.js`→`vue`, `golang`→`go`) are translated for you;
multi-word names are not. See
[skills & taxonomies](/docs/guides/skills-and-taxonomies).

## Commas mean OR — except `skill`

Every multi-value filter treats a comma-separated list as **OR**:

```bash
curl "…/v1/jobs?taxonomies=Software,Healthcare"   # either family
curl "…/v1/jobs?employment_type=FULL_TIME,INTERN" # either type
curl "…/v1/jobs?seniority=Director,Management"    # leadership, one request
```

The one exception is `skill`, where comma = **AND** — every listed skill must be
present, because "needs Python and AWS" is what a skill list means. For OR
across skills, [run one request per value and merge on
`id`](/docs/guides/skills-and-taxonomies#getting-or-across-skills).

## Shape parameters

Two more closed vocabularies live on the shape side of the query string, and
they're validated the same way:

- **`description_format`** — `text` | `html`. Omit the parameter and the
  response has no description key at all (smaller payloads).
- **`collapse`** — `true` | `title,organization`. Both name the same grouping;
  see [duplicates & collapsing](/docs/guides/deduplication#collapsing).

Any other value is a `400`.

## Validating before you send

The server does this for you now: a bad enum value is a `400` whose `hint`
lists the allowed values, so a client-side lookup table is a UX nicety rather
than a correctness requirement. The one list still worth checking dynamically
is `source` — it's a csv filter, not an enum (unknown values match nothing),
and it's the one vocabulary that changes on its own:

```js
const { sources } = await (
  await fetch("https://api.hyperjobs.io/v1/meta", { headers })
).json();

if (!(input in sources)) {
  throw new Error(`Unknown source: ${input}. Valid: ${Object.keys(sources).join(", ")}`);
}
```

## Related

<CardGroup cols={2}>
  <Card title="Filtering jobs" icon="Funnel" href="/docs/guides/filtering">
    The three matching modes, and every filter's semantics.
  </Card>
  <Card title="The Job object" icon="Braces" href="/docs/objects/job">
    Where each of these values shows up in a response.
  </Card>
  <Card title="Sources & ATS coverage" icon="Plug" href="/docs/reference/sources">
    The 17 `source` slugs and the platforms behind them.
  </Card>
  <Card title="Errors" icon="TriangleAlert" href="/docs/errors">
    What the 400 body looks like, and which filters still fail silently.
  </Card>
</CardGroup>
