# The Company object

> Every field on a company profile, its type, when it's null, and how the embedded version on a job differs from the full one.

Source: https://hyperjobs.io/docs/objects/company

The shape returned by
[`/v1/companies/{slug}`](/docs/api-reference/companies-retrieve) and
[`/v1/companies`](/docs/api-reference/companies-list), and embedded as
`company` on [every job](/docs/objects/job#company).

158,545 companies have a profile. Coverage within a profile is uneven — `name`
and `slug` are dependable, `funding` and `stock` exist for a small minority — so
treat every field below as optional.

## Example

```json title="A company with most fields populated"
{
  "slug": "linear",
  "name": "Linear",
  "domain": "linear.app",
  "website": "https://linear.app",
  "logo": "https://media.licdn.com/dms/image/…",
  "industry": "Software Development",
  "type": "Privately Held",
  "description": "Linear is a purpose-built tool for planning and building products…",
  "slogan": "The issue tracking tool you'll enjoy using",
  "specialties": ["issue tracking", "project management", "developer tools"],
  "employee_count": 120,
  "size_range": "51-200",
  "followers": 48210,
  "founded_year": 2019,
  "hq": {
    "country": "US",
    "region": "California",
    "locality": "San Francisco",
    "raw": "San Francisco, California, United States"
  },
  "funding": {
    "rounds": 4,
    "last_round_type": "Series B",
    "last_round_date": "2022-06-15",
    "last_round_amount_usd": 35000000,
    "total_investment_usd": 52000000,
    "crunchbase_slug": "linear-2",
    "crunchbase_categories": ["Software", "Productivity Tools"]
  },
  "stock": null,
  "affiliated_companies": [],
  "data_tier": "remco",
  "jobs_count": 12,
  "updated_at": "2026-07-02T09:14:33.000Z"
}
```

## The three shapes on a job

Before the field list, the distinction that causes the most confusion:
`job.company` is **not always** this object.

| Shape | When | How to detect |
| --- | --- | --- |
| **Full** `PublicCompany` | The posting resolved to a profile. 99.9997% of jobs. | `company.slug` is present |
| **Minimal fallback** | No profile matched. Six fields, built from what the posting itself declared. | `company` is non-null, `company.slug` is **absent** |
| `null` | The posting carries no employer name at all. | `company === null` |

The fallback carries only `name`, `domain`, `website`, `logo`, `industry`,
`description`. It's structurally a subset of the full object, which is exactly
what makes it easy to miss — the code path that reads `company.name` works fine
and the one that reads `company.employee_count` quietly gets `undefined`.

<Warning title="Detect the fallback with `slug`, not with field presence">
  `slug` is the only field guaranteed on a full profile and impossible on a
  fallback. Every other candidate — `industry`, `logo`, `description` — is
  nullable on the full object too, so its absence proves nothing.

  ```js
  const isEnriched = job.company?.slug != null;

  // Fine on both shapes.
  const name = job.company?.name ?? "Unknown";

  // Only meaningful when enriched — undefined on a fallback, and `undefined > 500`
  // is false, so a filter like this silently drops fallback companies.
  if (isEnriched && job.company.employee_count > 500) { /* … */ }
  ```

  A fallback has no `slug`, so there's nothing to fetch from
  [`/v1/companies/{slug}`](/docs/api-reference/companies-retrieve). Guard the
  lookup — building a URL with `undefined` in it earns a
  [404](/docs/errors#404-not-found).
</Warning>

The fallback is rare (roughly 2 jobs in 600,000 fail to resolve), but "rare"
isn't "never", and a crash on the unenriched path is the kind of bug that
surfaces in production weeks later.

## Identity

<Fields>
  <ResponseField name="slug" type="string">
    The company's identifier and primary key — `linear`, `stripe`. This is what
    [`/v1/companies/{slug}`](/docs/api-reference/companies-retrieve) takes, and
    what [`job.company.slug`](/docs/objects/job#company) gives you.

    Matched exactly. It's a LinkedIn-style slug, so it usually resembles the
    company name, but don't construct one from a name and expect it to
    resolve — read it from a job or from
    [`/v1/companies`](/docs/api-reference/companies-list).
  </ResponseField>
  <ResponseField name="name" type="string | null">
    Display name.
  </ResponseField>
  <ResponseField name="domain" type="string | null">
    Primary domain, e.g. `linear.app`. Bare hostname, no scheme.
  </ResponseField>
  <ResponseField name="website" type="string | null">
    Full website URL, including scheme. Related to `domain` but sourced
    separately — the two can disagree, and either can be null while the other
    isn't.
  </ResponseField>
  <ResponseField name="logo" type="string | null">
    Logo image URL. Hosted on the upstream provider's CDN, not by us, so it can
    rot independently of the profile. Fall back gracefully when it 404s.
  </ResponseField>
</Fields>

## Profile

<Fields>
  <ResponseField name="industry" type="string | null">
    Industry label, e.g. `Software Development`. A large open vocabulary rather
    than a fixed enum, and **not** the same vocabulary as a job's
    [`taxonomies`](/docs/reference/enums#taxonomies).
  </ResponseField>
  <ResponseField name="type" type="string | null">
    Organisation type as published — e.g. `Privately Held`. Free-form; treat it
    as display text.
  </ResponseField>
  <ResponseField name="description" type="string | null">
    Company description, plain text. Can run to several paragraphs.
  </ResponseField>
  <ResponseField name="slogan" type="string | null">
    Tagline, when the company publishes one.
  </ResponseField>
  <ResponseField name="specialties" type="string[]">
    Self-declared specialties. An array — empty rather than null when there are
    none.
  </ResponseField>
  <ResponseField name="affiliated_companies" type="string[]">
    Names of related companies — subsidiaries, parents, regional entities. An
    array, empty when there are none. These are **names, not slugs**, so they
    aren't directly fetchable.
  </ResponseField>
</Fields>

## Size and age

<Fields>
  <ResponseField name="employee_count" type="number | null">
    Reported headcount. A point-in-time figure from the last profile refresh, not
    a live number.
  </ResponseField>
  <ResponseField name="size_range" type="string | null">
    Headcount bracket as published, e.g. `51-200`. Can be present when
    `employee_count` is null, and the two are sourced independently — an
    `employee_count` outside its own `size_range` is unusual but possible.
  </ResponseField>
  <ResponseField name="followers" type="number | null">
    Social follower count. The sort key for
    [`/v1/companies`](/docs/api-reference/companies-list#sort-order).
  </ResponseField>
  <ResponseField name="founded_year" type="number | null">
    Year founded, as a number — `2019`, not `"2019"` and not a date.
  </ResponseField>
</Fields>

## HQ

<Fields>
  <ResponseField name="hq" type="object | null">
    Headquarters location. `null` when all four fields are null.

    <Expandable title="hq">
      <ResponseField name="country" type="string | null">
        **ISO-2 country code** — `"US"`, `"GB"`, `"DE"`. Not a full name. See
        [the warning below](#hq-country-is-iso-2-job-countries-are-not).
      </ResponseField>
      <ResponseField name="region" type="string | null">
        State, province, or region — `"California"`.
      </ResponseField>
      <ResponseField name="locality" type="string | null">
        City — `"San Francisco"`.
      </ResponseField>
      <ResponseField name="raw" type="string | null">
        The location string as published — `"San Francisco, California, United
        States"`.
      </ResponseField>
    </Expandable>
  </ResponseField>
</Fields>

### HQ country is ISO-2, job countries are not

<Warning title="Two country vocabularies, one dataset">
  The same country is spelled two different ways depending on which object you're
  holding:

  | Field | Value | Vocabulary |
  | --- | --- | --- |
  | `company.hq.country` | `"US"` | ISO-2 code |
  | [`job.location.countries`](/docs/objects/job#location) | `["United States"]` | Full names |

  This is a real inconsistency, not a documentation simplification. Two
  consequences:

  - **Joining requires a mapping.** `job.location.countries.includes(company.hq.country)`
    is always false. You need an ISO-2 → name table between them.
  - **The filters absorb it, the data doesn't.**
    [`?country=`](/docs/guides/filtering#locations) accepts full names or ISO-2
    on both `/v1/jobs` and `/v1/companies` — but the jobs filter searches the
    job's location and the companies filter searches HQ, and the *field values*
    keep their separate vocabularies.

  A company's HQ is also not where its jobs are — a US-headquartered company
  posts roles worldwide. Filter jobs by the job's location; use `hq` to describe
  the employer, not to locate the work.
</Warning>

## Funding and stock

<Fields>
  <ResponseField name="funding" type="object | null">
    Venture funding history. `null` when all fields are null — which is the
    common case, since most companies in the dataset have no venture funding to
    report.

    <Expandable title="funding">
      <ResponseField name="rounds" type="number | null">
        Number of funding rounds raised.
      </ResponseField>
      <ResponseField name="last_round_type" type="string | null">
        Most recent round — `"Series B"`, `"Seed"`.
      </ResponseField>
      <ResponseField name="last_round_date" type="string | null">
        Date of the most recent round.
      </ResponseField>
      <ResponseField name="last_round_amount_usd" type="number | null">
        Most recent round size in **USD**.
      </ResponseField>
      <ResponseField name="total_investment_usd" type="number | null">
        Total raised across all rounds, in **USD**.
      </ResponseField>
      <ResponseField name="crunchbase_slug" type="string | null">
        The company's Crunchbase identifier.
      </ResponseField>
      <ResponseField name="crunchbase_categories" type="string[] | null">
        Crunchbase category labels. A third category vocabulary, distinct from
        both `industry` and a job's `taxonomies`.
      </ResponseField>
    </Expandable>
  </ResponseField>
  <ResponseField name="stock" type="object | null">
    Public listing. `null` for private companies and for public ones whose
    listing we don't have — the absence of `stock` is not evidence a company is
    private.

    <Expandable title="stock">
      <ResponseField name="exchange" type="string | null">
        Exchange, e.g. `"NASDAQ"`.
      </ResponseField>
      <ResponseField name="ticker" type="string | null">
        Ticker symbol, e.g. `"CRM"`.
      </ResponseField>
    </Expandable>
  </ResponseField>
</Fields>

Both money fields are USD regardless of where the round was raised, which makes
them the only normalised currency in the API — [job
salaries](/docs/objects/job#salary) keep their original currency.

## Provenance

<Fields>
  <ResponseField name="data_tier" type="string | null">
    Where the profile's data came from, which is a rough proxy for how complete
    and how fresh it is. See [below](#data_tier-values).
  </ResponseField>
  <ResponseField name="jobs_count" type="number">
    The company's **live openings** — non-duplicate, non-expired postings in the
    pool right now. Present on [list](/docs/api-reference/companies-list) and
    [retrieve](/docs/api-reference/companies-retrieve) — **not** on the company
    embedded on a job. Recomputed roughly every 15 minutes; `0` means not
    currently hiring, not "no data". To filter on it, use `has_jobs=true` on
    [`/v1/companies`](/docs/api-reference/companies-list).
  </ResponseField>
  <ResponseField name="updated_at" type="string | null">
    When the profile was last refreshed, ISO 8601 UTC.

    Company profiles refresh on their own cadence, unrelated to [job
    ingestion](/docs/reference/coverage#refresh-cadence). A job posted an hour
    ago can carry a company profile that's months old — that's expected, not a
    bug.
  </ResponseField>
</Fields>

### `data_tier` values

The tiers you'll care about rank like this:

| Tier | Companies | Meaning |
| --- | --- | --- |
| `remco` | 12,392 | Richest profiles. Sorted first by [`/v1/companies`](/docs/api-reference/companies-list#sort-order). |
| `linkedin` | 67,357 | Standard enriched profile. |
| `free` | 75,880 | Thinnest profiles. The largest tier. |

<Note title="Ten values exist, not three">
  Those three cover about 98% of companies, but the field carries **ten** distinct
  values in live data. The rest describe how a company was matched to its web
  presence:

  | Tier | Companies |
  | --- | --- |
  | `free` | 75,880 |
  | `linkedin` | 67,357 |
  | `remco` | 12,392 |
  | `bidirectional` | 1,566 |
  | `sibling` | 467 |
  | `site-declared` | 349 |
  | `linkedin-fresh` | 260 |
  | `bidirectional-variant` | 122 |
  | `site-declared-mismatch` | 107 |
  | `site-declared-noweb` | 45 |
  | **Total** | **158,545** |

  Treat `data_tier` as an **open** vocabulary. Code that switches on three values
  and throws on a fourth will meet `sibling` eventually — fall through to a
  default instead. New tiers appear as matching strategies are added, and they
  arrive without a version bump.
</Note>

## Firmographic filtering

Server-side, from two directions:

1. **Search companies directly.**
   [`/v1/companies`](/docs/api-reference/companies-list) takes `q` (name
   substring), `industry`, `country` (HQ), `size`, `employee_count_gte`/`lt`,
   and `has_jobs=true`, with a real `total` and `offset` paging. "Find all
   fintech companies in Germany with 200+ employees" is now one query:
   `?industry=Financial Services&country=DE&employee_count_gte=200`.
2. **Filter jobs on company attributes.**
   [`/v1/jobs`](/docs/api-reference/jobs-list) has the company axis too —
   `company_industry`, `company_size`, `employee_count_gte`/`lt`,
   `funding_gte`/`lt`, `company_country`, `domain` — for when the question is
   really about the postings at companies like that.

For anything not covered by a filter (`founded_year`, `followers`, `stock`),
the embedded company on every job still makes client-side predicates free —
[company data](/docs/guides/companies) covers all three routes.

## Nullability at a glance

| Field | Null when |
| --- | --- |
| `slug` | Never on a full profile; **absent** on a job's fallback company |
| `jobs_count` | Never — `0` when there are no live openings; **absent** on a job's embedded company |
| `specialties`, `affiliated_companies` | Never — empty arrays instead of null |
| `hq`, `funding`, `stock` | All of the object's own fields are null |
| Everything else | Whenever the upstream profile didn't carry it |

Note the split: the two array fields go empty, the three nested objects go null.
`company.specialties.length === 0` is safe; `company.funding.rounds` is not.

<Note>
  This differs from [`job.location`](/docs/objects/job#location-is-never-null),
  which never collapses because one of its fields always resolves. On a company,
  the nested objects really do become `null` — always guard them.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Retrieve a company" icon="Building2" href="/docs/api-reference/companies-retrieve">
    Fetch one profile by `slug`.
  </Card>
  <Card title="List companies" icon="List" href="/docs/api-reference/companies-list">
    Search the directory — name, industry, HQ country, size, hiring status.
  </Card>
  <Card title="Company data" icon="Boxes" href="/docs/guides/companies">
    Firmographics — server-side filters and the embedded-company model.
  </Card>
  <Card title="The Job object" icon="Braces" href="/docs/objects/job">
    Where `company` shows up embedded, and in which shapes.
  </Card>
</CardGroup>
