Skip to main content
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.

Job record mental model

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.

At a glance

DetailValue
EndpointPOST https://api.crustdata.com/job/search
AuthAuthorization: Bearer YOUR_API_KEY
API versionx-api-version: 2025-11-01 header (required)
BodyRequired. Send {} to match the whole dataset; every realistic query uses filters.
Body keysfilters, cursor, limit (0–1000, default 20), sorts, fields, aggregations — all optional
Response{ "job_listings": [ Job, ... ], "next_cursor": string?, "total_count": integer?, "aggregations"?: [ ... ] }
Errors400 invalid request · 401 unauthorized · 500 internal

Guaranteed contract vs current behavior

TopicWhat it means
Endpoint, HTTP method, auth headersPOST /job/search, bearer auth, x-api-version: 2025-11-01.
Request body shapeOptional filters, cursor, limit, sorts, fields, aggregations. filters is a SearchCondition or a SearchConditionGroup.
Response body shape{ "job_listings", "next_cursor", "total_count", "aggregations"? }. Aggregation-only queries return "job_listings": [].
Supported operators=, !=, <, =<, >, =>, in, not_in, (.), [.]. See Filter operators.
limit bounds and defaultMinimum 0, maximum 1000, default 20. Set limit: 0 when you only want aggregations.
Error status codes400, 401, 500.
Indexed-field allowlistOnly 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.
Pricing and rate limitsSee Pricing and Rate limits for current numbers.

Examples

Worked examples like “SDR hiring in mid-market”, “companies that closed Series B”, and full-text keyword hunts.

Pagination & sorting

Sorting, cursor-based pagination, field selection, and aggregations with count and group_by.

Reference

Common indexed fields, annotated Job example, full field catalog, id map, bucket metadata, errors.

Request body

ParameterTypeRequiredDefaultDescription
filtersobjectNoSingle SearchCondition or nested SearchConditionGroup. Omit to match all indexed jobs.
cursorstringNoOpaque cursor from a prior response’s next_cursor. Pass it to fetch the next page with the same filter, sort, and field set.
limitintegerNo20Rows per page. Min 0, max 1000. Use 0 for aggregation-only queries.
sortsarray of SearchSortNoOrdering rules. Each item has field (dot-path) and order (asc or desc). Sorts are applied in array order.
fieldsstring[]Noall fieldsDot-paths to include in each returned job. Omit to return everything. Always specify fields in production for smaller payloads and faster responses.
aggregationsarray of AggregationRequestNoRoll-up queries. Supports count and group_by. Use with limit: 0 if you only want counts.

Response body

FieldTypeDescription
job_listingsJob[]Matching job listings for the current page. Empty array [] when limit is 0 or when an aggregation-only query is made.
next_cursorstring or nullOpaque cursor to fetch the next page. null when there are no more pages.
total_countinteger or nullTotal 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.
aggregationsarrayAggregation results, present only when the request included an aggregations array.

Rate limits and credits

Current pricing for indexed Jobs Search:
Pricing: 0.03 credits per result returned. A request with no results does not consume credits.
Default rate-limit is 30 requests per minute. Send an email to gtm@crustdata.co to discuss higher limits if needed for your use case.

Your first search: filter by company and title

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).
curl --request POST \
  --url https://api.crustdata.com/job/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "company.basic_info.company_id", "type": "=", "value": 631394 },
        { "field": "job_details.title",              "type": "=", "value": "Software Engineer" }
      ]
    },
    "fields": [
      "job_details.title",
      "job_details.url",
      "company.basic_info.name",
      "location.raw",
      "metadata.date_added"
    ],
    "sorts": [
      { "field": "metadata.date_added", "order": "desc" }
    ],
    "limit": 2
  }'
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.

Which search pattern should I use?

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.

Examples

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.
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.
curl --request POST \
  --url https://api.crustdata.com/job/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "company.basic_info.company_id", "type": "in", "value": [631394, 631811, 673947] },
        { "field": "metadata.date_added",           "type": "=>", "value": "2025-01-01" },
        {
          "op": "or",
          "conditions": [
            { "field": "job_details.title", "type": "(.)", "value": "Sales Development Representative" },
            { "field": "job_details.title", "type": "[.]", "value": "SDR" },
            { "field": "job_details.title", "type": "(.)", "value": "Business Development Representative" }
          ]
        }
      ]
    },
    "fields": [
      "job_details.title",
      "company.basic_info.name",
      "location.raw",
      "metadata.date_added"
    ],
    "sorts": [{ "field": "metadata.date_added", "order": "desc" }],
    "limit": 2
  }'
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

{
    "filters": {
        "op": "and",
        "conditions": [
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" },
            { "field": "metadata.date_added", "type": "<", "value": "2026-01-01" },
            {
                "op": "or",
                "conditions": [
                    { "field": "job_details.title", "type": "(.)", "value": "Software Engineer" },
                    { "field": "job_details.title", "type": "[.]", "value": "SWE" }
                ]
            }
        ]
    },
    "limit": 0,
    "aggregations": [
        { "type": "group_by", "field": "company.basic_info.company_id", "agg": "count", "size": 500 }
    ]
}
2

Query 2 — companies with Account Executive listings

