Job Opportunities API

Check the data. Then trust it.

endpoints

Companies

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

Last verified 2026-08-22 · Assumes: Authentication. · Markdown copy

1GET /v1/companies

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

In depthWhy it exists, what it is not, what people get wrong

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.

Exact contractTypes, defaults, ranges, errors, edge cases
limitintegerdefault 251–200
offsetinteger0–100000
cursorstring

The next_cursor from the previous page. Overrides offset.

countrystring
org_typestring
source_typestring
has_websiteboolean

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_discoveredboolean

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.

qstring

9 parameters, generated from /openapi.json when this page was built. The spec is served from the running API and is the contract; if this table and the spec ever disagree, the spec is right and this is a bug — please say so with the thumbs-down below.

CodeWhat it means
200A page of companies.
401Missing, unknown, revoked or expired key. All four are reported identically.
402The 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.
422A 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.
429Over 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.

The largest employers in the ledger
$ 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": 39368,
      "own_site_roles": 0,
      "source_types": [
        "career_site"
      ],
      "first_seen": "2026-08-02"
    },
    {
      "slug": "albertsons-companies",
      "name": "Albertsons Companies",
      "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 page was built (11 September 2026, 03:39 UTC). Run the command yourself and you will get today's rows, not these.

2GET /v1/companies/{slug}

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

In depthWhy it exists, what it is not, what people get wrong

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.

Exact contractTypes, defaults, ranges, errors, edge cases
slugstringrequired

1 parameter, generated from /openapi.json when this page was built. The spec is served from the running API and is the contract; if this table and the spec ever disagree, the spec is right and this is a bug — please say so with the thumbs-down below.

CodeWhat it means
200The company.
401Missing, unknown, revoked or expired key. All four are reported identically.
402The 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.
404No such row.
429Over your plan's per-minute or per-day limit. Retry-After says how long.
FieldTypeAlways presentMeaning
careers_urlstringno
countrystringno
first_seenstringno
industrystringno
logostringno
namestringno
open_rolesintegernoRoles retrievable from THIS API. Never a number the API cannot honour.
org_typestringno
own_site_rolesintegernoVacancies seen on the company own careers page.
sectorsarrayno
slugstringno
source_typesarraynoaggregator and agency are reserved and match no rows today; see the source_type enum.
websitestringno
website_verifiedstringnoWhen we fetched this domain and found the company own name on it.
One employer
$ 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": 136,
    "own_site_roles": 0,
    "source_types": [
      "ats"
    ],
    "first_seen": "2026-07-27"
  }
… 1 more lines
Real response, fetched from /public/companies/but when this page was built (11 September 2026, 03:39 UTC). Run the command yourself and you will get today's rows, not these.

3Company 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.

In depthWhy it exists, what it is not, what people get wrong

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.

Exact contractTypes, defaults, ranges, errors, edge cases
  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
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.

This page was rendered 11 September 2026, 03:39 UTC. Every figure on it comes from the endpoint named beside it, and every published request is re-sent against the live API before this site is allowed to build. If something here is wrong, the thumbs-down above reaches a person.