The AllRoles API
Programmatic access to the same dataset the board, the indexes and the workspace run on: current postings, the company registry, the append-only lifecycle event feed, point-in-time snapshots and bulk exports. Base URL: https://allopenroles.com — every data endpoint lives under /api/v1.
Quickstart
1. Get a key. API keys are part of the Data Team plan. When data access is enabled on your account, create a key on the keys page — the full key is shown exactly once. No key yet? See data plans.
2. Call an endpoint.
curl "https://allopenroles.com/api/v1/jobs?is_ai_role=true&limit=5" \
-H "Authorization: Bearer ar_live_…"3. Page with cursors. Every list response carries meta.next_cursor; pass it back as ?cursor= until it comes back null. Cursors are opaque — do not parse or fabricate them.
Authentication
Send your key on every request, either as Authorization: Bearer ar_live_… or as the x-api-key header. Keys are stored only as SHA-256 hashes; a lost key cannot be recovered, only revoked and replaced. A revoked key stops working immediately. Requests without a valid key answer 401 {"error":"unauthorized"}.
Rate limits
Each key may make 120 requests per minute (the limit is stored on the key and shown on the keys page). Most endpoints cost 1 request; the bulk export costs 10. Over the limit you get 429 {"error":"rate_limited"} with a Retry-After header in seconds. Limits are enforced per running server instance (an in-memory token bucket), so treat 120/min as a ceiling to stay comfortably under, not a quota to spend exactly.
Endpoints
| Endpoint | What it returns |
|---|---|
GET /api/v1/jobs | Postings, filtered and cursor-paginated (below) |
GET /api/v1/jobs/:id | One posting |
GET /api/v1/companies | Registry companies with live active-posting counts |
GET /api/v1/companies/:id | One company plus stats: active, new in 7d, closed in 7d |
GET /api/v1/changes | The lifecycle event feed (first_seen, field_change, closed, reopened) |
GET /api/v1/snapshots | The snapshot dates available for as-of queries |
GET /api/v1/snapshots/:date | The board as it stood on that date |
GET /api/v1/exports/jobs.csv | Bulk CSV export — live board or a snapshot date |
| GET /api/v1/openapi.json | The OpenAPI 3.1 description of everything here |
All JSON responses share one envelope: {data, meta: {as_of, count, next_cursor}}, and Cache-Control: private, no-store. count is the number of items in this page.
GET /api/v1/jobs — parameters
| Parameter | Meaning |
|---|---|
company | Registry company name (exact, case-insensitive) or a company_id UUID |
q | Title contains, case-insensitive |
country | Country code as stored, e.g. US |
seniority | intern · entry · mid · senior · staff_principal · manager · director · vp_executive |
workplace_type | remote · hybrid · onsite |
is_ai_role, is_early_career | true / false — the stored classification flags |
salary_min | Annualized USD at least: the advertised range midpoint, scaled by its pay period (hour ×2080, month ×12, week ×52, day ×260; unstated period = annual). Non-USD postings never match |
posted_after, posted_before | Bounds on posted_at (ISO 8601) |
first_seen_after | First seen by AllRoles at or after this time (ISO 8601) |
active | true (default) or false |
include=description | Adds the full posting body. Without it you get description_excerpt (300 characters) |
limit, cursor | Page size (default 50, max 200) and pagination cursor |
{
"data": [{
"id": "0f8c2f…",
"title": "Senior Machine Learning Engineer",
"company": "Databricks",
"company_id": "…",
"company_domain": "databricks.com",
"location": "San Francisco, CA",
"country": "US",
"seniority": "senior",
"role_family": "Engineering",
"workplace_type": "hybrid",
"is_ai_role": true,
"is_early_career": false,
"salary_min": 180000, "salary_max": 240000,
"salary_currency": "USD", "salary_period": "year",
"description_excerpt": "…",
"url": "https://…",
"posted_at": "2026-10-15T…", "first_seen_at": "2026-10-15T…",
"last_seen_at": "2026-10-15T…",
"tags_version": 1, "active": true
}],
"meta": { "as_of": "2026-10-15T…", "count": 1, "next_cursor": "eyJ…" }
}Full description text is the posting as published by the employer; redistribution is governed by your data license — the structured fields are the licensed product, the prose is reference material. See Methodology.
GET /api/v1/changes — the lifecycle feed
Every posting's lifecycle is an append-only event log: first_seen when a posting first appears, field_change when a tracked field moves (salary, title, location, … — for descriptions only the character length is recorded, never the text), closed when it leaves the board (with a close_reason), and reopened if it returns. Filters: since (default 24h ago; the window may not start more than 31 days ago), type, company_id, field.
{
"data": [{
"id": 81234, "job_id": "0f8c2f…", "company": "Databricks",
"type": "field_change", "field": "salary_max",
"old_value": "240000", "new_value": "255000",
"close_reason": null, "url": "https://…",
"occurred_at": "2026-10-15T…"
}],
"meta": { "as_of": "…", "count": 1, "next_cursor": null }
}As-of queries — point-in-time history
The jobs endpoints always show the board now. The snapshots endpoints show the board as it stood: one snapshot per day, written after the crawl, never restated and never backfilled. History begins 2026-10-15 — dates before that do not exist and answer 404. Snapshot rows carry the fields the snapshot stores (title, company, location, country, advertised pay, active flag); a posting closed that day appears with active: false in the day's full record — GET /api/v1/snapshots reports both row counts per date.
curl "https://allopenroles.com/api/v1/snapshots/2026-10-15?limit=100" \
-H "Authorization: Bearer ar_live_…"Bulk export
GET /api/v1/exports/jobs.csv downloads the full board as CSV in the export schema (the sample dataset's fields, plus tags_version). Pass ?as_of=YYYY-MM-DD for that day's snapshot instead of the live board; fields the snapshot does not store are empty rather than reconstructed (the one exception: company_domain is resolved from the current company registry, as in the live export). The file opens with #-comment manifest lines — generated_at, as_of, schema_version, row_count and the sha256 of the CSV body — and the same values ride in the X-Export-Rows, X-Export-Sha256, X-Export-Generated-At and X-Export-As-Of headers. One export call costs 10 rate-limit tokens.
Webhooks
Webhooks push the lifecycle feed to your endpoint instead of you polling /changes. Create one on the keys page: an https URL, the event types you want, and optionally a company scope (empty = all companies). The signing secret is shown exactly once.
Delivery is batched, not real-time: batches go out with the twice-daily crawl/snapshot cadence, as one POST per webhook per cycle containing every matching event since the last successful delivery (up to 500). Delivery is at-least-once — a failed endpoint is retried next cycle with the same window, and events sharing a timestamp may repeat. Answer 2xx to acknowledge; anything else keeps the window open. The last delivery outcome is shown on the keys page.
POST https://your-endpoint.example/hooks
X-AllRoles-Signature: sha256=<hmac>
{ "webhook_id": "…", "generated_at": "2026-10-15T…", "events": [ /* /changes shape */ ] }Verify the signature over the raw request body with your secret:
import { createHmac, timingSafeEqual } from "node:crypto";
function signatureValid(secret, rawBody, header) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const a = Buffer.from(expected), b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}Errors
| Status | Body |
|---|---|
| 400 | {"error":"bad_request","message":"…"} |
| 401 | {"error":"unauthorized","message":"…"} |
| 404 | {"error":"not_found","message":"…"} |
| 429 | {"error":"rate_limited","message":"…"} + Retry-After header |
| 500 / 503 | {"error":"server_error" | "unavailable","message":"…"} |
Dataset changes and additions are announced in the changelog; field definitions live in the data dictionary.