Job postings API · REST + MCP
One query, every ATS-hosted job, with what changed since yesterday.
Roleset crawls official public ATS feeds — Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Personio, Workable, Workday and Teamtailor — normalizes every posting into one schema, and records every change as an event. You get the index, not a scraper.
GET /v1/jobs/changes?since=…
meta.next_cursor — no gaps, no repeats.
sample
Start
First call, three ways
No SDK required. JSON in, JSON out, cursor pagination, RFC 7807 errors.
/stats and /schema are free and need no key.
# Every engineering role opened in the last 24 hours curl -H "Authorization: Bearer $ROLESET_KEY" \ "https://api.roleset.io/v1/jobs/changes?event=opened&department=Engineering" # Remote backend roles in Germany curl -H "Authorization: Bearer $ROLESET_KEY" \ "https://api.roleset.io/v1/jobs/search?q=backend&remote=true&country=DE" # Is this company growing, and where? curl -H "Authorization: Bearer $ROLESET_KEY" \ "https://api.roleset.io/v1/companies/acme.com/hiring-signal"
import httpx api = httpx.Client( base_url="https://api.roleset.io/v1", headers={"Authorization": f"Bearer {KEY}"}, ) # Walk the change feed. Keep the cursor; resume from it next run. cursor = None while True: page = api.get("/jobs/changes", params={ "since": "2026-09-05T00:00:00Z", "cursor": cursor, "limit": 100, }).json() for event in page["data"]: print(event["event"], event["job"]["title"], event["diff"]) cursor = page["meta"]["next_cursor"] if not cursor: break
// Five tools: search_jobs, job_changes, get_job, // company_jobs, hiring_signal { "mcpServers": { "roleset": { "type": "http", "url": "https://mcp.roleset.io", "headers": { "Authorization": "Bearer YOUR_KEY" } } } } # Claude Code claude mcp add --transport http roleset https://mcp.roleset.io \ --header "Authorization: Bearer $ROLESET_KEY"
Endpoints
Seven, and one of them is the point
/jobs/changes is what separates this from a list of feed URLs. Poll it
instead of re-running a search and diffing the results yourself.
| Endpoint | What it answers | Billed |
|---|---|---|
GET /jobs/changes |
Everything that opened, changed, closed or reopened since your cursor, with a diff of the fields that moved. | per event |
GET /jobs/search |
Keywords, title, company, city, country, remote, ATS, department, employment type, posting date. | per record |
GET /jobs/{id} |
One posting with its full description. | 1 record |
GET /companies/{id}/jobs |
Every open role at one company, by domain, board slug or name. | per record |
GET /companies/{id}/hiring-signal |
Open roles by department, with 7- and 30-day deltas. | 1 call |
GET /stats |
Index size and freshness. | free |
GET /schema |
JSON Schema of the job record. | free |
The record
One schema across nine platforms
Nine ATS platforms disagree about almost everything. Normalizing that disagreement is the work you are paying not to do.
Titles
Sr. Backend Engineer (m/w/d) #4412 and
Senior Backend Engineer — Berlin both normalize to
senior backend engineer. Gender markers, requisition ids and
location suffixes come off.
Locations
Free text becomes
{city, region, country, remote}. Hybrid (Vancouver,
British Columbia) resolves to a city, a province and CA.
Salary
Structured to {min, max, currency, interval} when the board
publishes one. CA$45/hour is CAD, not USD. When the string
says "competitive", the fields stay null rather than guessing.
Duplicates
One company running two boards yields one live record. The newest wins; the
other keeps a superseded_by pointer so nothing disappears.
Freshness
Every active board, inside 24 hours
And we tell you when. Every response carries meta.last_crawled_min —
the oldest successful crawl behind the exact rows you were handed, so you can
caveat an answer instead of guessing at it.
| Platform | Boards | Origin |
|---|---|---|
| Greenhouse | 6,720 | boards-api.greenhouse.io |
| SmartRecruiters | 5,811 | api.smartrecruiters.com |
| Personio | 4,724 | *.jobs.personio.com |
| Ashby | 3,890 | api.ashbyhq.com |
| Recruitee | 3,417 | *.recruitee.com |
| Lever | 2,610 | api.lever.co |
Pricing
Pay for what you pull
A record is one job posting, or one change event, returned to you.
/stats and /schema are free and need no key.
| Plan | Included each month | Beyond that | Price |
|---|---|---|---|
| Free | 500 records | Requests stop; nothing is charged | $0 · no card |
| Starter | 50,000 records | $2.00 per 1,000 |
$49 / month
|
| Growth | 500,000 records | $1.00 per 1,000 |
$199 / month
|
| Higher volume | Tell us what you need | — | talk to us |
Prices exclude sales tax and VAT. Paddle is the merchant of record and adds them at checkout where they apply, so a US or EU card sees a little more than the figure above.
No surprise bills
We email you at 80% and 100% of your included records. Overage is billed in arrears on your next invoice, so you see it before you pay it — and it is never charged unless you agreed to the rate.
A ceiling, not a cliff
Every account has a monthly limit that stops requests outright, so runaway automation cannot run up an unbounded bill. Going over your included records does not interrupt anything.
30-day money back
Not what you need? Email us within 30 days of a subscription payment and we refund it, no explanation required. See the refund policy.
No signup form
Take a free key below — no account, no email, no card. We store a hash of the key and nothing else about you. For a paid plan, email index@roleset.io and we set it up within a working day — your key keeps working while we do.
Take a free key
500 records a month. It is issued the moment you ask, and shown once — we keep only a hash of it, so we cannot resend it.
Copy this now. It is shown once.
Try it — this command already has your key in it:
Or take it somewhere that already runs your pipelines
The same index, as a no-code actor and as a tool your agent can call. Both read this API — the data, the freshness and the change feed are identical.
io.roleset/jobs.
How to connect →
What is not in here
The index has edges, and they are on purpose
- No personal data. No applicants, no recruiter names, no contact details. Email addresses and phone numbers are stripped from descriptions before anything is written down.
- No aggregators. Official public ATS endpoints only. No LinkedIn, no Indeed, no HTML scraping.
- No inferred fields. Seniority and skills are not guessed at. If a board does not publish a salary, the salary is null.
- A posting can close between crawls. Freshness is a 24-hour
guarantee, not a live socket.
last_crawled_mintells you where you stand.