# Companies

> What does a company hiring profile contain, and how is is_hiring decided?

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

Cost: 1 credit / profile

## Overview

A company hiring profile answers one question: **is this company hiring right now, and in which direction?**

You look a company up by its domain. The profile rolls up every open canonical job BetterJobs knows for that company, across all sources:

- `open_jobs_count`: open canonical jobs right now.
- `is_hiring`: a three-state answer (`true`, `false`, `null`) with a `confidence` and a plain-English `basis`.
- `hiring_pulse`: the 30-day trend (`up`, `flat`, `down`) and the change in open jobs.
- `top_job_families`: where the hiring is.
- `sources[]`: how many open jobs each provider sees, and when it last saw the company.

The same `company` object (`id`, `name`, `domain`) is embedded in every job and event, so you can join profiles to jobs on `company.id`.

### How is\_hiring is decided

`is_hiring` is derived from the company’s canonical jobs and how many sources corroborate them. The decision is never a bare boolean. Every answer comes with:

- `value`: `true`, `false` or `null`.
- `confidence`: 0 to 1.
- `basis`: the reason in plain English, for example `"42 open jobs corroborated by 4 sources in the last 30 days"`.

The three values mean different things:

| `is_hiring.value` | Meaning                                          | Example `basis` (illustrative)                                |
| ----------------- | ------------------------------------------------ | ------------------------------------------------------------- |
| `true`            | Open jobs exist and sources corroborate them.    | `42 open jobs corroborated by 4 sources in the last 30 days`  |
| `false`           | Sources see the company and it has no open jobs. | `All 6 open jobs closed in the last 14 days across 3 sources` |
| `null`            | Unknown. Not enough signal to decide.            | `No careers page or ATS found for this domain`                |

> Never treat null as false
>
> `null` means BetterJobs could not find enough signal: no careers page, no ATS, no source coverage. The company may be hiring heavily through channels no source sees. Do not put `null` companies on a “not hiring” list. Read `basis` and `confidence` before you act on either value.

