# Work at AI Lab > Search open roles at AI labs of every kind and read full job details through a public read-only JSON API. ## About Work at AI Lab is a job board listing open roles at AI labs of every kind. The public API below exposes the same search, filters, and ranking as the website. All responses are JSON. No authentication is needed. ## API ### GET /api/v1/jobs — search open jobs Search params (q, page, dept, fn, loc, mode, lab, kind, pay, minPay, posted) — all optional. Validation is strict: an invalid value, an unknown parameter name, or a repeated single-value parameter returns 400 (see Errors); nothing is silently ignored or widened. Slug-like values (dept, fn, lab, kind, mode, pay) are case-insensitive; surrounding whitespace is trimmed. - q: string — free-text query, e.g. "research engineer". Optional. - page: positive integer, default 1. 0, negatives, decimals and non-numbers return 400. A page past the end returns 200 with an empty jobs array. - dept: string, once — department slug or value from /api/v1/filters (departments[].slug or .value). - fn: string, repeatable — job-function slug from /api/v1/filters, e.g. fn=research-engineering&fn=ml-infrastructure. - loc: string, once — case-insensitive substring of the location text, e.g. "San Francisco". It is not a work mode (use mode) and does not expand abbreviations. - mode: repeatable — one of: remote, hybrid, onsite. Repeat the parameter; comma lists are rejected. - lab: string, repeatable — lab slug (the value from /api/v1/filters labs, not the label), e.g. lab=anthropic. - kind: repeatable — lab kind, e.g. kind=frontier (see /api/v1/filters for valid values). - pay: "1" or "true" — only jobs that list compensation. "0" or "false" — no filter. - minPay: whole US dollars as plain digits, 0–999999999, e.g. minPay=200000 — only jobs paying at least this much (USD annual-equivalent). No $, commas, decimals, exponents or "k" suffix (150k is rejected; send 150000). minPay=0 counts as set. - posted: once — recency window in days, one of 1, 2, 3, 7, 14 or 30, e.g. posted=7 — only jobs with COALESCE(posted_at, first_seen_at) inside the rolling window from now. Any other value, an empty value, or a repeated posted returns 400. Repeatable parameters are repeated, never comma-joined: mode=remote&mode=hybrid. Unknown lab, dept and fn values are errors too, not empty results: resolve them from /api/v1/filters first. The same contract as JSON Schema (all properties optional, no additional properties; repeatable params are given as repeated query keys, every value is a string in the URL): ```json { "type": "object", "properties": { "q": { "description": "Free-text query, e.g. \"research engineer\".", "type": "string", "maxLength": 500 }, "page": { "description": "1-based page number as a positive integer string. Default 1.", "examples": [ "2" ], "type": "string", "pattern": "^[1-9]\\d{0,6}$" }, "dept": { "description": "Department: the `slug` or `value` of an entry in /api/v1/filters departments. Single value.", "examples": [ "research" ], "type": "string", "minLength": 1 }, "fn": { "description": "Job-function slug from /api/v1/filters (departments[].functions[].value). Repeat the parameter for several.", "examples": [ [ "research-engineering" ] ], "type": "array", "items": { "type": "string", "minLength": 1 } }, "loc": { "description": "Case-insensitive substring of the job location text, e.g. \"San Francisco\". Not a work mode — use mode for remote/hybrid/onsite.", "examples": [ "San Francisco" ], "type": "string", "minLength": 1 }, "mode": { "description": "Work mode. Repeat the parameter for several: mode=remote&mode=hybrid.", "type": "array", "items": { "type": "string", "enum": [ "remote", "hybrid", "onsite" ] } }, "lab": { "description": "Lab slug (the `value` in /api/v1/filters labs, not the label). Repeat the parameter for several.", "examples": [ [ "anthropic" ] ], "type": "array", "items": { "type": "string", "minLength": 1 } }, "kind": { "description": "Lab kind from /api/v1/filters kinds. Repeat the parameter for several.", "type": "array", "items": { "type": "string", "enum": [ "frontier", "big-tech", "research", "infrastructure", "applied", "robotics", "defense", "bio-science", "quant-finance" ] } }, "pay": { "description": "\"1\" or \"true\": only jobs that list compensation. \"0\" or \"false\": no filter.", "type": "string", "enum": [ "0", "1", "true", "false" ] }, "minPay": { "description": "Minimum annual pay in whole USD as plain digits (no $, commas, decimals or k suffix). 0 counts as set.", "examples": [ "200000" ], "type": "string", "pattern": "^\\d{1,9}$" }, "posted": { "description": "Recency window in days (rolling, from now): only jobs with COALESCE(posted_at, first_seen_at) inside the window.", "examples": [ "posted=7" ], "type": "string", "enum": [ "1", "2", "3", "7", "14", "30" ] } }, "additionalProperties": false } ``` ### Errors Invalid requests return 400 with every problem at once: ```json { "error": "invalid_parameters", "message": "...", "issues": [ { "param": "minPay", "received": "150k", "problem": "...", "expected": "...", "example": "minPay=150000" }, { "param": "lab", "received": "open-ai", "problem": "Unknown lab \"open-ai\".", "didYouMean": ["openai"] } ], "docs": "https://workatailab.com/llms.txt" } ``` Read issues, fix each param (expected, example, validValues and didYouMean say how), and retry. A 200 response always reflects exactly the filters you sent. Page size is fixed at 30; there is no page-size param. Search response fields: total, page, hasNext, next, jobs. next is the absolute URL of the next page, or null. Each job has: id, title, lab { slug, name }, locations, workMode, department, function, salary, postedAt, excerpt, url, detail. - id: the job UUID. - title: the job title. - lab: { slug, name } or null. - locations: string array. - workMode: lowercase work mode, a valid mode value, or null. - department: department title or null. - function: function title or null. - salary: the same display string the site shows, or null. - postedAt: ISO date string. - excerpt: short plain-text summary or null. - url: absolute canonical job page on https://workatailab.com, or null. - detail: absolute URL of that job's detail endpoint. ### GET /api/v1/jobs/{id} — one job in full Replace {id} with the job UUID. Response fields: id, title, lab { slug, name, about }, locations, workMode, department, function, salary, postedAt, excerpt, url, detail, description, highlights, idealCandidate, closed. - description: full description as plain text (no HTML), or null. - highlights: string array. - idealCandidate: string or null. - lab.about: lab profile ({ products, strengths, facts }) or null. - closed: boolean. Closed jobs return 200 with closed true. - Unknown or malformed ids return 404 JSON { error }. ### GET /api/v1/filters — valid filter values Response fields: labs, departments, modes, kinds. - labs: [{ value, label }] — pass value as lab. - departments: [{ value, slug, label, functions: [{ value, label }] }] — pass department slug (or value) as dept and function value as fn. - modes: [{ value, label }] — pass value as mode. - kinds: [{ value, label }] — pass value as kind. ## Examples - https://workatailab.com/api/v1/filters - https://workatailab.com/api/v1/jobs?q=machine+learning+engineer - https://workatailab.com/api/v1/jobs?q=research+engineer&mode=remote - https://workatailab.com/api/v1/jobs?q=engineer&minPay=200000 ## Guidance for agents - Infer the person's profession first from their CV or profile (job family such as software engineering, research, product management, design, data, GTM, operations — plus IC vs manager and seniority). A software, product, full-stack, or front-end engineer is a software-engineering IC, not a manager or a product manager. - Match profession before skills: only shortlist jobs in the same profession family and on the same IC/manager track. Shared keywords are not a match — "product" in "product engineer" never qualifies a Product Manager role, and "AI" or "engineering" never qualifies a Deployment Strategist, Forward-Deployed, Solutions, or Manager / Engineering-Manager role for an IC engineer. If fewer jobs match the profession, return fewer — never fill the list with other professions. Drop family mismatches silently; never present them as adjacent, stretch, or alternative picks. - Turn the person's profession into several queries: try role titles, seniorities, and skill keywords separately rather than one long query. - Call /api/v1/filters first to resolve filter values; never guess lab, dept, fn, mode, or kind values. Pass each entry's value (slug), not its label. - On a 400, do not drop the failing filter and carry on: fix it from issues and retry, so the person's constraints (such as a pay floor) are never silently lost. - Shortlist the top ~5–10 profession-matching jobs per request and open each one's detail endpoint (GET /api/v1/jobs/{id}) — never recommend or rank a job from search excerpt alone; search results already carry salary, location, and excerpt. Read description plus highlights, idealCandidate, and lab.about, then rank by compatibility with the person's day-to-day work (what they will build, own, and use — not keyword overlap), giving a 1–2 sentence fit reason per pick grounded in the description and silently dropping (or briefly noting) shortlist items the description disqualifies. Do not open every result — respect the fair-use rule below. - Always give the person the job's url (the canonical site page) for reading and applying — never the lab's own application URL. ## Fair use - Responses are cached; do not bulk-crawl the catalogue. - There is no page-size param and no bulk export; page through results with next.