# The Job object

> Every field on a posting, its type, when it's null, and the derivations that make a few of them surprising.

Source: https://hyperjobs.io/docs/objects/job

The shape returned by [`/v1/jobs`](/docs/api-reference/jobs-list),
[`/v1/jobs/feed`](/docs/api-reference/jobs-feed),
[`/v1/jobs/modified`](/docs/api-reference/jobs-modified), and
[`/v1/jobs/{id}`](/docs/api-reference/jobs-retrieve). The list endpoints wrap it
in an envelope (`{ data, total, limit, offset, next_cursor }` on `/v1/jobs`);
the retrieve endpoint returns it at the top level.

Nearly every field is nullable. Postings come from 64 different sources
with no shared schema, so what one board publishes as a structured salary another
buries in prose. Write clients that expect `null` everywhere except `id`.

## Example

```json title="A job with everything populated"
{
  "id": "ashby:7c1f2a94-3e17-4d8b-9c05-1f6ab2e40d33",
  "title": "Staff Backend Engineer",
  "company": {
    "slug": "linear",
    "name": "Linear",
    "domain": "linear.app",
    "website": "https://linear.app",
    "logo": "https://logo.hyperjobs.io/linear.app",
    "industry": "Software Development",
    "employee_count": 120
  },
  "location": {
    "remote": true,
    "countries": ["United States"],
    "cities": null,
    "regions": null,
    "coords": null,
    "timezones": ["America/New_York"],
    "continent": "North America",
    "raw": ["Remote (US)"]
  },
  "employment_type": ["FULL_TIME"],
  "work_arrangement": "Remote",
  "office_days": null,
  "remote_location": ["United States"],
  "department": "Engineering",
  "team": "Sync Engine",
  "requisition_id": "ENG-142",
  "seniority": "Mid-Senior level",
  "experience_level": "5-10",
  "salary": {
    "min": 190000,
    "max": 240000,
    "currency": "USD",
    "unit": "YEAR",
    "annual_min": 190000,
    "annual_max": 240000,
    "summary": "$190,000 – $240,000 a year"
  },
  "working_hours": 40,
  "skills": ["typescript", "graphql", "postgresql"],
  "keywords": ["distributed systems", "real-time sync"],
  "taxonomies": ["Software"],
  "education_requirements": ["bachelor degree"],
  "visa_sponsorship": null,
  "job_language": "en",
  "requirements_summary": "8+ years building distributed backend systems…",
  "core_responsibilities": "Own the sync engine's server-side…",
  "benefits": ["Equity", "Health, dental & vision", "401(k) matching"],
  "hiring_manager": null,
  "apply_url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
  "url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
  "source": "ashby",
  "source_type": "ats",
  "source_domain": "jobs.ashbyhq.com",
  "posted_at": "2026-07-08T14:02:11.000Z",
  "expires_at": null,
  "modified_at": "2026-07-12T08:30:05.000Z",
  "modified_fields": ["salary_max"],
  "created_at": "2026-07-08T14:10:22.000Z",
  "updated_at": "2026-07-14T02:15:40.000Z"
}
```

