JobsPipe
API Reference

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

FieldTypeDescription
total_resultsnumber | nullTotal matches. null unless include_total_results was set.
truncated_resultsnumberNumber of jobs returned in this data array.
total_companiesnumber | nullTotal distinct companies across matches, when computed.
truncated_companiesnumberDistinct companies in this page.
next_cursorstring | nullCursor for the next page, or null if there are no more results.
FieldTypeDescription
idstring | numberStable ID for the posting on its source - store it and look the same job up later.
job_titlestringThe posting's job title.
normalized_titlestring | nullTitle normalized to a canonical form.
urlstring | nullCanonical posting URL.
final_urlstring | nullReserved for compatibility - always null. See reserved fields.
source_urlstring | nullURL on the source the job was first seen on.
sourcesobject[]Every source the job has been seen on.
companystring | nullHiring company name.
company_domainstring | nullCompany website domain, for CRM and account matching.
company_objectobject | nullStructured 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

FieldTypeDescription
namestring | nullCompany name.
domainstring | nullBare website host, e.g. stripe.com.
urlstring | nullCompany website.
logostring | nullLogo image URL.
employee_countnumber | nullExact headcount from the company's public profile.
employee_count_minnumber | nullLower bound when only a size band is known (10000 for "10,000+").
linkedin_urlstring | nullCompany LinkedIn page.
descriptionstring | nullCompany description.
locationobject | nullHeadquarters: 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

FieldTypeDescription
providerstringSource name, e.g. linkedin, greenhouse.
urlstring | nullThe posting's URL on that source.
seen_atstring | nullWhen 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

FieldTypeDescription
date_postedstring | nullWhen the job was posted, in the source's own format.
discovered_atstring | nullWhen JobsPipe first ingested the posting.
last_seen_atstring | nullWhen a recheck last confirmed the posting was still live (YYYY-MM-DD HH:MM:SS, UTC).
verified_atstring | nullWhen we last rechecked the posting, whether or not anything had changed (YYYY-MM-DD HH:MM:SS, UTC).
statusstringactive or closed. Only active postings are returned unless the status filter says otherwise.
closed_atstring | nullWhen the posting was detected closed.
closed_reasonstring | nullWhy it closed: closed, gone or stale.
repostedbooleanWhether the role was reposted.
date_repostedstring | nullReserved for compatibility - always null. See reserved fields.
has_blurred_databooleanWhether 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

FieldTypeDescription
locationstring | nullDisplay location string.
short_locationstring | nullCondensed location.
long_locationstring | nullFull location string.
countrystring | nullPrimary country name.
country_codestring | nullPrimary ISO country code.
countriesstring[]All associated country names.
country_codesstring[]All associated ISO country codes.
citiesstring[]Associated cities.
continentsstring[]Associated continents.
state_codestring | nullState/region code.
postal_codestring | nullPostal code.
latitudenumber | nullLatitude.
longitudenumber | nullLongitude.
remoteboolean | nullRemote flag.
hybridboolean | nullHybrid flag.
work_arrangementstring | 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.

FieldTypeDescription
salary_stringstring | nullPay as written in the posting, e.g. $120K - $150K a year.
salary_currencystring | nullISO 4217 currency code, e.g. USD, GBP, EUR.
min_annual_salarynumber | nullMinimum pay annualized, in the original currency.
max_annual_salarynumber | nullMaximum pay annualized, in the original currency.
min_annual_salary_usdnumber | nullmin_annual_salary in USD; null for non-USD postings.
max_annual_salary_usdnumber | nullmax_annual_salary in USD; null for non-USD postings.
avg_annual_salary_usdnumber | nullMidpoint of the USD min and max when both are present.

Classification & content

FieldTypeDescription
senioritystring | nullOne of entry_level, mid_level, senior, director, executive.
is_managerboolean | nullWhether the role manages people.
job_functionstring | nullBroad function of the role, e.g. Engineering.
employer_typestringWho posted the job: employer, agency or broker.
employment_statusesstring[]Employment types, e.g. ["full_time"].
easy_applyboolean | nullReserved for compatibility - always null. See reserved fields.
technology_slugsstring[]The subset of keyword_slugs that are technologies.
keyword_slugsstring[]Skill slugs extracted from title + description against a curated lexicon.
occupation_codestring | nullISCO-08 unit group of the role (4-digit), e.g. 2512.
occupation_labelstring | nullHuman-readable ISCO-08 label, e.g. Software Developers.
isic_divisionstring | nullISIC Rev.4 industry division of the employer (2-digit), e.g. 62.
isic_division_labelstring | nullHuman-readable ISIC division label.
hiring_teamarrayReserved for compatibility - always empty. See reserved fields.
descriptionstring | nullFull job description.
matching_phrasesstring[]Reserved for compatibility - always empty.
matching_wordsstring[]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.

FieldPopulated
verified_at, sources100.0%
country, countries96.0%
country_code, country_codes94.8%
continents94.7%
last_seen_at93.1%
seniority31.2%
normalized_title, is_manager, job_function8.9%
work_arrangement, hybrid0%
latitude, longitude, postal_code0%

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:

FieldValueUse instead
final_urlnullurl, or source_url for the link on the source.
easy_applynull-
hiring_team[]-
date_repostednullreposted, 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.

On this page