{
    "filters": {
        "op": "and",
        "conditions": [
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" },
            { "field": "metadata.date_added", "type": "<", "value": "2026-01-01" },
            {
                "op": "or",
                "conditions": [
                    { "field": "job_details.title", "type": "(.)", "value": "Account Executive" },
                    { "field": "job_details.title", "type": "[.]", "value": "AE" }
                ]
            }
        ]
    },
    "limit": 0,
    "aggregations": [
        { "type": "group_by", "field": "company.basic_info.company_id", "agg": "count", "size": 500 }
    ]
}
3

Intersect the company ids client-side

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
Combine an inclusive headcount range with keyword search on the title field.
{
    "filters": {
        "op": "and",
        "conditions": [
            { "field": "company.headcount.total", "type": "=>", "value": 51 },
            { "field": "company.headcount.total", "type": "=<", "value": 500 },
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" },
            {
                "op": "or",
                "conditions": [
                    { "field": "job_details.title", "type": "(.)", "value": "Sales Development Representative" },
                    { "field": "job_details.title", "type": "[.]", "value": "SDR" },
                    { "field": "job_details.title", "type": "(.)", "value": "Business Development Representative" }
                ]
            }
        ]
    },
    "fields": [
        "crustdata_job_id",
        "job_details.title",
        "company.basic_info.crustdata_company_id",
        "company.basic_info.name",
        "company.basic_info.primary_domain",
        "company.headcount.total",
        "location.raw",
        "metadata.date_added"
    ],
    "sorts": [{ "field": "metadata.date_added", "order": "desc" }],
    "limit": 50
}
{
    "filters": {
        "op": "and",
        "conditions": [
            { "field": "company.funding.last_round_type", "type": "=", "value": "series_b" },
            { "field": "company.funding.last_fundraise_date", "type": "=>", "value": "2025-01-01" },
            { "field": "company.funding.last_fundraise_date", "type": "<", "value": "2025-07-01" },
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" }
        ]
    },
    "fields": [
        "job_details.title",
        "company.basic_info.crustdata_company_id",
        "company.basic_info.name",
        "company.basic_info.primary_domain",
        "company.funding.last_round_type",
        "company.funding.last_fundraise_date",
        "company.funding.total_investment_usd"
    ],
    "sorts": [
        { "field": "company.funding.last_fundraise_date", "order": "desc" }
    ],
    "limit": 100
}
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.
{
    "filters": {
        "op": "and",
        "conditions": [
            {
                "field": "location.country",
                "type": "in",
                "value": ["USA", "United States", "United States of America"]
            },
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" },
            { "field": "metadata.date_added", "type": "<", "value": "2026-01-01" }
        ]
    },
    "limit": 0,
    "aggregations": [
        { "type": "group_by", "field": "job_details.workplace_type", "agg": "count", "size": 10 }
    ]
}
{
    "filters": {
        "op": "and",
        "conditions": [
            { "field": "content.description", "type": "(.)", "value": "kubernetes" },
            { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" }
        ]
    },
    "fields": [
        "job_details.title",
        "company.basic_info.name",
        "location.raw",
        "metadata.date_added"
    ],
    "sorts": [{ "field": "metadata.date_added", "order": "desc" }],
    "limit": 20
}
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.
curl --request POST \
  --url https://api.crustdata.com/job/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "company.basic_info.company_id", "type": "=",   "value": 631394 },
        { "field": "job_details.title",             "type": "(.)", "value": "Software Engineer" }
      ]
    },
    "limit": 0,
    "aggregations": [ { "type": "count" } ]
  }'
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.
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.
curl --request POST \
  --url https://api.crustdata.com/job/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "company.basic_info.company_id", "type": "in", "value": [631394, 680992, 673947] },
        { "field": "metadata.date_added",           "type": "=>", "value": "2025-01-01" },
        {
          "field": "job_details.title",
          "type": "in",
          "value": [
            "Account Executive",
            "Business Development Representative",
            "Sales Development Representative"
          ]
        }
      ]
    },
    "fields": [
      "job_details.title",
      "company.basic_info.name",
      "location.raw",
      "metadata.date_added"
    ],
    "sorts": [{ "field": "metadata.date_added", "order": "desc" }],
    "limit": 3
  }'
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.
When your filter includes a text operator ((.)), you can sort by relevance to surface the strongest title matches first instead of the most recent.
curl --request POST \
  --url https://api.crustdata.com/job/search \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --data '{
    "filters": {
      "op": "and",
      "conditions": [
        { "field": "job_details.title",   "type": "(.)", "value": "machine learning" },
        { "field": "metadata.date_added", "type": "=>",  "value": "2025-01-01" }
      ]
    },
    "fields": [
      "job_details.title",
      "company.basic_info.name",
      "location.raw",
      "metadata.date_added"
    ],
    "sorts": [{ "field": "relevance", "order": "desc" }],
    "limit": 3
  }'
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.

What to do next

  • Discover filter values — use Autocomplete to find the exact title, category, or company-name values a filter accepts.
  • Paginate and aggregate — see Pagination & sorting for cursor pagination, sorting, field selection, and aggregations.
  • Look up fields — see Reference for the full Job catalog, id map, bucket metadata, and errors.
  • Fetch fresh listings for one company — see Live Search.
  • Inspect the full schema — read the OpenAPI reference.