Job Schema
Every field returned for a job, plus the response metadata.
A successful POST /v1/jobs/search returns { metadata, data }, where data is an array of normalized job objects. Every job follows the same shape regardless of source. Most fields are nullable - a source that doesn't supply a value yields null.
Metadata
| Field | Type | Description |
|---|---|---|
total_results | number | null | Total matches. null unless include_total_results was set. |
truncated_results | number | Number of jobs returned in this data array. |
total_companies | number | null | Total distinct companies across matches, when computed. |
truncated_companies | number | Distinct companies in this page. |
next_cursor | string | null | Cursor for the next page, or null if there are no more results. |
Identity & links
| Field | Type | Description |
|---|---|---|
id | string | number | Stable ID for the posting on its source - store it and look the same job up later. |
job_title | string | The posting's job title. |
normalized_title | string | null | Title normalized to a canonical form. |
url | string | null | Canonical posting URL. |
final_url | string | null | Reserved for compatibility - always null. See reserved fields. |
source_url | string | null | URL on the source the job was first seen on. |
sources | object[] | Every source the job has been seen on. |
company | string | null | Hiring company name. |
company_domain | string | null | Company website domain, for CRM and account matching. |
company_object | object | null | Structured company details, or null when nothing beyond the name is known. |
IDs are per source. The same role posted to LinkedIn and to the company's Greenhouse board comes
back as two records with two IDs, so do not use id to deduplicate across sources - measured
cross-source duplication is about 1.3%.
company_object
| Field | Type | Description |
|---|---|---|
name | string | null | Company name. |
domain | string | null | Bare website host, e.g. stripe.com. |
url | string | null | Company website. |
logo | string | null | Logo image URL. |
employee_count | number | null | Exact headcount from the company's public profile. |
employee_count_min | number | null | Lower bound when only a size band is known (10000 for "10,000+"). |
linkedin_url | string | null | Company LinkedIn page. |
description | string | null | Company description. |
location | object | null | Headquarters: street, city, region, postal_code, country. |
Every field is independently optional. Coverage depends on how the company was resolved, so a job
may carry a domain and a logo but no employee_count. The object itself is null rather than
an object full of nulls when we know nothing beyond the name, so if (job.company_object) is a
safe test.
sources
| Field | Type | Description |
|---|---|---|
provider | string | Source name, e.g. linkedin, greenhouse. |
url | string | null | The posting's URL on that source. |
seen_at | string | null | When the job was seen on that source. |
sources lists every source we have seen the posting on, not only the first, and is present on
every job. source_url is unchanged: it still points at the source the job was first seen on, so
existing integrations keep reading the same value.
Read the array rather than assuming its length. Postings are keyed per source today, so it normally holds a single entry.
Dates & lifecycle
| Field | Type | Description |
|---|---|---|
date_posted | string | null | When the job was posted, in the source's own format. |
discovered_at | string | null | When JobsPipe first ingested the posting. |
last_seen_at | string | null | When a recheck last confirmed the posting was still live (YYYY-MM-DD HH:MM:SS, UTC). |
verified_at | string | null | When we last rechecked the posting, whether or not anything had changed (YYYY-MM-DD HH:MM:SS, UTC). |
status | string | active or closed. Only active postings are returned unless the status filter says otherwise. |
closed_at | string | null | When the posting was detected closed. |
closed_reason | string | null | Why it closed: closed, gone or stale. |
reposted | boolean | Whether the role was reposted. |
date_reposted | string | null | Reserved for compatibility - always null. See reserved fields. |
has_blurred_data | boolean | Whether some fields are blurred on the source. |
verified_at advances on every recheck; last_seen_at advances only on the rechecks that found
the posting still open. A verified_at newer than last_seen_at therefore means the most recent
check did not confirm the job, which is an earlier signal than closed_at, set only once the
posting is confirmed gone.
The timestamp formats differ by origin. last_seen_at, verified_at and closed_at are ours and
are UTC, written YYYY-MM-DD HH:MM:SS. discovered_at and sources[].seen_at are ISO-8601 with an
offset. date_posted is whatever the source publishes: most give a full ISO-8601 timestamp, some
give a bare YYYY-MM-DD, so parse it as either rather than assuming a fixed shape.
Location
| Field | Type | Description |
|---|---|---|
location | string | null | Display location string. |
short_location | string | null | Condensed location. |
long_location | string | null | Full location string. |
country | string | null | Primary country name. |
country_code | string | null | Primary ISO country code. |
countries | string[] | All associated country names. |
country_codes | string[] | All associated ISO country codes. |
cities | string[] | Associated cities. |
continents | string[] | Associated continents. |
state_code | string | null | State/region code. |
postal_code | string | null | Postal code. |
latitude | number | null | Latitude. |
longitude | number | null | Longitude. |
remote | boolean | null | Remote flag. |
hybrid | boolean | null | Hybrid flag. |
work_arrangement | string | null | "remote", "hybrid" or "onsite"; null when the posting doesn't say. |
work_arrangement is the three-way form of the workplace question. The remote boolean reads
false for hybrid and onsite alike, so it cannot separate them; work_arrangement can. hybrid
is derived from work_arrangement, so the two always agree. Filter on it with
work_arrangement_or. See
field coverage for how much of the corpus carries it today.
Compensation
Pay is present only when the posting discloses it, which is roughly a quarter of postings; the fields are null otherwise. min_annual_salary and max_annual_salary are annualized to a yearly figure (hourly, daily, weekly and monthly rates are scaled up), while salary_string preserves the original text. The _usd fields are populated for USD postings; for other currencies they are null, since no exchange-rate conversion is applied.
| Field | Type | Description |
|---|---|---|
salary_string | string | null | Pay as written in the posting, e.g. $120K - $150K a year. |
salary_currency | string | null | ISO 4217 currency code, e.g. USD, GBP, EUR. |
min_annual_salary | number | null | Minimum pay annualized, in the original currency. |
max_annual_salary | number | null | Maximum pay annualized, in the original currency. |
min_annual_salary_usd | number | null | min_annual_salary in USD; null for non-USD postings. |
max_annual_salary_usd | number | null | max_annual_salary in USD; null for non-USD postings. |
avg_annual_salary_usd | number | null | Midpoint of the USD min and max when both are present. |
Classification & content
| Field | Type | Description |
|---|---|---|
seniority | string | null | One of entry_level, mid_level, senior, director, executive. |
is_manager | boolean | null | Whether the role manages people. |
job_function | string | null | Broad function of the role, e.g. Engineering. |
employer_type | string | Who posted the job: employer, agency or broker. |
employment_statuses | string[] | Employment types, e.g. ["full_time"]. |
easy_apply | boolean | null | Reserved for compatibility - always null. See reserved fields. |
technology_slugs | string[] | The subset of keyword_slugs that are technologies. |
keyword_slugs | string[] | Skill slugs extracted from title + description against a curated lexicon. |
occupation_code | string | null | ISCO-08 unit group of the role (4-digit), e.g. 2512. |
occupation_label | string | null | Human-readable ISCO-08 label, e.g. Software Developers. |
isic_division | string | null | ISIC Rev.4 industry division of the employer (2-digit), e.g. 62. |
isic_division_label | string | null | Human-readable ISIC division label. |
hiring_team | array | Reserved for compatibility - always empty. See reserved fields. |
description | string | null | Full job description. |
matching_phrases | string[] | Reserved for compatibility - always empty. |
matching_words | string[] | Reserved for compatibility - always empty. |
employer_type reports "employer" for every posting today - the classifier that would separate
staffing agencies and republishing job boards from direct employers has not run yet, so the
employer_type_or and employer_type_not filters do not narrow anything either.
seniority maps our internal lead bucket to "director" to keep the vocabulary stable, which
files staff and principal individual contributors under a value that reads as people management. is_manager is the field
that tells them apart: a principal engineer with no reports is is_manager: false, an actual
director is true, and null means we don't know.
Field coverage
Fill rates measured on a stratified sample of 2,153 production postings drawn from all ten sources. Stratified means every source is represented well enough to measure, not in its production proportion, so these are per-field fill rates rather than a prediction of what a given query returns.
| Field | Populated |
|---|---|
verified_at, sources | 100.0% |
country, countries | 96.0% |
country_code, country_codes | 94.8% |
continents | 94.7% |
last_seen_at | 93.1% |
seniority | 31.2% |
normalized_title, is_manager, job_function | 8.9% |
work_arrangement, hybrid | 0% |
latitude, longitude, postal_code | 0% |
country and countries were 72.0% and continents 0% before location resolution was reworked;
the rest of the location fields are unchanged.
work_arrangement is not populated yet. It fills in as postings are re-ingested and backfilled,
and hybrid, which is derived from it, fills in with it. Until then, use remote.
latitude, longitude and postal_code are passed through from the source payload, and no
posting in the sample supplied them. LinkedIn, about half the corpus, publishes none of the three
and no workplace type either - its public pages simply do not carry them, so those fields stay
null for LinkedIn postings permanently. Indeed, about 40% of the corpus, does publish
coordinates and postal codes, so these fields fill in for Indeed postings as re-ingestion reaches
them.
Reserved for compatibility
The response shape is frozen, so a few fields stay in it that JobsPipe does not populate. They are always present and always empty:
| Field | Value | Use instead |
|---|---|---|
final_url | null | url, or source_url for the link on the source. |
easy_apply | null | - |
hiring_team | [] | - |
date_reposted | null | reposted, which is populated. |
matching_phrases | [] | - |
matching_words | [] | - |
Do not branch on these fields. They are documented so that a client generated from the schema knows not to expect data in them, not because they are pending.
Fields are added over time and are always additive. Treat unknown fields as forward-compatible and read only the ones you need.