DEV Community

earnnova-dev
earnnova-dev

Posted on

The Recruitee Careers API: A No-Auth Endpoint with 100% Structured Salary Data

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Salary is structured, not scraped. salary.min / salary.max / salary.currency / salary.period are machine-readable. You don't regex "€1,200 – 1,500 / month" out of HTML.
  2. Work mode is explicit. remote, hybrid, on_site booleans mean you can filter a "remote only" pool without fuzzy-matching location strings.
  3. Timestamps are real. published_at and close_at let you do freshness/dedupe/"is this stale" logic for free.
  4. 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']}")
Enter fullscreen mode Exit fullscreen mode

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)