# Companies

The employer directory, the single-company endpoint, and an honest account of why company slugs are not yet stable.

**What this covers:** GET /v1/companies; GET /v1/companies/{slug}; Company slugs are not yet guaranteed stable.

**Assumed knowledge:** [Authentication](./api-authentication.md).

**Canonical HTML:** https://jobopportunitiesapi.org/docs/endpoints/companies  
**Machine-readable index:** https://jobopportunitiesapi.org/docs/ai/index.md  
**Last verified:** 2026-08-22  
**Superseded by:** the live API at https://api.jobopportunitiesapi.org and its spec at https://jobopportunitiesapi.org/openapi.json — where this file and the API disagree, the API is right.

---

<a id="endpoint-companies"></a>

## 1. GET /v1/companies

Employers whose live roles this API returns. Every company here has at least one row you can actually fetch.

### 1.1 In depth

The guarantee that makes this endpoint worth having: `open_roles` and `GET /v1/jobs?company=<slug>` reconcile. A directory entry advertising roles the API will not serve is a broken promise, so `open_roles` counts only rows you can retrieve.

There is a second population behind `include_discovered=true`: companies we watched hiring on their own careers page under a source we do not redistribute. They arrive with `open_roles: 0`, a non-zero `own_site_roles` and a `careers_url` pointing at the page we saw them on. They are **evidence a company is hiring**, not inventory you can buy — useful for enrichment, misleading if you treat them as listings.

### 1.2 Exact contract

| parameter | type | required | default | allowed values | range | comma-separated | description |
| --- | --- | --- | --- | --- | --- | --- | --- |
| limit | integer | no | 25 | — | 1–200 | no | — |
| offset | integer | no | — | — | 0–100000 | no | — |
| cursor | string | no | — | — | — | no | The next_cursor from the previous page. Overrides offset. |
| country | string | no | — | — | — | no | — |
| org_type | string | no | — | — | — | no | — |
| source_type | string | no | — | — | — | no | — |
| has_website | boolean | no | — | — | — | no | Only companies whose domain we fetched and found their own name on. An absent website means we could not prove one, not that they have none. |
| include_discovered | boolean | no | — | — | — | no | Also return the companies we watched hiring on their own careers page under a source we do not redistribute. They arrive with `open_roles: 0` and a non-zero `own_site_roles`, and `careers_url` points at the page we saw them on. Useful for enrichment; not inventory. |
| q | string | no | — | — | — | no | — |

_9 parameters for `GET /v1/companies`, generated from `https://jobopportunitiesapi.org/openapi.json`. The spec is served by the running API and is the contract._

| code | meaning |
| --- | --- |
| 200 | A page of companies. |
| 401 | Missing, unknown, revoked or expired key. All four are reported identically. |
| 402 | The plan's monthly RECORD allowance is spent. Distinct from 429: a 429 means slow down and retry, a 402 means the licence is used up until the 1st of the month. Do not retry. Every metered response carries `X-RateLimit-Records-Remaining`, so this is visible long before it happens. |
| 422 | A parameter we will not guess at: an unknown source_type or provider, an unparseable timestamp, a malformed cursor. Note the deliberate exceptions, which are clamps rather than errors: `limit` is capped at the plan maximum (a non-numeric value falls back to the default), and comma-separated lists are truncated to the per-parameter maximum shown on each. Unknown query parameters are ignored. |
| 429 | Over your plan's per-minute or per-day limit. `Retry-After` says how long. |

Ordered by `GREATEST(open_roles, own_site_roles) DESC, name, slug`. The slug on the end is what makes the ordering total: without it, tie groups covering about a fifth of all companies could be reordered between two requests, and offset paging then silently repeated and skipped rows.

> **This is the one endpoint with offsets — and a hard cap** — `offset` is capped at 100,000, and an offset beyond it is a **422**, not a silently clamped 200. Use `cursor` for deep paging; it is lossless and has no limit. `cursor` overrides `offset` when both are sent.

### 1.3 Worked examples

