Developers

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

EndpointWhat it returns
GET /api/v1/jobsPostings, filtered and cursor-paginated (below)
GET /api/v1/jobs/:idOne posting
GET /api/v1/companiesRegistry companies with live active-posting counts
GET /api/v1/companies/:idOne company plus stats: active, new in 7d, closed in 7d
GET /api/v1/changesThe lifecycle event feed (first_seen, field_change, closed, reopened)
GET /api/v1/snapshotsThe snapshot dates available for as-of queries
GET /api/v1/snapshots/:dateThe board as it stood on that date
GET /api/v1/exports/jobs.csvBulk CSV export — live board or a snapshot date
GET /api/v1/openapi.jsonThe 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

ParameterMeaning
companyRegistry company name (exact, case-insensitive) or a company_id UUID
qTitle contains, case-insensitive
countryCountry code as stored, e.g. US
seniorityintern · entry · mid · senior · staff_principal · manager · director · vp_executive
workplace_typeremote · hybrid · onsite
is_ai_role, is_early_careertrue / false — the stored classification flags
salary_minAnnualized 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_beforeBounds on posted_at (ISO 8601)
first_seen_afterFirst seen by AllRoles at or after this time (ISO 8601)
activetrue (default) or false
include=descriptionAdds the full posting body. Without it you get description_excerpt (300 characters)
limit, cursorPage 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

StatusBody
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.

Manage keys & webhooks