Most job APIs hide salary behind a paywall or a per-seat license. One doesn't.
If you build on recruiting data — dashboards, comp-intel tools, agent pipelines, or just a personal "is this role hot" tracker — the Recruitee careers-site API is one of the cleanest no-auth endpoints I've found: a plain GET that returns every open offer with a full structured salary object, no key, no login.
The endpoint
Every Recruitee-hosted careers page exposes a JSON API at a predictable URL:
GET https://<COMPANY>.recruitee.com/api/offers
I tested it live on aaff.recruitee.com (a Dutch staffing company) — 348 open offers, and all 348 carried a structured salary object:
curl -s https://aaff.recruitee.com/api/offers | python3 -m json.tool
The schema (what you actually get)
Each offer is a flat object. The fields that matter for data work:
-
title,company_name,department,category_code,tags -
salary— the whole object, not a string:{ "min": "18.50", "max": "18.50", "period": "month", "currency": "EUR" } -
remote,hybrid,on_site— explicit work-mode booleans -
location,city,country,country_code,state_code,postal_code -
published_at,updated_at,close_at— real timestamps, not "posted" -
careers_apply_url/careers_url— direct apply + page URLs -
description— full HTML body (7k+ chars for a single role) -
mailbox_email,employment_type_code,experience_code,education_code
That's a complete, normalized job record from one request. No pagination to stitch, no JS rendering, no anti-bot wall — it's the raw data behind the careers page, served as JSON.
Why this is useful
-
Salary is structured, not scraped.
salary.min/salary.max/salary.currency/salary.periodare machine-readable. You don't regex"€1,200 – 1,500 / month"out of HTML. -
Work mode is explicit.
remote,hybrid,on_sitebooleans mean you can filter a "remote only" pool without fuzzy-matching location strings. -
Timestamps are real.
published_atandclose_atlet you do freshness/dedupe/"is this stale" logic for free. - It's per-company. Any company on Recruitee has this endpoint. I've confirmed the format is consistent across boards.
A minimal parser
import urllib.request, json
def recruitee_offers(company_slug):
url = f"https://{company_slug}.recruitee.com/api/offers"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
data = json.loads(urllib.request.urlopen(req, timeout=20).read().decode())
return data.get("offers", [])
remote = [o for o in recruitee_offers("aaff") if o.get("remote") and o.get("salary")]
print(len(remote), "remote roles with salary")
for o in remote[:5]:
s = o["salary"]
print(o["title"], f"{s['min']}-{s['max']} {s['currency']}/{s['period']}")
Honest caveats
- It's a public endpoint that is not documented by Recruitee as a stable API. It can change, rate-limit, or disappear — treat it as a discovery, not a contractual feed.
- It's per-company: it reflects whatever each company has listed on its Recruitee board. It's a great signal, not the whole market.
- I've verified it live on one board (348/348 salary). Coverage across other boards is the next thing I'd test at scale before building on it.
The bigger picture
Endpoints like this are exactly why I built a remote-jobs data pipeline that ingests many boards into one normalized schema — so you don't have to know the URL pattern of each ATS. But if you need a single clean source with structured salary right now, this one works today.
TL;DR — GET https://<slug>.recruitee.com/api/offers = no-auth, JSON, structured salary + remote booleans + real timestamps. Test it, then decide if it belongs in your data stack.
Top comments (0)