Puesto Labs
IntroductionAuthenticationPagination & sortingSyncing with the changes feedErrorsRate limitsBuilding with AI

Here you can add a description about your company or product

© Copyright 2026 Puesto Labs. All Rights Reserved.

About
  • Blog
  • Contact
Product
  • Documentation
  • llms.txt
Legal
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
Puesto Docs

Count jobs

GET
/api/v1/jobs/count

Count the jobs matching a filter set. Accepts the same filters as Search jobs (pagination, sorting, and description knobs are ignored) and returns only the total, which counts by any-listed-country (a job tagged with the country as any of its locations), matching Search jobs exactly. The default 90-day freshness window applies identically to the count. One qualifier: the count is sort-agnostic, so it also includes the small share of jobs (~2%) that carry no posted date — rows the default sort=posted_at pagination skips entirely and only sort=first_seen_at can enumerate. That only shows up when no date filter is active (a first_seen_since-only query, or status=closed with no dates); under the default 90-day window both endpoints exclude those jobs alike. The count is capped at 100000: when more jobs match, the response is { "count": 100000, "capped": true } instead of the exact total (the capped field is absent whenever the count is exact). The cap applies to EVERY count, including full-text ones — a broad q (especially with q_scope=full) is the most likely way to see capped: true. Supplying q counts full-text matches with the same syntax, scope tiers, symbol-token handling, synonym expansion, and guardrails as Search jobs, including the identical complexity budget: your own terms and operators must total at most 40 nodes, counted before synonym expansion (over budget returns 400 with detail = search query too complex: N terms/operators (max 40); a pure-exclusion q also returns 400; a q that still exceeds the time budget returns 422 — rarer now that the cap bounds the work, but unchanged in meaning). See Search jobs for the per-shape node costs and the synonym rules. When the full-text count matches nothing, the same single title-substring fallback that Search jobs runs fires here too and the response is flagged fuzzy: true, so sizing a query with a count before fetching it no longer reports 0 for a query that Search jobs answers from its fallback lane. Each endpoint still picks its lane from its OWN result, so the same posted-date qualifier above applies: when no date filter is active and every full-text match happens to carry no posted date, the count is a non-fuzzy non-zero while the default sort=posted_at search sees an empty page and falls back. Under any posted_at predicate — including the default 90-day window — the two always pick the same lane. level, workplace, and employment_type each accept MULTIPLE values — repeat the parameter or comma-separate them (e.g. level=mid,senior), exactly like country. A job is counted 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.

Location filters. country, state and location scope the count exactly as they scope a search, from the same GET /locations/search codes: country takes US, state takes US-TX, and location takes city and region slugs (dallas-tx-us, emea). All three are multi-value (repeated or comma-separated) and count a job if ANY of its resolved locations matches. The three together are ONE place selection: they UNION both within a filter and across the three, so count(state=US-NY&location=dallas-tx-us) is the size of New York ∪ Dallas — it is at least as large as either alone, never their (usually tiny) intersection. Nested selections resolve to the superset (country=us&state=US-CA counts the whole US). Every non-location filter still ANDs against that selection. Because adding a location code to an existing place selection can only grow the set (only the first code narrows, against no location filter at all), counting a broad location union is the most likely way to see capped: true — a 20-city location list plus a large country can exceed the cap. A code in the wrong parameter is rejected with 400 (location=us-ny → use state=US-NY), so a mis-namespaced code never silently inflates or zeroes a count. The former city= substring filter has been removed; like any unknown parameter it is now IGNORED rather than rejected, so a leftover city= silently counts everything.

Experience filters. experience_min / experience_max narrow the count exactly as they narrow a search: both read the job's STATED minimum requirement (experience_min_years, whole years 0-50), and jobs with no stated requirement are EXCLUDED whenever either is supplied — so an experience-filtered count is always smaller than the same count without them.

Visa sponsorship. sponsors_visa narrows the count exactly as it narrows a search, in the same three modes. not_false counts everything the posting does not rule out — confirmed sponsors plus the un-stated majority — dropping only confirmed non-sponsors; it is the headline mode, and the only one that keeps null-flag jobs. true counts CONFIRMED SPONSORS (the posting states sponsorship is available) and false counts confirmed non-sponsors (it explicitly states there is none); both EXCLUDE the null-flag jobs, so count(true) + count(false) is far below the unfiltered count — the gap is un-stated postings, not non-sponsoring employers — and count(not_false) is much larger than either. Coverage of the flag is still thin, with true much rarer than false; counting without the filter is the way to size the full catalog. 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, which are a large share of the corpus. See Search jobs for the per-level definitions table.

Authorization

bearerAuth
AuthorizationBearer <token>

Provide your secret API key (prefixed sk_) as a bearer token: Authorization: Bearer sk_....

In: header

Query Parameters

title?string

Case-insensitive substring match on the job title (3-200 characters).

Length3 <= length <= 200
q?string

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

Length2 <= length <= 300
q_scope?string

Which 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).

Default"title"

Value in

  • "title"
  • "full"
country?array<>

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

Itemsitems <= 20
state?array<string>

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

Itemsitems <= 20
location?array<>

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

Itemsitems <= 20
workplace?array<>

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

Itemsitems <= 3
employment_type?array<>

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

Itemsitems <= 5
level?array<>

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

Itemsitems <= 7
experience_min?integer

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

Range0 <= value <= 50
experience_max?integer

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

Range0 <= value <= 50
language?string

Filter by ISO 639-1 language code (2 letters, case-insensitive).

Length2 <= length <= 2
salary_min?number

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

Range0 <= value
salary_max?number

Keep jobs whose stated salary range starts at or below this amount.

Range0 <= value
salary_currency?string

Filter by ISO 4217 currency code (3 letters, case-insensitive).

Length3 <= length <= 3
has_salary?boolean

When true, return only jobs that specify a salary; when false, only jobs without a salary.

sponsors_visa?|

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.

company_id?integer

Filter by the numeric company id.

status?string

Filter by listing status. open (default) returns active listings; closed returns delisted ones.

Default"open"

Value in

  • "open"
  • "closed"
posted_since?string

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.

Formatdate-time
posted_until?string

Only jobs posted at or before this ISO-8601 timestamp, including a UTC offset (e.g. 2024-01-01T00:00:00+00:00).

Formatdate-time
first_seen_since?string

Only jobs first seen at or after this ISO-8601 timestamp, including a UTC offset (e.g. 2024-01-01T00:00:00+00:00).

Formatdate-time

Response Body

application/json

application/problem+json

application/problem+json

application/problem+json

application/problem+json

curl -X GET "https://example.com/api/v1/jobs/count"
{  "count": 0,  "capped": true,  "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"    }  ]}