# Jobs

> What is in a job record, and what do null and empty values mean?

Source: https://docs.betterjobs.cc/data/jobs/

Cost: 1 credit / unique jobDuplicates, jobs you already paid for, empty pages and dry runs are free.

## Overview

A job is one real opening. BetterJobs calls it a **canonical job**.

Several providers often list the same opening. BetterJobs merges those listings into one record with one stable `id` (`job_...`). Each provider that saw the opening appears in `sources[]`, with the fields it contributed. You pay for the canonical job once, not once per provider.

Every job carries three kinds of data:

- **What the job is**: `title`, `company`, `location`, `employment_type`, `seniority`, `job_family`, `salary`, `description`, `apply_url`.
- **Where it is in its life**: `posted_at`, `first_seen_at`, `last_seen_at`, `last_verified_at`, `status`, `closed_reason`, `repost_count`.
- **How far to trust it**: `p_real`, `sources[]`, `license`.

Read [Canonical jobs](https://docs.betterjobs.cc/concepts/canonical-jobs.md) for how merging works and [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md) for `sources[]` and `p_real`.

### Null, empty and missing

Three states mean three different things. Do not collapse them.

| You see       | It means                        | Example                                                                                |
| ------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
| `null`        | Unknown. No source reported it. | `"salary": null` means no source reported pay. It does not mean the job is unpaid.     |
| `[]`          | Verified none.                  | `"fields": []` on a source means it corroborated the job but contributed no field.     |
| Field missing | Not part of this payload.       | `job.closed` events carry only `id`, `title`, `company`, `status` and `closed_reason`. |

> null is not false
>
> `location.remote: null` means remote status is unknown. It is not the same as `false` (on-site or hybrid). Filter with `remote: true` or `remote: false` only when you want to drop the unknowns.

The per-field meaning of `null` is in the **null / \[] means** column below.

## Field dictionary

Every field on a job, with its type, how it is derived and what `null` means. Each row has an anchor, for example [`salary.min`](#field-job-salary-min). The [Field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md) adds the company fields and the names each provider uses.

| Field                       | Type                                                                             | Description                                                              | null / \[] means                                                 | Derivation | Example                                                                            |
| --------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------- |
| `id`                        | `string`                                                                         | Canonical job id (`job_...`). Stable across sources and requests.        | Never null                                                       | normalized | `"job_01JC8X4M2Q7RV3T9KD5W6YH0AB"`                                                 |
| `title`                     | `string`                                                                         | Job title as posted.                                                     | Never null                                                       | raw        | `"Head of Revenue Operations"`                                                     |
| `company.id`                | `string`                                                                         | Canonical company id (`cmp_...`).                                        | Never null                                                       | normalized | `"cmp_4Rk7TzP1aQ"`                                                                 |
| `company.name`              | `string`                                                                         | Company name.                                                            | Never null                                                       | normalized | `"Acme Robotics"`                                                                  |
| `company.domain`            | `string \| null`                                                                 | Primary web domain of the company.                                       | Unknown. No source reported it.                                  | normalized | `"acme-robotics.example"`                                                          |
| `location.city`             | `string \| null`                                                                 | City.                                                                    | Unknown. No source reported it.                                  | normalized | `"Berlin"`                                                                         |
| `location.region`           | `string \| null`                                                                 | State, province or region.                                               | Unknown. No source reported it.                                  | normalized | `"Berlin"`                                                                         |
| `location.country_code`     | `string \| null`                                                                 | ISO 3166-1 alpha-2 country code.                                         | Unknown. No source reported it.                                  | normalized | `"DE"`                                                                             |
| `location.remote`           | `boolean \| null`                                                                | `true` remote, `false` on-site or hybrid.                                | Unknown. Not the same as `false`.                                | normalized | `false`                                                                            |
| `employment_type`           | `full_time \| part_time \| contract \| internship \| temporary \| null`          | Employment type.                                                         | Unknown. No source reported it.                                  | normalized | `"full_time"`                                                                      |
| `seniority`                 | `intern \| junior \| mid \| senior \| lead \| director \| vp \| c_level \| null` | Seniority level.                                                         | Unknown. No source reported it.                                  | inferred   | `"lead"`                                                                           |
| `job_family`                | `string \| null`                                                                 | Job family, e.g. `engineering`, `sales`, `operations`.                   | Unknown. No source reported it.                                  | inferred   | `"operations"`                                                                     |
| `salary`                    | `Salary \| null`                                                                 | Pay range. See the `salary.*` fields.                                    | Unknown. No source reported pay.                                 | normalized | `{"min":110000,"max":135000,"currency":"EUR","period":"year","origin":"declared"}` |
| `salary.min`                | `number \| null`                                                                 | Lower bound in `salary.currency` per `salary.period`.                    | Unknown lower bound.                                             | normalized | `110000`                                                                           |
| `salary.max`                | `number \| null`                                                                 | Upper bound in `salary.currency` per `salary.period`.                    | Unknown upper bound.                                             | normalized | `135000`                                                                           |
| `salary.currency`           | `string`                                                                         | ISO 4217 currency code.                                                  | Never null                                                       | normalized | `"EUR"`                                                                            |
| `salary.period`             | `year \| month \| hour`                                                          | Pay period.                                                              | Never null                                                       | normalized | `"year"`                                                                           |
| `salary.origin`             | `declared \| inferred`                                                           | `declared` = stated in the posting. `inferred` = estimated by a source.  | Never null                                                       | normalized | `"declared"`                                                                       |
| `description`               | `string \| null`                                                                 | Plain-text job description.                                              | Unknown. No source reported it.                                  | raw        | `"Acme Robotics is hiring a Head of Revenue Operations..."`                        |
| `apply_url`                 | `string \| null`                                                                 | Where a candidate applies.                                               | Unknown. No source reported it.                                  | raw        | `"https://jobs.acme-robotics.example/revops-lead/apply"`                           |
| `posted_at`                 | `date-time \| null`                                                              | Date the employer posted the job.                                        | Unknown. Use `first_seen_at` instead.                            | raw        | `"2026-10-08T00:00:00Z"`                                                           |
| `first_seen_at`             | `date-time`                                                                      | Earliest time any source saw the job.                                    | Never null                                                       | normalized | `"2026-10-08T06:40:00Z"`                                                           |
| `last_seen_at`              | `date-time`                                                                      | Latest time any source saw the job.                                      | Never null                                                       | normalized | `"2026-10-11T06:10:00Z"`                                                           |
| `last_verified_at`          | `date-time \| null`                                                              | Latest time the job was confirmed live at its origin.                    | Never verified at origin.                                        | normalized | `"2026-10-11T06:10:00Z"`                                                           |
| `status`                    | `open \| closed`                                                                 | Lifecycle status.                                                        | Never null                                                       | normalized | `"open"`                                                                           |
| `closed_reason`             | `filled \| expired \| removed \| unknown \| null`                                | Why the job closed.                                                      | The job is open.                                                 | normalized | `null`                                                                             |
| `repost_count`              | `integer`                                                                        | Times the same job was re-listed.                                        | Never null                                                       | normalized | `0`                                                                                |
| `p_real`                    | `number (0-1)`                                                                   | Probability the job is a real open req, from cross-source corroboration. | Never null                                                       | inferred   | `0.94`                                                                             |
| `sources`                   | `Source[]`                                                                       | Every provider that saw this job, with what it contributed.              | Never null                                                       | raw        | `[{"provider":"theirstack",...}]`                                                  |
| `sources[].provider`        | `ProviderSlug`                                                                   | Provider slug.                                                           | Never null                                                       | raw        | `"theirstack"`                                                                     |
| `sources[].provider_job_id` | `string`                                                                         | The provider's own id for this posting.                                  | Never null                                                       | raw        | `"ts_88213377"`                                                                    |
| `sources[].url`             | `string \| null`                                                                 | Posting URL as this provider saw it.                                     | The provider did not report a URL.                               | raw        | `"https://jobs.acme-robotics.example/revops-lead"`                                 |
| `sources[].first_seen_at`   | `date-time`                                                                      | When this provider first saw the job.                                    | Never null                                                       | raw        | `"2026-10-08T07:12:00Z"`                                                           |
| `sources[].last_seen_at`    | `date-time`                                                                      | When this provider last saw the job.                                     | Never null                                                       | raw        | `"2026-10-11T04:02:00Z"`                                                           |
| `sources[].fields`          | `string[]`                                                                       | Job fields this source contributed to the canonical record.              | `[]` = the source corroborated the job but contributed no field. | raw        | `["salary","seniority"]`                                                           |
| `license.display`           | `boolean`                                                                        | You may show this job to your end users.                                 | Never null                                                       | normalized | `true`                                                                             |
| `license.resale`            | `boolean`                                                                        | You may resell or redistribute this job as data.                         | Never null                                                       | normalized | `false`                                                                            |

Derivation tells you how much BetterJobs touched the value:

- **raw**: passed through from a source as posted.
- **normalized**: mapped to one format or enum (ISO codes, our enums, merged timestamps).
- **inferred**: estimated from other data, for example `seniority` from the title. Enum values are listed in [Taxonomies](https://docs.betterjobs.cc/data/taxonomies.md).

## Record sample

The first record of [`jobs.json`](/samples/jobs.json). Illustrative data: the company and domain are fictional.

jobs.json — record 1 (illustrative)

```json
{
  "id": "job_01JCSAMPLE01X4M2Q7RV3T9KD5W",
  "title": "Head of Revenue Operations",
  "company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" },
  "location": { "city": "Berlin", "region": "Berlin", "country_code": "DE", "remote": false },
  "employment_type": "full_time",
  "seniority": "lead",
  "job_family": "operations",
  "salary": { "min": 110000, "max": 135000, "currency": "EUR", "period": "year", "origin": "declared" },
  "description": "Own forecasting, CRM hygiene and the GTM tool stack across DACH.",
  "apply_url": "https://jobs.acme-robotics.example/revops-lead/apply",
  "posted_at": "2026-10-08T00:00:00Z",
  "first_seen_at": "2026-10-08T06:40:00Z",
  "last_seen_at": "2026-10-11T06:10:00Z",
  "last_verified_at": "2026-10-11T06:10:00Z",
  "status": "open",
  "closed_reason": null,
  "repost_count": 0,
  "p_real": 0.94,
  "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://jobs.acme-robotics.example/revops-lead",
      "first_seen_at": "2026-10-08T09:30:00Z",
      "last_seen_at": "2026-10-10T23:40:00Z",
      "fields": ["job_family"]
    }
  ],
  "license": { "display": true, "resale": false }
}
```

How to read it: the BetterJobs index found the posting first and supplied the core fields. TheirStack added pay and seniority. Techmap added the job family. Three listings, one job, one credit.

The full set of 10 sample jobs includes closed jobs, `null` salaries and unknown remote status. See [Sample data](https://docs.betterjobs.cc/data/samples.md).

## Coverage

Coverage is the share of jobs that have a value for a field. BetterJobs does not publish static coverage percentages during the v1 preview. Per-field figures across the whole index are published at GA.

What you get today is coverage measured on **your own results**. Every search response includes `metadata.field_coverage`, the fraction (0-1) of returned jobs with a non-null value, per field.

metadata.field\_coverage (illustrative)

```json
{
  "salary": 0.41,
  "seniority": 0.97,
  "location.remote": 0.88,
  "description": 0.99
}
```

How it is computed: for each field, the number of returned jobs where the field is not `null`, divided by the number of returned jobs. Merging raises coverage, because a field missing from one provider’s listing can come from another. `sources[].fields` shows which provider filled which field.

Try it against the keyless sandbox. It returns fixed illustrative data and charges nothing.

**curl**

```bash
curl -s https://api.betterjobs.cc/v1/sandbox/jobs/search \
  -H "Content-Type: application/json" \
  -d '{"filters":{"title_or":["Head of RevOps"],"country_code_or":["DE","AT","CH"],"posted_within_days":7},"limit":10}' \
  | jq '.metadata.field_coverage'
```

**Python**

```python
import requests


resp = requests.post(
    "https://api.betterjobs.cc/v1/sandbox/jobs/search",
    json={
        "filters": {
            "title_or": ["Head of RevOps"],
            "country_code_or": ["DE", "AT", "CH"],
            "posted_within_days": 7,
        },
        "limit": 10,
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["metadata"]["field_coverage"])
```

**TypeScript**

```ts
const resp = await fetch("https://api.betterjobs.cc/v1/sandbox/jobs/search", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    filters: {
      title_or: ["Head of RevOps"],
      country_code_or: ["DE", "AT", "CH"],
      posted_within_days: 7,
    },
    limit: 10,
  }),
});
if (!resp.ok) throw new Error(`BetterJobs ${resp.status}`);
const body = await resp.json();
console.log(body.metadata.field_coverage);
```

> Filters change coverage
>
> `salary_min_gte` drops every job whose `salary` is `null`. Your `field_coverage.salary` becomes 1.0, but you lose the jobs that do not state pay. Use it only when jobs with unknown pay are useless to you.

To see which providers fed a result, read `metadata.providers.hit` and `metadata.providers.contributions` (unique jobs each provider contributed first). Coverage also depends on your plan: `GET /v1/providers` shows which sources your plan enables. Provider-level claims are on each [provider page](https://docs.betterjobs.cc/providers.md).

## Freshness

Every job carries its own timestamps. Use them instead of trusting a global freshness claim.

| Field                                               | Answers                                                                               |
| --------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `posted_at`                                         | When did the employer post it? `null` when no source knows. Use `first_seen_at` then. |
| `first_seen_at`                                     | When did any source first see it? `posted_within_days` filters on this.               |
| `last_seen_at`                                      | When did any source last see it?                                                      |
| `last_verified_at`                                  | When was it last confirmed live at its origin? `null` = never verified.               |
| `sources[].first_seen_at`, `sources[].last_seen_at` | The same, per provider.                                                               |

Merged freshness metrics (for example, the share of jobs found on their posting day) are published at GA. Until then, measure it on your own data: compare `first_seen_at` with `posted_at` on jobs where both are set. Each provider’s self-published refresh rate is on its [provider page](https://docs.betterjobs.cc/providers.md).

Jobs move from `open` to `closed`, and `repost_count` goes up when the same job is re-listed. Read [Freshness and lifecycle](https://docs.betterjobs.cc/concepts/freshness-and-lifecycle.md) for the full lifecycle, and [Events](https://docs.betterjobs.cc/data/events.md) to be told when it changes.

## API

| Operation                                            | Endpoint                       | Cost                                                       |
| ---------------------------------------------------- | ------------------------------ | ---------------------------------------------------------- |
| [Search jobs](/api/operations/searchjobs/)           | `POST /v1/jobs/search`         | Cost: 1 credit / unique job                                |
| [Get a job](/api/operations/getjob/)                 | `GET /v1/jobs/{id}`            | Cost: 1 credit / jobFree if you already paid for this job. |
| [Sandbox search](/api/operations/sandboxsearchjobs/) | `POST /v1/sandbox/jobs/search` | Cost: Free                                                 |

Search returns up to 100 jobs per page. Page with `next_cursor` (see [Pagination](https://docs.betterjobs.cc/platform/pagination.md)). Filters are listed in [Filters](https://docs.betterjobs.cc/platform/filters.md). Re-reading a job you already paid for is free; `GET /v1/billing/ledger` proves it.

## Bulk

For more than one page, use an **async search**. `POST /v1/searches` collects up to 10,000 jobs and returns `202` with a search `id`. Poll `GET /v1/searches/{id}` or pass `webhook_url` to receive `search.completed`. Reading results is free; jobs are charged as the search collects them.

> Branch on status
>
> A `200` from `GET /v1/searches/{id}` can carry `queued`, `running`, `completed`, `partial`, `failed` or `on_hold`. `on_hold` means you ran out of credits; the search resumes after a top-up.

Details: [Async searches](https://docs.betterjobs.cc/platform/async-searches.md), [Create a search](/api/operations/createsearch/), [Get a search](/api/operations/getsearch/). For daily upserts and backfills, follow [Sync patterns](https://docs.betterjobs.cc/guides/sync-patterns.md).

**CSV.** The API returns JSON. CSV export is a plan feature, listed from the Growth plan (see [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md)). [`jobs.csv`](/samples/jobs.csv) shows a flattened layout: nested fields become columns such as `salary_min`, and source slugs are pipe-joined in `source_providers`.
