Every job tells you which providers saw it, what each one contributed and when. On top of that, BetterJobs gives you two confidence signals: p_real on jobs and is_hiring on companies. This page explains how to read all of them, and what null means.
sources[]: who said what
Section titled “sources[]: who said what”Each canonical job carries a sources[] array. One entry per provider that saw the opening:
"sources": [ { "provider": "betterjobs", "provider_job_id": "bj_idx_5521907", "url": "https://jobs.acme-robotics.example/revops-lead", "first_seen_at": "2026-10-08T06:40:00Z", "last_seen_at": "2026-10-11T06:10:00Z", "fields": ["title", "description", "apply_url", "location", "employment_type", "posted_at"] }, { "provider": "theirstack", "provider_job_id": "ts_88213377", "url": "https://jobs.acme-robotics.example/revops-lead", "first_seen_at": "2026-10-08T07:12:00Z", "last_seen_at": "2026-10-11T04:02:00Z", "fields": ["salary", "seniority"] }, { "provider": "techmap", "provider_job_id": "tm_3f9a2c71", "url": "https://boards.example/acme-robotics/revops-lead", "first_seen_at": "2026-10-08T09:30:00Z", "last_seen_at": "2026-10-10T23:40:00Z", "fields": ["job_family"] }]Illustrative record from the spec. Read it like this:
| Key | Answers |
|---|---|
provider |
Which source. betterjobs is the BetterJobs index. |
provider_job_id |
The provider’s own id for its posting. Useful for support with that provider. Not a primary key. |
url |
The posting as this provider saw it. Two sources can point to different URLs for the same job. |
first_seen_at, last_seen_at |
When this provider first and last saw the job. See Freshness and lifecycle. |
fields |
Which canonical fields this source supplied. |
So in the example, salary and seniority came from TheirStack, job_family from Techmap, and the rest from the BetterJobs index.
Declared vs inferred
Section titled “Declared vs inferred”Some values are stated by the employer. Others are estimated by a source. The response tells you which where it matters:
salary.originisdeclared(stated in the posting) orinferred(estimated by a source). Do not show an inferred range as the employer’s offer.seniorityandjob_familyare classified from the title and text. The field dictionary marks each field asraw,normalizedorinferred.
p_real: is this a real open job?
Section titled “p_real: is this a real open job?”p_real is a probability from 0 to 1 that the job is a real open req. It comes from cross-source corroboration: independent sources seeing the same opening push it up.
How to use it:
- Sort or threshold before outbound. Pick a cutoff that fits your cost of a wrong contact.
- Combine with
consensus. Theconsensusstrategy drops jobs seen by fewer thanmin_sourcessources.p_realgrades the jobs that remain. - Compare with a single provider. Per JobsPipe docs, JobsPipe publishes a
ghost_scoreper job.p_realpoints the other way: higher means more likely real.
is_hiring: yes, no, or unknown
Section titled “is_hiring: yes, no, or unknown”Company profiles (GET /v1/companies/{domain}) answer “is this company hiring?” with three values, not two:
is_hiring.value |
Meaning | What to do |
|---|---|---|
true |
Open jobs, corroborated. | Hiring-based plays are on. |
false |
Evidence the company stopped hiring, for example all its jobs closed. | Pause hiring-based plays. |
null |
Unknown. Not enough signal. | Do not treat as false. Check again later or try another channel. |
Each value comes with confidence (0 to 1) and a plain-English basis:
{ "value": true, "confidence": 0.96, "basis": "42 open jobs corroborated by 4 sources in the last 30 days" }{ "value": null, "confidence": 0.0, "basis": "No careers page or ATS found for this domain" }Both illustrative, from the spec. Northwind Traders (northwind.example) gets null because no careers page or ATS was found. That says nothing about whether it hires.
curl https://api.betterjobs.cc/v1/companies/acme-robotics.example \ -H "Authorization: Bearer $BETTERJOBS_API_KEY" \ -H "BetterJobs-Version: 2026-10-01"import osimport requests
resp = requests.get( "https://api.betterjobs.cc/v1/companies/acme-robotics.example", headers={ "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}", "BetterJobs-Version": "2026-10-01", },)resp.raise_for_status()hiring = resp.json()["is_hiring"]
if hiring["value"] is None: print("unknown:", hiring["basis"])elif hiring["value"]: print("hiring", hiring["confidence"])else: print("not hiring", hiring["confidence"])const resp = await fetch('https://api.betterjobs.cc/v1/companies/acme-robotics.example', { headers: { Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`, 'BetterJobs-Version': '2026-10-01', },});if (!resp.ok) throw new Error(`BetterJobs ${resp.status}`);const { is_hiring } = await resp.json();
if (is_hiring.value === null) console.log('unknown:', is_hiring.basis);else if (is_hiring.value) console.log('hiring', is_hiring.confidence);else console.log('not hiring', is_hiring.confidence);A watch fires company.hiring_stopped only when value changes to false. A change to null never fires it. See Detect hiring changes.
Null semantics
Section titled “Null semantics”One rule covers jobs and company profiles:
| You see | It means |
|---|---|
null |
Unknown. No source reported it. |
[] |
Verified none. For example top_job_families: [] = no open jobs found. |
| Field omitted | Not part of this payload, for example the lifecycle-only job in job.closed and job.reposted events. |
false is a real answer, never a stand-in for unknown. location.remote: null means nobody said; location.remote: false means on-site or hybrid. The field dictionary lists what null means for each field.
How complete is a result?
Section titled “How complete is a result?”Every search response reports fill rates for the jobs it returned in metadata.field_coverage: the fraction (0 to 1) of returned jobs with a non-null value, per field.
"field_coverage": { "salary": 0.41, "seniority": 0.97, "location.remote": 0.88, "description": 0.99 }Illustrative numbers from the spec. Read your own from each response: they depend on the query, the strategy and the providers on your plan. Measured fill rates per provider will be published at GA.
License travels with the job
Section titled “License travels with the job”Every job also carries license.display and license.resale. They tell you what you may do with the data. See Licensing.
Related
Section titled “Related”- Canonical jobs: how
sources[]is built - Field dictionary:
p_realand every other field - Company profiles
- Get a company hiring profile