The largest employers in the ledger

```console
$ curl -s 'https://api.jobopportunitiesapi.org/public/companies?limit=3' \
  | jq -r '.data[] | "\(.open_roles)\t\(.name)\t\(.website // "—")"'
{
  "data": [
    {
      "slug": "kroger",
      "name": "Kroger",
      "logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/kroger.png",
      "country": "US",
      "sectors": [
        "Retail",
        "Healthcare",
        "Operations & Admin"
      ],
      "open_roles": 47886,
      "own_site_roles": 0,
      "source_types": [
        "career_site"
      ],
      "first_seen": "2026-08-02"
    },
    {
      "slug": "albertsons",
      "name": "Albertsons",
      "website": "albertsonscompanies.com",
      "website_verified": "2026-07-31",
      "logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/albertsons.png",
      "country": "US",
      "sectors": [
        "Retail",
        "Operations & Admin",
        "Hospitality"
… 31 more lines
```

_Real response, fetched from `/public/companies?limit=3` when this file was built (4 September 2026, 06:10 UTC)._

**See also**

- [The company row](./api-fields.md#company-fields)
- [GET /v1/companies/{slug}](./endpoints-companies.md#endpoint-company)
- [Company — slug and domain](./api-parameters.md#params-company)

<a id="endpoint-company"></a>

## 2. GET /v1/companies/{slug}

One employer, by slug. Returns the same object the directory returns, for the one row.

### 2.1 In depth

The slug is the one on a job row's `company_slug` and in the website's `/company/<slug>` URLs. A slug that has been RETIRED now answers **301** with the current slug in `Location`, so use `curl -L` and follow it. Only a slug that never existed is a 404 — and on this endpoint specifically, a 404 is not necessarily permanent. See [the next section](#slug-instability).

### 2.2 Exact contract

| parameter | type | required | default | allowed values | range | comma-separated | description |
| --- | --- | --- | --- | --- | --- | --- | --- |
| slug | string | yes | — | — | — | no | — |

_1 parameters for `GET /v1/companies/{slug}`, generated from `https://jobopportunitiesapi.org/openapi.json`. The spec is served by the running API and is the contract._

| code | meaning |
| --- | --- |
| 200 | The company. |
| 401 | Missing, unknown, revoked or expired key. All four are reported identically. |
| 402 | The plan's monthly RECORD allowance is spent. Distinct from 429: a 429 means slow down and retry, a 402 means the licence is used up until the 1st of the month. Do not retry. Every metered response carries `X-RateLimit-Records-Remaining`, so this is visible long before it happens. |
| 404 | No such row. |
| 429 | Over your plan's per-minute or per-day limit. `Retry-After` says how long. |


| field | type | always present | allowed values | meaning |
| --- | --- | --- | --- | --- |
| careers_url | string | no | — | — |
| country | string | no | — | — |
| first_seen | string | no | — | — |
| industry | string | no | — | — |
| logo | string | no | — | — |
| name | string | no | — | — |
| open_roles | integer | no | — | Roles retrievable from THIS API. Never a number the API cannot honour. |
| org_type | string | no | — | — |
| own_site_roles | integer | no | — | Vacancies seen on the company own careers page. |
| sectors | array | no | — | — |
| slug | string | no | — | — |
| source_types | array | no | — | `aggregator` and `agency` are reserved and match no rows today; see the source_type enum. |
| website | string | no | — | — |
| website_verified | string | no | — | When we fetched this domain and found the company own name on it. |

### 2.3 Worked examples

One employer

```console
$ curl -s https://api.jobopportunitiesapi.org/public/companies/but | jq '.data'
{
  "data": {
    "slug": "but",
    "name": "BUT",
    "website": "but.com",
    "website_verified": "2026-07-31",
    "logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/but.png",
    "country": "FR",
    "sectors": [
      "Retail",
      "Operations & Admin",
      "Hospitality"
    ],
    "open_roles": 121,
    "own_site_roles": 0,
    "source_types": [
      "ats"
    ],
    "first_seen": "2026-07-27"
  }
… 1 more lines
```

_Real response, fetched from `/public/companies/but` when this file was built (4 September 2026, 06:10 UTC)._

**See also**

- [Company slugs are not yet guaranteed stable](./endpoints-companies.md#slug-instability)
- [The company row](./api-fields.md#company-fields)

<a id="slug-instability"></a>

## 3. Company slugs are not yet guaranteed stable

A company's slug can change between refreshes when duplicate rows compete for the same bare name. Key on company_domain where you have one.

### 3.1 In depth

A slug is derived from the company's display name, and display names improve over time. That is a genuine improvement — an employer whose ATS token was `jj` becomes `johnson-johnson` once the real name is resolved — and it moves the slug. Separately, when two company rows resolve to the same bare slug, one keeps it and the other gets a short hash suffix. Which one keeps it is not guaranteed to be the same across refreshes.

You can see both forms in the live data: uncontested names are bare (`but`), contested ones carry a suffix (`stripe-ce7566`, `docusign-f13edb`). The suffix is not a version number and it is not derived from anything you can compute.

> **The workaround, until this is fixed** — **Key on `company_domain`** where you have one — about 37% of companies carry a domain, and it is the identifier your own systems already hold. **Treat a 404 on a slug that used to work as “re-resolve this company”, not as “this company is gone”.** Re-resolve by domain, or by `?q=` on the directory.

### 3.2 Exact contract

1. Store `company_domain` as your key wherever the company has one, and the slug only as a display and link value.
2. A retired slug answers **301** to the current one; follow the redirect and you are done. On a genuine **404** — a slug that never existed — look the company up again: `GET /v1/jobs?company_domain=<domain>&limit=1` gives you the current `company_slug` from any live row, and `GET /v1/companies?q=<name>` finds it by name.
3. Do not cache a slug for longer than you cache the row it came from.
4. If you publish URLs containing a slug, be prepared to redirect. The website does the same thing for the same reason.

Re-resolve a company that 404s

```bash
A=https://api.jobopportunitiesapi.org
H="Authorization: Bearer $JOA_KEY"
domain=stripe.com
# Any live row for that domain carries the company's CURRENT slug.
curl -s -H "$H" "$A/v1/jobs?company_domain=$domain&limit=1" \
  | jq -r '.data[0].company_slug'
```

This is documented as a caveat rather than a guarantee because it is one. When the underlying fix lands, this section will be replaced with a statement of the guarantee and a date. Until then, believe the caveat.

**See also**

- [Identity: ids, slugs and domains](./ledger-data-model.md#ids-and-slugs)
- [Company — slug and domain](./api-parameters.md#params-company)
- [Hiring signal for a CRM](./recipes-build.md#recipe-crm-enrichment)

---

## Where to go next

This file is part of **Endpoints**. Others in the same group:

- [Listings](./endpoints-jobs.md) — The four endpoints that return job rows: the live list, one listing, the closure list, and the cheap id-only closure feed.
- [The delta feed](./endpoints-changes.md) — One ordered stream of everything that changed — created, updated, withdrawn and delisted — so you never have to re-read the ledger to find out what moved.
- [Bulk export](./endpoints-export.md) — The whole corpus as a stream of NDJSON, resumable to the exact row, with one failure mode you must handle: the error can arrive as the last line of a 200.
- [Key and metadata](./endpoints-meta.md) — Four endpoints that describe the API rather than return rows: your key, the filter vocabularies, the freshness report and the provider list.
- [Keyless endpoints](./endpoints-public.md) — Everything under /public/*, what each returns, and which of them have no keyed equivalent at all.

Always useful:

- [index.md](./index.md) — the map of every file here
- [BUILD-A-SITE.md](./BUILD-A-SITE.md) — the paste-whole brief for building against this API
- [quickstart.md](./quickstart.md) — zero to a first authenticated response
- [api-errors.md](./api-errors.md) — every status code and whether to retry it
