Query the indexed Crustdata job dataset with structured filters, cursor-based pagination, sorting, field selection, and aggregations.
Use this when you need to find, segment, or count job listings across
the full Crustdata job dataset — for hiring-trend analysis, building target
account lists from recent hiring activity, monitoring specific roles, or
powering a dashboard.This page walks you through the basics: the job record mental model, your
first search, the response shape, and choosing a search pattern, then folds
in worked example recipes you can copy, paste, and adapt. For filter grammar,
operators, and the full field catalog, see Search reference.
metadata.date_added is the job-posted date: the date the listing was
added on the source portal. Every query in this page that filters on
metadata.date_added is asking about job-posted time, not Crustdata
indexing time. Treat this endpoint as a query interface over Crustdata’s
indexed dataset, not as a direct poll of an employer-managed listings feed.
POST https://api.crustdata.com/job/search
Replace YOUR_API_KEY in each example with your actual API key. All
requests require the x-api-version: 2025-11-01 header.
Every job listing returned by Search Jobs is a single Job object with five
top-level groups:
crustdata_job_id and job_details — The stable job id plus the
posting’s own metadata: title, category, URL, workplace type, and number
of openings.
company — The hiring company’s firmographics at index time: basic
info, headcount, followers, revenue, funding, locations, and competitors.
No extra /company/enrich call required.
location — The job’s advertised location (city, state, country, raw
string), not the company HQ.
content — Full job description text. Use the (.) filter operator
on content.description to keyword-hunt.
metadata — Job timing metadata: date_added for the date the
listing was posted (added on the source portal) and date_updated for
the most recent refresh. These are your primary sort and filter fields
for recent windows.
Jobs ID cheat sheet. The Jobs APIs use three id concepts — keep them straight:
crustdata_job_id — the Crustdata job identifier. Returned on every Job. Use it as your dedupe key.
company.basic_info.crustdata_company_id — the Crustdata company identifier returned on every Job.
company.basic_info.company_id (filter alias) — the dot-path used in filters and aggregations.column for indexed Search Jobs. It points to the same integer as company.basic_info.crustdata_company_id. This alias is not sortable; for deterministic pagination, sort on metadata.date_added instead.
When you group_by on company.basic_info.company_id, each bucket also returns metadata.company_name, metadata.company_website_domain, and metadata.linkedin_id for labeling.
=, !=, <, =<, >, =>, in, not_in, (.), [.]. See Filter operators.
limit bounds and default
Minimum 0, maximum 1000, default 20. Set limit: 0 when you only want aggregations.
Error status codes
400, 401, 500.
Indexed-field allowlist
Only indexed fields can appear in filters, sorts, or aggregations.field. See Reference for the detailed catalog, or Common indexed fields for the most-used subset.
Matching job listings for the current page. Empty array [] when limit is 0 or when an aggregation-only query is made.
next_cursor
string or null
Opaque cursor to fetch the next page. null when there are no more pages.
total_count
integer or null
Total number of jobs matching the filter across all pages. Can be null for very broad queries where computing an exact total would be prohibitively expensive — never assume it is populated.
aggregations
array
Aggregation results, present only when the request included an aggregations array.
Find the most recent Software Engineer listings at Stripe (filtered via the
company.basic_info.company_id alias, which maps to crustdata_company_id = 631394).
Always send fields. The full Job schema is large (firmographics +
location + description + metadata). Fetching only the dot-paths you need
keeps responses small, fast, and predictable.
Use Search Jobs. You can slice millions of indexed job listings by
company, title, category, location, date, or any other indexed field — and
roll up results with count or group_by aggregations. Pair it with
cursor-based pagination to walk through large result sets.
Call POST /company/identify first to resolve
the domain, name, or profile URL into a crustdata_company_id, then use
that id in your Jobs Search filters.
Use Autocomplete to discover valid
values for a field — start typing a title, category, or company name and get
back matching values you can drop straight into a Search Jobs filter.
Pass "limit": 0 and an aggregations array to Search Jobs. You get the
total match count (and optional group_by buckets) without consuming any
row payload. See
Aggregations for examples.
Stay on Search Jobs and filter by indexed company, title, category,
funding, location, or date fields. Use cursor pagination to walk the full
result set and dedupe companies with
company.basic_info.crustdata_company_id.
Use Live Search to fetch fresh job
listings directly from a single company’s source profile. Narrow by
design — one company per call, no filters, no pagination.
Worked recipes you can copy, paste, and adapt. Each example is a full working
request. For the core walkthrough (job record mental model, your first search,
choosing a search pattern), see the sections above. For filter grammar,
operators, and the full field catalog, see Search reference.
For sorting, pagination, field selection, and aggregations, see
Pagination & sorting.
SDR / BDR keyword search across multiple companies
The long forms "Sales Development Representative" and "Business Development Representative" use (.) (all-words match), but the short
acronym "SDR" uses [.] (exact phrase). Short acronyms with (.)
can overmatch — e.g. "SDR" would also match "USDR". Use [.] for
2–3 character acronyms.
Companies indexing both Software Engineers and Account Executives
Because filters operate on individual job rows, you cannot ask for
“companies with both roles” in a single query. Instead, run two
bounded-window aggregations and intersect the company ids client-side.
Watch out for short-acronym false positives.(.) is an all-words
match, so a query of "AE" in job_details.title can also match
unrelated titles. Prefer [.] for 2–3 character acronyms.
1
Query 1 — companies with Software Engineer listings
engineering_ids = {b["key"] for b in response_1["aggregations"][0]["buckets"]}ae_ids = {b["key"] for b in response_2["aggregations"][0]["buckets"]}both = engineering_ids & ae_ids
Mid-market companies indexing SDR listings
Combine an inclusive headcount range with keyword search on the title field.
Hiring volume by workplace type in the United States
Country values are not normalized.location.country can appear as
"USA", "United States", or "United States of America". The in
array below covers the three most common forms, but for full coverage
you should first run a group_by on location.country and collect the
exact bucket keys present in your dataset slice.
To get just the total number of jobs matching a filter, send a count
aggregation with limit: 0. No job rows are returned, so the call consumes
no per-result credits, and the total comes back in both total_count and the
aggregation value.
For a simple total, read total_count. The count aggregation returns the
same number in aggregations[0].value and is handy when you send it
alongside other aggregations in one request.
Exact-title match across companies
Use in on job_details.title when you want listings whose title is exactly
one of a known set — for example a normalized list of sales titles across a
target account list. This is the precise alternative to fuzzy (.) all-words
matching.
in matches the whole title exactly (case-insensitive), so
"Sales Development Representative" will not match "Senior Sales Development Representative". When you want partial or word-level matches
instead, use (.) for all-words or [.] for an exact contiguous phrase.
Rank keyword hits by relevance
When your filter includes a text operator ((.)), you can sort by relevance
to surface the strongest title matches first instead of the most recent.
relevance needs a text query. Sorting by relevance without a (.)
(or [.]) text condition in your filter returns 400 with
Unsupported columns in conditions: ['relevance (no text query present)'].
Pair relevance with at least one text filter, or sort by
metadata.date_added instead.