There's no `description` key in that response, and that's the default — see
[the description field](#description-is-absent-by-default).

## Identity

<Fields>
  <ResponseField name="id" type="string">
    Stable, source-prefixed identifier — `ashby:7c1f2a94-…`,
    `greenhouse:12345`. The prefix names the board the posting came from, but
    as a **stored** value, not the public slug — a Lever id starts `lever.co:`
    even though its `source` field reads `lever`. Ids are identifiers, not
    vocabulary; read the board from the `source` field instead of parsing it.

    The only field that is never null. Pass it to
    [`/v1/jobs/{id}`](/docs/api-reference/jobs-retrieve) exactly as received —
    there's no normalisation beyond URL-decoding.
  </ResponseField>
  <ResponseField name="title" type="string | null">
    The job title, cleaned up for display. **This is not the raw string the
    `?title=` filter searches** — see [de-SHOUTing](#title-is-de-shouted).
  </ResponseField>
  <ResponseField name="source" type="string | null">
    The board the posting was ingested from: `ashby`, `greenhouse`, `lever`…
    This field, the `?source=` filter, and the keys of
    [`/v1/meta`](/docs/api-reference/meta)`.sources` all share one vocabulary —
    the **public slug**. A value read here can be fed straight back as a
    filter. See [sources](/docs/reference/sources).
  </ResponseField>
  <ResponseField name="source_type" type="string | null">
    Provenance class of the source: `ats` — a board the employer authors and
    manages applicants on, the canonical record (join.com included);
    `jobboard` — a third-party board (LinkedIn, Wellfound, …); `aggregator` —
    a re-listing site. Filterable via `?source_type=` (comma = OR). Current
    pool: ~616k `ats`, ~87k `jobboard` — but read live splits off
    [`/v1/meta`](/docs/api-reference/meta)`.sources` rather than trusting this
    sentence.
  </ResponseField>
  <ResponseField name="source_domain" type="string | null">
    The careers-site host the posting was scraped from —
    `jobs.ashbyhq.com`, `boards.greenhouse.io`, or the company's own careers
    domain. Finer-grained provenance than `source`.
  </ResponseField>
  <ResponseField name="url" type="string | null">
    The posting's canonical URL on the source board.
  </ResponseField>
  <ResponseField name="apply_url" type="string | null">
    Where to apply. Falls back to `url` when the board doesn't publish a distinct
    apply link, so in practice these two are often identical.

    This is the employer's own application form — postings are read from company
    boards, not from an aggregator, so sending a candidate here sends them to the
    real ATS. See [attribution](/docs/reference/sources#attribution-and-provenance).
  </ResponseField>
</Fields>

## Company

<Fields>
  <ResponseField name="company" type="object | null">
    The employer. Comes back in one of three shapes, and telling them apart
    matters — see [the three shapes](#company-has-three-shapes).

    <Expandable title="company (full)">
      <ResponseField name="slug" type="string">
        The company's identifier, and the key to
        [`/v1/companies/{slug}`](/docs/api-reference/companies-retrieve).
        **Present only on the full object** — its absence is how you detect the
        minimal fallback.
      </ResponseField>
      <ResponseField name="name" type="string | null">
        Company name.
      </ResponseField>
      <ResponseField name="domain" type="string | null">
        Primary domain, e.g. `linear.app`.
      </ResponseField>
      <ResponseField name="website" type="string | null">
        Company website URL.
      </ResponseField>
      <ResponseField name="logo" type="string | null">
        Logo image URL.
      </ResponseField>
      <ResponseField name="industry" type="string | null">
        Industry label.
      </ResponseField>
      <ResponseField name="description" type="string | null">
        Company description. Not the job description.
      </ResponseField>
    </Expandable>

    The full object carries considerably more than the seven fields above —
    funding, headcount, HQ, specialties. [The Company
    object](/docs/objects/company) documents all of it.
  </ResponseField>
  <ResponseField name="hiring_manager" type="object | null">
    `{ name, email }` when the posting names a hiring manager or recruiting
    contact; `null` when it doesn't (which is most of the time). Either field
    inside can be null on its own — a name without an email is common.
  </ResponseField>
</Fields>

### `company` has three shapes

| Shape | When | How to detect |
| --- | --- | --- |
| **Full** `PublicCompany` | The posting resolved to an enriched company profile. 99.9997% of jobs. | `company.slug` is present |
| **Minimal fallback** | No profile matched, but the posting carries its own employer details. | `company` is non-null, `company.slug` is **absent** |
| `null` | The posting has no employer name at all. Vanishingly rare. | `company === null` |

The minimal fallback has exactly six fields — `name`, `domain`, `website`,
`logo`, `industry`, `description` — built from what the posting itself declared.
It looks like a full company object until you reach for `employee_count` or
`funding` and get `undefined`.

```js
// Don't branch on field-by-field presence. Branch on `slug`.
const isEnriched = job.company?.slug != null;

if (isEnriched) {
  const full = await getCompany(job.company.slug); // /v1/companies/{slug}
}
```

<Note>
  Since a fallback company has no `slug`, there's nothing to look up on
  [`/v1/companies/{slug}`](/docs/api-reference/companies-retrieve). Guard the
  lookup rather than building a URL with `undefined` in it — that's a
  [404](/docs/errors#404-not-found), not an empty result.
</Note>

## Location

<Fields>
  <ResponseField name="location" type="object">
    Where the job is. **In practice never null** — see
    [the note below](#location-is-never-null).

    <Expandable title="location">
      <ResponseField name="remote" type="boolean">
        Whether the posting is remote. Always a real boolean, never null — it
        defaults to `false` rather than being omitted.
      </ResponseField>
      <ResponseField name="countries" type="string[] | null">
        Countries as **full names** — `["United States"]`, not `["US"]`. What
        the `?country=` filter searches (the filter itself accepts full names
        or ISO-2).
      </ResponseField>
      <ResponseField name="cities" type="string[] | null">
        Cities. What `?city=` searches.
      </ResponseField>
      <ResponseField name="regions" type="string[] | null">
        States, provinces, or other sub-national regions. What `?region=`
        searches.
      </ResponseField>
      <ResponseField name="coords" type="{ lat, lng }[] | null">
        Geocoded coordinates, one pair per parsed location. Not filterable —
        there's no radius search — but good for maps and client-side distance
        work.
      </ResponseField>
      <ResponseField name="timezones" type="string[] | null">
        IANA timezone names for the parsed locations —
        `["America/New_York"]`. Not filterable.
      </ResponseField>
      <ResponseField name="continent" type="string | null">
        The continent, one of seven values. What `?continent=` searches — see
        [enums](/docs/reference/enums).
      </ResponseField>
      <ResponseField name="raw" type="string[] | null">
        The location strings exactly as the board published them — `"Remote
        (US)"`, `"NYC or SF"`. Useful when the parsed fields lose nuance. Not
        filterable.
      </ResponseField>
    </Expandable>
  </ResponseField>
</Fields>

### `location` is never null

Every other nested object on a job collapses to `null` when all of its fields are
empty. `location` doesn't, because `remote` always resolves to a real boolean —
`false` when nothing indicates otherwise. A non-null field means the object never
qualifies for the collapse.

The consequence: a posting with no known geography still returns a `location`
object, just an unhelpful one.

```json
// A job with no parsed geography at all. Not null.
"location": { "remote": false, "countries": null, "cities": null, "regions": null, "raw": null }
```

So `if (job.location)` is always true and tells you nothing. Test the field you
actually need — `job.location.countries?.length` — instead.

<Warning title="Countries here are full names; company HQ is ISO-2">
  `job.location.countries` gives `["United States"]`. The same country on
  [`company.hq.country`](/docs/objects/company#hq) gives `"US"`. The two
  vocabularies are genuinely different, so joining a job's country against a
  company's HQ country needs a mapping table in between.

  The **filter** doesn't care: `?country=US` and `?country=United States`
  both work (an unknown ISO-2 code is a `400`). The payload
  asymmetry is what remains.
</Warning>

## Classification

<Fields>
  <ResponseField name="employment_type" type="string[] | null">
    A real array of upper-case values — `["FULL_TIME"]`,
    `["FULL_TIME","TEMPORARY"]`. An array because a posting can carry several
    at once. (A pre-release draft returned this as a raw JSON string needing a parse —
    [not anymore](#employment_type-education_requirements-and-benefits-are-real-arrays-now).)
    Filter with `?employment_type=FULL_TIME`.
  </ResponseField>
  <ResponseField name="work_arrangement" type="string | null">
    One of `On-site`, `Remote`, `Hybrid`. More expressive than
    `location.remote`, which can't represent hybrid. The filter matches
    exactly, case and all — a wrong value is a `400` that lists the allowed
    ones.
  </ResponseField>
  <ResponseField name="office_days" type="number | null">
    Days per week in the office, when a hybrid posting specifies it — `3` for
    "three days in office". `null` otherwise, including on most hybrid
    postings.
  </ResponseField>
  <ResponseField name="remote_location" type="string[] | null">
    The remote-from restriction, when the posting declares one —
    `["United States"]` for "remote (US only)". Where you must live to take
    the remote job; distinct from `location.countries`, which is where the
    role is based.
  </ResponseField>
  <ResponseField name="department" type="string | null">
    The department the role sits in, as the board publishes it —
    `"Engineering"`, `"Go-To-Market"`. Free-form, no fixed vocabulary.
  </ResponseField>
  <ResponseField name="team" type="string | null">
    The team within the department, when the board distinguishes one —
    `"Sync Engine"`. Sparser than `department`.
  </ResponseField>
  <ResponseField name="requisition_id" type="string | null">
    The employer's own requisition / job code — `"ENG-142"`. Useful for
    joining against an employer's internal systems; opaque otherwise.
  </ResponseField>
  <ResponseField name="working_hours" type="number | null">
    Weekly working hours when the posting states them — `40`, `32`. Common on
    European postings, rare elsewhere.
  </ResponseField>
  <ResponseField name="seniority" type="string | null">
    One of `Internship`, `Entry level`, `Associate`, `Mid-Senior level`,
    `Director`, `Management`. See [enums](/docs/reference/enums#seniority).
  </ResponseField>
  <ResponseField name="experience_level" type="string | null">
    Years of experience as a band: `0-2`, `2-5`, `5-10`, `10+`. Related to
    `seniority` but derived separately — the two can disagree, and neither is
    computed from the other at read time. See
    [enums](/docs/reference/enums#experience_level).
  </ResponseField>
  <ResponseField name="education_requirements" type="string[] | null">
    A real array of lower-case values — `["bachelor degree"]`. (A raw JSON
    string in a pre-release draft —
    [not anymore](#employment_type-education_requirements-and-benefits-are-real-arrays-now).)
    Filter with `?education=bachelor degree`.
  </ResponseField>
  <ResponseField name="skills" type="string[] | null">
    Canonical skill tokens — `["typescript", "graphql", "postgresql"]`.
    Present on about 84% of postings.

    Displayed values don't always round-trip as `?skill=` filters. See
    [skills & taxonomies](/docs/guides/skills-and-taxonomies).
  </ResponseField>
  <ResponseField name="keywords" type="string[] | null">
    Broader tags than `skills` — domains, methods, concepts:
    `["distributed systems", "real-time sync"]`. Multi-word and free-form,
    not filterable; use them for display and client-side relevance, not
    queries.
  </ResponseField>
  <ResponseField name="taxonomies" type="string[] | null">
    Job families from a fixed set of 28 Title-Case values — `["Software"]`,
    `["Finance & Accounting"]`. Present on about 90% of postings.
    All 28 are listed in [enums](/docs/reference/enums#taxonomies).
  </ResponseField>
  <ResponseField name="visa_sponsorship" type="boolean | null">
    `true` when the posting mentions sponsorship. `null` means the posting is
    silent, **not** that sponsorship is unavailable. Filter with
    `?visa_sponsorship=only` — or `exclude` to hide sponsoring postings.
  </ResponseField>
  <ResponseField name="job_language" type="string | null">
    Detected language of the posting, e.g. `en`. Filter with `?language=`,
    which takes the ISO code (`en`) or the full name (`English`).
  </ResponseField>
</Fields>

### `employment_type`, `education_requirements`, and `benefits` are real arrays now

In a pre-release draft of this API, `employment_type` and `education_requirements`
came back as **raw JSON strings** (`"[\"FULL_TIME\"]"`) and `benefits` was a prose
blob — the single most common surprise in that payload. All three are real JSON
arrays, consistent with `skills` and `taxonomies` next to them. If your client
runs a defensive `JSON.parse` on them, it can go: parsing an array throws.

```js
job.employment_type;                       // → ["FULL_TIME"] — a real array
job.employment_type.includes("FULL_TIME"); // → true, no parse step
job.benefits;                              // → ["Equity", "401(k) matching"]
```

The values are arrays because a posting can carry several at once
(`["FULL_TIME","TEMPORARY"]`). The **filters** take bare values, not JSON:
`?employment_type=FULL_TIME` and `?education=bachelor degree` — you never send
the encoded form. [Enums](/docs/reference/enums#employment_type) lists every
value and every observed combination.

## Salary

<Fields>
  <ResponseField name="salary" type="object | null">
    Parsed compensation. `null` unless the posting published something machine-
    readable — about 36% of the dataset qualifies.

    <Expandable title="salary">
      <ResponseField name="min" type="number | null">
        Lower bound, **in `unit`** — not necessarily yearly.
      </ResponseField>
      <ResponseField name="max" type="number | null">
        Upper bound, in `unit`.
      </ResponseField>
      <ResponseField name="currency" type="string | null">
        Currency code. **Not normalised** — a EUR posting stays in EUR, and no
        conversion is applied anywhere in the object.
      </ResponseField>
      <ResponseField name="unit" type="string | null">
        The period `min` and `max` are quoted in — `YEAR`, `HOUR`, `MONTH`, and
        others. Read this before comparing `min`/`max` across postings.
      </ResponseField>
      <ResponseField name="annual_min" type="number | null">
        `min` normalised to a yearly figure. Comparable across postings **in the
        same currency**. This is the field `?salary_lte=` compares against —
        and the one to check client-side when you need a strict salary floor.
      </ResponseField>
      <ResponseField name="annual_max" type="number | null">
        `max` normalised to a yearly figure. This is the field `?salary_gte=`
        compares against.
      </ResponseField>
      <ResponseField name="summary" type="string | null">
        Human-readable range as published, e.g. `"$190,000 – $240,000 a year"`.
        Display it rather than reconstructing a string from the numbers.
      </ResponseField>
    </Expandable>
  </ResponseField>
</Fields>

`salary` collapses to `null` only when **all seven** fields are null. A posting
with nothing but a `summary`, or nothing but a `currency`, still returns an
object — so a non-null `salary` doesn't imply a usable number.

```js
// Non-null salary ≠ comparable number.
const comparable = job.salary?.annual_min != null && job.salary.currency === "USD";
```

Two traps worth internalising before you build on this: `min`/`max` are in
`unit`, so an hourly posting's `min: 85` is not a small salary; and
`?salary_gte=` is **range-overlap** — it matches jobs whose band *reaches* the
value (`annual_max >= X`), meaning "could pay this much", not "the floor is at
least this much". For a strict floor, compare `annual_min` client-side. Both
are covered in [salary data](/docs/guides/salary) and
[filtering](/docs/guides/filtering#salary).

## Content

<Fields>
  <ResponseField name="description" type="string | null">
    The full posting body. **Absent unless you ask for it** — see
    [below](#description-is-absent-by-default). Present on 99.5% of postings when
    requested.
  </ResponseField>
  <ResponseField name="requirements_summary" type="string | null">
    Condensed requirements. Always plain text, and always included — it doesn't
    depend on `description_format`.
  </ResponseField>
  <ResponseField name="core_responsibilities" type="string | null">
    Condensed responsibilities. Plain text, always included.
  </ResponseField>
  <ResponseField name="benefits" type="string[] | null">
    Benefits as a real array of strings — `["Equity", "401(k) matching"]`.
    Always included. (Prose in a pre-release draft —
    [not anymore](#employment_type-education_requirements-and-benefits-are-real-arrays-now).)
  </ResponseField>
</Fields>

### `description` is absent by default

Pass `?description_format=text` or `?description_format=html` to include it.
Without it the key is **missing from the JSON entirely** — not `null`:

```js
"description" in job;      // false by default
job.description;           // undefined, not null
job.description ?? "";     // "" — works, but hides which case you're in
```

`text` gives clean plain text; `html` gives sanitized HTML. Any other value is
a [`400`](/docs/errors) — parameters aren't silently dropped anymore.

The three summary fields above are cheaper if all you need is a précis: they're
always present and never require the flag.

<Tip>
  Descriptions are the bulk of a posting's bytes. Fetching them 200 at a time
  from [`/v1/jobs`](/docs/api-reference/jobs-list) is the usual reason a request
  feels slow — list without them, then pull the ones you need from
  [`/v1/jobs/{id}`](/docs/api-reference/jobs-retrieve).
</Tip>

## Timestamps

<Fields>
  <ResponseField name="posted_at" type="string | null">
    When the posting went live, ISO 8601 UTC. Falls back to the ATS creation date
    when the board doesn't publish a posting date. This is the field results are
    sorted by and that `?time_frame=` measures against.
  </ResponseField>
  <ResponseField name="expires_at" type="string | null">
    When the posting expires, if the board declares one. Usually `null` — most
    boards don't publish expiry, and postings are instead reaped by
    [board diffing](/docs/reference/coverage#expiry-and-reaping).

    [`/v1/jobs`](/docs/api-reference/jobs-list) and
    [`/v1/jobs/feed`](/docs/api-reference/jobs-feed) both exclude expired
    postings; an expired posting stays retrievable by id while it's in the
    pool, so check this field there. To learn which cached ids to purge, use
    [`/v1/jobs/expired`](/docs/api-reference/jobs-expired).
  </ResponseField>
  <ResponseField name="created_at" type="string | null">
    When **we** first indexed the posting. This is the timestamp the
    [feed](/docs/api-reference/jobs-feed) windows and orders by, and what
    `?created_gte=` filters on — the anchor for incremental sync.
  </ResponseField>
  <ResponseField name="modified_at" type="string | null">
    When a **tracked field** last changed — title, salary, apply URL, location,
    and a dozen others. `null` until the first tracked change.
    [`/v1/jobs/modified`](/docs/api-reference/jobs-modified) is the feed of
    these changes.
  </ResponseField>
  <ResponseField name="modified_fields" type="string[] | null">
    Which tracked fields changed in the most recent modification —
    `["salary_max", "apply_url"]`. The vocabulary is listed on
    [`/v1/jobs/modified`](/docs/api-reference/jobs-modified).
  </ResponseField>
  <ResponseField name="updated_at" type="string | null">
    When the posting's row last changed for any reason, falling back to when it
    was first ingested. Broader and noisier than `modified_at` — prefer
    `modified_at` for "did something meaningful change".
  </ResponseField>
</Fields>

All of these are ISO 8601 strings in UTC (`2026-07-08T14:02:11.000Z`), never
epoch numbers.

<Note>
  The fallbacks mean these fields are rarely null, but they don't all mean quite
  the same thing across postings: a `posted_at` may be a board's real publication
  date or the ATS record's creation date, and an `updated_at` may be a genuine
  edit or simply our first sight of the posting. They're reliable for ordering
  and for "newer than X". They're weaker as claims about the employer's own
  timeline.
</Note>

## `openings`

<Fields>
  <ResponseField name="openings" type="number">
    Number of postings folded into this row. **Only present when you pass
    `?collapse=true`** — the key is absent from every uncollapsed response.
  </ResponseField>
</Fields>

`collapse=true` folds a company's repeats of the same title across many cities
into one row. The surviving row is a real posting, not a synthetic one, and
`openings` tells you how many it stands for. See
[collapsing](/docs/guides/deduplication#collapsing).

## Derivations

Four fields aren't stored as returned. Each of these is deliberate, and each has
a consequence worth knowing.

### `title` is de-SHOUTed

Boards publish a lot of `SENIOR SOFTWARE ENGINEER, EMEA`. The returned `title` is
normalised for display:

| Published | Returned |
| --- | --- |
| `SENIOR SOFTWARE ENGINEER` | `Senior Software Engineer` |
| `QA ANALYST II` | `QA Analyst II` |
| `DIRECTOR OF SALES AND MARKETING` | `Director of Sales and Marketing` |

ALL-CAPS titles become Title Case, two- and three-letter acronyms (`HR`, `QA`,
`IT`) keep their capitalisation, and minor words (`of`, `and`, `the`) are
lower-cased. Titles that weren't shouting are left alone.

<Warning title="`?title=` searches the raw title, not the one you got back">
  The cleanup happens on the way out. The stored value is untouched, and the
  `?title=` filter matches against **that** — so the string in your response is
  not guaranteed to be the string the filter sees.

  It rarely bites, because [`title` search is
  word-based and case-insensitive](/docs/guides/filtering#text-search):
  `?title=engineer` finds both `ENGINEER` and `Engineer`. It can bite on
  **quoted phrases** whose punctuation or spacing de-SHOUTing changed, or when
  you feed a returned title back in verbatim expecting an exact hit. Search
  for distinctive words, not the whole title.
</Warning>

### `apply_url` falls back to `url`

`apply_url` is the board's apply link when there is one, and the posting URL
otherwise. There's no way to tell which you got — compare the two fields if you
care.

### `posted_at` and `updated_at` fall back

`posted_at` uses the board's posting date, then the ATS creation date.
`updated_at` uses the board's update date, then our first ingestion time. Neither
is null just because the board published nothing.

### `location` never collapses

Covered [above](#location-is-never-null) — `remote` always resolves, so the
object always survives.

## Nullability at a glance

| Field | Null when |
| --- | --- |
| `id` | Never |
| `location` | Never *(in practice — `remote` always resolves)* |
| `company` | Only if the posting carries no employer name at all |
| `salary` | All seven of its fields are null (~63% of postings) |
| `description` | **Absent**, not null, unless `description_format` is set |
| `openings` | **Absent** unless `collapse=true` |
| Everything else | Whenever the source board didn't publish it |

## Related

<CardGroup cols={2}>
  <Card title="The Company object" icon="Building2" href="/docs/objects/company">
    The full shape of `job.company` when it's enriched.
  </Card>
  <Card title="Enums & vocabularies" icon="ListChecks" href="/docs/reference/enums">
    Every valid value for `seniority`, `employment_type`, `taxonomies`, and the rest.
  </Card>
  <Card title="Filtering jobs" icon="Funnel" href="/docs/guides/filtering">
    Which fields are filterable, and the matching mode behind each one.
  </Card>
  <Card title="Sources & ATS coverage" icon="Plug" href="/docs/reference/sources">
    What `source` means, the values it can take, and which are live.
  </Card>
</CardGroup>