`company.hiring_stopped` fires only when `value` changes to `false`. A change to `null` never fires it. See [Events](https://docs.betterjobs.cc/data/events.md).

Read [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md) for how corroboration feeds confidence.

## Field dictionary

| Field                               | Type                                    | Description                                   | null / \[] means                        | Derivation | Example                                                                            |
| ----------------------------------- | --------------------------------------- | --------------------------------------------- | --------------------------------------- | ---------- | ---------------------------------------------------------------------------------- |
| `company`                           | `Company`                               | Company reference: `id`, `name`, `domain`.    | Never null                              | normalized | `{"id":"cmp_4Rk7TzP1aQ","name":"Acme Robotics","domain":"acme-robotics.example"}`  |
| `open_jobs_count`                   | `integer`                               | Open canonical jobs right now.                | Never null                              | normalized | `42`                                                                               |
| `is_hiring.value`                   | `boolean \| null`                       | Whether the company is hiring.                | Unknown. Never treat `null` as `false`. | inferred   | `true`                                                                             |
| `is_hiring.confidence`              | `number (0-1)`                          | Confidence in `is_hiring.value`.              | Never null                              | inferred   | `0.96`                                                                             |
| `is_hiring.basis`                   | `string`                                | Plain-English reason for the value.           | Never null                              | inferred   | `"42 open jobs corroborated by 4 sources in the last 30 days"`                     |
| `hiring_pulse.direction`            | `up \| flat \| down`                    | Direction of open jobs over the last 30 days. | Never null                              | inferred   | `"up"`                                                                             |
| `hiring_pulse.open_jobs_30d_change` | `integer`                               | Change in open jobs over the last 30 days.    | Never null                              | normalized | `9`                                                                                |
| `top_job_families`                  | `{job_family, open_jobs}[]`             | Job families with the most open jobs.         | `[]` = no open jobs found.              | inferred   | `[{"job_family":"engineering","open_jobs":18}]`                                    |
| `sources`                           | `{provider, open_jobs, last_seen_at}[]` | Open jobs per provider for this company.      | `[]` = no source sees this company.     | raw        | `[{"provider":"theirstack","open_jobs":31,"last_seen_at":"2026-10-11T04:02:00Z"}]` |

The fields of the embedded `company` object (`company.id`, `company.name`, `company.domain`) are described with the job fields in the [Field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md).

## Record sample

The three profiles in [`companies.json`](/samples/companies.json) cover the three states of `is_hiring`. Illustrative data: companies and domains are fictional.

**Hiring (true)**

companies.json — record 1 (illustrative)

```json
{
  "company": { "id": "cmp_4Rk7TzP1aQ", "name": "Acme Robotics", "domain": "acme-robotics.example" },
  "open_jobs_count": 42,
  "is_hiring": {
    "value": true,
    "confidence": 0.96,
    "basis": "42 open jobs corroborated by 4 sources in the last 30 days"
  },
  "hiring_pulse": { "direction": "up", "open_jobs_30d_change": 9 },
  "top_job_families": [
    { "job_family": "engineering", "open_jobs": 18 },
    { "job_family": "sales", "open_jobs": 11 },
    { "job_family": "operations", "open_jobs": 6 }
  ],
  "sources": [
    { "provider": "betterjobs", "open_jobs": 38, "last_seen_at": "2026-10-11T06:10:00Z" },
    { "provider": "theirstack", "open_jobs": 31, "last_seen_at": "2026-10-11T04:02:00Z" },
    { "provider": "techmap", "open_jobs": 27, "last_seen_at": "2026-10-10T23:40:00Z" },
    { "provider": "coresignal", "open_jobs": 25, "last_seen_at": "2026-10-10T18:15:00Z" }
  ]
}
```

**Not hiring (false)**

companies.json — record 2 (illustrative)

```json
{
  "company": { "id": "cmp_7Jd4FgH1sA", "name": "Globex Analytics", "domain": "globex.example" },
  "open_jobs_count": 0,
  "is_hiring": {
    "value": false,
    "confidence": 0.91,
    "basis": "All 6 open jobs closed in the last 14 days across 3 sources"
  },
  "hiring_pulse": { "direction": "down", "open_jobs_30d_change": -6 },
  "top_job_families": [],
  "sources": [
    { "provider": "betterjobs", "open_jobs": 0, "last_seen_at": "2026-10-11T05:00:00Z" },
    { "provider": "jobspipe", "open_jobs": 0, "last_seen_at": "2026-10-11T03:30:00Z" },
    { "provider": "theirstack", "open_jobs": 0, "last_seen_at": "2026-10-10T20:00:00Z" }
  ]
}
```

**Unknown (null)**

companies.json — record 3 (illustrative)

```json
{
  "company": { "id": "cmp_9Hs2VbN6eW", "name": "Northwind Traders", "domain": "northwind.example" },
  "open_jobs_count": 0,
  "is_hiring": {
    "value": null,
    "confidence": 0.0,
    "basis": "No careers page or ATS found for this domain"
  },
  "hiring_pulse": { "direction": "flat", "open_jobs_30d_change": 0 },
  "top_job_families": [],
  "sources": []
}
```

Compare records 2 and 3. Both have `open_jobs_count: 0`. Record 2 is `false` because three sources see the company and all its jobs closed. Record 3 is `null` because no source sees the company at all (`sources: []`).

## Coverage

BetterJobs does not publish a static count of covered companies during the v1 preview. It is published at GA.

Coverage for one company is visible in its profile:

- `sources[]` lists every provider that sees the company, with its own `open_jobs` and `last_seen_at`. `[]` means no source sees this company.
- `is_hiring.value: null` with a `basis` such as `No careers page or ATS found for this domain` marks a coverage gap, not a hiring answer.
- `open_jobs_count` counts canonical jobs. It is usually lower than the sum of `sources[].open_jobs`, because the same job seen by four providers counts once.

Your plan decides which providers can contribute. `GET /v1/providers` shows what your plan enables.

## Freshness

- `sources[].last_seen_at` shows when each provider last saw the company.
- `hiring_pulse` covers the last 30 days.
- The underlying jobs carry `first_seen_at`, `last_seen_at` and `last_verified_at`. Fetch them with `company_domain_or` (below) when you need job-level timing.

To be told when a company changes state instead of polling, create a company watch. It delivers `company.hiring_started`, `company.hiring_stopped` and job events. See [Detect hiring changes](https://docs.betterjobs.cc/guides/detect-hiring-changes.md).

## API

**curl**

```bash
curl -s 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",
    },
    timeout=30,
)
resp.raise_for_status()
profile = resp.json()


hiring = profile["is_hiring"]["value"]
if hiring is None:
    print("Unknown:", profile["is_hiring"]["basis"])  # not the same as False
else:
    print("Hiring" if hiring else "Not hiring", profile["is_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 profile = await resp.json();


const { value, confidence, basis } = profile.is_hiring;
if (value === null) console.log("Unknown:", basis); // not the same as false
else console.log(value ? "Hiring" : "Not hiring", confidence);
```

| Operation                                                        | Endpoint                     | Cost                                                                                    |
| ---------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------- |
| [Get a company hiring profile](/api/operations/getcompany/)      | `GET /v1/companies/{domain}` | Cost: 1 credit / profile                                                                |
| [Create a watch](/api/operations/createwatch/) (`type: company`) | `POST /v1/watches`           | Cost: FreeEach job.opened event a watch delivers costs 1 credit; other events are free. |

Pass the domain without scheme or path: `acme-robotics.example`, not `https://www.acme-robotics.example/`.

## Bulk

There is no batch profile endpoint. For many companies, pick the pattern that fits:

- **Their open jobs**: one search with `company_domain_or` set to a list of domains. You pay per unique job, not per company. See [Find companies hiring](https://docs.betterjobs.cc/guides/find-companies-hiring.md).
- **Ongoing monitoring**: one company watch per domain. Changes arrive as [events](https://docs.betterjobs.cc/data/events.md).
- **Many profiles**: call `GET /v1/companies/{domain}` per domain, within your [rate limit](https://docs.betterjobs.cc/platform/rate-limits.md). Each call costs 1 credit, so cache profiles instead of re-fetching them.
