- Documentation
- Guides
Company data
The embedded-company model — full records, minimal fallbacks, data tiers, and how to filter firmographics server-side and client-side.
Every job carries its company inline. Not an id to resolve, not a _links
stub — the whole firmographic record, in the same response, on every row.
{
"id": "greenhouse:4820193",
"title": "Senior Data Engineer",
"company": {
"slug": "figma",
"name": "Figma",
"domain": "figma.com",
"industry": "Software Development",
"employee_count": 1400,
"size_range": "1001-5000",
"hq": { "country": "US", "region": "California", "locality": "San Francisco" },
"funding": { "last_round_type": "Series E", "total_investment_usd": 748000000 }
}
}That's the point of the product. A job feed that gives you a company name means you still have to go and find out who that company is; one request here answers "remote Rust roles at Series-B-or-later companies with under 500 people" without a second call, because the answer was already in the payload.
There are ~158,545 companies with firmographic records behind those embeds.
Three shapes of company#
job.company is not one type. It's one of three, and code that assumes the
first will throw on the second.
Full PublicCompanyThe job resolved to a company record. You get every field below. This is 99.9997% of jobs.
Minimal fallbackNo company record matched, so the object is built from the job's own
organization_* columns. Only { name, domain, website, logo, industry, description } — every firmographic field is absent.
nullOnly when the posting has no organization name at all.
Detect the shape with `slug`
slug is present on the full record and absent on the fallback. It's the only
reliable discriminator — name and domain exist on both.
const isFull = (c) => Boolean(c?.slug);
// Safe against all three shapes.
const size = isFull(job.company) ? job.company.employee_count : null;Reaching for job.company.funding.last_round_type without this check works
fine across thousands of rows and then throws on a fallback. Optional chaining
hides the crash but not the bug: you'll silently treat "we don't have this
company" as "this company has no funding".
The PublicCompany record#
slugThe LinkedIn slug, and the primary key. This is what
/v1/companies/{slug} takes.
nameDisplay name.
domainPrimary domain, e.g. figma.com.
websiteFull URL.
logoLogo URL.
industryFree-text industry label. Not an enum — don't filter on exact equality.
typeCompany type, e.g. privately held, public.
descriptionLong-form company description.
sloganTagline.
specialtiesSelf-declared focus areas.
employee_countHeadcount as a number. The field to filter on.
size_rangeBucketed headcount, e.g. 1001-5000. Useful when employee_count is null.
followersLinkedIn followers. Also the secondary sort on
/v1/companies.
founded_yearYear founded.
hqHeadquarters location.
fundingFunding history.
stockPublic listing, when there is one.
affiliated_companiesRelated entities — subsidiaries, regional arms.
data_tierProvenance of the record. See below — there are more values than documented.
jobs_countLive openings — non-duplicate, non-expired postings in the pool right now. Present on both company endpoints (not on the embed); recomputed roughly every 15 minutes.
updated_atWhen the firmographic record was last refreshed.
hq, funding, and stock collapse to null when all of their own fields are
null — you never get an object full of nulls. Check the parent before reading a
child: company.funding?.total_investment_usd. Same behaviour as
salary on a job.
Country codes are inconsistent#
`hq.country` is ISO-2. `job.location.countries` is full names.
Two country fields, in the same response, in different formats:
{
"location": { "countries": ["United States"] },
"company": { "hq": { "country": "US" } }
}This is a genuine inconsistency, not a subtlety. The consequences:
- Comparing them directly never matches.
job.location.countries.includes(job.company.hq.country)is always false. - The
countryfilter on/v1/jobsaccepts either form —country=USandcountry=United Statesboth work — but it filters the job's location, never the company's HQ. - HQ is filterable too, separately:
country=DEon/v1/companies, orcompany_country=DEon a jobs query.
Keep a mapping if you need to relate the two field values, and normalise at your boundary rather than at each comparison.
data_tier has 10 values, not 3#
data_tier records where a firmographic record came from, which is a proxy for
how complete it is. It's documented as three values in a quality order:
remco > linkedin > free.
Ten values are live. Filtering to the documented three misses ~2,900 records.
| Tier | Records | Documented? |
|---|---|---|
free | 75,880 | Yes |
linkedin | 67,357 | Yes |
remco | 12,392 | Yes |
bidirectional | 1,566 | No |
sibling | 467 | No |
site-declared | 349 | No |
linkedin-fresh | 260 | No |
bidirectional-variant | 122 | No |
site-declared-mismatch | 107 | No |
site-declared-noweb | 45 | No |
An allowlist of the three documented values drops roughly 2,900 real records
with no error and no signal that anything was dropped — including
linkedin-fresh, which is a better record than plain linkedin, not a worse
one.
Treat data_tier as an open set. If you're filtering on quality, exclude
the tiers you don't want rather than allowlisting the ones you do, and check
the field you actually care about instead:
// Fragile — silently drops 7 live tiers.
const good = c.data_tier === "remco" || c.data_tier === "linkedin";
// Better — asks the real question.
const usable = c.employee_count != null && c.industry != null;Firmographic filtering, three ways#
The firmographics are filterable server-side, from two directions — with the client-side embed pattern as the fallback for anything the filters don't cover.
1. Search companies directly#
/v1/companies is a real search now: q
(name substring), industry, country (HQ — full name or ISO-2), size,
employee_count_gte/lt, and has_jobs=true, with a total and offset
paging. When the question is company-first — "which fintech companies in
Germany are hiring?" — ask it directly:
curl -H "Authorization: Bearer $HYPERJOBS_KEY" \
"https://api.hyperjobs.io/v1/companies?industry=Financial%20Services&country=DE&has_jobs=true"Every result carries jobs_count, so "is hiring" and "how hard" arrive in the
same payload.
2. Filter jobs on company attributes#
/v1/jobs grew a company axis:
company_industry, company_size, employee_count_gte/lt,
funding_gte/lt, company_country, and domain. When the question is
job-first — "remote Rust roles at 50–500-person companies" — the firmographic
predicate goes into the jobs query:
const qs = new URLSearchParams({
skill: "rust",
work_arrangement: "Remote", // exact and case-sensitive
employee_count_gte: "50",
employee_count_lt: "501", // exclusive bound
limit: "200",
});
const { data, total } = await fetch(`https://api.hyperjobs.io/v1/jobs?${qs}`, {
headers: { Authorization: `Bearer ${process.env.HYPERJOBS_KEY}` },
}).then((r) => r.json());One request, and total counts exactly the set you asked for — the API
applied the size predicate, so there's no page-the-world step to count matches.
(An early draft of this API had no company axis here, which forced a
client-side filter over embeds that total couldn't reflect; that whole
workaround never shipped.)
3. Filter client-side over the embeds#
Still the route for fields with no server-side filter — founded_year,
followers, stock, funding stage (only amounts are filterable). The
firmographics arrived with every job, so local predicates cost nothing extra:
const matches = data.filter((job) => {
const c = job.company;
// No slug means the minimal fallback — founded_year doesn't exist on it, so
// this company can't answer the question either way.
if (!c?.slug) return false;
return c.founded_year != null && c.founded_year >= 2020;
});When you mix modes, remember what total means: it counts what the API
filtered, not what your local predicate kept. To count a client-side-filtered
set you still have to page the whole thing — see walking a full result
set.
When to call the company endpoints#
/v1/companieswhenever the question is about companies rather than postings — name lookup (?q=), firmographic search, or building a directory. (An early draft made this a browse-only, top-200 endpoint; it isn't one.)/v1/companies/{slug}when you have a slug from somewhere else and want the record without a job attached. It matches onlinkedin_slugand 404s on an unknown one — the only endpoint here that 404s on a bad value rather than returning empty.
When you just fetched a job, you rarely need either — the embed already carries
the whole firmographic record. (jobs_count is the one field only the company
endpoints add.)
Next#
The full field reference.
List companiesThe company search — every filter, and the response envelope.
Filtering jobsEverything you can push server-side.
The Job objectWhere company sits on a posting.