# Provenance and confidence

> Where did each field come from, and how much should I trust it?

Source: https://docs.betterjobs.cc/concepts/provenance-and-confidence/

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

Each [canonical job](https://docs.betterjobs.cc/concepts/canonical-jobs.md) carries a `sources[]` array. One entry per provider that saw the opening:

```json
"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](https://docs.betterjobs.cc/providers/betterjobs-index.md).                               |
| `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](https://docs.betterjobs.cc/concepts/freshness-and-lifecycle.md). |
| `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.

> An empty fields list still counts
>
> `fields: []` means the source saw the job and corroborated it, but contributed no field to the canonical record. It still counts as corroboration.

## 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.origin` is `declared` (stated in the posting) or `inferred` (estimated by a source). Do not show an inferred range as the employer’s offer.
- `seniority` and `job_family` are classified from the title and text. The [field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md) marks each field as `raw`, `normalized` or `inferred`.

## `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`.** The [`consensus` strategy](https://docs.betterjobs.cc/concepts/waterfall.md#strategies) drops jobs seen by fewer than `min_sources` sources. `p_real` grades the jobs that remain.
- **Compare with a single provider.** Per JobsPipe docs, JobsPipe publishes a `ghost_score` per job. `p_real` points the other way: higher means more likely real.

> p\_real is a probability, not a verdict
>
> A job with `p_real: 0.6` is not fake. It is less corroborated. A brand-new job seen by one source can start lower and rise as other sources pick it up. The calibration method and measured accuracy will be published at GA.

## `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`:

```json
{ "value": true, "confidence": 0.96, "basis": "42 open jobs corroborated by 4 sources in the last 30 days" }
```

```json
{ "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.

> Never treat null as false
>
> If you write `if not profile["is_hiring"]["value"]`, a company you know nothing about lands in the “not hiring” bucket. Check for `null` explicitly. Per Reqbeat docs, Reqbeat draws the same line: `coverage_status: no_ats_signal` means unknown, not “not hiring”.

**curl**

```bash
curl https://api.betterjobs.cc/v1/companies/acme-robotics.example \
  -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
  -H "BetterJobs-Version: 2026-10-01"
```

**Python**

```python
import os
import 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"])
```

**TypeScript**

```ts
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](https://docs.betterjobs.cc/guides/detect-hiring-changes.md).

## 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](https://docs.betterjobs.cc/data/field-dictionary.md) lists what `null` means for each field.

## 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.

```json
"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

Every job also carries `license.display` and `license.resale`. They tell you what you may do with the data. See [Licensing](https://docs.betterjobs.cc/concepts/licensing.md).

## Related

- [Canonical jobs](https://docs.betterjobs.cc/concepts/canonical-jobs.md): how `sources[]` is built
- [Field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md#field-job-p_real): `p_real` and every other field
- [Company profiles](https://docs.betterjobs.cc/data/companies.md)
- [Get a company hiring profile](/api/operations/getcompany/)
