Recruitee Jobs API
Query jobs from Recruitee boards through one API — and how Recruitee's offers endpoint, multi-city locations and string-typed salaries behave.
Recruitee is collaborative hiring software, popular with small and mid-sized
European companies. Boards sit at <company>.recruitee.com, and one
unauthenticated call returns every live offer with its description inline. The
data model is unusually generous — Recruitee publishes structured education,
seniority, category, working hours and salary where most platforms publish a
string — and the one thing it will do to you is rate-limit you.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=recruitee" \
--data-urlencode "time_frame=7d" \
--data-urlencode "limit=2"{
"data": [
{
"id": "recruitee:heliolabs_senior-frontend-developer",
"title": "Senior Frontend Developer",
"company": {
"slug": "helio-labs",
"name": "Helio Labs",
"domain": "heliolabs.nl",
"website": "https://heliolabs.nl",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Information Technology",
"employee_count": 70
},
"location": {
"remote": false,
"countries": ["Germany"],
"cities": ["Berlin"],
"regions": ["Berlin"],
"raw": ["Berlin, Berlin, Germany"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "Hybrid",
"seniority": "Mid-Senior level",
"education_requirements": ["bachelor degree"],
"job_language": "en",
"salary": {
"min": 5200,
"max": 6800,
"currency": "EUR",
"unit": "MONTH",
"annual_min": 62400,
"annual_max": 81600,
"summary": null
},
"skills": ["react", "typescript"],
"taxonomies": ["Software"],
"apply_url": "https://heliolabs.recruitee.com/o/senior-frontend-developer/c/new",
"url": "https://heliolabs.recruitee.com/o/senior-frontend-developer",
"source": "recruitee",
"posted_at": "2026-07-10T08:00:00.000Z",
"expires_at": null,
"updated_at": "2026-07-14T06:12:55.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}Note unit: "MONTH" with annual_min filled in beside it. Monthly pay is normal
on European boards, and min: 5200 is not a low salary. This is the trap
salary data is about.
How Recruitee boards actually work#
One GET, no auth, no pagination, whole board:
curl "https://<company>.recruitee.com/api/offers/"<company> is the subdomain. Response is { "offers": [...] }. The trailing
slash matters more than it should.
The description is only half the description#
description holds the body. requirements is a separate HTML block holding
the qualifications — present on roughly 90–100% of offers. They are two different
fields, and the posting page renders both. Read only description and you store a
job ad with its requirements section missing, from a field that looks perfectly
populated.
We concatenate them.
Structured fields, and they're actually filled#
Recruitee's coded fields are close to fully populated where they exist:
| Field | Vocabulary |
|---|---|
education_code | bachelor_degree, master_degree, high_school, … — near 100% |
category_code | healthcare, engineering, information_technology, … — near 100% |
experience_code | entry_level, mid_level, experienced, manager, … |
employment_type_code | fulltime, parttime, contract, … |
max_hours_per_week | ~98% on EU boards |
experience_code is Recruitee's own vocabulary, not LinkedIn's, so
experienced has to be mapped rather than passed through.
Three workplace booleans#
remote, hybrid and on_site all ship, all near-100% present. They are not
mutually exclusive: a hybrid job also reads remote: true on some boards. So
hybrid has to win first, then remote, then on_site — check remote first and
every hybrid role is mislabelled. Older boards omit the newer flags entirely and
only carry on_site, where on_site: false is the only remote signal available.
The language is in the key-set#
translations is an object keyed by language code. Exactly one key means the
posting is in that language — a genuinely structured language signal, on roughly
95% of offers, hiding in the shape of an object rather than in a field.
What bites you#
Salary bounds are strings
Recruitee returns {"min": "1400", "max": "2100"} — quoted. Arithmetic on them
concatenates instead of adding, and a strict schema rejects the whole offer.
Coerce before you use them.
Roughly 30% of offers are multi-city, and the flat fields carry only one.
city / state_code / country describe one of a posting's locations. The
full set is in locations[], each entry with a full region name rather than a
code. Worse, on multi-city offers the display location string is frequently a
useless Remote job. Read the flat fields and you lose most of a posting's
geography while everything still looks populated. Older boards omit locations[],
so you need the flat fields as a fallback and the array as the primary.
country_code is ISO-2 and will be read as a US state. Berlin, DE parsed
naively becomes Berlin, Delaware. Resolve the code to a canonical country name
before you build the string.
Slugs are only unique within a company. Two companies both have an offer
slugged sales-manager. A key of recruitee:<slug> collides across boards — ours
is scoped to recruitee:<company>_<slug>.
close_at exists but is nearly always null — around 2% of offers. A null
expiry doesn't mean the job runs forever; it means Recruitee didn't say. Expiry
here comes from board diffing, not from the field.
The requirements bullets are mostly not skills. They read like
Je bent van onbesproken gedrag (politiescreening) — whole sentences, not tags.
Harvesting every <li> as a skill fills your skill index with prose. We keep only
bullets short enough to plausibly be a term.
Or use ours#
Recruitee rate-limits per IP, and we measured 38% of requests lost to 429s at concurrency 5. That's why it sits in the proxy-only tier here alongside Workable — it works direct at low volume and falls over at scale, which is the failure mode you don't discover until you've built the thing.
Past the fetch: description and requirements joined, locations[] read so
multi-city offers keep their cities, ISO codes resolved to real country names, the
hybrid-first boolean order, salary bounds coerced from strings and normalised so a
monthly EUR band is comparable to an annual USD one, company-scoped ids, and the
company join. We track 1,344 Recruitee boards.
Recruitee is attempted every third cycle — see coverage — so a new posting can take a few hours to land.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key