Search the public job catalog with cursor-based pagination. Returns a page of job summaries and a next_cursor for the following page. Add q for full-text search (Google-style: quoted "phrases", OR, -exclusion; a pure-exclusion query is rejected). Implicit AND binds tighter than OR and a -term attaches only to its adjacent group, so "machine learning" OR "data scientist" -senior means A OR (B AND NOT senior) and can still return Senior ML titles; to exclude everywhere, distribute the term: "machine learning" -senior OR "data scientist" -senior. By default q matches the job title only (precise — best for common role terms like developer/engineer, which otherwise match description boilerplate); pass q_scope=full to also search the description (broader reach — best for niche skills or tools named only in the requirements text). A phrase query under q_scope=full combined with very narrow filters (a small country, a rare language) can hit the search timeout; the default title scope is immune, so prefer it for phrase precision. q composes as a filter with the standard sorts: sort=posted_at (default) and sort=first_seen_at are descending keyset sorts with unbounded cursor pagination, applying q as a filter over the newest-first keyset. Symbol-bearing tech tokens (c++, c#, .net) are matched as a title substring rather than full-text (they collapse under English tokenization); a mixed query such as python c++ still runs as full-text. If a full-text search matches nothing, one title-substring fallback runs automatically and the response is flagged fuzzy: true; GET /jobs/count runs the same fallback on the same trigger and flags it the same way, so a count and a search normally reach the same verdict on whether a query has results — subject to the posted-date qualifier documented on Count jobs. q respects the default 90-day freshness window unless a date filter is supplied. A well-formed but overly broad q that exceeds the search time budget returns 422 (narrow the query); a malformed q (over the complexity budget below, or only exclusions) returns 400. level, workplace, and employment_type each accept MULTIPLE values — repeat the parameter or comma-separate them (e.g. level=mid,senior&workplace=remote,hybrid), exactly like country. A job matches if it has any of the values given for that filter (within-field OR); different filters still narrow each other (across-field AND). The ONE exception is the location trio (country, state, location), which behaves as a single place selection and unions across the three — see Location filters below.
Full-text query budget. The terms and operators of YOUR q must total at most 40 nodes. The budget is counted on the parsed query BEFORE any server-side synonym expansion, so a query costs exactly what you can see; going over returns 400 with detail = search query too complex: N terms/operators (max 40), where N is your query's node count. Per-shape costs:
| shape | nodes | practical max |
|---|---|---|
bare word (developer) | 1, plus 1 for each OR/AND that joins it | ~20 OR'd words |
quoted two-word phrase ("backend developer") | 3, plus 1 for its OR | ~10 phrases |
quoted three-word phrase ("senior backend developer") | 5, plus 1 for its OR | ~6 phrases |
-term exclusion | 3 | — |
English stopwords are dropped while parsing and cost nothing — including INSIDE a phrase, so "director of engineering" costs 3 nodes (it parses as a two-word phrase), not 5. The OR'd multi-phrase "title picker" shape ("frontend developer" OR "backend developer" OR ...) is first-class at up to ~10 two-word phrases; q is separately capped at 300 characters, but the node budget is normally the binding limit.
Synonyms. Terms are expanded server-side against a curated role vocabulary (developer also matches Programmer/Engineer titles, common abbreviations such as ts, qa → quality assurance). That expansion NEVER counts against your budget, and when it would grow a query extremely large the search silently runs WITHOUT synonym enrichment instead of erroring — your exact terms always still match, only the extra recall is dropped. Expansion is DIRECTIONAL by curation: developer reaches Engineer/Programmer titles, but engineer does NOT reach Developer titles (deliberate — "Development"/"Developer" share a stem, so that direction would pull in "Business Development Manager"-style titles). Include both words when you want the whole role family; the budget has room for it.
Location filters. Three multi-value filters, one per granularity, all fed by the SAME code field of GET /locations/search. Take a suggestion, read its kind, and pass its code to the matching filter:
suggestion kind | example code | filter |
|---|---|---|
country | US | country |
state | US-TX | state |
city | dallas-tx-us | location |
region | emea | location |
All three take repeated or comma-separated values (location=dallas-tx-us,austin-tx-us) and match a job on ANY of its resolved locations (a job listed in Dallas and London matches either).
The three filters together are ONE place selection, not three independent narrowings: a job matches if any of its resolved locations matches any selected code of any kind. Selection is a UNION both within a filter and across the three, so state=US-NY&location=dallas-tx-us returns New York jobs PLUS Dallas jobs — exactly what a chip-style location picker means when a user adds a state and a city. Nested selections resolve to the superset rather than to their intersection: country=us&state=US-CA is the whole US, and state=US-TX&location=dallas-tx-us is all of Texas. Consequences worth knowing: once a location code is present, adding another can only ever GROW the result set (the FIRST code still narrows — an unfiltered search is not a place selection; to narrow again, remove codes or narrow on a non-location filter), and every OTHER filter — q, level, workplace, dates — still ANDs against the place selection as a whole, so level=intern&state=US-NY&location=dallas-tx-us is interns in (New York ∪ Dallas).
Codes are exact and opaque: an unknown one matches nothing rather than erroring, and there is no substring or fuzzy location matching anywhere in this API — resolve the place first, then filter by its code. Pass each code to the filter its kind names: a code in the wrong parameter is REJECTED with 400 (location=us-ny tells you to use state=US-NY; state=dallas-tx-us tells you to use location), since under a union a wrong-namespace value would otherwise be an inert arm that changed nothing. Each job in the response carries its own locations array whose entries use the identical kind/code pair, so a result can be turned into the query that finds more like it without another lookup.
The former city= substring filter has been REMOVED — there is no replacement for partial city-name matching, by design (it matched the primary location only, missed accents, and was the slowest query shape in the API). Note that unknown query parameters are IGNORED rather than rejected, so a leftover city=dallas does not error: it silently searches everywhere. Check for it when migrating.
Experience filters. experience_min and experience_max both read the job's STATED minimum requirement (experience_min_years), never its upper bound: experience_min=5 keeps jobs asking for 5+ years, and experience_max=3 keeps the jobs a candidate with 3 years qualifies for. Combine them for a band (experience_min=2&experience_max=4). Values are whole years, 0-50. Jobs where the employer stated no requirement (experience_min_years is null) are EXCLUDED whenever either filter is supplied — filter only when you want that trade-off. experience_max_years is returned data only; no filter reads it.
Visa sponsorship. Every job carries a TRI-STATE sponsors_visa flag extracted from the posting text: true = the posting states sponsorship is available, false = it explicitly states there is none (including a hard U.S.-citizenship requirement), null = the posting says nothing we could classify. null means UNKNOWN — never "no". The sponsors_visa FILTER is therefore a three-mode selector, not a boolean:
sponsors_visa | Keeps | Use it for |
|---|---|---|
not_false | Confirmed sponsors and unknowns; drops only confirmed non-sponsors. | The practical default for visa-dependent candidates: hide the guaranteed rejections, keep everything still in play. |
true | Confirmed sponsors only. | Certainty over reach — a strict, much smaller set. |
false | Confirmed non-sponsors only. | Building an exclusion list; not a job search. |
| omitted | Everything. | No visa filtering at all. |
Unlike has_salary — a presence check — this is not "has a value vs. has none". not_false is the ONLY mode that keeps null-flag jobs; true and false both EXCLUDE them, so those two result sets do not add up to the unfiltered total, and not_false is not their union. Coverage of the flag is still thin — most postings state no policy, and confirmed sponsors are much rarer than confirmed non-sponsors — so a sponsors_visa=true search returns far fewer jobs than an unfiltered one; that is missing information, not an absence of sponsoring employers, which is exactly why not_false (not true) is the recommended filter. Values are case-insensitive, and 1 / 0 are accepted as true / false.
Seniority levels. level is derived, not copied from the posting: it reflects the strongest employer signal, in order: explicit title keyword → seniority set in the employer's ATS → stated years-of-experience requirement (0–1 entry / 2–4 mid / 5+ senior) → conservative title fallbacks. A level filter excludes unclassified (null) jobs.
level | Means | How it is derived |
|---|---|---|
intern | Internship or co-op role. | Title or ATS-stated; never from years. |
entry | 0–1 years. | Title (junior / graduate / entry-level / …), ATS, a stated 0–1 years (including an explicit "no experience required" = 0), or — last resort — a weak title hint (bare roman I, a year, college). |
mid | 2–4 years. | An explicit "mid-level" / "intermediate" title, ATS, or a stated 2–4 years. Never a bare-title guess. |
senior | 5+ years, individual contributor. | Title senior / sr, ATS, or a stated 5+ years. |
staff | Staff / principal / distinguished IC rung. | Title only. |
lead | Team lead. | Title "lead", or an ATS Management / Manager-Supervisor value. |
executive | Director / VP / head-of / chief. | Title or ATS; never from years. |
null | The employer stated nothing. | No title signal, no ATS field, no stated years — experience_min_years is null on exactly these rows. |
Skills. Every job carries a skills field: the tools, technologies, certifications, licenses, languages and competencies the posting asks for (see the response schema). skills: null means NOT EXTRACTED (the posting has no description, is not in English, or has not been processed yet); skills: [] means extracted, and no known skill was found. It is display only for now: there is no skills filter. To find postings that name a tool, search for it with q and q_scope=full.
Skills vocabulary. The skills vocabulary is published in one file, skills-vocabulary.json, shaped {"version": N, "skills": [{"slug", "name", "kind", "aliases": [...]}]} and sorted by slug. version is the skills release (it also changes when extraction logic changes, not only the vocabulary); the file is replaced when a release ships, so re-fetch it now and then and compare version. It lists every slug the current release returns. A slug is never renamed or reused, but a release can retire one: a job extracted before that release may carry a retired slug until it is re-extracted, so do not treat a slug missing from the file as an error. name is a display name that can change between releases, so store slugs, not names. kind is an OPEN set, as in responses: ignore kinds you do not recognize. aliases are the spellings we recognize in postings (GCP and Google Cloud Platform → google-cloud, Kafka → kafka, LLMs → llms). To map a skill LIST (a candidate profile or resume skills section, one skill per item) onto our slugs, build one lookup from every alias, lowercased with whitespace collapsed, to its slug, and look each whole item up: no alias belongs to two skills under that normalization. The lookup is not for scanning prose. About one alias in ten is an acronym or an ordinary word (Go, R, Access, Word, Teams, Lambda) that we accept in postings only with exact capitals or supporting context, so treat a match on such an alias with care. Umbrella terms (fields and practice areas such as "machine learning", "DevOps" or "product management") are deliberately not skills: they never appear in skills and are not in the file. Skills vocabulary includes O*NET 30.2 (USDOL/ETA, CC BY 4.0) and ESCO v1.2.1 (European Commission, CC BY 4.0).
Authorization
bearerAuth Provide your secret API key (prefixed sk_) as a bearer token: Authorization: Bearer sk_....
In: header
Query Parameters
Case-insensitive substring match on the job title (3-200 characters).
3 <= length <= 200Full-text search across the job. Google-style syntax: quoted "exact phrase", OR, and -term exclusion (a query that is ONLY exclusions is rejected with 400). Operator precedence: implicit AND binds tighter than OR, and a -term attaches only to its adjacent group — so "machine learning" OR "data scientist" -senior parses as A OR (B AND NOT senior) and CAN still return Senior ML titles. To exclude a term everywhere, distribute it across each branch: "machine learning" -senior OR "data scientist" -senior. COMPLEXITY BUDGET: the terms and operators of YOUR query must total at most 40 nodes, counted on the parsed query before any server-side expansion, so the cost of a query is exactly what you can see. Over budget returns 400 with detail = search query too complex: N terms/operators (max 40) (N is your query's node count). Per-shape costs: a bare word is 1 node plus 1 for each OR/AND joining it (so ~20 OR'd words), a quoted two-word phrase is 3 nodes plus 1 for its OR (~10 phrases), a quoted three-word phrase is 5 plus 1 (~6), and a -term exclusion is 3. English stopwords are dropped while parsing and cost nothing — including INSIDE a phrase, so "director of engineering" costs 3 nodes (it parses as a two-word phrase), not 5. The OR'd multi-phrase "title picker" shape ("frontend developer" OR "backend developer" OR ...) is first-class — up to about 10 two-word phrases. q is also capped at 300 characters, but the node budget is normally the binding limit. SYNONYMS: terms are expanded server-side against a curated role vocabulary (e.g. developer also matches Programmer/Engineer titles, common abbreviations such as ts, and qa → quality assurance). That expansion NEVER counts against your budget, and if it would grow the query extremely large the search silently runs WITHOUT synonym enrichment rather than erroring — your exact terms always still match. Expansion is DIRECTIONAL by curation: developer reaches Engineer/Programmer titles, but engineer does NOT reach Developer titles (deliberate — "Development"/"Developer" share a stem, so that direction would pull in "Business Development Manager"-style titles). For full role-family coverage include both words; the budget has room. By default q matches the job TITLE only (precise — best for common role terms); pass q_scope=full to also search the description. Phrase queries under q_scope=full combined with very narrow filters (a small country, a rare language) can hit the search timeout; the default title scope is immune, so prefer it for phrase precision. q composes as a filter with the standard sort/cursor pagination (newest-first over the matches) and respects the default 90-day freshness window unless a date filter is given. Symbol-bearing tech tokens (c++, c#, .net) are matched as a title substring instead of full-text (they collapse under English tokenization). If a full-text search yields no rows a single title-substring fallback runs and its results are flagged fuzzy: true.
2 <= length <= 300Which fields q searches. title (default) matches the job title only — precise, best for common role terms (e.g. developer, engineer), which otherwise match description boilerplate. full also searches the description — broader reach, best for niche skills or tools that appear only in the requirements text (a language or framework not named in the title). Only affects results when q is set (ignored otherwise).
"title"Value in
- "title"
- "full"
Filter by ISO 3166-1 alpha-2 country code(s). Repeat the parameter or comma-separate values (e.g. country=us,de). A job matches if it is tagged with any of the given countries. Up to 20 values; case-insensitive. country, state and location form ONE place selection: a job matches if ANY of its resolved locations matches ANY selected code of ANY of the three kinds, so country=de&location=dallas-tx-us returns German jobs PLUS Dallas jobs (a union, not an intersection). Nested selections resolve to the superset — country=us&state=US-CA is just the US. When a bare state code is also supplied, the country additionally scopes that code (see state).
items <= 20Filter by state / region reference code(s). Use full <COUNTRY>-<ADMIN1> codes such as state=US-CA,US-NY (repeat the parameter or comma-separate; case-insensitive). To filter to ONLY a state, pass the full code alone — do NOT also pass its country: country, state and location form ONE place selection (a job matches if ANY of its resolved locations matches ANY selected code of ANY of the three kinds), so country=us&state=US-CA selects ALL United States jobs — the country unions with the state rather than narrowing it, exactly as state=US-NY&location=dallas-tx-us returns New York jobs PLUS Dallas jobs and state=US-TX&location=dallas-tx-us is just Texas (nested selections resolve to the superset). If country IS supplied, bare admin1 codes are accepted as a spelling convenience and auto-scoped to each given country — country=us&state=CA reads CA as US-CA (with multiple countries a bare code expands against each: country=us,ca&state=CA → US-CA,CA-CA) — but the countries themselves stay in the selection, so this form still selects the whole country; it is useful only when you genuinely want the country AND are abbreviating its states. A bare code WITHOUT country is ambiguous and matches nothing — pass the full US-CA form. Look up valid codes via GET /locations/states?country=<code> (its code field is the full value; admin1_code is the bare form). A job matches if it is tagged with any of the resulting regions. Up to 20 region codes, counted AFTER auto-scoping. A city / region slug is NOT accepted here and is rejected with 400: city slugs (dallas-tx-us, singapore-sg) and the five region slugs (amer, apac, emea, latam, worldwide) belong in location.
items <= 20Filter by canonical city / region code(s). Values are the EXACT code of a GET /locations/search row whose kind is city or region — a city slug such as dallas-tx-us, or one of the five region slugs (amer, apac, emea, latam, worldwide). Repeat the parameter or comma-separate values (e.g. location=dallas-tx-us,emea); values are case-insensitive and trimmed. A job matches if any of its resolved locations carries any of the given codes (within-field OR), so a job listed in both Dallas and Austin matches either. Up to 20 values. Codes are opaque — pass a /locations/search code through verbatim rather than composing one by hand; an unknown code simply matches nothing. country, state and location form ONE place selection: a job matches if ANY of its resolved locations matches ANY selected code of ANY of the three kinds, so location=dallas-tx-us&state=US-NY returns Dallas jobs PLUS New York jobs (a union, not an intersection). Nested selections resolve to the superset — location=dallas-tx-us&state=US-TX is just Texas. Country and state codes BELONG IN the country / state filters and are rejected here with 400 rather than silently matching nothing: a two-letter value (us) is a country code, and a <COUNTRY>-<ADMIN1> value (us-ny, de-by) is a state code. The kind field on each /locations/search suggestion tells you which of the three filters its code belongs to.
items <= 20Filter by workplace arrangement. Repeat the parameter or comma-separate values (e.g. workplace=remote,hybrid). A job matches if it has any of the given values.
items <= 3Filter by employment type. Repeat the parameter or comma-separate values (e.g. employment_type=full_time,contract). A job matches if it has any of the given values.
items <= 5Filter by seniority level. Repeat the parameter or comma-separate values (e.g. level=mid,senior). A job matches if it has any of the given values. level is derived, not copied from the posting: it reflects the strongest employer signal, in order: explicit title keyword → seniority set in the employer's ATS → stated years-of-experience requirement (0–1 entry / 2–4 mid / 5+ senior) → conservative title fallbacks. Jobs we could not classify (level is null) are EXCLUDED whenever this filter is supplied.
items <= 7Keep jobs whose stated MINIMUM years-of-experience requirement is at least this many years (experience_min_years >= n). Both experience filters read the job's stated minimum requirement — never its upper bound — so experience_min=5 returns the jobs asking for 5 or more years. Jobs with no stated requirement (experience_min_years is null) are EXCLUDED whenever either experience filter is supplied. Whole years, 0-50.
0 <= value <= 50Keep jobs whose stated MINIMUM years-of-experience requirement is at most this many years (experience_min_years <= n) — i.e. experience_max=3 returns the jobs a candidate with 3 years of experience qualifies for. Like experience_min it reads the job's stated minimum requirement, never its upper bound. Jobs with no stated requirement (experience_min_years is null) are EXCLUDED whenever either experience filter is supplied. Whole years, 0-50.
0 <= value <= 50Filter by ISO 639-1 language code (2 letters, case-insensitive).
2 <= length <= 2Keep jobs whose stated salary range reaches at least this amount. Jobs that state only a minimum (no maximum) match when that minimum is at least this value.
0 <= valueKeep jobs whose stated salary range starts at or below this amount.
0 <= valueFilter by ISO 4217 currency code (3 letters, case-insensitive).
3 <= length <= 3When true, return only jobs that specify a salary; when false, only jobs without a salary.
Filter on the tri-state visa-sponsorship flag (sponsors_visa), extracted from the posting text. This is a THREE-MODE selector, not a boolean. not_false keeps every job whose posting does not rule sponsorship out — confirmed sponsors plus the very large majority whose posting says nothing — and drops only the postings that explicitly refuse; for visa-dependent candidates this is the practical default: keeps confirmed sponsors and unknowns, drops only confirmed non-sponsors. true is the strict answer: CONFIRMED SPONSORS only — postings that affirmatively state sponsorship is available. That set is narrow (only a small fraction of postings state a policy at all, and confirmed sponsors are far rarer than confirmed refusals), so reach for it when you want certainty rather than coverage. false returns CONFIRMED NON-SPONSORS: postings that explicitly say there is no sponsorship, including a hard U.S.-citizenship eligibility requirement — an exclusion list, not a job search. Note this differs from has_salary, which is a presence check: sponsors_visa=false means "the posting says no", NOT "the posting is silent". Most postings say nothing and carry sponsors_visa: null — NULL means UNKNOWN, never "no". not_false is the ONLY mode that keeps those null-flag jobs; true and false both EXCLUDE them, so true and false do not add up to the unfiltered total and not_false is not their union. Omit the parameter to search everything regardless of the flag. In a query string the value is case-insensitive, and 1 / 0 are accepted as true / false. Coverage of the underlying flag grows as more postings are processed.
Filter by the numeric company id.
Filter by listing status. open (default) returns active listings; closed returns delisted ones.
"open"Value in
- "open"
- "closed"
Only jobs posted at or after this ISO-8601 timestamp, including a UTC offset (e.g. 2024-01-01T00:00:00+00:00). When none of posted_since, posted_until, or first_seen_since are provided and status is open, results default to the last 90 days.
date-timeOnly jobs posted at or before this ISO-8601 timestamp, including a UTC offset (e.g. 2024-01-01T00:00:00+00:00).
date-timeOnly jobs first seen at or after this ISO-8601 timestamp, including a UTC offset (e.g. 2024-01-01T00:00:00+00:00).
date-timeSort field for the descending keyset pagination. Defaults to posted_at. A full-text q composes with either sort as a filter.
"posted_at"Value in
- "posted_at"
- "first_seen_at"
Maximum number of jobs to return. Defaults to 100; values above 500 are clamped to 500.
0 < value100Opaque pagination cursor taken from a previous response next_cursor. Must be used with the same sort value that produced it.
Include the job description in each result: off (default, omitted), text (markdown as description_md), or html (description_html).
"off"Value in
- "off"
- "text"
- "html"
RESTRICTED. When set to source, attach the job's source identity (type and external_id, plus the board's other spellings as alias_ids) to each result. Requires the source_identity feature entitlement on the calling account; keys without it receive 403. Not available on self-serve plans — contact us if you need it.
Value in
- "source"
Response Body
application/json
application/problem+json
application/problem+json
application/problem+json
application/problem+json
application/problem+json
curl -X GET "https://example.com/api/v1/jobs"{ "data": [ { "id": 0, "source_id": 0, "company_id": 0, "external_job_id": "string", "url": "string", "apply_url": "string", "title": "string", "location": "string", "locations": [ { "kind": "country", "code": "string", "name": "string", "admin1_name": "string", "country_code": "string" } ], "workplace": "remote", "employment_type": "full_time", "level": "intern", "experience_min_years": 0, "experience_max_years": 0, "sponsors_visa": true, "skills": [ { "slug": "string", "name": "string", "kind": "string", "requirement": "required" } ], "language": "string", "salary_raw": "string", "salary_min": 0, "salary_max": 0, "salary_currency": "string", "salary_period": "year", "posted_at": "string", "expires_at": "string", "content_hash": "string", "first_seen_at": "string", "closed_at": "string", "created_at": "string", "updated_at": "string", "source": { "type": "string", "external_id": "string", "alias_ids": [ "string" ] }, "description_md": "string", "description_html": "string", "company": { "id": 0, "name": "string", "website_url": "string", "logo_url": "string" } } ], "next_cursor": "string", "limit": 0, "fuzzy": true}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}{ "type": "string", "title": "string", "status": 0, "detail": "string", "errors": [ { "field": "string", "message": "string" } ]}