# Introduction Source: https://docs.crustdata.com/general/introduction Crustdata's public Company, Person, Job, Web, and Social Post APIs for search, enrich, identify, autocomplete, and content retrieval. Crustdata provides public APIs for the data layer behind sales, recruiting, and research workflows. You can query indexed datasets of companies, people, jobs, and social posts, enrich a known entity with fresh attributes, resolve identifiers, search the web, and fetch page content — all over a single versioned REST surface. Every endpoint in this version requires `Authorization: Bearer ` and `x-api-version: 2025-11-01`. Live endpoints (real-time retrieval from the web) are enterprise-only and plan-specific — review [Pricing](/general/pricing) before you build against them. ## API areas Five product areas, grouped by entity. Pick the area for the entity you work with; each area's introduction page walks the simplest query and links onward to filters, pagination, and the full reference. Search the indexed company dataset, enrich a known company, resolve a domain/name/URL to a Crustdata ID, and surface valid filter values with autocomplete. Search the indexed people dataset, enrich a known person, discover developer profiles, and surface valid filter values with autocomplete. Search the indexed job-listings dataset with filters, cursor pagination, and aggregations. Fetch real-time openings for a single company on demand. Search the web across news, web, scholar, social, and deep-research sources, or fetch the HTML content of any URL. Enrich recent social posts for a person or company, or search posts by keyword, recency, and author attributes. Available on plans with live access. ## What lives where Every API area's section opens with a short overview (orientation), then progressive how-to pages (filters, pagination, recipes), and finishes with reference (the full request / response schema). Use this map to pick the right page for your task: | If you want to… | Go to | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Understand what an API area does | Each area's **Introduction** page | | Build a query step-by-step | The area's **Filters**, **Pagination**, **Recipes** pages | | Look up an exact field, parameter, or error code | The area's **Reference** page or the interactive [API reference](/openapi-specs/2025-11-01/introduction) | | Estimate cost | [Pricing](/general/pricing) | | Check throughput limits | [Rate limits](/general/rate-limits) | | Port an existing integration from the legacy APIs | [Migration guides](/migrations/overview) | ## First call If you haven't called the API yet, start with the simplest possible request — Company search by name, then layer filters from there. ```bash curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_info.name", "type": "=", "value": "Crustdata" }, "limit": 1 }' ``` Continue with [Company search](/company-docs/search/introduction) or jump to any of the [API areas](#api-areas) above. ## Migrating from the legacy APIs? If you have an integration on the legacy `/screener/*` or `/data_lab/*` endpoints, head to the [Migration guides overview](/migrations/overview) — it lists every legacy endpoint, its current replacement, and the universal changes (auth scheme, version header, error envelope) that apply across every migration. # Pricing Source: https://docs.crustdata.com/general/pricing Learn how Crustdata credits work, what common endpoints cost, and how to estimate usage before you ship. Pricing can change by plan, entitlement, and endpoint version. Confirm the current credit cost in your dashboard or with the Crustdata team before you plan production usage. Crustdata uses a credit model across its APIs. Use low-cost search endpoints to narrow your list, then enrich only the records you want to inspect in detail. Use search endpoints for low-cost discovery, then enrich only the records you want to keep. Person enrich starts with a base profile and adds cost only when you ask for higher-value data. Web live endpoints are available on self-serve plans. Company and Person live endpoints remain plan-gated. Credits are currently valid for **6 months** from the purchase date. ## Self-serve endpoint pricing This page lists pricing for the endpoints currently documented in this docs site. It does not include unpublished or not-yet-documented routes. Use these tables to estimate common self-serve search, identify, and enrich costs. To see what a call actually cost, read the [`X-Credits-Used` response header](/general/credits#per-call-usage-the-x-credits-used-header) returned on every request. ### Person endpoints | Endpoint | Credit usage | Notes | | ----------------------------- | ------------------- | ---------------------------------------------------------- | | `/person/search` | **0.03** per result | Best for low-cost discovery and filtering. | | `/person/search/autocomplete` | **Free** | Use for typeahead and filter builders. | | `/person/enrich` | **1–7** per record | Starts with a base profile and scales with requested data. | | `/person/contact/enrich` | **1–5** per matched person | Billed per requested contact type — no base credit. Enterprise plans. | | `/batch/person/contact/enrich` | **1–5** per matched person | Same per-type prices as `/person/contact/enrich`. Enterprise plans. | | `/dev_platform/enrich` | **1** per result | Available on enterprise plans. | ### Company endpoints | Endpoint | Credit usage | Notes | | ------------------------------ | ------------------- | -------------------------------------------------------------- | | `/company/search` | **0.03** per result | Lightweight company discovery. | | `/company/search/autocomplete` | **Free** | Useful for search UX and filter pickers. | | `/company/enrich` | **2–4** per record | Base **2**; requesting `technographics` adds **+2** per company that returns technographics data. | | `/company/identify` | **Free** | Resolve a company from a domain or other supported identifier. | | `/employee_review/enrich` | **1** per company | Available on enterprise plans. | ### Job endpoints | Endpoint | Credit usage | Notes | | ------------- | ------------------- | ------------------------------------------------------ | | `/job/search` | **0.03** per result | Indexed job discovery, filtering, and hiring analysis. | ### Web endpoints | Endpoint | Credit usage | Notes | | ------------------ | --------------- | ------------------------------------------------------------- | | `/web/search/live` | **1** per query | Web search. Available on self-serve and enterprise plans. | | `/web/enrich/live` | **1** per page | Web page fetch. Available on self-serve and enterprise plans. | ### Social Post endpoints | Endpoint | Credit usage | Notes | | ----------------------------------------------- | ----------------------------------- | ------------------------------ | | `/social_post/professional_network/search/live` | **1** per post · **3** per post with `exact_keyword_match` (reactors/comments unavailable — use enrich) | Available on enterprise plans. | | `/social_post/professional_network/enrich/live` | **1** per post · **5** per 100 reactors *or* comments · **10** per 100 reactors *and* comments | Available on enterprise plans. | ### Watcher endpoints | Endpoint | Credit usage | Notes | | ----------------------- | ------------------------- | ---------------------------------------------------------------------------- | | `/watch/person/search` | **0.5** per new person | Discovery watcher. First run is a free baseline (up to 5 matches). | | `/watch/company/search` | **2** per new company | Discovery watcher. First run is a free baseline (up to 5 matches). | | `/watch/person` | **5–150** per changed entity | Entity watcher. First run is a free baseline; no charge on runs with no change. 5 at the default 30-day freshness; fresher data costs more (see below). | | `/watch/company` | **5** per changed entity | Entity watcher. First run is a free baseline; no charge on runs with no change. | You're charged only for what's delivered on recurring runs — a discovery run that finds nothing new, or an entity run where nothing changed, costs nothing. Each watcher lives under its dataset: [Person → Watch](/watcher-docs/person/entity) and [Company → Watch](/watcher-docs/company/entity), each with an Entity and a Discovery watcher. For a **person** entity watcher, the per-notification rate is tiered by `config.refresh_frequency_days` — how fresh the tracked data is kept. It does not depend on how often the watch runs. | `refresh_frequency_days` | Credits per changed entity | | ------------------------ | -------------------------- | | not set / `30` (default) | 5 | | `14` | 10 | | `7` | 20 | | `3` | 50 | | `1` | 150 | Company entity watchers run at the 30-day baseline (5 credits per changed company). ## Batch endpoint | Endpoint | Credit usage | Notes | | ------------------------------------------------- | ------------------- | ----------------------------------------------------------------- | | `/batch/company/search` | **0.03** per result | Same as company search pricing. | | `/batch/company/enrich` | **2–4** per record | Same as company enrich pricing, including the `technographics` add-on. | | `/batch/company/professional_network/search/live` | **2** per company | Same as live company search pricing. | | `/batch/person/search` | **0.03** per result | Same as person search pricing. | | `/batch/person/enrich` | **1–7** per record | Same as person enrich pricing. | | `/batch/person/professional_network/search/live` | **2** per profile | Same as live people search pricing. | | `/batch/person/professional_network/enrich/live` | **7** per profile | Same as live people enrich pricing. | | `/batch/person/identify` | **1** per match | Reverse email lookup. Only emails that resolve to a person are billed; unmatched are free. | | `/batch/job/search` | **0.03** per result | Same as job search pricing. | | `/batch/social_post/professional_network/enrich/live` | **1** per post · **5** per 100 reactors *or* comments · **10** per 100 reactors *and* comments | Same as social post enrich pricing. Enterprise plans. | | `/batch/{batch_id}` | **Free** | Current pricing assumption: batch status checks are non-billable. | | `/batch` | **Free** | Current pricing assumption: batch listing is non-billable. | ## Person enrich pricing Person enrich uses additive pricing. You pay for the base profile first, then add credits only for the extra data you request. | Data returned | Additional credits | What you get | | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------- | | Base profile | **1** | Core person identity, role, location, experience, education, certifications, skills, and social handles | | Personal email data | **+2** | Personal email details | | Phone data | **+2** | Direct phone numbers | | Business email data | **+1** | Verified business email details | | Developer platform data | **+1** | Developer platform profile, repos, orgs, and activity | The maximum current cost for a single enriched person record is **7 credits**. The base credit is charged once per matched record. ### Common credit patterns | Use case | Typical data requested | Credits | | ------------------------------------- | ------------------------------------- | ------- | | Basic profile lookup | Base profile only | **1** | | Sales outreach | Base profile + business email | **2** | | Full outbound prospecting | Base profile + business email + phone | **4** | | Recruiting with personal contact data | Base profile + personal email + phone | **5** | | Maximum person enrich payload | Base profile + all add-ons | **7** | For person enrich, send a JSON body with either `professional_network_profile_urls` or `business_emails`, based on the identifier you have. ## Person Contact pricing [`/person/contact/enrich`](/person-docs/contact/enrich) bills **per requested contact type, per matched person — there is no base credit** (the endpoint returns contact data only). | Contact type requested | Credits per matched person | | ---------------------------- | -------------------------- | | `contact.business_emails` | **1** | | `contact.personal_emails` | **2** | | `contact.phone_numbers` | **2** | Only the types you request are fetched and billed. Omitting `fields` requests all three (**5** per matched person, the maximum). `contact.websites` is not one of the billed contact types — requesting it currently uses **0** credits. Identifiers with no match are not billed. [`/batch/person/contact/enrich`](/person-docs/contact/batch) bills the same per-type prices, per matched person (maximum **5**) — counted per type, not per record (four business-email candidates count once). ## Person enrich request examples Use these examples to enrich by profile URL, request specific fields, or reverse-lookup a person from a business email. ```bash Base profile request (1 credit) curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary" ] }' ``` ```bash Request specific fields curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary" ], "fields": [ "basic_profile.summary", "professional_network.followers" ] }' ``` ```bash Reverse lookup by business email curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "business_emails": [ "abhilash@crustdata.com" ] }' ``` For full request patterns and field selection behavior, see [Person enrichment](/person-docs/enrichment/introduction). ## Enterprise-only and plan-gated live endpoint pricing Company and Person live endpoints are plan-gated and can have custom quotas or contract pricing. Web Search and Web Fetch are available on self-serve plans. The live Company and Person endpoints below still require the right plan or enterprise access. ### Live people, company, and job data | Endpoint | Credit usage | Notes | | ------------------------------------------- | ----------------- | --------------------------------------------------------- | | `/person/professional_network/search/live` | **2** per profile | Fresh live people search. | | `/person/professional_network/enrich/live` | **7** per profile | Fresh profile retrieval when cached enrich is not enough. | | `/company/professional_network/search/live` | **2** per company | Fresh live company search. | | `/job/professional_network/search/live` | **2** per result | Fresh live job search for a single company. | ### Shared live-search endpoints | Endpoint | Credit usage | Notes | | ------------------------------------------- | ------------ | -------------------------------------------------------------- | | `/professional_network/search/autocomplete` | **Free** | Shared live-search autocomplete when enabled for your account. | ## Free endpoints These endpoints do not currently consume credits: - `/person/search/autocomplete` - `/company/search/autocomplete` - `/company/identify` - `/professional_network/search/autocomplete` (when enabled for your enterprise plan) ## Search vs enrich | If you need to... | Best endpoint type | Why | | -------------------------------------------- | ------------------ | --------------------------------------------------------------- | | Explore a large audience cheaply | Search | Search is priced for discovery and returns lightweight records. | | Build full profiles or contact-ready records | Enrich | Enrich returns detailed person or company data. | | Power typeahead or filter suggestions | Autocomplete | Autocomplete is free and designed for interactive search UIs. | ## Estimate usage before you launch Separate self-serve, enterprise live, and utility requests. They are not billed in the same way. Search pricing depends on results returned, not only the number of calls. Contact data, developer platform data, and enterprise live workflows can raise per-record cost. Model normal traffic first, then add weekly or monthly growth assumptions. Keep headroom for queue bursts, replay jobs, and production debugging. ## Rate limit considerations Default rate limits vary by endpoint, ranging from 10 requests per minute on live (real-time) endpoints to 45 requests per minute on autocomplete endpoints. See [Rate limits](/general/rate-limits) for the full per-endpoint table. For throughput planning, see [Rate limits](/general/rate-limits). ## Frequently asked questions Search is billed per result returned. For example, 10 results at `0.03` credits per result use `0.30` credits. A request that returns no results uses no credits. Start with the base profile. Add business email, personal email, phone, or developer platform data only when your workflow uses it. Billable requests return an error until you purchase more credits or your account is replenished under your plan. Yes. Credits are valid for 6 months from the date of purchase. For plan, quota, or enterprise pricing questions, contact Crustdata support through your account channel. # Rate limits Source: https://docs.crustdata.com/general/rate-limits Learn how to design clients for Crustdata API rate limits and avoid 429 responses. Exact per-endpoint limits can change by plan and endpoint version. Verify active limits in your dashboard. Rate limits vary by endpoint. The defaults below apply to most accounts. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. ## Core behavior Crustdata applies per-endpoint request limits. Operationally, you should assume that **steady request distribution** is safer than bursts, even when total requests per minute look acceptable. ## Default rate limits by endpoint | Endpoint | Default `rate-limit` (RPM) | | --- | --- | | `/person/enrich` | 15 | | `/person/search` | 30 | | `/person/professional_network/enrich/live` | 10 | | `/person/professional_network/search/live` | 10 | | `/person/contact/enrich` | 15 | | `/person/search/autocomplete` | 300 | | `/company/enrich` | 15 | | `/company/search` | 30 | | `/company/identify` | 30 | | `/company/search/autocomplete` | 300 | | `/company/professional_network/search/live` | 10 | | `/employee_review/enrich` | 15 | | `/job/search` | 30 | | `/job/professional_network/search/live` | 10 | | `/web/search/live` | 10 | | `/web/enrich/live` | 10 | | `/dev_platform/enrich` | 15 | | `/social_post/professional_network/enrich/live` | 10 | | `/social_post/professional_network/search/live` | 10 | | `/batch/person/enrich` | 30 | | `/batch/person/search` | 30 | | `/batch/person/professional_network/search/live` | 5 | | `/batch/person/professional_network/enrich/live` | 5 | | `/batch/person/contact/enrich` | 5 | | `/batch/company/enrich` | 30 | | `/batch/company/search` | 30 | | `/batch/company/professional_network/search/live` | 5 | | `/batch/job/search` | 30 | | `/batch/job/professional_network/search/live` | 5 | | `/batch/social_post/professional_network/enrich/live` | 5 | | `/account/endpoints` | 300 | | `/account/credits` | 300 | ## Implementation guidance To reduce `429 Too Many Requests` responses: 1. Spread traffic across the full minute instead of burst-sending. 2. Use retry logic with exponential backoff and jitter. 3. Keep request queues bounded. 4. Add circuit breakers around non-critical enrichment flows. 5. Monitor request logs and alert on sustained `429` rates. ## Client-side best practices - Centralize throttling in one shared HTTP client. - Use endpoint-specific concurrency and QPS controls. - Prioritize business-critical requests when backpressure starts. - Cache stable results where your workflow allows it. ## Rollout checklist - Start with conservative throughput. - Increase gradually while tracking latency and error rates. - Recalibrate limits when you add new endpoints or workflows. For higher-throughput needs, request custom limits through Crustdata support. # Credits Source: https://docs.crustdata.com/general/credits Check your remaining Crustdata API credit balance and recurring credit grant with a single free GET request. Use this endpoint to check your remaining API credit balance and your recurring credit grant: how many credits you receive each cycle, how often, and when the next refresh lands. Poll it to monitor usage and confirm you have enough credits before a large batch of requests. You can also view credits and usage in your [dashboard](https://app.crustdata.com/). This endpoint is **free** — checking your balance does not consume any credits. For how credits are charged per endpoint, see [Pricing](/general/pricing). ## Endpoint ``` GET https://api.crustdata.com/account/credits ``` It takes no query parameters or request body. Authenticate with your API key in the `Authorization` header and send the required `x-api-version: 2025-11-01` header — requests without it return `400`. Rate limited to 300 requests per minute. ## Example request ```bash Request curl --request GET \ --url https://api.crustdata.com/account/credits \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```python Python import requests resp = requests.get( "https://api.crustdata.com/account/credits", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, ) print(resp.json()["account"]["credits"]) ``` ```json Response { "account": { "credits": 9406.5, "recurring_credits": 10000, "recurring_credits_frequency": "monthly", "recurring_credits_refresh_date": "2026-08-01T00:00:00+00:00" } } ``` ## Response fields | Field | Type | Description | | ----------------------------------------- | -------------- | -------------------------------------------------------------------------------------------- | | `account.credits` | number | Remaining API credits on your account — the shared pool all your API keys draw from. May be fractional. | | `account.recurring_credits` | number | Credits granted each billing cycle. `0` if your plan has no recurring grant. | | `account.recurring_credits_frequency` | string \| null | Grant cadence (for example `monthly`). `null` if no recurring grant. | | `account.recurring_credits_refresh_date` | string \| null | ISO timestamp of the next grant refresh. `null` if no recurring grant. | | `account.wallets` | array | Per-wallet breakdown — only present when your account has credit wallets enabled. See below. | | `account.credit_topups` | array | One entry per top-up still holding credits — only present when your account has live top-ups. See below. | ## Credit wallets Accounts with credit wallets enabled hold their balance in two wallets, and the response includes a `wallets` list breaking the balance down: - **`recurring`** — your plan's cycle grant. Refreshes on `next_refresh_date` and is spent first. - **`topup`** — credits you purchase. They roll over month to month and expire on `expires_at`; every new purchase extends the expiry. ```json Response (wallets enabled) { "account": { "credits": 128179.25, "recurring_credits": 1000, "recurring_credits_frequency": "monthly", "recurring_credits_refresh_date": "2026-08-13T00:00:00+00:00", "wallets": [ { "type": "recurring", "credits": 850.0, "next_refresh_date": "2026-08-13T00:00:00+00:00", "expires_at": null }, { "type": "topup", "credits": 127329.25, "next_refresh_date": null, "expires_at": "2027-07-31T23:59:59Z" } ] } } ``` | Field | Type | Description | | --------------------------- | -------------- | ------------------------------------------------------------------------------ | | `wallets[].type` | string | `recurring` or `topup`. | | `wallets[].credits` | number | Credits currently attributed to this wallet. May be fractional. | | `wallets[].next_refresh_date` | string \| null | Next refresh of the recurring wallet. `null` on the top-up wallet. | | `wallets[].expires_at` | string \| null | When the top-up wallet's credits expire. `null` on the recurring wallet. | Gate spending decisions on `account.credits` — it is the balance that controls API access. `wallets` is informational: during brief settlement windows the wallet rows may not sum exactly to `account.credits`. If your account does not have credit wallets, the `wallets` key is absent and the response is unchanged. ## Top-up breakdown Accounts with live top-ups also receive a `credit_topups` list — one entry per top-up still holding credits, sorted soonest expiry first. Use it to see how much of each top-up remains and when it expires. ```json Response excerpt (live top-ups) { "account": { "credit_topups": [ { "type": "granted", "granted_credits": 2000.0, "remaining_credits": 2000.0, "granted_at": "2026-08-13T00:00:00+00:00", "expires_at": "2026-11-13T00:00:00+00:00" }, { "type": "purchase", "granted_credits": 130000.0, "remaining_credits": 125329.25, "granted_at": "2026-07-31T23:59:59Z", "expires_at": "2027-07-31T23:59:59Z" } ] } } ``` | Field | Type | Description | | ---------------------------------- | -------------- | ---------------------------------------------------------------------------------- | | `credit_topups[].type` | string | How the top-up entered your account: `purchase` (one-time purchase), `auto_topup` (automatic reload), or `granted` (added by Crustdata). | | `credit_topups[].granted_credits` | number | Credits this top-up originally added. | | `credit_topups[].remaining_credits`| number | Credits from this top-up still unspent. May be fractional. | | `credit_topups[].granted_at` | string | ISO timestamp the top-up was added. | | `credit_topups[].expires_at` | string \| null | When this top-up's unspent credits expire. `null` for top-ups without an expiry. | Credits expiring soonest are always spent first, so the first entry shrinks before the others. `credit_topups` is informational like `wallets` — the key is absent when your account has no live top-ups, and the rest of the response is unchanged. ## Errors | Status | Body | Cause | | ------ | --------------------------------------------- | -------------------------------------------- | | `401` | `{ "message": "Missing API key in request" }` | No `Authorization` header. | | `401` | `{ "message": "Invalid API key in request" }` | `Authorization` header carries an invalid key. | | `400` | `{ "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. ..." } }` | No `x-api-version` header. | | `429` | — | More than 300 requests per minute. | ## Per-call usage: the `X-Credits-Used` header Every response from the data API endpoints — search, enrich, identify, autocomplete, web, and batch — includes an `X-Credits-Used` header reporting the exact credits that request deducted: ```bash Request curl -i --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"filters": {"field": "basic_profile.name", "type": "(.)", "value": "Jane"}, "limit": 1}' ``` ```http Response headers HTTP/2 200 x-credits-used: 0.03 ``` The value is an exact decimal (for example `3`, `0.09`). Endpoints that don't consume credits report `0`, and the header also appears on error responses, so you can log it on every call. When reconciling header values against your balance: - Balance deduction rounds a fractional cost up to the next whole credit, so header values summed across requests can be slightly less than your balance change. - Asynchronous work is billed when the job runs, not at submission: batch job submissions and background-job searches report `0`. - Responses generated before a request reaches the API — for example a `429` from rate limiting — do not carry the header. ## Auto top-up Auto top-up automatically buys more credits when your balance runs low, so your requests keep working instead of failing at zero. You set it up in your dashboard. Auto top-up is configured in your [dashboard](https://app.crustdata.com/) ### Turn it on Go to the **Credits** page in your [dashboard](https://app.crustdata.com/). Under **Auto top-up**, click **Turn on**. Add a card and authorize automatic charges. Choose the balance **threshold** that triggers a top-up and the **reload amount** to add each time, then click **Save**. ### Settings - **Threshold:** when your balance falls below this number of credits, a top-up runs. - **Reload amount:** how many credits to add each time. Set this comfortably above your threshold so one top-up gives you real runway before the next one. - **Monthly limit** (optional): the most credits Auto top-up can add in a calendar month. Once you reach it, top-ups pause until the next month. - **Notification emails:** the addresses that receive the Auto top-up notices. Each top-up is charged to your saved card at your standard credit rate, including any volume discount. See [Pricing](/general/pricing) for rates. The exact charge is shown before you save. ### How you're charged Each top-up charges your saved card automatically and adds the credits as soon as the payment succeeds. You get an email receipt, and the invoice appears in your **Invoices** tab. ### If a payment fails If a charge is declined, we retry it. If it keeps failing, we turn off Auto top-up and email you so you can update your card. ### Turn it off or change your card Turn off Auto top-up any time from the Credits page. Use **Replace card** to change the card. Removing your card also turns off Auto top-up. ## Per-key monthly limit Account credits are shared, but a workspace admin can cap how many of them an individual API key spends each month. Set the cap on the **API Keys** page in your [dashboard](https://app.crustdata.com/). It applies to that one key and resets on the 1st of each month (UTC), and your other keys keep drawing on the account balance as usual. Use it for a key you hand to a teammate, a customer, or an unattended job, so one key cannot spend the whole balance. The cap covers the endpoints that consume credits. ### When the cap is reached Calls made with that key return `402` until the cap resets or an admin raises it: ```json { "error": { "type": "insufficient_credits", "message": "This API key's monthly credit limit has been exhausted. It resets on Aug 01, 2026 (UTC). A workspace admin can raise or disable the limit in the dashboard.", "metadata": [] } } ``` The status and `error.type` are the same as an account-level out-of-credits response, so a client that already handles `402` needs no new code. Only the message differs, which is how you tell the two apart in a log. The reset sentence appears when a reset date is known. Raising or removing the cap takes effect on the next call. ### Stopping a key Set a key's monthly limit to `0` to block it from the next call onward. It stays blocked until an admin raises the limit — it does not start working again on the 1st of the month. ## What to do next - **Understand charges** — see [Pricing](/general/pricing) for per-endpoint credit costs. - **Avoid 429s** — review [Rate limits](/general/rate-limits) when polling at scale. - **Check API access** — see [Permissions](/general/permissions) for which endpoints and fields your account can use. # Permissions Source: https://docs.crustdata.com/general/permissions List every Crustdata API endpoint with your account's access status, enabled and disabled response fields, and effective rate limits — in one free GET request. Use this endpoint to see exactly what your API key can do: which endpoints your account can call, which response fields each returns, and the rate limit that applies to you per endpoint. It is read-only — to change your access, contact your Crustdata account manager. This endpoint is **free** — checking your permissions does not consume any credits. It is rate limited to 300 requests per minute. ## Endpoint ``` GET https://api.crustdata.com/account/endpoints ``` Authenticate with your API key in the `Authorization` header and send the required `x-api-version: 2025-11-01` header — requests without it return `400`. Permissions are account-wide: every API key on your account starts with the same access. An admin can narrow an individual key further — see [Per-key endpoint access](#per-key-endpoint-access). ## Filter, or the response is hard to read Unfiltered, this endpoint returns all 32 product endpoints, and every entry lists each response field your account can and cannot receive as a dot-path string. That is 1,929 field strings in total, and `/company/enrich` alone contributes 558 enabled and 58 disabled. As compact JSON on a single line the whole payload is around 76 KB, which is why `--format json` on the [CLI](/for-agents/cli) gives you a wall of text. Start with a single endpoint to learn the shape: ```bash curl --request GET \ --url 'https://api.crustdata.com/account/endpoints?path=/web/enrich/live' \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` That entry is five fields long and fits on a screen. Then widen with `category` or `status`. If you do want everything, pipe it through `jq` and drop the field lists: ```bash curl -s --request GET \ --url 'https://api.crustdata.com/account/endpoints' \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' \ | jq -r '.endpoints[] | [.status, .effective_rate_limit_rpm, .category, .path] | @tsv' ``` ## Query parameters All parameters are optional filters and combine with AND. A filter that matches nothing returns `200` with an empty `endpoints` array, not a `404`. | Parameter | Example | Effect | | ---------- | ----------------------- | ------------------------------------------------------------- | | `path` | `?path=/company/enrich` | Exact endpoint-path match. Case-sensitive, leading slash required, and a prefix such as `/company` matches nothing. | | `category` | `?category=Company` | Endpoints in that product category. Case-insensitive. URL-encode the space and ampersand in `LinkedIn & Social`. | | `status` | `?status=enabled` | Only `enabled` or `disabled` endpoints. Case-sensitive; any other value returns `400`. | ## Example request ```bash Request curl --request GET \ --url 'https://api.crustdata.com/account/endpoints?path=/company/enrich' \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```python Python import requests resp = requests.get( "https://api.crustdata.com/account/endpoints", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, ) for endpoint in resp.json()["endpoints"]: print(endpoint["path"], endpoint["status"], endpoint["effective_rate_limit_rpm"]) ``` ```json Response { "api_version": "2025-11-01", "token_limit_rpm": null, "endpoints": [ { "path": "/company/enrich", "category": "Company", "status": "enabled", "effective_rate_limit_rpm": 15, "fields": { "enabled": ["basic_info", "basic_info.all_domains", "basic_info.company_type", "..."], "disabled": ["hiring.recent_openings", "social_posts", "..."] } } ] } ``` The `enabled` and `disabled` lists are truncated above. In the real response this one entry carries 558 enabled and 58 disabled field paths. ## Response fields | Field | Type | Description | | ------------------------------------ | -------------- | -------------------------------------------------------------------------------------------------- | | `token_limit_rpm` | number \| null | Aggregate requests-per-minute ceiling for your API key across all endpoints. `null` when no account-wide ceiling is set, which is the usual case; the per-endpoint limit applies instead. | | `endpoints[].path` | string | Endpoint path, as you would call it. | | `endpoints[].category` | string | Product category: `Company`, `Person`, `Jobs`, `Web`, `LinkedIn & Social`, or `Other`. | | `endpoints[].status` | string | `enabled` — your account can call it; `disabled` — it requires an upgrade. | | `endpoints[].effective_rate_limit_rpm` | number \| null | The requests-per-minute limit that applies to your account for this endpoint. | | `endpoints[].fields.enabled` | string[] | Response fields your account receives from this endpoint. | | `endpoints[].fields.disabled` | string[] | Response fields withheld from your account. | ## How to read the field lists - On an **enabled** endpoint, `fields.disabled` lists the specific fields your plan does not include — the endpoint works, but those fields are omitted from its responses. - On a **disabled** endpoint, `fields.enabled` is empty and `fields.disabled` lists everything the endpoint can return — the full set you would unlock by enabling it. - **Batch endpoints** (`/batch/...`) list the fields of the job envelope they return (`batch_id`, `status`, `status_url`, and so on), not the fields of the records the job produces. Read the matching non-batch endpoint's entry for those. A batch endpoint with no field-gated payload lists nothing at all. - Field names are dotted paths (for example `basic_info.company_type`) matching the response structure of the endpoint. Both lists are sorted, and so is the `endpoints` array itself. ## Errors | Status | Body | Cause | | ------ | --------------------------------------------- | -------------------------------------------- | | `401` | `{ "message": "Missing API key in request" }` | No `Authorization` header. | | `401` | `{ "message": "Invalid API key in request" }` | `Authorization` header carries an invalid key. | | `400` | `{ "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. ..." } }` | No `x-api-version` header. | | `400` | `{ "error": { "type": "invalid_request", "message": "Account endpoints requires API version 2025-11-01. ..." } }` | Unsupported `x-api-version` value. | | `400` | `{ "error": "Invalid status 'x'; expected one of ['enabled', 'disabled']" }` | `status` filter outside `enabled`/`disabled`. | | `429` | — | More than 300 requests per minute. | ## Per-key endpoint access Account permissions set the ceiling for every key on the account. A workspace admin can restrict an individual key to a subset of those endpoints on the **API Keys** page in your [dashboard](https://app.crustdata.com/). A key with no restriction can call everything the account has enabled. Calling an endpoint the key is not allowed to use returns `403`: ```json { "error": { "type": "permission_error", "message": "This API key does not have access to /person/search. A workspace admin can update the key's endpoint access in the dashboard.", "metadata": [] } } ``` The status and `error.type` match an account-level permission failure, so clients need no new handling. The message names the endpoint that was refused. ## What to do next - **Check your balance** — see [Credits](/general/credits) before large batches. - **Understand per-endpoint limits** — see [Rate limits](/general/rate-limits). - **See what fields cost** — review [Pricing](/general/pricing) for credit costs per endpoint. # llms.txt Source: https://docs.crustdata.com/for-agents/llms A machine-readable index of the Crustdata documentation that AI tools and agents use to read the docs efficiently. `llms.txt` is a machine-readable index of this documentation, written for large language models and AI agents. It maps every page to a one-line summary so a model can pull the right reference directly instead of crawling the whole site. The docs expose three machine-readable tiers — pick by how much context you have: | Tier | URL | Size | Use it to | | ---- | --- | ---- | --------- | | Index | [`/llms.txt`](https://static-assets.crustdata.com/llms.txt) | ~35 KB | Discover pages and pick what to read | | Page | append `.md` to any docs URL (e.g. [`/general/pricing.md`](/general/pricing.md)) | 5–50 KB each | Fetch exactly the pages you need | | Full corpus | [`/llms-full.txt`](https://static-assets.crustdata.com/llms-full.txt) | ~1.5 MB | Ingest everything at once (RAG indexing, local search) — larger than most single model contexts by design | ## What's in it The index groups every page by product area, mirroring this site: | Section | Contents | | ------- | -------- | | Getting started | Introduction, Pricing, Rate limits, Credits, and the MCP server | | Company, Person, Job, Web, Social Post | Each API area's guides and reference | | Migration guides | Legacy endpoint → current replacement mappings | | API reference | Every endpoint in the `2025-11-01` version | ## Use it with an AI tool Give your AI tool or agent the URL `https://static-assets.crustdata.com/llms.txt` — the complete index, covering everything listed above including the migration guides and the full API reference. Tools that support the `llms.txt` convention can also auto-discover `/llms.txt` on the docs domain, but that copy lists only a subset of pages, so paste the complete index URL when you can. The model reads the index, then fetches only the pages it needs as Markdown — keeping answers accurate and context small. ## What to do next - **Read the index** — open [`llms.txt`](https://static-assets.crustdata.com/llms.txt) to see every page and its summary. - **Connect an assistant** — the [MCP server](/for-agents/mcp) lets Claude, Cursor, and other clients query Crustdata in natural language. - **Start from the top** — the [Introduction](/general/introduction) maps the five API areas. # MCP Server Source: https://docs.crustdata.com/for-agents/mcp Connect Crustdata's B2B data platform to Claude and other AI assistants via MCP (Model Context Protocol). The Crustdata MCP server gives AI assistants direct access to 800M+ professional profiles and 200M+ companies. Connect it to Claude.ai, Claude Code, Cursor, or any MCP-compatible client and search, enrich, and analyze B2B data through natural language. **New** — this is our latest MCP server, and we'd love for you to try it. - **Ask in plain English.** No query language, no endpoints to learn — just describe what you want. - **Complex asks, one step.** A request like *"find AI startups hiring engineers, then get me the founders' emails"* runs start to finish in a single step. - **Know the cost, instantly.** Every request reports the exact credits it spent, in real time — no surprises. - **Connect in seconds.** Sign in with your Crustdata account — there's no API key to copy or paste. ## Quick start In Claude, open **Customize** → **Connectors** → **Add custom connector**. - **Name:** Crustdata - **URL:** `https://install.crustdata.com/mcp` Click **Add** to save. Open the new connector under **Not connected** and click **Connect**, then sign in with your Crustdata account. There's no API key to copy or paste — signing in authorizes the connection. No account yet? [Sign up at crustdata.co](https://crustdata.co). Try one of the example prompts below. ## Example prompts Once connected, you can ask Claude questions like: | Prompt | What it does | |--------|--------------| | "Find software engineering jobs at Anthropic" | Searches the job database with filters | | "What jobs does Stripe have open right now?" | Fetches live job listings in real-time | | "Find the profile for jane.doe@acme.com" | Reverse lookup by business email | | "Show me AI startups with 100-500 employees" | Searches the company database with a headcount filter | | "Who are the engineering leaders at Anthropic?" | Searches people by seniority and company | | "Get me the email for this person's profile" | Enriches a person profile with contact info | | "Compare job openings at Anthropic, OpenAI, and Cohere" | Batch job search across multiple companies | | "What is Anthropic posting about on social media?" | Fetches recent social media posts | | "How many credits do I have?" | Checks your account balance | ## What you can do Find and enrich companies by industry, headcount, location, funding, and more — with full firmographic profiles. Search people by title, seniority, company, and skills, then enrich profiles with business emails, personal emails, and phone numbers. Search millions of job listings with filters and breakdowns, or pull a company's live openings on demand. Track recent social posts by person, company, or keyword, and search and read content from across the web. Credit costs vary by request and plan. See [Pricing](/general/pricing) for per-request credit costs and [Rate limits](/general/rate-limits) for limits. ## Cost transparency Every request reports the **exact credits it spent, in real time** — measured from what the request actually did, never estimated. Enrichment always takes the cheapest correct path, so you never over-spend by accident. ## Sign-in Sign in with your Crustdata account when you connect — there's **no API key to copy or paste**. Your key is never stored, and you can disconnect any time from your MCP client. ## Programmatic access You can use the same server from your own code — agent frameworks, backend jobs, or anywhere the sign-in flow isn't practical. Connect any MCP client to the endpoint and pass your Crustdata API key as a bearer token: - **Endpoint:** `https://install.crustdata.com/mcp` - **Header:** `Authorization: Bearer YOUR_API_KEY` For MCP clients that take a JSON config with custom headers: ```json { "mcpServers": { "crustdata": { "type": "streamable-http", "url": "https://install.crustdata.com/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` ### Catalog resources The server publishes its own usage guide as MCP resources — markdown pages an agent can read directly instead of re-deriving the tool list and schemas each session. Read `crustdata://catalog` first; it carries the must-know rules and an index of the sections: | Resource | What's in it | |----------|--------------| | `crustdata://catalog` | Start here — the must-know rules and an index of the sections below | | `crustdata://catalog/quickstart` | How to drive the server: writing a script that composes tool calls | | `crustdata://catalog/tools` | Every callable tool with its one-line purpose and cost | | `crustdata://catalog/query-builder` | Filters, operators, and matching behavior | | `crustdata://catalog/results` | Shaping results: `project`, `compact`, `paginate`, `parallelMap` | | `crustdata://catalog/fields` | Common filter columns and identifiers per tool | | `crustdata://catalog/patterns` | Recipes: search → enrich, identify → enrich, bulk, pagination | | `crustdata://catalog/ranking` | Bounding result pages server-side and ranking in code | | `crustdata://catalog/cost` | Credit pricing and the cheapest-path rules | | `crustdata://catalog/full` | The entire guide in one read | Any MCP client can fetch these with a standard resource read — `session.read_resource("crustdata://catalog")` in the Python SDK, or `client.readResource({ uri: "crustdata://catalog" })` in TypeScript. ## Common workflows **Research a company** — identify the company, enrich it with the fields you care about, and check its recent social activity. **Find decision-makers** — search people by company, seniority, and title, then enrich for contact info. **Hiring analysis** — search jobs by company, title, and location, and break the results down by category or location. **Competitive intelligence** — compare openings across competitors and track their social activity. ## Support For help or feedback, contact [support@crustdata.co](mailto:support@crustdata.co). # Claude Code plugin Source: https://docs.crustdata.com/for-agents/claude-plugin Connect Crustdata to Claude Code with one command: MCP data tools covering 800M+ people profiles and 200M+ companies, ready-to-run skills, and one sign-in. The Crustdata plugin for Claude Code bundles the [MCP server](/for-agents/mcp) and ready-to-run skills into a single install. One command adds the data tools (800M+ people profiles and 200M+ companies) and the skills; sign in once and you're set. This installs Crustdata in **Claude Code**. For **Claude.ai** or **Claude Desktop**, add the [MCP connector](/for-agents/mcp) instead. - **One command to install.** The data tools and the skills come together, no config to hand-write. - **Skills, ready to run.** They install with the plugin and appear under the `/crustdata` namespace. - **Sign in once.** No API key to copy or paste; the tools and the skills share a single sign-in. ## Quick start Add the Crustdata marketplace and install the plugin: ```bash claude plugin marketplace add crustdata/skills claude plugin install crustdata@crustdata-plugin ``` This adds the Crustdata MCP server and the skills. Run **`/crustdata:login`** and your browser opens for a one-time sign-in, no key to paste. Until you sign in, Crustdata tool calls simply point you at the command; nothing nags you at session start. Prefer a key? Set `CRUSTDATA_API_KEY` in your environment instead and skip the sign-in. No account yet? [Sign up at crustdata.co](https://crustdata.co). Ask Claude a question, or run a skill from the `/crustdata` menu. Try one of the example prompts below. ## Example prompts Once installed, you can ask Claude questions like: | Prompt | What it does | |--------|--------------| | "Find AI startups hiring engineers, then get me the founders' emails" | Search, then enrich, in a single step | | "Who are the engineering leaders at Anthropic?" | Searches people by seniority and company | | "What jobs does Stripe have open right now?" | Fetches live job listings | | "Enrich this company with headcount and funding" | Pulls a full firmographic profile | | "How many credits do I have?" | Checks your account balance | ## What's included The full Crustdata [MCP server](/for-agents/mcp): search and enrich companies, people, and jobs, and read social and web content, in plain English. Ready-made workflows install with the plugin and appear under `/crustdata`, so common tasks run end to end. The data tools and the skills share a single sign-in. No API key to copy or paste. Credit costs vary by request and plan. See [Pricing](/general/pricing) for per-request credit costs and [Rate limits](/general/rate-limits) for limits. ## Sign-in Run **`/crustdata:login`** to sign in with your Crustdata account. It's a one-time browser sign-in, no API key to copy or paste. The data tools and the skills share that single sign-in, tokens refresh silently, and if it ever expires a Crustdata tool call points you back at the command. `CRUSTDATA_API_KEY` works as an override if you'd rather pass a key directly. ## Support For help or feedback, contact [support@crustdata.co](mailto:support@crustdata.co). # Crustdata CLI Source: https://docs.crustdata.com/for-agents/cli The Crustdata API from your terminal: search, identify, and enrich companies and people, run bulk jobs, and pipe clean JSON into anything. The `crustdata` CLI puts the full API in your terminal: every endpoint is a typed command with real flags. It renders markdown tables when you're looking and raw JSON when you're piping, so the same commands serve you, your scripts, and your agents. ## Quick start ```bash curl -fsSL https://static-assets.crustdata.com/cli/install.sh | sh ``` On Windows: ```powershell irm https://static-assets.crustdata.com/cli/install.ps1 | iex ``` The installer verifies checksums, puts the binary in `~/.crustdata/bin`, and adds that directory to your PATH (set `CRUSTDATA_NO_MODIFY_PATH=1` to skip). Open a new terminal, then upgrade later with `crustdata upgrade`. ```bash crustdata auth login ``` Opens your browser: sign in, pick your team, and the CLI stores the resulting API key. No browser on the machine? `--no-browser` prints the sign-in URL and asks you to paste the redirect back. For CI and scripts, pipe a key in with `crustdata auth login --with-token < key.txt`, or set `CRUSTDATA_API_KEY`. ```bash crustdata company enrich --domains stripe.com --fields basic_info,headcount ``` ## Commands | Command | What it does | |---------|--------------| | `company search / identify / enrich / reviews / suggest` | Firmographics, employee reviews, field suggestions | | `person search / enrich / contacts / dev / suggest` | Profiles, contact details, dev-platform data | | `job search / suggest` | Job listings from the indexed dataset | | `social-post search / fetch` | Social posts by keyword, person, or company | | `web search / fetch` | Real-time web search and page fetch | | `batch ` | Bulk jobs: submit, `--wait`, download JSONL results | | `account credits / endpoints` | Balance and rate limits | | `auth login / logout / status / token` | Authentication (browser sign-in or API key) | | `docs / docs read / docs fs` | Search and read these docs from the terminal | | `mcp install / list / remove` | Wire the Crustdata MCP into agent clients | | `upgrade` | Update the CLI in place (checksum-verified) | Add `--live` to query the professional network in real time instead of the indexed dataset. Four commands take it: `company search`, `person search`, `person enrich`, and `job search`. ## Filters Search commands take repeatable `--filter 'field op value'` expressions that AND together: ```bash crustdata company search \ --filter 'headcount.total > 100' \ --filter 'locations.country in United States,Canada' \ --limit 25 ``` Operators come straight from the API: `= != < > =< => in not_in (.) [.]`. `in` and `not_in` take a comma-separated list. Field names are the API's dot-paths, so an unknown one comes back as `invalid_request: Unsupported columns in conditions`. Check them with `--dry-run`, tab completion, or the field tables in the API reference. For `or` logic and nested groups, pass a full JSON body with `--input query.json` (or `--input -` for stdin). Flags override matching body fields. Every request command supports `--dry-run` (print the composed body without sending it, at no cost) and `--example` (print real request examples from the API spec). Tab completion knows the filter fields: run `crustdata completion zsh` to set it up. ## Output - On a terminal: markdown tables. Piped: raw response JSON. Force either with `--format json|md`. - Filter JSON inline with the built-in `--jq` (no jq install needed): `crustdata account credits --jq .account.credits` - stdout carries data only; progress and hints go to stderr. Exit codes: `0` success, `1` failure (API error, bad flags, or a missing argument), `4` auth error. - Nothing auto-paginates, because pages cost credits. Follow `next_cursor` explicitly. ## Examples Every example below runs as written (tested against v0.2.5). ### Companies ```bash # Enrich one company crustdata company enrich --domains stripe.com --fields basic_info,headcount # Pull a single value out of the enrichment crustdata company enrich --domains stripe.com --fields basic_info \ --jq '.[0].matches[0].company_data.basic_info.name' # Resolve a name to a company record crustdata company identify --names "Stripe" --exact-match # Indexed search: filters AND together crustdata company search \ --filter 'headcount.total > 10000' \ --filter 'locations.country in United States' \ --limit 2 # OR logic lives in a JSON body; flags still override fields like limit echo '{"filters":{"op":"or","conditions":[ {"field":"basic_info.primary_domain","type":"=","value":"stripe.com"}, {"field":"basic_info.primary_domain","type":"=","value":"openai.com"} ]}}' | crustdata company search --input - --limit 3 --jq .total_count # Live search against the professional network (note: different field names, page required) crustdata company search --live --filter 'REGION in United States' --page 1 # Employee reviews for a company crustdata company reviews --crustdata-company-id 631394 # Suggest valid values for a search field crustdata company suggest country 'Uni' ``` ### People ```bash # Indexed person search crustdata person search \ --filter 'basic_profile.location.country = United States' --limit 2 # Live person search: professional-network vocabulary, page required crustdata person search --live \ --filter 'CURRENT_TITLE in CEO' \ --filter 'REGION in United States' --page 1 # Enrich a profile crustdata person enrich \ --professional-network-profile-urls https://www.linkedin.com/in/dvdhsu/ # Contact details (fields use dotted paths) crustdata person contacts \ --professional-network-profile-urls https://www.linkedin.com/in/dvdhsu/ \ --fields contact.business_emails # Dev-platform profile from a GitHub URL crustdata person dev --profile-url https://github.com/rajnandan1 # Suggest values for a person search field crustdata person suggest basic_profile.city 'San' ``` ### Jobs, posts, and the web ```bash # Fuzzy-match job titles with the (.) operator crustdata job search --filter 'job_details.title (.) engineer' --limit 2 # Social posts by keyword (supports OR/AND, up to 6 terms) crustdata social-post search --keyword 'series B' --limit 2 # A company's recent posts crustdata social-post fetch --company-domain stripe.com --limit 2 # Search the web in real time crustdata web search --query 'crustdata' ``` ### Account and housekeeping ```bash # Credit balance as a bare number crustdata account credits --jq .account.credits # Preview a bulk submission without spending anything crustdata batch person contacts \ --professional-network-profile-urls https://www.linkedin.com/in/dvdhsu/ \ --fields contact.business_emails --dry-run # Update the CLI in place crustdata upgrade ``` ### Dry runs, spec examples, and completion Nothing in this group spends credits or needs auth, except live value completion. ```bash # See exactly what --filter compiles into, without sending it crustdata company search --dry-run \ --filter 'headcount.total > 10000' \ --filter 'locations.country in United States' --limit 5 # Live mode composes the flat array shape instead of the condition tree crustdata person search --dry-run --live \ --filter 'CURRENT_TITLE in CEO' --page 1 # Watch flags override an --input body echo '{"limit":100,"cursor":"abc"}' | \ crustdata company search --input - --limit 5 --dry-run # List the spec's ready-made request bodies, then print one crustdata company search --example crustdata company search --example=recently_funded_us_companies # Chain: spec example as the body, your flags on top, preview before sending crustdata company search --example=recently_funded_us_companies > /tmp/q.json crustdata company search --input /tmp/q.json --limit 3 --dry-run ``` Tab completion, one-time setup per shell: ```bash source <(crustdata completion zsh) # or bash / fish / powershell crustdata company search --filter 'head # headcount.* fields from the spec vocabulary crustdata company search --live --filter ' # vocabulary flips to REGION, COMPANY_HEADCOUNT, ... crustdata company search --filter 'headcount.total # the 10 operators ``` Completion covers field names and operators. For the values themselves, ask the API directly with `crustdata company suggest country 'Uni'` or `crustdata person suggest basic_profile.city 'San'`. Named examples need the `=` form (`--example=name`); bare `--example` lists what's available. ## Bulk jobs ```bash crustdata batch person enrich --input people.json --wait --download ``` Submits the job, polls until it completes, and downloads the results as decompressed JSONL. Or fire-and-forget and come back later with `crustdata batch status ` and `crustdata batch download `. ## Search the docs The CLI reads these docs over their public MCP server, so answers arrive where you're already working. No authentication needed: ```bash crustdata docs "how do credits work" # full-text search with page links crustdata docs read general/rate-limits # print a whole page crustdata docs fs -- tree /api-reference # browse the docs as a filesystem ``` ## Set up the Crustdata MCP `crustdata mcp install` wires the hosted [Crustdata MCP server](/for-agents/mcp) into your agent clients: ```bash crustdata mcp install # interactive picker crustdata mcp install claude-code # installs the Crustdata plugin (MCP + skills) crustdata mcp install cursor windsurf crustdata mcp list # per-client install state and auth mode crustdata mcp remove cursor # deletes only the crustdata entry ``` Supported targets: `claude-code` (plugin by default, `--mcp-only` for the bare server), `cursor`, `vscode`, `windsurf`, `claude-desktop` (via the mcp-remote bridge), and `claude-web` (printed manual steps, since connectors are account-level). Config edits are merge-preserving — only the `crustdata` entry is ever touched, and `--dry-run` prints the exact change without writing anything. Auth defaults to OAuth on first connect inside each client. For headless setups, `--with-key` bakes your stored API key into the client config (plaintext, with a loud warning), and `--with-key=env` writes a `${env:CRUSTDATA_API_KEY}` reference instead. ## For scripts and agents Piped output is always raw JSON, with no TTY-detection surprises. `--jq` runs everywhere without dependencies, and `--help` on any command includes runnable examples. Agents can explore safely: `--dry-run` composes any request without spending credits. ### Non-interactive authentication The browser flow only triggers on a real terminal — when stdin is a pipe, the CLI never opens a browser, and the paste prompt only appears when you give it nothing else, so agents never hang on a hidden prompt. Pick one of three paths: ```bash # 1. Environment variable: no login step, nothing written to disk. # Best for ephemeral sandboxes. Beats the config file when both exist. export CRUSTDATA_API_KEY=cd_xxx # 2. Stdin: stores the key in the config file, keeps it out of shell history echo "$KEY" | crustdata auth login --with-token # 3. Inline flag: one-shot, but the key lands in shell history crustdata auth login --token cd_xxx ``` `auth login` validates the key against the API before saving and exits with code `4` on a bad key, so a broken credential fails at setup instead of mid-task. Check state anytime with `crustdata auth status`. # Company Search Source: https://docs.crustdata.com/company-docs/search/introduction Learn how to search for companies using structured filters, from simple domain lookups to multi-filter queries. **Use this when** you want to explore a market, build a target account list, or segment companies by criteria like geography, industry, revenue, funding, or headcount. The Company Search API lets you find companies by domain, country, industry, revenue, funding, headcount, and more. This page walks you through the basics: your first search, the response shape, and combining filters, then folds in worked example recipes you can copy, paste, and adapt. For the operator list, field catalog, and validation rules, see [Search reference](/company-docs/search/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/company/search ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Default | Description | | --------- | -------- | -------- | ---------- | ------------------------------------------------------------------------------------ | | `filters` | object | No | — | A single filter condition or a nested `and`/`or` group. Omit to match all companies. | | `fields` | string[] | No | all fields | Dot-path fields to include in each company object. Always specify in production. | | `sorts` | object[] | No | — | Sort rules. Each has `field` (dot-path) and `order` (`asc` or `desc`). | | `limit` | integer | No | `20` | Companies per page. Max: `1000`. | | `cursor` | string | No | — | Pagination cursor from a previous response. | **Looking for the list of fields you can filter on?** See [Searchable fields](/company-docs/search/reference#searchable-fields) in the search reference for the full table of `filters.field` values (with sortable flags). Use [Autocomplete](/company-docs/autocomplete/introduction) to discover valid *values* for fields like `basic_info.industries`, `taxonomy.professional_network_industry`, or `locations.country`. ### Response body | Field | Type | Description | | ------------- | --------------- | ----------------------------------------------------------- | | `companies` | array | Matching company records with requested `fields`. | | `next_cursor` | string or null | Cursor for the next page. `null` when no more pages. | | `total_count` | integer or null | Total matching companies (may be `null` for broad queries). | ### Rate limits and credits Pricing: 0.03 credits per result returned. A request with no results does not consume credits. - **Rate limit:** 30 requests per minute. Search results are intentionally lightweight so you can explore and segment companies at a low credit cost. When you need the full company profile, use [Company Enrich](/company-docs/enrichment/introduction). `or`/nested logic, well-funded-by-country, recently founded. Cursor-based pagination and sort rules for stable ordering. Operators, searchable fields, response fields, validation, errors. --- ## Your first search: find a company by domain The simplest search finds a company by its exact primary domain. You pass a single filter with the `=` operator. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_info.primary_domain", "type": "=", "value": "retool.com" }, "limit": 1, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "basic_info.year_founded", "headcount.total", "locations.country", "funding.total_investment_usd" ] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "year_founded": 2017 }, "headcount": { "total": 443 }, "locations": { "country": "USA" }, "funding": { "total_investment_usd": 141000000.0 } } ], "next_cursor": "H4sIAMWlBWoC_xXMMQ7DIAxA0atEzB2MDYb0KlUVgTHKUBU1IUMV5e6l6_vSP83n0O27rGlfzX0yORcIzqGr3iolmIWkAGCiEH0pGB2j44pBVLmCEFPNA0AtxiRgbpPZ29bH68FEfqbnkN56ei3Sjvc_2OsHdWUiVXYAAAA=", "total_count": 1 } ``` ### Understanding the response Every search response has three fields: - **`companies`** — an array of matching company records. Each record contains the fields you requested in `fields`. - **`next_cursor`** — a pagination token. Pass it in the next request to get the next page. `null` means there are no more pages. - **`total_count`** — how many companies match your filters across the full database (may be `null` for very broad queries). ### How to interpret results - **`next_cursor` is `null`:** You have reached the last page. No more results. - **`total_count` is `null`:** The exact count is too expensive to compute for this query. Use `next_cursor` to determine if more pages exist. - **Empty `companies` array:** No companies matched your filters. Broaden your filters or check values with [Autocomplete](/company-docs/autocomplete/introduction). ### Controlling which fields come back The `fields` parameter lets you pick exactly which fields to include. This keeps your responses small and focused. If you omit `fields`, the API returns all available fields for each company. --- ## Combine filters with `and` Real searches need more than one criterion. Wrap multiple conditions inside an `op: "and"` group to require all of them. This search finds software development companies headquartered in the USA, sorted by headcount (largest first). ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" }, { "field": "locations.country", "type": "in", "value": ["USA"] } ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country" ] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 6034577, "basic_info": { "name": "Amazon", "primary_domain": "aboutamazon.com" }, "headcount": { "total": 763362 }, "locations": { "country": "USA" } }, { "crustdata_company_id": 4926893, "basic_info": { "name": "Google", "primary_domain": "goo.gle" }, "headcount": { "total": 335569 }, "locations": { "country": "USA" } } ], "next_cursor": "H4sIADxxqGkC_w3MMQ7CMAwF0KtEnjsksZ...", "total_count": null } ``` Response trimmed for clarity. The key difference from the first example: instead of a single `filters` object, you now have a group with `op: "and"` and a `conditions` array. Every condition must match for a company to be included. For `or` and nested logic, or for finding well-funded or recently founded companies, see the [Examples](#examples) below. To walk through large result sets, see [Pagination and sorting](/company-docs/search/reference#paginate-through-results). --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request. For the core walkthrough (first search, combining filters with `and`), see the sections above. For the operator list, field catalog, and validation rules, see [Search reference](/company-docs/search/reference). Use `op: "or"` when a company should match **any** condition. You can also nest `and`/`or` groups for complex queries. This search finds companies that are either in the software development industry or have over $5M in total investment, AND are headquartered in the USA. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "op": "and", "conditions": [ { "op": "or", "conditions": [ { "field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" }, { "field": "funding.total_investment_usd", "type": ">", "value": 5000000 } ] }, { "field": "locations.country", "type": "=", "value": "USA" } ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": [ "crustdata_company_id", "basic_info.name", "headcount.total", "taxonomy.professional_network_industry", "funding.total_investment_usd" ] }' ``` The outer `and` requires both conditions: the inner `or` matches either software development companies or well-funded companies, and the outer condition restricts to US-headquartered companies. This is a common pattern for sales and investor research: find companies in a specific market with significant funding. This search finds US-based companies with over $10M in total investment, sorted by funding (highest first). ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "locations.country", "type": "in", "value": ["USA"] }, { "field": "funding.total_investment_usd", "type": ">", "value": 10000000 } ] }, "sorts": [{"field": "funding.total_investment_usd", "order": "desc"}], "limit": 2, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "locations.country", "funding.total_investment_usd", "headcount.total" ] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 6035590, "basic_info": { "name": "VMware", "primary_domain": "broadcom.com" }, "locations": { "country": "USA" }, "funding": { "total_investment_usd": 100000000000.0 }, "headcount": { "total": 11925 } }, { "crustdata_company_id": 631466, "basic_info": { "name": "OpenAI", "primary_domain": "openai.com" }, "locations": { "country": "USA" }, "funding": { "total_investment_usd": 79000120000.0 }, "headcount": { "total": 7397 } } ], "next_cursor": "H4sIAC1xqGkC_w3MOw7CMAwA0KtEmTv4E...", "total_count": null } ``` Response trimmed for clarity. Combine estimated revenue bounds with the latest funding round type, date, and amount. You can also require an investor to appear in the company's funding history. This search finds companies whose estimated revenue range is entirely between $1M and $50M, whose latest round was Series A or Series B on or after 2025-01-01, whose latest round raised at least $5M, and that list Sequoia Capital as an investor. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "revenue.estimated.lower_bound_usd", "type": "=>", "value": 1000000}, {"field": "revenue.estimated.upper_bound_usd", "type": "=<", "value": 50000000}, {"field": "funding.last_round_type", "type": "in", "value": ["series_a", "series_b"]}, {"field": "funding.last_fundraise_date", "type": "=>", "value": "2025-01-01"}, {"field": "funding.last_round_amount_usd", "type": "=>", "value": 5000000}, {"field": "funding.investors", "type": "in", "value": ["Sequoia Capital"]} ] }, "sorts": [{"field": "funding.last_fundraise_date", "order": "desc"}], "limit": 1, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "revenue.estimated.lower_bound_usd", "revenue.estimated.upper_bound_usd", "funding.last_round_type", "funding.last_fundraise_date", "funding.last_round_amount_usd", "funding.investors" ] }' ``` `funding.investors` contains known investors across the company's funding history. Combining it with `funding.last_*` filters means the company has that investor and its latest round matches the other conditions. It does not mean that investor participated in the latest round. Use comparison operators like `>` and `<` on numeric or date fields. This search finds companies founded after 2020, sorted by headcount. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_info.year_founded", "type": ">", "value": 2020 }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.year_founded", "headcount.total", "locations.country" ] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 4069929, "basic_info": { "name": "Kendi İşim", "year_founded": 2023 }, "headcount": { "total": 181137 }, "locations": { "country": "TUR" } }, { "crustdata_company_id": 1038926, "basic_info": { "name": "Stellantis", "year_founded": 2021 }, "headcount": { "total": 114361 }, "locations": { "country": "NLD" } } ], "next_cursor": "H4sIAC9xqGkC_w3MMQ7CMAwF0KtEmTvEdkh...", "total_count": null } ``` Response trimmed for clarity. Filter on `roles.distribution.` to find companies by the number of employees in a specific function — for example, companies with a large engineering team. Combine it with an industry or location filter to narrow the result. ```bash Request — software companies with 1,000+ engineers curl --request POST \ --url https://api.crustdata.com/company/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": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" }, { "field": "roles.distribution.engineering", "type": ">", "value": 1000 } ] }, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total"], "sorts": [{ "field": "headcount.total", "order": "desc" }], "limit": 3 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Amazon", "primary_domain": "aboutamazon.com" }, "headcount": { "total": 771499 } }, { "basic_info": { "name": "Google", "primary_domain": "goo.gle" }, "headcount": { "total": 342901 } }, { "basic_info": { "name": "Microsoft", "primary_domain": "microsoft.com" }, "headcount": { "total": 231238 } } ], "next_cursor": "H4sIA...", "total_count": 276 } ``` Response trimmed for clarity. `roles.distribution.` is **filter-only** — you can filter on it, but it is not returned in the search response (requesting it in `fields` returns `400`). Select `headcount.total` or other returnable fields instead. The valid function names are listed in the [searchable fields reference](/company-docs/search/reference#searchable-fields). Filter on the dotted period path `headcount.growth_percent.6m` to find companies whose 6-month headcount growth exceeds a threshold. The same shape applies to `.1m`, `.3m`, `.12m` and to `headcount.growth_absolute.*` for absolute employee deltas. ```bash Request — mid-size companies with >15% 6-month growth curl --request POST \ --url https://api.crustdata.com/company/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": "headcount.growth_percent.6m", "type": ">", "value": 15 }, { "field": "headcount.total", "type": ">", "value": 200 }, { "field": "headcount.total", "type": "<", "value": 2000 } ] }, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total", "headcount.growth_percent"], "sorts": [{ "field": "headcount.total", "order": "desc" }], "limit": 2 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "IRT", "primary_domain": "irt.uy" }, "headcount": { "total": 1991, "growth_percent": { "1m": 3.0, "3m": 3.0, "6m": 17.26, "12m": 32.73 } } }, { "basic_info": { "name": "MS Office", "primary_domain": "msofice.org" }, "headcount": { "total": 1990, "growth_percent": { "1m": 29.22, "3m": 29.22, "6m": 32.76, "12m": 47.3 } } } ], "next_cursor": "H4sIAE5P...", "total_count": 4685 } ``` The dotted period paths (`headcount.growth_percent.6m`, etc.) are **filterable but not sortable** — passing one as a `sorts.field` returns `400 "Unsupported columns"`. Sort on `headcount.total` (or another sortable field) and use the growth filter to narrow the population. `/company/search` returns the growth map under period keys `1m`, `3m`, `6m`, `12m`. The same underlying data is also returned by [`/company/enrich`](/company-docs/enrichment/introduction), but with different keys (`mom`, `qoq`, `six_months`, `yoy`, `two_years`). Pass a known company's `crustdata_company_id` to `competitors.company_ids` (or its domains to `competitors.websites`) to pull the companies Crustdata tracks as its competitors. Here, competitors of OpenAI (`631466`). ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "competitors.company_ids", "type": "in", "value": [631466] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 3, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 15358, "basic_info": { "name": "Aprecomm", "primary_domain": "aprecomm.ai" }, "headcount": { "total": 75 } }, { "crustdata_company_id": 49618, "basic_info": { "name": "Automaton AI Infosystem Pvt. Ltd.", "primary_domain": "automatonai.com" }, "headcount": { "total": 46 } }, { "crustdata_company_id": 609348, "basic_info": { "name": "Cron AI", "primary_domain": "cronai.ai" }, "headcount": { "total": 37 } } ], "next_cursor": "H4sIABHRQWoC_x...", "total_count": 520 } ``` Response trimmed for clarity. `competitors.company_ids` and `competitors.websites` are **filter-only** — you can filter on them, but they are not returned in the response (passing one in `fields` returns `400 "Invalid fields"`). Select returnable fields like `headcount.total` instead. Filter on `funding.investors` to find every company an investor has backed. This search finds Sequoia Capital portfolio companies, sorted by total funding. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "funding.investors", "type": "in", "value": ["Sequoia Capital"] }, "sorts": [{"field": "funding.total_investment_usd", "order": "desc"}], "limit": 3, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "funding.total_investment_usd"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 631466, "basic_info": { "name": "OpenAI", "primary_domain": "openai.com" }, "funding": { "total_investment_usd": 201125120000.0 } }, { "crustdata_company_id": 635252, "basic_info": { "name": "Anthropic", "primary_domain": "anthropic.com" }, "funding": { "total_investment_usd": 63740377627.0 } }, { "crustdata_company_id": 681042, "basic_info": { "name": "Oracle", "primary_domain": "oracle.com" }, "funding": { "total_investment_usd": 55750000000.0 } } ], "next_cursor": "H4sIABHRQWoC_y...", "total_count": 1199 } ``` Response trimmed for clarity. Request `funding.investors` in `fields` to see the company's all-time investor list. This field does not identify participants in a specific funding round. Combine `basic_info.company_type` with an industry and a country filter — a common first step in alumni or talent-flow workflows. This search finds UK universities, ranked by headcount. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.company_type", "type": "=", "value": "Educational Institution"}, {"field": "taxonomy.professional_network_industry", "type": "=", "value": "Higher Education"}, {"field": "locations.country", "type": "in", "value": ["GBR"]} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 3, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.company_type", "headcount.total", "locations.country"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 1201786, "basic_info": { "name": "University of Oxford", "company_type": "Educational Institution" }, "headcount": { "total": 23154 }, "locations": { "country": "GBR" } }, { "crustdata_company_id": 1182667, "basic_info": { "name": "The University of Manchester", "company_type": "Educational Institution" }, "headcount": { "total": 20301 }, "locations": { "country": "GBR" } }, { "crustdata_company_id": 770034, "basic_info": { "name": "University of Birmingham", "company_type": "Educational Institution" }, "headcount": { "total": 14943 }, "locations": { "country": "GBR" } } ], "next_cursor": "H4sIABLRQWoC_x...", "total_count": 3593 } ``` Response trimmed for clarity. Filter on `locations.country` with ISO 3-alpha codes (`USA`, `GBR`, `CAN`, `IND`). This search finds Canada-headquartered companies with more than 100 employees. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "locations.country", "type": "in", "value": ["CAN"]}, {"field": "headcount.total", "type": ">", "value": 100} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 3, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 890813, "basic_info": { "name": "KPMG", "primary_domain": "kpmg.com" }, "headcount": { "total": 241782 }, "locations": { "country": "CAN" } }, { "crustdata_company_id": 909849, "basic_info": { "name": "TD", "primary_domain": "td.com" }, "headcount": { "total": 105531 }, "locations": { "country": "CAN" } }, { "crustdata_company_id": 1049593, "basic_info": { "name": "RBC", "primary_domain": "rbc.com" }, "headcount": { "total": 99801 }, "locations": { "country": "CAN" } } ], "next_cursor": "H4sIABPRQWoC_x...", "total_count": 11618 } ``` Response trimmed for clarity. `locations.country` accepts **both** ISO 3166-1 alpha-3 codes (`"USA"`, `"CAN"`) and full country names (`"United States"`, `"Canada"`) — either form matches the same companies, so ISO-3 filters like the one above keep working unchanged. Response values are the normalized full country names. See [Normalized location facets](/company-docs/search/reference#locations). The `geo_distance` filter finds companies whose headquarters is within a specific distance of a point. Apply it to the `locations.headquarters` field. This search finds companies with more than 100 employees headquartered within 50 km of Palo Alto. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "locations.headquarters", "type": "geo_distance", "value": { "location": "Palo Alto, CA", "distance": 50, "unit": "km" } }, {"field": "headcount.total", "type": ">", "value": 100} ] }, "limit": 25, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.headquarters", "locations.city", "locations.state", "locations.country"] }' ``` The `value` is an object whose centre is given as **either** a `location` string (geocoded server-side) **or** an explicit `lat_lng` pair (which skips geocoding). If both are supplied, `lat_lng` wins. `distance` is required and must be positive; `unit` is optional and defaults to `km` (also accepts `mi`, `miles`, `m`, `meters`, `ft`, `feet`). See the [`geo_distance` reference](/company-docs/search/reference#geo_distance--radius-around-headquarters) for the full value-object table. ### Search by explicit coordinates Use `lat_lng` when you already have coordinates (for example, from a map picker) or you want to skip the geocoding step. The example below finds companies headquartered within 25 miles of latitude `37.4419`, longitude `-122.143` (Palo Alto). ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "locations.headquarters", "type": "geo_distance", "value": { "lat_lng": [37.4419, -122.143], "distance": 25, "unit": "mi" } }, "limit": 5 }' ``` The `geo_exclude` filter is the inverse of `geo_distance` — it removes companies headquartered **inside** the radius and keeps everyone else. Use it to carve out a metro you already cover, or to target companies outside a region. This search finds companies that are **not** headquartered within 100 km of London. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "locations.headquarters", "type": "geo_exclude", "value": { "location": "London, UK", "distance": 100, "unit": "km" } }, "limit": 5 }' ``` `geo_exclude` accepts the same value object as `geo_distance` (`location` or `lat_lng`, a required `distance`, and an optional `unit`), so you can also exclude a radius around explicit coordinates. Filter on `followers.count` and `followers.six_months_growth_percent` to find companies with a large, fast-growing audience. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "followers.count", "type": ">", "value": 100000}, {"field": "followers.six_months_growth_percent", "type": ">", "value": 20} ] }, "sorts": [{"field": "followers.count", "order": "desc"}], "limit": 2, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "followers.count", "followers.six_months_growth_percent"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 7434923, "basic_info": { "name": "Zip, a puzzle by LinkedIn", "primary_domain": "linkedin.com" }, "followers": { "count": 45891930, "six_months_growth_percent": 114.27916071283744 } }, { "crustdata_company_id": 1523288, "basic_info": { "name": "Queens, a puzzle by LinkedIn", "primary_domain": "linkedin.com" }, "followers": { "count": 9457905, "six_months_growth_percent": 125.50956541643271 } } ], "next_cursor": "H4sIABTRQWoC_x...", "total_count": 1617 } ``` Response trimmed for clarity. `followers.count` is sortable; the follower-growth fields are filterable but not sortable. Pass one or more exact primary domains to `basic_info.primary_domain` with the `in` operator to resolve known domains in a single call. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_info.primary_domain", "type": "in", "value": ["retool.com", "serverobotics.com"] }, "limit": 10, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 628895, "basic_info": { "name": "Serve Robotics", "primary_domain": "serverobotics.com" }, "headcount": { "total": 404 }, "locations": { "country": "USA" } }, { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com" }, "headcount": { "total": 416 }, "locations": { "country": "USA" } } ], "next_cursor": null, "total_count": 2 } ``` A widely shared domain (for example, a platform domain that many small profiles reuse) can return several low-headcount records alongside the primary company. To resolve a single best-match company from a domain, use [Company Identify](/company-docs/identify/introduction) or [Company Enrich](/company-docs/enrichment/introduction) with `exact_match: true`. When you already have `crustdata_company_id` values (from a previous search or Identify call), fetch those exact records with the `in` operator. This is the most deterministic way to pull specific companies. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "crustdata_company_id", "type": "in", "value": [631466, 635252] }, "limit": 10, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 631466, "basic_info": { "name": "OpenAI", "primary_domain": "openai.com" }, "headcount": { "total": 9538 } }, { "crustdata_company_id": 635252, "basic_info": { "name": "Anthropic", "primary_domain": "anthropic.com" }, "headcount": { "total": 4832 } } ], "next_cursor": null, "total_count": 2 } ``` Use `revenue.public_markets.ipo_date` to find companies that went public in a date window, or `revenue.acquisition_status` to find acquired companies. **Companies that IPO'd in 2020–2021 (1,000+ employees):** ```bash Request — recent IPOs curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.company_type", "type": "=", "value": "Public Company"}, {"field": "revenue.public_markets.ipo_date", "type": "=>", "value": "2020-01-01"}, {"field": "revenue.public_markets.ipo_date", "type": "=<", "value": "2021-12-31"}, {"field": "headcount.total", "type": ">", "value": 1000} ] }, "sorts": [{"field": "revenue.public_markets.ipo_date", "order": "desc"}], "limit": 2, "fields": ["basic_info.name", "basic_info.primary_domain", "revenue.public_markets.ipo_date", "headcount.total"] }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Stellantis", "primary_domain": "stellantis.com" }, "revenue": { "public_markets": { "ipo_date": "2021-01-18" } }, "headcount": { "total": 117523 } }, { "basic_info": { "name": "Concentrix", "primary_domain": "concentrix.com" }, "revenue": { "public_markets": { "ipo_date": "2020-12-01" } }, "headcount": { "total": 218080 } } ], "next_cursor": "H4sIAGjRQWoC_x...", "total_count": 343 } ``` **Acquired software companies:** ```bash Request — acquired companies curl --request POST \ --url https://api.crustdata.com/company/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": "revenue.acquisition_status", "type": "=", "value": "acquired"}, {"field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development"} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": ["basic_info.name", "basic_info.primary_domain", "revenue.acquisition_status", "headcount.total"] }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Instagram", "primary_domain": "instagram.com" }, "revenue": { "acquisition_status": "acquired" }, "headcount": { "total": 53313 } }, { "basic_info": { "name": "PayPal", "primary_domain": "paypal.com" }, "revenue": { "acquisition_status": "acquired" }, "headcount": { "total": 36990 } } ], "next_cursor": "H4sIAGjRQWoC_y...", "total_count": 12877 } ``` Responses trimmed for clarity. `revenue.acquisition_status` matches the lowercase value `"acquired"`. Combine `funding.last_round_type` with `funding.last_fundraise_date` to find companies that recently raised a specific round. This search finds companies whose latest round was Series A/B/C and closed on or after 2024-01-01. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "funding.last_round_type", "type": "in", "value": ["series_a", "series_b", "series_c"]}, {"field": "funding.last_fundraise_date", "type": "=>", "value": "2024-01-01"} ] }, "sorts": [{"field": "funding.last_fundraise_date", "order": "desc"}], "limit": 2, "fields": ["basic_info.name", "basic_info.primary_domain", "funding.last_round_type", "funding.last_fundraise_date", "funding.last_round_amount_usd"] }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Upside", "primary_domain": "joinupside.com" }, "funding": { "last_round_type": "series_a", "last_fundraise_date": "2026-06-25", "last_round_amount_usd": 20000000.0 } }, { "basic_info": { "name": "Warp", "primary_domain": "warp.co" }, "funding": { "last_round_type": "series_b", "last_fundraise_date": "2026-06-25", "last_round_amount_usd": 60000000.0 } } ], "next_cursor": "H4sIAC_RQWoC_x...", "total_count": 9749 } ``` Response trimmed for clarity. Round-type values are lowercase with underscores (`series_a`, `series_b`). Use `=>` / `=<` for date ranges — `>=` and `<=` are not supported. Filter on `taxonomy.categories` and `basic_info.markets` for fine-grained segmentation. This search finds AI companies in the software-development industry, ranked by headcount. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "taxonomy.categories", "type": "in", "value": ["Artificial Intelligence (AI)"]}, {"field": "basic_info.industries", "type": "in", "value": ["Software Development"]} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Amazon", "primary_domain": "aboutamazon.com" }, "headcount": { "total": 768927 } }, { "basic_info": { "name": "Google", "primary_domain": "goo.gle" }, "headcount": { "total": 308114 } } ], "next_cursor": "H4sIAFjRQWoC_x...", "total_count": 19844 } ``` Category and market values are case-sensitive for `in`. Use [Autocomplete](/company-docs/autocomplete/introduction) to find exact values — for example, the AI category is stored as `"Artificial Intelligence (AI)"`. For public-market tags like `"NASDAQ"`, filter on `basic_info.markets`. Use the `(.)` fuzzy operator on `basic_info.name` to match name variants and tolerate typos, combined with a `basic_info.company_type` filter. This search finds privately held companies with "robotics" in the name. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.company_type", "type": "=", "value": "Privately Held"}, {"field": "basic_info.name", "type": "(.)", "value": "robotics"} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 3, "fields": ["basic_info.name", "basic_info.primary_domain", "basic_info.company_type", "headcount.total"] }' ``` ```json Response { "companies": [ { "basic_info": { "name": "ECOVACS ROBOTICS", "primary_domain": "ecovacs.com", "company_type": "Privately Held" }, "headcount": { "total": 1006 } }, { "basic_info": { "name": "VEX Robotics", "primary_domain": "vexrobotics.com", "company_type": "Privately Held" }, "headcount": { "total": 850 } }, { "basic_info": { "name": "Torc Robotics", "primary_domain": "torc.ai", "company_type": "Privately Held" }, "headcount": { "total": 848 } } ], "next_cursor": "H4sIADHRQWoC_x...", "total_count": 4428 } ``` Response trimmed for clarity. `(.)` is fuzzy (tolerates typos, ignores word order); use `[.]` for exact token matching. --- ## What to do next - **Paginate and sort** — see [Pagination and sorting](/company-docs/search/reference#paginate-through-results) to walk through all matching companies. - **Look up operators and fields** — see [Search reference](/company-docs/search/reference) for operators, searchable fields, response fields, validation, and errors. - **Enrich a company** — use [Company Enrich](/company-docs/enrichment/introduction) to get a detailed profile for a known company. - **Discover filter values** — use [Company Autocomplete](/company-docs/autocomplete/introduction) to find valid values for industries, categories, and countries before building search filters. # Company Search reference Source: https://docs.crustdata.com/company-docs/search/reference Reference for Company Search: filter operators, searchable fields, response fields, validation rules, and errors. Reference material for [Company Search](/company-docs/search/introduction): the full list of filter operators, searchable fields with sortable flags, response fields, validation rules, and error responses. For walk-through examples, see [Company Search](/company-docs/search/introduction) and [Examples](/company-docs/search/introduction#examples). --- ## Filter operator reference Company Search accepts the following `filters.type` operators. | Operator | Value shape | Meaning | Example value | Notes | | -------------- | ------------------------------ | ---------------------------- | ------------------------ | ---------------------------------------------------------------------------- | | `=` | scalar | Exact match | `"retool.com"` | Case-insensitive for text fields | | `!=` | scalar | Not equal | `"acquired"` | | | `<` | scalar (number or date string) | Less than | `10000` | Numbers and date strings | | `=<` | scalar (number or date string) | Less than or equal | `50000000` | **Not** `<=` | | `>` | scalar (number or date string) | Greater than | `2020` | Numbers and date strings | | `=>` | scalar (number or date string) | Greater than or equal | `"2024-01-01"` | **Not** `>=` | | `in` | array | Value is in list | `["USA", "GBR"]` | Case-sensitive. Matches if **any** array element matches **any** list value. | | `not_in` | array | Value is not in list | `["acquired"]` | | | `is_null` | `null` | Field has no value | `null` | The `value` key is required — send `"value": null`. | | `is_not_null` | `null` | Field has a non-null value | `null` | The `value` key is required — send `"value": null`. | | `(.)` | string | Fuzzy text search | `"openai"` | Tolerates typos, matches word variants | | `[.]` | string | Case-insensitive exact-phrase match | `"Software Development"` | Words must be contiguous and in order; no partial words or typos | | `geo_distance` | object | HQ within radius of a location or explicit coordinate | `{"location": "Palo Alto, CA", "distance": 50, "unit": "km"}` | Only on `locations.headquarters`. See [`geo_distance`](#geo_distance--radius-around-headquarters) | | `geo_exclude` | object | HQ outside radius of a location or explicit coordinate | `{"location": "London, UK", "distance": 100, "unit": "km"}` | Inverse of `geo_distance` — same value object and field. See [`geo_exclude`](#geo_exclude--exclude-a-radius) | The operators `>=` and `<=` are **not supported**. Use `=>` and `=<` instead. ### `geo_distance` — radius around headquarters `geo_distance` keeps companies whose **headquarters** is within a radius of a point. Both geo operators work on the `locations.headquarters` field only. Supply the centre of the radius using **one** of: - `location` — a string that is geocoded server-side (e.g. `"Palo Alto, CA"`). - `lat_lng` — explicit coordinates as `[lat, lng]`. Skips geocoding. If both are supplied, `lat_lng` wins. `distance` is required; `unit` defaults to `km`. | Field | Type | Required | Notes | | ---------- | -------- | -------- | -------------------------------------------------------------------------- | | `location` | string | One of | Geocoded server-side. Ignored when `lat_lng` is also set. | | `lat_lng` | number[] | One of | Two-element `[lat, lng]`. Lat in `[-90, 90]`, lng in `[-180, 180]`. | | `distance` | number | Yes | Radius around the centre. Must be positive. | | `unit` | string | No | One of `km`, `mi`, `miles`, `m`, `meters`, `ft`, `feet`. Defaults to `km`. | See [`geo_distance` examples](/company-docs/search/introduction#examples) for end-to-end requests using both `location` and `lat_lng`. ### `geo_exclude` — exclude a radius `geo_exclude` is the inverse of `geo_distance`: it removes companies headquartered **inside** the radius and keeps everyone else. It takes the same value object — `location` or `lat_lng`, a required `distance`, and an optional `unit` (defaults to `km`) — and works on the same `locations.headquarters` field. Use it to carve out a metro you already cover, or to target companies outside a region. ```json { "field": "locations.headquarters", "type": "geo_exclude", "value": { "location": "London, UK", "distance": 100, "unit": "km" } } ``` ## Searchable fields These are common fields that can be used in the `field` key of a filter condition. Some indexed filter fields are search-only and are not returned in the response payload. For the full schema, see the [API reference](/openapi-specs/2025-11-01/introduction). Fields are grouped by family below. Every field listed is filterable; the **Sortable** column marks the subset also valid in `sorts`. ### Identity & timestamps | Field | Type | Filterable | Sortable | Description | | ---------------------------------- | -------- | ---------- | -------- | ------------------------------ | | `crustdata_company_id` | integer | Yes | Yes | Crustdata company ID | | `basic_info.company_id` | integer | Yes | Yes | Internal source company ID | | `metadata.growth_calculation_date` | datetime | Yes | Yes | Anchor date for growth metrics | | `updated_at` | datetime | Yes | Yes | Last profile update timestamp | | `indexed_at` | datetime | Yes | Yes | Last index timestamp | ### Basic info | Field | Type | Filterable | Sortable | Description | | ------------------------------------- | -------- | ---------- | -------- | -------------------------------------------- | | `basic_info.name` | string | Yes | Yes | Company name | | `basic_info.primary_domain` | string | Yes | Yes | Primary website domain | | `basic_info.website` | string | Yes | No | Full website URL | | `basic_info.professional_network_url` | string | Yes | No | Company profile URL | | `basic_info.professional_network_id` | string | Yes | No | Company profile ID | | `basic_info.company_type` | string | Yes | No | e.g., `"Privately Held"`, `"Public Company"` | | `basic_info.year_founded` | integer | Yes | Yes | Year founded (e.g., `2017`) | | `basic_info.employee_count_range` | string | Yes | Yes | e.g., `"201-500"` | | `basic_info.markets` | string[] | Yes | No | Market tags | | `basic_info.industries` | string[] | Yes | No | Industry tags | ### Revenue & funding | Field | Type | Filterable | Sortable | Description | | ---------------------------------------- | -------- | ---------- | -------- | ---------------------------------- | | `revenue.estimated.lower_bound_usd` | integer | Yes | Yes | Revenue lower bound (USD) | | `revenue.estimated.upper_bound_usd` | integer | Yes | Yes | Revenue upper bound (USD) | | `revenue.acquisition_status` | string | Yes | No | e.g., `"acquired"` | | `revenue.public_markets.ipo_date` | date | Yes | Yes | IPO date (public companies) | | `revenue.public_markets.fiscal_year_end` | string | Yes | No | Fiscal year end (public companies) | | `revenue.public_markets.stock_symbols` | string[] | Yes | No | Ticker symbols (public companies) | | `funding.total_investment_usd` | number | Yes | Yes | Total disclosed funding (USD) | | `funding.last_round_amount_usd` | number | Yes | Yes | Last funding round amount (USD) | | `funding.last_fundraise_date` | date | Yes | Yes | Last funding date | | `funding.last_round_type` | string | Yes | No | Latest round type, e.g., `"series_a"` | | `funding.investors` | string[] | Yes | No | All-time investor names; not round-specific | ### Headcount & roles | Field | Type | Filterable | Sortable | Description | | ------------------------------------- | ------- | ---------- | -------- | --------------------------------------------- | | `headcount.total` | integer | Yes | Yes | Total employee count | | `headcount.largest_headcount_country` | string | Yes | Yes | Country with the most employees | | `headcount.growth_percent.1m` | number | Yes | No | 1-month headcount growth percent | | `headcount.growth_percent.3m` | number | Yes | No | 3-month headcount growth percent | | `headcount.growth_percent.6m` | number | Yes | No | 6-month headcount growth percent | | `headcount.growth_percent.12m` | number | Yes | No | 12-month headcount growth percent | | `headcount.growth_absolute.1m` | integer | Yes | No | 1-month absolute employee delta | | `headcount.growth_absolute.3m` | integer | Yes | No | 3-month absolute employee delta | | `headcount.growth_absolute.6m` | integer | Yes | No | 6-month absolute employee delta | | `headcount.growth_absolute.12m` | integer | Yes | No | 12-month absolute employee delta | | `roles.distribution.` | integer | Yes | No | Employee count in a function (see list below) | | `roles.distribution_percent` | object | Yes | No | Share of employees per function | | `roles.growth_6m` | object | Yes | No | 6-month role growth | | `roles.growth_yoy` | object | Yes | No | Year-over-year role growth | The `headcount.growth_percent.*` and `headcount.growth_absolute.*` dotted period paths are **filterable but not sortable**. To rank results by growth, filter on the period you care about and sort on `headcount.total` (or another sortable field). See [Find high-growth mid-size companies](/company-docs/search/introduction#examples). Replace `` with one of: `accounting`, `administrative`, `arts_and_design`, `business_development`, `community_and_social_services`, `consulting`, `customer_success_and_support`, `education`, `engineering`, `entrepreneurship`, `finance`, `healthcare_services`, `human_resources`, `information_technology`, `legal`, `marketing`, `media_and_communication`, `military_and_protective_services`, `operations`, `product_management`, `program_and_project_management`, `purchasing`, `quality_assurance`, `real_estate`, `research`, `sales`, `support`. For example, `roles.distribution.engineering` filters on the number of engineers. See [Find companies by team size in a function](/company-docs/search/introduction#examples). ### Locations | Field | Type | Filterable | Sortable | Description | | -------------------------- | ------ | ---------- | -------- | ------------------------------------------------------------------------------------ | | `locations.country` | string | Yes | Yes | HQ country — filter with ISO-3 codes or full names; returns the normalized full name | | `locations.state` | string | Yes | No | HQ state / region, normalized (e.g. `"California"`) | | `locations.city` | string | Yes | No | HQ city, normalized (e.g. `"Palo Alto"`) | | `locations.headquarters` | string | Yes | No | Full HQ location string — also the target of `geo_distance` / `geo_exclude` | | `locations.street_address` | string | Yes | No | HQ street address (e.g. `"510 Townsend St."`) | | `locations.all_office_addresses` | string[] | Yes | No | All office addresses, HQ and non-HQ; match a city or region substring with `(.)` | **Normalized location facets.** `locations.state` and `locations.city` filter against and return normalized values derived from geocoding each company's headquarters address — the value you see in a response is the value you filter with. `locations.country` **accepts both** vocabularies: ISO-3 codes (`"USA"`, `"GBR"`) and full country names (`"United States"`) each match all companies, so existing ISO-3 filters keep working unchanged. Responses return the normalized full country name. Use [Autocomplete](/company-docs/autocomplete/introduction) to discover indexed values before filtering. ### Taxonomy | Field | Type | Filterable | Sortable | Description | | -------------------------------------------- | -------- | ---------- | -------- | ---------------------- | | `taxonomy.professional_network_industry` | string | Yes | No | Primary industry label | | `taxonomy.categories` | string[] | Yes | No | Category tags | | `taxonomy.professional_network_specialities` | string[] | Yes | No | Speciality tags | ### Followers | Field | Type | Filterable | Sortable | Description | | ------------------------------------- | ------- | ---------- | -------- | ---------------------------------- | | `followers.count` | integer | Yes | Yes | Follower count | | `followers.mom_percent` | number | Yes | No | Month-over-month follower growth % | | `followers.qoq_percent` | number | Yes | No | Quarter-over-quarter growth % | | `followers.six_months_growth_percent` | number | Yes | No | 6-month follower growth % | | `followers.yoy_percent` | number | Yes | No | Year-over-year follower growth % | ### Social profiles | Field | Type | Filterable | Sortable | Description | | -------------------------------------- | ------ | ---------- | -------- | ----------------------- | | `social_profiles.twitter_url` | string | Yes | No | Company X/Twitter URL | | `social_profiles.crunchbase.url` | string | Yes | No | Company Crunchbase URL | | `social_profiles.crunchbase.uuid` | string | Yes | No | Company Crunchbase UUID | | `social_profiles.professional_network` | string | Yes | No | Company profile URL | ### Competitors (filter-only) | Field | Type | Filterable | Sortable | Description | | ------------------------- | --------- | ---------- | -------- | ------------------------ | | `competitors.company_ids` | integer[] | Yes | No | Competitor Crustdata IDs | | `competitors.websites` | string[] | Yes | No | Competitor domains | ### Technographics (filter-only) | Field | Type | Filterable | Sortable | Description | | -------------------------------------------- | -------- | ---------- | -------- | ---------------------------------------- | | `technographics.total_technologies` | integer | Yes | Yes | Number of detected technologies | | `technographics.top_technologies` | string[] | Yes | No | Most notable technology names | | `technographics.technologies.name` | string[] | Yes | No | Detected technology names | | `technographics.technologies.category` | string[] | Yes | No | Detected technology category slugs | | `technographics.technologies.super_category` | string[] | Yes | No | Detected technology super-category slugs | Technographics values are **not returned** in search responses — these fields are filter-only. `technographics.total_technologies` is filterable and sortable, but it is **not returned** in results. `technographics.top_technologies` is filterable but likewise not returned. `technographics.technologies.name` is likewise not returned, and `technographics.technologies.category` and `technographics.technologies.super_category` are likewise not returned. To get the values, request the `technographics` field group on [Company Enrich](/company-docs/enrichment/reference#technographics-fields). Use [Autocomplete](/company-docs/autocomplete/introduction#common-fields-to-autocomplete) with the `technology` and `technology_category` fields to discover valid filter values. Use [Autocomplete](/company-docs/autocomplete/introduction) to discover exact values for fields like `basic_info.industries`, `taxonomy.professional_network_industry`, `locations.country`, `basic_info.company_type`, and `funding.last_round_type`. ## Response fields Each company in the response can include these sections (depending on `fields`): | Section | Key fields | Description | | ----------------- | ---------------------------------------------------------------------------------- | --------------------------------- | | `basic_info` | `name`, `primary_domain`, `website`, `professional_network_url`, `year_founded` | Core identity and profile | | `headcount` | `total` | Employee footprint | | `funding` | `total_investment_usd`, `last_round_amount_usd`, `investors` | Funding and investor data | | `locations` | `country`, `state`, `city`, `headquarters`, `street_address`, `all_office_addresses` | Headquarters and office locations — `country`, `state`, and `city` are normalized geocoded values | | `taxonomy` | `professional_network_industry`, `categories`, `professional_network_specialities` | Industry and category tags | | `revenue` | `estimated`, `public_markets`, `acquisition_status` | Revenue and market data | | `hiring` | `openings_count`, `openings_growth_percent` | Hiring demand | | `followers` | `count`, `mom_percent`, `yoy_percent` | Social follower metrics | | `social_profiles` | `twitter_url`, `professional_network`, `crunchbase.url`, `crunchbase.uuid` | Third-party profile links | | `software_reviews` | `average_rating`, `review_count`, and review-count growth percentages | Software review metrics (when available) | ### Filterable but not returned Some indexed fields can be used in `filters` to narrow results but are **not** part of the response payload — search is lightweight discovery, not full enrichment. Filter on them, then fetch their values with [Company Enrich](/company-docs/enrichment/introduction). | Field family | Filterable | Returned | | ---------------------------------------------------------------------------------------- | ---------- | -------- | | `competitors.*` (`company_ids`, `websites`) | Yes | No | | `roles.*` (`distribution.`, `distribution_percent`, `growth_6m`, `growth_yoy`) | Yes | No | | `technographics.*` (`total_technologies`, `top_technologies`, `technologies.name`, `technologies.category`, `technologies.super_category`) | Yes | No | --- ## Validation rules Default/max limits and pagination behavior reflect current platform behavior. See the [API reference](/openapi-specs/2025-11-01/introduction) for the formal OpenAPI contract. | Rule | Behavior | | ------------------------- | ----------------------------------------------------------------------------------------- | | `filters` is optional | Omitting filters matches all companies. Always use filters in production to control cost. | | `fields` is optional | Omitting returns all fields (large payload). Always specify in production. | | `limit` range | 1–1000. Default: 20. | | `sorts` with pagination | Always include `sorts` when paginating to ensure stable result ordering. | | `cursor` must match query | Changing `filters`, `sorts`, or `fields` between pages invalidates the cursor. | **Filter sizing.** To match many values of a field, put them all in a single `in` condition rather than many separate conditions — `in` compiles to one efficient query. A single `in` list of up to ~5,000–10,000 values returns in a few seconds; larger lists get progressively slower, and very large lists (roughly 50,000+) are rejected. For bigger sets, split the values into ~5,000–10,000 chunks across multiple requests and merge. The request body is hard-capped at **10 MB** (larger bodies return `413 request_too_large`), though with typical payloads you reach the query-size limit first. ## Errors Common error responses for Search: ```json 400 — Unsupported field { "error": { "type": "invalid_request", "message": "Unsupported columns in conditions: ['nonexistent_field']", "metadata": [] } } ``` ```json 400 — Invalid operator { "error": { "type": "invalid_request", "message": "'filters.type' must be one of: =, !=, <, =<, >, =>, in, not_in, contains, not_contains, is_null, is_not_null, (.), [.]. Got '>='.", "metadata": [ { "field": "filters.type", "type": "enum", "message": "'filters.type' must be one of: =, !=, <, =<, >, =>, in, not_in, contains, not_contains, is_null, is_not_null, (.), [.]. Got '>='." } ] } } ``` Use `=>` for greater-than-or-equal and `=<` for less-than-or-equal. The operators `>=` and `<=` are not supported. ```json 401 — Invalid API key { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` --- ## API reference summary | Detail | Value | | ---------------- | ---------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /company/search` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | `filters` (condition or group), `fields`, `sorts`, `limit`, `cursor` | | **Response** | `{ companies, next_cursor, total_count }` | | **Pagination** | Cursor-based. Pass `next_cursor` in `cursor`. `null` = last page. | | **Empty result** | `200` with `"companies": []` | | **Errors** | `400` (bad field/operator), `401` (bad auth), `403` (permission/credits), `500` (server error) | ## How sorts work The `sorts` parameter orders your results. Each sort rule needs: - **`column`** — a dot-path field (e.g., `funding.total_investment_usd`, `headcount.total`, `basic_info.name`). - **`order`** — either `asc` (ascending) or `desc` (descending). You can provide multiple sort rules. The API applies them in order. For the full list of sortable fields, see [Search reference](/company-docs/search/reference#searchable-fields). --- ## Paginate through results When your search matches more companies than your `limit`, use cursor-based pagination to walk through all pages. **First page:** send your normal search request. ```bash First page curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "locations.country", "type": "in", "value": ["USA"] }, "sorts": [{"field": "crustdata_company_id", "order": "asc"}], "limit": 100, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain" ] }' ``` **Next page:** take the `next_cursor` value from the response and pass it in your next request. Keep the same `filters`, `sorts`, `limit`, and `fields`. ```bash Next page curl --request POST \ --url https://api.crustdata.com/company/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "locations.country", "type": "in", "value": ["USA"] }, "sorts": [{"field": "crustdata_company_id", "order": "asc"}], "limit": 100, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain" ], "cursor": "PASTE_NEXT_CURSOR_VALUE_HERE" }' ``` Continue until `next_cursor` is `null`, which means you have reached the last page. Changing `filters`, `sorts`, or `fields` between pages invalidates the cursor. Always include `sorts` when paginating to guarantee stable ordering. See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. # Company Autocomplete Source: https://docs.crustdata.com/company-docs/autocomplete/introduction Learn how to discover valid field values for Company Search filters using the Company Autocomplete API. **Use this when** you need to discover valid filter values before building a Company Search query — for example, finding the exact industry label or country code the API expects. The Company Autocomplete API helps you discover the exact field values the indexed Company Search API expects. Use it before you build filters for industries, geographies, company types, funding stages, and more. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/company/search/autocomplete ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request fields | Field | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `field` | string | Yes | Searchable company field to autocomplete, such as `basic_info.industries`, `taxonomy.professional_network_industry`, or `locations.country`. | | `query` | string | Yes | Partial text to match. Use `""` to get the most common values. | | `limit` | integer | No | Maximum suggestions to return. Default: `20`. Max: `100`. | | `filters` | object | No | Optional condition or nested `and`/`or` group to narrow the suggestion pool. | ### Response body | Field | Type | Description | | ---------------------- | ------ | ------------------------------------------------------------------------ | | `suggestions` | array | Matching values sorted by relevance (or by frequency when `query` is empty). | | `suggestions[].value` | string | The exact field value to reuse in a Company Search filter. | ### Rate limits and pricing Pricing: Free. - **Rate limits:** For current plan-specific limits, see [Rate limits](/general/rate-limits). --- ## When to use autocomplete Use this endpoint when you want to: - Build filter dropdowns or typeahead inputs in your product. - Validate the exact values a Company Search filter expects. - Explore the dataset vocabulary for a field before writing search queries. --- ## Your first autocomplete request: discover industry values Start with the field you want to query and a partial search string. Here, `tech` returns matching values from `basic_info.industries`. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "basic_info.industries", "query": "tech", "limit": 5 }' ``` ```json Response { "suggestions": [ { "value": "Technology, Information and Media" }, { "value": "Technology, Information and Internet" }, { "value": "Information Technology & Services" }, { "value": "Technical and Vocational Training" }, { "value": "Space Research and Technology" } ] } ``` ### Understanding the response Autocomplete returns an object with one key: - **`suggestions`** — an array of matching values, sorted by relevance. Each suggestion contains: - **`value`** — the exact field value to reuse in a Company Search filter. Use the returned `value` exactly as-is in your next search request. This avoids empty results caused by typos, casing differences, or unsupported variants. When `query` is non-empty, suggestions are ranked by match relevance. When `query` is an empty string, suggestions are ranked by frequency. --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request you can adapt to your own fields and filters. Use `basic_info.name` to surface company names matching a partial string. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "basic_info.name", "query": "hub", "limit": 5 }' ``` ```json Response { "suggestions": [ { "value": "HubSpot" }, { "value": "HUB International" }, { "value": "HubSpot Academy" }, { "value": "Hublot" }, { "value": "Hubbell Incorporated" } ] } ``` Use an empty string for `query` when you want the most frequent values instead of a text match. This is useful when you are exploring a field for the first time. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "locations.country", "query": "", "limit": 10 }' ``` ```json Response { "suggestions": [ { "value": "United States of America" }, { "value": "France" }, { "value": "Germany" }, { "value": "United Kingdom" }, { "value": "Italy" }, { "value": "Australia" }, { "value": "United States" }, { "value": "India" }, { "value": "Spain" }, { "value": "Brazil" } ] } ``` Suggestions are ranked by frequency, so you can quickly see the most common values in the dataset. Autocomplete can scope results to a subset of companies. The `filters` field accepts either: - a single condition with `field`, `type`, and `value` - a logical group with `op` and `conditions` This example returns industry suggestions only for US-based companies. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "basic_info.industries", "query": "", "limit": 5, "filters": { "field": "locations.country", "type": "=", "value": "USA" } }' ``` ```json Response { "suggestions": [ { "value": "Professional Services" }, { "value": "" }, { "value": "Manufacturing" }, { "value": "Technology, Information and Media" }, { "value": "Hospitals and Health Care" } ] } ``` Use indexed values in `filters`. Run autocomplete on a field first if you are not sure of the exact value the dataset uses. You can also use nested `and`/`or` groups. Filter values can be strings, numbers, or booleans, and array values can contain strings or numbers — pass numeric values as numbers rather than strings where the underlying field is numeric. ```bash curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "taxonomy.professional_network_industry", "query": "", "limit": 5, "filters": { "op": "and", "conditions": [ { "field": "locations.country", "type": "=", "value": "USA" }, { "field": "headcount.latest_count", "type": ">", "value": 100 } ] } }' ``` For the full list of supported operators, see [Supported filter operators](#supported-filter-operators). This is the canonical end-to-end Company API workflow. You do not know the exact industry value the Search API expects. Autocomplete discovers it, Search finds matching companies, and Enrich fills in the details. ### Step 1: Discover valid industry values ```bash Request curl --request POST \ --url https://api.crustdata.com/company/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"field": "basic_info.industries", "query": "software", "limit": 3}' ``` ```json Response { "suggestions": [ { "value": "Software Development" }, { "value": "IT System Custom Software Development" }, { "value": "Mobile Computing Software Products" } ] } ``` **Extract:** Take `suggestions[0].value` → `"Software Development"`. Use this exact string in your Search filter. **If empty:** If `suggestions` is `[]`, your query did not match any indexed values. Try a broader term (for example, `"tech"` instead of `"software engineering"`). ### Step 2: Search for matching companies ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.industries", "type": "in", "value": ["Software Development"]}, {"field": "locations.country", "type": "=", "value": "USA"} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 3, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 12345, "basic_info": { "name": "Acme Corp", "primary_domain": "acme.com" }, "headcount": { "total": 8500 } }, { "crustdata_company_id": 67890, "basic_info": { "name": "Retool", "primary_domain": "retool.com" }, "headcount": { "total": 450 } }, { "crustdata_company_id": 628895, "basic_info": { "name": "Serve Robotics", "primary_domain": "serverobotics.com" }, "headcount": { "total": 120 } } ], "next_cursor": "H4sIAJj5zGkC...", "total_count": 217318 } ``` **Extract:** Take `companies[].crustdata_company_id` values → `[12345, 67890, 628895]`. Pass these to Enrich. **If empty:** If `companies` is `[]`, no companies matched your filters. Broaden your conditions or re-run autocomplete to verify filter values. **To get more results:** Pass `next_cursor` as `cursor` in the next request. Stop paginating when `next_cursor` is `null`. See [Pagination & sorting](/company-docs/search/reference#paginate-through-results). ### Step 3: Enrich the top matches ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [12345, 67890, 628895], "fields": ["basic_info", "headcount", "funding", "hiring"] }' ``` ```json Response [ { "matched_on": "12345", "match_type": "crustdata_company_id", "matches": [ { "confidence_score": 1.0, "company_data": { "basic_info": { "name": "Acme Corp", "primary_domain": "acme.com", "company_type": "Privately Held", "year_founded": 2015, "industries": ["Software Development"] }, "headcount": { "total": 8500 }, "funding": { "total_investment_usd": 250000000, "last_round_type": "series_d" }, "hiring": { "openings_count": 42 } } } ] } ] ``` **Extract:** Each item in the top-level array corresponds to one input ID. Access the profile via `response[i].matches[0].company_data`. **If a match is empty:** If `matches` is `[]` for an identifier, that company was not found. The request still succeeds (`200 OK`) for the other identifiers. See [Partial batch failure](/company-docs/enrichment/reference#partial-batch-failure). --- ## Reference Reference material for Company Autocomplete: common fields you can autocomplete, supported filter operators, errors, and the API summary. ### Common fields to autocomplete | Field | Why you would use it | | ------------------------------------------ | ------------------------------------------------------------------------------- | | `basic_info.industries` | Find exact industry labels before building industry filters. | | `basic_info.name` | Surface company names matching a partial string. | | `basic_info.markets` | Discover indexed market labels (e.g. `NASDAQ`, `NYSE`). | | `basic_info.employee_count_range` | List the canonical employee-count bucket labels. | | `taxonomy.professional_network_industry` | Match primary industry values used in company search. | | `taxonomy.professional_network_specialities` | Discover specialty tags used by the dataset. | | `taxonomy.categories` | List the high-level category tags used by the dataset. | | `locations.country` | Discover country values used by the indexed dataset. | | `locations.headquarters` | Discover indexed HQ location strings. | | `basic_info.company_type` | Explore company type labels used in the dataset. | | `funding.last_round_type` | Find valid funding stage labels before filtering by recent financing data. | | `funding.investors` | Discover investor names indexed in the dataset. | | `revenue.acquisition_status` | List the indexed acquisition status labels. | | `revenue.public_markets.stock_symbols` | Discover indexed ticker symbols for public companies. | | `revenue.public_markets.fiscal_year_end` | Closed enum: returns the twelve calendar months in chronological order. | | `technology` | Discover detected technology names for the `technographics.technologies.name` and `technographics.top_technologies` search filters. | | `technology_category` | Discover technology category slugs for the `technographics.technologies.category` search filter. | The `field` parameter accepts a broader set than this table — the table highlights the most useful fields for building filter dropdowns. For the full live allowlist, send a deliberately invalid `field` and read the `Valid fields` list returned in the `400` error. The shape of that error message is for debugging only and may change without notice. `revenue.public_markets.fiscal_year_end` is a **closed-enum** static field — an empty `query` returns the twelve months (`January`...`December`) in chronological order without any backend lookup. Use it to populate a static fiscal-year-end dropdown without an extra round-trip. ### Supported filter operators `=`, `!=`, `<`, `=<`, `>`, `=>`, `in`, `not_in`, `contains`. The operators `>=` and `<=` are **not supported**. Use `=>` and `=<` instead. ### Common errors and edge cases If no values match your query, the API returns an empty array: ```json { "suggestions": [] } ``` If you send an unsupported field name, the API returns a `400` with the list of valid fields: ```json { "error": { "type": "invalid_request", "message": "Field 'invalid_field' is not supported on scope 'company'. Valid fields: basic_info.company_type, basic_info.employee_count_range, basic_info.industries, basic_info.markets, basic_info.name, basic_info.primary_domain, ...", "metadata": [] } } ``` If that happens, double-check the field path against the supported list and use a field that is available in the indexed Company Search schema. ### API reference summary | Detail | Value | | ------------------- | ----------------------------------------------------------------- | | **Endpoint** | `POST /company/search/autocomplete` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Required params** | `field`, `query` | | **Optional params** | `limit` (default: 20, max: 100), `filters` | | **Response** | `{ "suggestions": [{ "value": "..." }] }` | | **Empty result** | `200` with `"suggestions": []` | | **Errors** | `400` (unsupported field), `401` (bad auth), `500` (server error) | For pricing, see [Pricing](/general/pricing). For rate-limit guidance, see [Rate limits](/general/rate-limits). See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Use values in a search** — head to [Company Search](/company-docs/search/introduction) once you've picked a value. - **Enrich a company** — use [Company Enrich](/company-docs/enrichment/introduction) after you find a company. # Filter value enumeration Source: https://docs.crustdata.com/company-docs/filter-values Downloadable lists of the valid values for company search filter fields — industries, categories, company types, funding round types, headcount ranges, and countries. Several search filter fields accept values from a fixed vocabulary. The [autocomplete API](/company-docs/autocomplete/introduction) is always the current source of truth, but when you build filters programmatically against a large set, these downloadable snapshots are convenient. These files are point-in-time snapshots. For always-current values — and for fields with very large or dynamic vocabularies — use the [autocomplete API](/company-docs/autocomplete/introduction). ## Company filter values Each file is the value list for one company-search filter field. | Field | Values | Download | | --------------------------------- | ------ | -------------------------------------------------------------------- | | `basic_info.industries` | 433 | [industries.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/people-search/static-linkedin-industries.json) | | `taxonomy.categories` | 801 | [categories.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/company-discovery/crunchbase_categories.json) | | `basic_info.company_type` | 8 | [company-types.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/company-discovery/company_type.json) | | `funding.last_round_type` | 28 | [funding-round-types.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/company-discovery/last_funding_round_type.json) | | `basic_info.employee_count_range` | 9 | [headcount-ranges.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/company-discovery/employee_count_range.json) | | `locations.country` | 196 | [countries.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/company-discovery/largest_headcount_country.json) | **Countries** — `countries.json` is a list of `{ iso_alpha3, name }` objects. Filter `locations.country` with the **`iso_alpha3`** value (for example `"USA"`, `"GBR"`), not the full country name. **Headcount** — `headcount-ranges.json` lists the indexed `basic_info.employee_count_range` buckets (`2-10`, `11-50`, … `10001+`). The real-time [Company Live Search](/company-docs/search/live-search) `COMPANY_HEADCOUNT` filter uses comma-formatted buckets instead (`1,001-5,000`, `Self-employed`, …) — discover those with autocomplete. ## Person filter values Person-search vocabularies (regions, titles, and employer industries) are large and change more often. Use [Person Autocomplete](/person-docs/autocomplete/introduction) to discover current values for person-search filter fields. ## What to do next - **Discover values interactively** — [Company Autocomplete](/company-docs/autocomplete/introduction) - **Build a search** — [Company Search](/company-docs/search/introduction) - **Person values** — [Person filter values](/person-docs/filter-values) # Company Batch Search Source: https://docs.crustdata.com/company-docs/search/batch-search Run one company search query asynchronously and receive the whole result set as a single file — from the indexed database (up to 10,000 records) or retrieved from the web in real time (up to 1,000 records). This page covers two asynchronous batch endpoints: - [**Batch search**](#batch-search) — results served from the indexed database, higher caps and faster (up to 10,000 records). - [**Batch live search**](#batch-live-search) — results retrieved from the web in real time, fresher but capped lower (up to 1,000 records). Batch live search is available on the **live** plan. The [Batch live search](#batch-live-search) section below documents its gating and behavior. --- ## Batch search Batch search runs one query asynchronously and delivers the **entire result set** as a single file. Where [Company Search](/company-docs/search/introduction) returns one cursor page per call, a batch job walks every page for you. ``` POST https://api.crustdata.com/batch/company/search ``` For fresher results retrieved from the web in real time, see [Batch live search](#batch-live-search) below. Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ### How batch search differs from non-batch search - **One query, whole result set.** You submit a single query; the job paginates server-side until `max_results` is reached or the matches run out. - **`max_results` is the only volume control.** It clamps to **10,000** and defaults to that cap when omitted. Values above the cap are silently clamped; zero or negative values return `400`. The non-batch paging knobs — `limit`, `page`, `preview` — are silently ignored. - **Flat records.** Each line in the results file has exactly the non-batch search record shape. No envelope. - **Exact field projection.** When you pass `fields`, each record contains exactly those fields — nothing more. Omit `fields` to get every field your account can read. --- ### Database batch search `POST /batch/company/search` takes the same filter fields and operators as [Company Search](/company-docs/search/reference), with one extra rule: the top level of `filters` **must** be an `{op, conditions}` group — a bare `{field, type, value}` condition is rejected with `400`. Groups nest inside `conditions` for complex queries. This finds companies with more than 1,000 employees: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/company/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": "headcount.total", "type": ">", "value": 1000} ] }, "max_results": 2, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "batch_id": "ee438e6f-7449-481f-9a2d-2ef57ddab24a", "status": "pending", "entity": "company", "action": "search", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/ee438e6f-7449-481f-9a2d-2ef57ddab24a" } ``` Search jobs always report `identifier_count: 1` — the one query. When the job completes, the downloaded file contains flat records with exactly the requested fields: ```json The results file (all records) {"basic_info": {"name": "Aarti Industries Ltd.", "primary_domain": "aarti-industries.com"}, "headcount": {"total": 4923}} {"basic_info": {"name": "Aditya Birla Capital", "primary_domain": "adityabirlacapital.com"}, "headcount": {"total": 31747}} ``` #### Two warnings before you submit large jobs Unknown filter field names are **not rejected at submit time** the way non-batch search rejects them — a typo in `field` simply produces a job that completes with 0 results. Double-check field names against the [search reference](/company-docs/search/reference) first. Do not pass a non-empty `sorts` array — the job will complete with **0 results**. Sort the downloaded file instead, for example `jq -s 'sort_by(.headcount.total) | reverse | .[]' results.jsonl`. --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. --- ### Errors ```json 400 — no filters { "error": { "type": "invalid_request", "message": "`filters` must be provided for search", "metadata": [] } } ``` ```json 400 — invalid max_results { "error": { "type": "invalid_request", "message": "`max_results` must be a positive integer", "metadata": [] } } ``` ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## Batch live search Batch live search is available on the **live** plan. Batch live search runs one query asynchronously and delivers the **entire result set** as a single file, with results retrieved from the web in real time. Where [Company Live Search](/company-docs/search/live-search) returns one cursor page per call, a batch job walks every page for you. ``` POST https://api.crustdata.com/batch/company/professional_network/search/live ``` For results served from the indexed database — higher caps and faster — see [Batch search](#batch-search) above. Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ### How batch live search differs from non-batch search - **One query, whole result set.** You submit a single query; the job paginates server-side until `max_results` is reached or the matches run out. - **`max_results` is the only volume control.** It clamps to **1,000** and defaults to that cap when omitted. Values above the cap are silently clamped; zero or negative values return `400`. The non-batch paging knobs — `limit`, `page`, `preview` — are silently ignored. - **Flat records.** Each line in the results file has exactly the non-batch search record shape. No envelope. - **Exact field projection.** When you pass `fields`, each record contains exactly those fields — nothing more. Omit `fields` to get every field your account can read. --- ### Live batch search `POST /batch/company/professional_network/search/live` retrieves results from the web in real time — fresher than the database, capped at **1,000** records per job, fetched internally in pages of 25, and slower. Live jobs are the most likely to end `failed` with an `error_message`. Live filters use a **flat list** of `{field, type, value}` objects — not the `{op, conditions}` group — and `field` takes an uppercase filter type instead of a dotted path: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["11-50"]}, {"field": "REGION", "type": "in", "value": ["United States"]} ], "max_results": 2, "fields": ["basic_info.name", "headcount.total"] }' ``` ```json Response { "batch_id": "eeba5683-b309-40a8-862d-b89e7ca78276", "status": "pending", "entity": "company", "action": "search_live", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/eeba5683-b309-40a8-862d-b89e7ca78276" } ``` ```json The results file (all records) {"basic_info": {"name": "Annie Rose Inc"}, "headcount": {"total": 31}} {"basic_info": {"name": "Stanford Law Review"}, "headcount": {"total": 18}} ``` Valid filter types: `INDUSTRY`, `COMPANY_HEADCOUNT`, `REGION`, `COMPANY_HEADQUARTERS`, `ANNUAL_REVENUE`, `NUM_OF_FOLLOWERS`, `FORTUNE`, `DEPARTMENT_HEADCOUNT`, `DEPARTMENT_HEADCOUNT_GROWTH`, `COMPANY_HEADCOUNT_GROWTH`, `KEYWORD_COMPANY`, `ACCOUNT_ACTIVITIES`, `JOB_OPPORTUNITIES` — the same filters as the non-batch [Company Live Search](/company-docs/search/live-search), which documents per-filter value formats. An unknown filter type returns `400` listing the valid values. Company filters additionally support the `between` match type with an object value and an optional `sub_filter` qualifier: ```json A between filter {"field": "ANNUAL_REVENUE", "type": "between", "value": {"min": 1, "max": 500}, "sub_filter": "USD"} ``` Instead of `filters`, you can replay a saved company search URL: ```json Request body { "professional_network_search_url": "https://www.linkedin.com/sales/search/company?query=...", "max_results": 100 } ``` Provide either `filters` or `professional_network_search_url` — omitting both returns `400`. --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. --- ### Errors ```json 400 — neither filters nor a search URL { "error": { "type": "invalid_request", "message": "Either `filters` or `professional_network_search_url` must be provided for live search", "metadata": [] } } ``` ```json 400 — invalid max_results { "error": { "type": "invalid_request", "message": "`max_results` must be a positive integer", "metadata": [] } } ``` ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## What to do next - **Build queries interactively first** — iterate with [Company Search](/company-docs/search/introduction) or [Company Live Search](/company-docs/search/live-search) page by page, then submit the final query as a batch job. - **Look up operators and fields** — see the [search reference](/company-docs/search/reference). - **Enrich the companies you found** — see [Batch Company Enrich](/company-docs/enrichment/batch). - **Search people in batch** — see [Batch Person Search](/person-docs/search/batch-search). - **Search jobs in batch** — see [Batch Job Search](/job-docs/search/batch-search). - **Full schema** — see the [API reference](/openapi-specs/2025-11-01/introduction). # Company Realtime Search Source: https://docs.crustdata.com/company-docs/search/live-search Search companies in real time using structured live-search filters. **Use this when** you need live company results from the source — for example, recently funded companies or companies that match specific live-search criteria. The Company Realtime Search API lets you search companies in real time from the source. Unlike the standard [Company Search](/company-docs/search/introduction), which queries Crustdata's indexed database, this endpoint runs a live search using structured filters. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/company/professional_network/search/live ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------------------------------- | | `filters` | array | Yes | Array of filter objects. Each has `field`, `type`, and `value`. | | `page` | integer | Yes | Page number to return. Each page returns up to 25 results. | ### Response body | Field | Type | Description | | --------------------- | ------ | ------------------------------------------ | | `companies` | array | Company profiles matching the search. | | `total_display_count` | string | Total matching companies (e.g., `"32K+"`). | ### Rate limits and credits Pricing: 2 credits per company returned. Availability: Enterprise-only. - **Rate limits:** Live endpoint throughput is custom by plan. See [Rate limits](/general/rate-limits). --- ## Search vs Realtime Search | | **Search** | **Realtime Search** | | ---------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | **Data source** | Crustdata indexed database | Live search from the source | | **Filter syntax** | `{ "field": "dotpath", "type": "op", "value": ... }` | `[{ "field": "ENUM_TYPE", "type": "in", "value": [...] }]` | | **Filter field names** | Dot-path fields like `locations.country` | Enums like `REGION`, `INDUSTRY` | | **Country values** | ISO3 codes (`"USA"`, `"GBR"`) | Full names (`"United States"`) | | **Operators** | `=`, `!=`, `>`, `<`, `=>`, `=<`, `in`, `not_in`, `(.)`, `[.]` | `in`, `not in` | | **Boolean logic** | Nested `and`/`or` groups | Multiple filters combined with implicit AND | | **Pagination** | Cursor-based (`cursor` + `next_cursor`) | Page-based (`page`, 25 results per page) | | **Response shape** | `{ companies, next_cursor, total_count }` | `{ companies, total_display_count }` | | **Field selection** | `fields` parameter to choose response fields | Not supported — returns all available fields | | **Best for** | Structured segmentation, large-scale list building, precise filters | Live prospecting, hiring signals, funding events | Use [Company Search](/company-docs/search/introduction) when you need precise structured filters, field selection, and cursor-based pagination over Crustdata's indexed data. Use **Realtime Search** when you want live results from the source — for example, companies actively hiring or with recent funding events. --- ## Discover valid filter values Live search uses a different value vocabulary than indexed search. The dedicated **[Live Search Autocomplete](/person-docs/search/live-autocomplete)** endpoint (`POST /professional_network/search/autocomplete`) returns valid values for the open-ended fields — `industry`, `region`, `title`, and `school`. Use the returned values exactly as-is in your live-search filter `value` arrays. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "industry", "query": "tech", "limit": 5 }' ``` See **[Live Search Autocomplete](/person-docs/search/live-autocomplete)** for all four fields, the field-to-filter mapping, and worked examples. Fields that take a fixed value set (like `COMPANY_HEADCOUNT` and `ACCOUNT_ACTIVITIES`) are not autocompleted — see [Filter fields with fixed values](#filter-fields-with-fixed-values). `/professional_network/search/autocomplete` can be permission-gated. If you receive `403 permission_error`, ask support to enable access for your account. --- ## Search with structured filters Use the `filters` array to define search criteria programmatically. Each filter has a `field`, `type` (operator), and `value`. Some filters also accept an optional `sub_filter`. ### Find mid-size tech companies This search finds companies with 51–200 employees in the Technology industry. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"] }, { "field": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"] } ], "page": 1 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Nielsen Norman Group", "primary_domain": "nngroup.com", "website": "https://www.nngroup.com/", "description": "Evidence-Based User Experience (UX) Research, Training, and Consulting...", "company_type": "Privately Held", "year_founded": 1998, "employee_count_range": "51-200", "industries": ["usability", "user experience", "UX consulting"] }, "headcount": { "total": 191 }, "locations": { "country": "United States", "city": "Silicon Valley", "state": "California", "headquarters": "Silicon Valley, California, United States" }, "revenue": { "estimated": { "lower_bound_usd": 2500000, "upper_bound_usd": 5000000 } }, "people": { "decision_makers_count": "14" } } ], "total_display_count": "32K+" } ``` Response trimmed to one company for clarity. The API returns up to 25 companies per page. ### Understanding the response Every realtime search response has two fields: - **`companies`** — an array of company profiles matching your search. Each profile includes basic info, headcount, taxonomy, location, revenue estimates, and decision-maker counts. - **`total_display_count`** — the total number of matching companies across all pages (as a string, e.g., `"32K+"`). This is approximate — exact counts are not available for large result sets. ### How to interpret results - **`total_display_count` is a string:** Values like `"32K+"` mean approximately 32,000+ matches. Parse as an approximate indicator, not an exact count. - **Empty `companies` array:** No more results for this page. Stop paginating. - **Filter values are strict:** Use the shared autocomplete endpoint to discover valid enumerated values before building filters. --- ## More filter examples ### Large companies outside the US with revenue $1M–$500M ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["1,001-5,000", "5,001-10,000", "10,001+"] }, { "field": "ANNUAL_REVENUE", "type": "between", "value": {"min": 1, "max": 500}, "sub_filter": "USD" }, { "field": "REGION", "type": "not in", "value": ["United States"] } ], "page": 1 }' ``` ### Recently funded companies ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "ACCOUNT_ACTIVITIES", "type": "in", "value": ["Funding events in past 12 months"] } ], "page": 1 }' ``` --- ## Paginate through results Use the `page` parameter to walk through pages of results. Each page returns up to 25 companies. **First page:** set `page` to `1`. **Next page:** increment `page` by 1 and repeat the same request. ```bash Page 2 curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"] } ], "page": 2 }' ``` Continue incrementing `page` until the `companies` array comes back empty. --- ### Filter object schema Each filter in the `filters` array must have this shape: | Key | Type | Required | Description | | ------------ | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | `field` | string | Yes | A filter type (see table below). | | `type` | string | Yes | Operator: `"in"`, `"not in"`, or `"between"`. | | `value` | string, string[], or `{min,max}` | Yes | A string or array of strings for discrete filters, or a `{min, max}` object for range filters. | | `sub_filter` | string | No | Optional qualifier for certain filters, such as `"USD"` for `ANNUAL_REVENUE`. | All filters in the array are combined with implicit AND logic. Nested `or` groups are not supported in Realtime Search — use [indexed Search](/company-docs/search/introduction) for complex boolean queries. ## Available filter fields | Field | Description | Operators | Example values | | ----------------------------- | -------------------- | ------------------------- | ---------------------------------------------------------------- | | `INDUSTRY` | Industry | `in`, `not in` | `"Software Development"` | | `COMPANY_HEADCOUNT` | Employee count range | `in`, `not in` | `"51-200"`, `"1,001-5,000"` | | `REGION` | Geographic region | `in`, `not in` | `"United States"`, `"Europe"` | | `COMPANY_HEADQUARTERS` | HQ location | `in`, `not in` | City or country names | | `ANNUAL_REVENUE` | Revenue range | `in`, `not in`, `between` | `"1-500"` or `{ "min": 1, "max": 500 }` with `sub_filter: "USD"` | | `NUM_OF_FOLLOWERS` | Follower count | `in`, `not in` | Follower count ranges | | `FORTUNE` | Fortune list | `in` | `"Fortune 500"` | | `DEPARTMENT_HEADCOUNT` | Department size | `in`, `not in` | Department headcount ranges | | `DEPARTMENT_HEADCOUNT_GROWTH` | Dept growth rate | `in`, `not in` | Growth rate ranges | | `COMPANY_HEADCOUNT_GROWTH` | Headcount growth | `in`, `not in` | Growth rate ranges | | `KEYWORD_COMPANY` | Keyword search | `in` | Any keyword string | | `ACCOUNT_ACTIVITIES` | Company activities | `in` | `"Funding events in past 12 months"` | | `JOB_OPPORTUNITIES` | Hiring activity | `in` | Use autocomplete for current values | Filter values use the Realtime Search vocabulary, not Crustdata's indexed values. For example, regions use full names like `"United States"`, not ISO3 codes like `"USA"`. Use `/professional_network/search/autocomplete` to discover current values for the open-ended fields (`INDUSTRY`, `REGION`, `COMPANY_HEADQUARTERS`). The fields below take a **fixed set of values** that autocomplete does not enumerate. ### Filter fields with fixed values Values are matched as exact strings, and commas can be part of a single value — a range like `1,001-5,000` is one array element. If your client or shell splits list input on commas, quote or escape these values before building the array. | Field | Accepted values | | -------------------- | ----------------------------------------------------------------------------------------------------------- | | `COMPANY_HEADCOUNT` | `1-10`, `11-50`, `51-200`, `201-500`, `501-1,000`, `1,001-5,000`, `5,001-10,000`, `10,001+` | | `ACCOUNT_ACTIVITIES` | `Senior leadership changes in last 3 months`, `Funding events in past 12 months` | | `JOB_OPPORTUNITIES` | `Hiring on Linkedin` | `ANNUAL_REVENUE` takes a min/max **in millions** with a `sub_filter` currency — one of `USD`, `AED`, `AUD`, `BRL`, `CAD`, `CNY`, `DKK`, `EUR`, `GBP`, `HKD`, `IDR`, `ILS`, `INR`, `JPY`, `NOK`, `NZD`, `RUB`, `SEK`, `SGD`, `THB`, `TRY`, `TWD`. `DEPARTMENT_HEADCOUNT` and `DEPARTMENT_HEADCOUNT_GROWTH` take a department name (the same 26-value set as the person-side `FUNCTION` field). ## Supported operators | Operator | Description | Use with | | --------- | -------------------------- | -------------------------------------- | | `in` | Value is in list | Most filter fields | | `not in` | Value is not in list | `REGION`, `INDUSTRY`, etc. | | `between` | Value falls within a range | Range filters such as `ANNUAL_REVENUE` | --- ## Response fields Each company in the response includes these sections: | Section | Key fields | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `basic_info` | `name`, `primary_domain`, `website`, `description`, `company_type`, `year_founded`, `employee_count_range`, `logo_permalink`, `industries`, `professional_network_url`, `professional_network_id` | Core identity and profile | | `headcount` | `total` | Total employee count | | `taxonomy` | `professional_network_industry` | Industry classification | | `locations` | `country`, `city`, `state`, `headquarters` | Headquarters location | | `revenue` | `estimated.lower_bound_usd`, `estimated.upper_bound_usd` | Revenue range estimate | | `people` | `decision_makers_count` | Number of decision makers | --- ## Validation rules | Rule | Behavior | | ------------- | ---------------------------------------------- | | `filters` | Provide one or more live-search filters. | | `page` | Required for filter-based requests. | | Empty results | Returns `200` with an empty `companies` array. | ## Errors ```json 400 — Invalid filter key { "error": { "type": "invalid_request", "message": "'filter_type' is not supported. Use 'field' instead.", "metadata": [] } } ``` ```json 401 — Invalid API key { "message": "Invalid API key in request" } ``` ## API reference summary | Detail | Value | | ---------------- | ----------------------------------------------------------------------------- | | **Endpoint** | `POST /company/professional_network/search/live` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | `filters` array plus `page` | | **Response** | `{ companies, total_display_count }` | | **Pagination** | Page-based. 25 results per page. Increment `page` until `companies` is empty. | | **Empty result** | `200` with `"companies": []` | | **Errors** | `400` (bad filter/URL), `401` (bad auth), `500` (server error) | Use `/professional_network/search/autocomplete` to discover current live-search filter values when the endpoint is enabled for your account. For credit pricing, see [Pricing](/general/pricing). For throughput guidance, see [Rate limits](/general/rate-limits). See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request against `POST /company/professional_network/search/live`. All filters in the array are combined with implicit AND. For the filter-field catalog, operators, and validation rules, see the sections below. Combine `COMPANY_HEADCOUNT`, `ANNUAL_REVENUE` (with the `USD` sub-filter), and a negated `REGION` to segment large, high-revenue companies headquartered outside the United States. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["1,001-5,000", "5,001-10,000", "10,001+"] }, { "field": "ANNUAL_REVENUE", "type": "between", "value": {"min": 1, "max": 500}, "sub_filter": "USD" }, { "field": "REGION", "type": "not in", "value": ["United States"] } ], "page": 1 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Nintendo", "primary_domain": "http://www.nintendo.com", "website": "http://www.nintendo.com", "company_type": "Public Company", "year_founded": 1980, "employee_count_range": "1001-5000", "professional_network_url": "https://www.linkedin.com/company/nintendo/" }, "headcount": { "total": 7706 }, "taxonomy": { "professional_network_industry": "Computer Games" }, "locations": { "city": "Redmond", "country": "United States", "headquarters": "Redmond, Washington, United States", "state": "Washington" }, "revenue": { "estimated": { "lower_bound_usd": 500000000, "upper_bound_usd": 1000000000 } }, "people": { "decision_makers_count": "326" } } ], "total_display_count": "21K+" } ``` Response trimmed to one company for clarity. The API returns up to 25 companies per page. `ANNUAL_REVENUE` requires the `between` operator and the `"USD"` sub-filter. Pair an `INDUSTRY` filter with `JOB_OPPORTUNITIES` to find companies in a market that are currently hiring — a strong intent signal for sales and recruiting outreach. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"] }, { "field": "JOB_OPPORTUNITIES", "type": "in", "value": ["Hiring on Linkedin"] } ], "page": 1 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Salesforce", "primary_domain": "http://www.salesforce.com", "website": "http://www.salesforce.com", "company_type": "Public Company", "employee_count_range": "10001+", "professional_network_url": "https://www.linkedin.com/company/salesforce/" }, "headcount": { "total": 76180 }, "taxonomy": { "professional_network_industry": "Software Development" }, "locations": { "city": "San Francisco", "country": "United States", "headquarters": "San Francisco, California, United States", "state": "California" }, "revenue": { "estimated": { "lower_bound_usd": 1000000000, "upper_bound_usd": 1000000000000 } }, "people": { "decision_makers_count": "10K+" } } ], "total_display_count": "30K+" } ``` Response trimmed for clarity. Use `/professional_network/search/autocomplete` to discover current values for `INDUSTRY` and `JOB_OPPORTUNITIES` before building filters. Use `ACCOUNT_ACTIVITIES` to surface companies with recent funding events — a common trigger for prospecting and investor research. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "ACCOUNT_ACTIVITIES", "type": "in", "value": ["Funding events in past 12 months"] } ], "page": 1 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Morgan Stanley", "primary_domain": "http://www.morganstanley.com", "website": "http://www.morganstanley.com", "company_type": "Public Company", "employee_count_range": "10001+", "professional_network_url": "https://www.linkedin.com/company/morgan-stanley/" }, "headcount": { "total": 99786 }, "taxonomy": { "professional_network_industry": "Financial Services" }, "locations": { "city": "New York", "country": "United States", "headquarters": "New York, New York, United States", "state": "NY" }, "revenue": { "estimated": { "lower_bound_usd": 1000000000, "upper_bound_usd": 1000000000000 } }, "people": { "decision_makers_count": "31K+" } } ], "total_display_count": "1K+" } ``` Response trimmed for clarity. Stack multiple discrete filters to narrow a population precisely. This search finds mid-size (51–200) software companies headquartered in the United States. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"] }, { "field": "INDUSTRY", "type": "in", "value": ["Software Development"] }, { "field": "REGION", "type": "in", "value": ["United States"] } ], "page": 1 }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Inworld AI", "primary_domain": "https://inworld.ai", "website": "https://inworld.ai", "company_type": "Privately Held", "year_founded": 2021, "employee_count_range": "51-200", "professional_network_url": "https://www.linkedin.com/company/inworld-ai/" }, "headcount": { "total": 87 }, "taxonomy": { "professional_network_industry": "Software Development" }, "locations": { "city": "Mountain View", "country": "United States", "headquarters": "Mountain View, California, United States", "state": "California" }, "revenue": { "estimated": { "lower_bound_usd": 10000000, "upper_bound_usd": 20000000 } }, "people": { "decision_makers_count": "15" } } ], "total_display_count": "6K+" } ``` Response trimmed for clarity. All three filters are combined with implicit AND. If you already have a saved company-search URL, pass it as `professional_network_search_url` to replay it directly instead of rebuilding the filter array. Copy the URL from your search results page so it carries your saved criteria. When you use a search URL, `page` is optional. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_search_url": "https://www.linkedin.com/sales/search/company?query=(keywords:robotics)" }' ``` ```json Response { "companies": [ { "basic_info": { "name": "Agility Robotics", "primary_domain": "http://agilityrobotics.com/", "website": "http://agilityrobotics.com/", "company_type": "Privately Held", "employee_count_range": "201-500", "professional_network_url": "https://www.linkedin.com/company/agilityrobotics/" }, "headcount": { "total": 420 }, "taxonomy": { "professional_network_industry": "Robotics Engineering" }, "locations": { "city": "Salem", "country": "United States", "headquarters": "Salem, Oregon, United States", "state": "Oregon" }, "revenue": { "estimated": { "lower_bound_usd": 20000000, "upper_bound_usd": 50000000 } }, "people": { "decision_makers_count": "35" } } ], "total_display_count": "55K+" } ``` Response trimmed for clarity. The replayed search returns the same envelope as a filter-based request — `companies` plus `total_display_count`. Provide either `filters` (with `page`) or `professional_network_search_url`. ## What to do next - **Search Crustdata's database** — use [Company Search](/company-docs/search/introduction) to query indexed company data with more filter options and sorting. - **Discover live filter values** — use `/professional_network/search/autocomplete` when it is enabled for your account. - **Enrich a company** — use [Company Enrich](/company-docs/enrichment/introduction) to get a detailed profile for a known company. - **Full API reference** — see the [OpenAPI spec](/openapi-specs/2025-11-01/introduction) for the complete request/response schema. # Live Search Autocomplete Source: https://docs.crustdata.com/person-docs/search/live-autocomplete Discover valid values for real-time (live) search filters — industry, region, title, and school — via the shared /professional_network/search/autocomplete endpoint. Real-time (live) search uses a different value vocabulary than indexed search. Use this endpoint to discover valid values for the open-ended live-search filter fields before you build a [Person Live Search](/person-docs/search/live-search) or [Company Live Search](/company-docs/search/live-search) query. It is **shared** across person and company live search. This is **separate** from [Person Autocomplete](/person-docs/autocomplete/introduction) and [Company Autocomplete](/company-docs/autocomplete/introduction), which serve the **indexed** `/person/search` and `/company/search` endpoints. The field names and the returned values differ — use this page for live search. ## Endpoint ``` POST https://api.crustdata.com/professional_network/search/autocomplete ``` ## Request | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------ | | `field` | string | Yes | One of `industry`, `region`, `title`, `school`. | | `query` | string | Yes | Prefix/substring to match against the field's values. | | `limit` | integer | No | Maximum number of suggestions to return. | ## Which live-search fields support autocomplete? Only the four open-ended fields above. The other live-search filter fields take a **fixed set of values** that this endpoint does not enumerate — see [Person Live Search → Filter fields with fixed values](/person-docs/search/live-search#filter-fields-with-fixed-values) and [Company Live Search → Filter fields with fixed values](/company-docs/search/live-search#filter-fields-with-fixed-values). Company filters (`CURRENT_COMPANY`, `PAST_COMPANY`) take a profile URL, domain, or name directly — no autocomplete needed. Map the autocomplete `field` to the live-search filter field as follows: | Autocomplete `field` | Live-search filter field(s) | | -------------------- | --------------------------------------------- | | `title` | `CURRENT_TITLE`, `PAST_TITLE` | | `industry` | `INDUSTRY` | | `region` | `REGION`, `COMPANY_HEADQUARTERS` | | `school` | `SCHOOL` | ## Examples ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "industry", "query": "tech", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Technical and Vocational Training" }, { "value": "Technology, Information and Internet" }, { "value": "Technology, Information and Media" } ] } ``` Use the returned value verbatim in an `INDUSTRY` filter, e.g. `{"field": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"]}`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "region", "query": "new york", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "New York City Metropolitan Area" }, { "value": "New York County, New York, United States" }, { "value": "New York Mills, Minnesota, United States" } ] } ``` Region values feed both `REGION` (the person's region) and `COMPANY_HEADQUARTERS`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "title", "query": "technical account", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Technical Account Executive" }, { "value": "Technical Account Manager" }, { "value": "Technical Accountant" } ] } ``` Title values feed `CURRENT_TITLE` and `PAST_TITLE`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "school", "query": "harvard univ", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Harvard University" }, { "value": "Harvard University -- Executive Education" }, { "value": "Harvard University Graduate School of Design" } ] } ``` School values feed the `SCHOOL` filter. Default `rate-limit` is 10 requests per minute. Email [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits. This endpoint can be permission-gated. If you receive `403 permission_error`, ask support to enable live-search access for your account. ## What to do next - **Person live search** — use the values in [Person Live Search](/person-docs/search/live-search) - **Company live search** — use the values in [Company Live Search](/company-docs/search/live-search) - **Indexed autocomplete** — for the indexed `/person/search` and `/company/search` endpoints, see [Person Autocomplete](/person-docs/autocomplete/introduction) and [Company Autocomplete](/company-docs/autocomplete/introduction) # Company Enrich Source: https://docs.crustdata.com/company-docs/enrichment/introduction Learn how to enrich company records using domains, profile URLs, names, or IDs, and get a detailed company profile. **Use this when** you already know the company and want its full profile — for research, scoring, personalization, or diligence. The Company Enrich API takes an identifier you already have — a website domain, a profile URL, a company name, or a Crustdata company ID — and returns a detailed company profile with headcount, funding, industry, hiring, and more. The same endpoint supports both single-company lookups and multi-company requests. This page covers the basics (your first enrichment and the response shape) plus worked example recipes. For the request/response schema and errors, see [Enrich reference](/company-docs/enrichment/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/company/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Default | Description | | ----------------------------------------------------------------------------------- | -------- | -------------- | ---------------- | -------------------------------------------------------------------- | | `domains` / `professional_network_profile_urls` / `names` / `crustdata_company_ids` | array | Yes — one only | — | Submit exactly one identifier type. Max 25 per request. | | `fields` | string[] | No | `["basic_info"]` | Sections of `company_data` to include in the response. | | `exact_match` | boolean | No | `null` | Set to `true` to restrict results to exact `primary_domain` matches. | **Looking for the list of sections you can request?** See [Valid `fields` values](/company-docs/enrichment/reference#valid-fields-values) for the full table of section group names you can pass to `fields`. ### Response body The response is a top-level array. Each entry corresponds to one input identifier. | Field | Type | Description | | ---------------------------- | ------ | ---------------------------------------------------------------------------- | | `matched_on` | string | The input identifier you submitted | | `match_type` | string | `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url` | | `matches` | array | Candidate matches. Empty for no-match inputs. | | `matches[].confidence_score` | number | Higher is better. `1.0` is common for direct identifier lookups. | | `matches[].company_data` | object | Full enriched company profile. | ### Rate limits and pricing Pricing: 2 credits per record. Requesting technographics adds +2 credits per company that returns technographics data. - **Rate limit:** 15 requests per minute. If you only need lightweight discovery, start with [Company Search](/company-docs/search/introduction), then enrich the companies you want in full detail. Request parameters, response fields, valid `fields` values, `company_data` sections, validation, errors. --- ## Your first enrichment: look up a company by domain The simplest enrichment takes a website domain and returns matching company profiles. Pass the domain in the `domains` array. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["retool.com"] }' ``` ```json Response [ { "matched_on": "retool.com", "match_type": "domain", "matches": [ { "confidence_score": 11.0, "company_data": { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "all_domains": ["retool.com"], "website": "https://retool.com/", "description": "Build internal software better with AI...", "company_type": "Privately Held", "year_founded": 2017, "employee_count_range": "201-500", "markets": ["PRIVATE"] } } } ] } ] ``` `description` in `basic_info` is truncated in this snippet for readability. Other fields reflect the default response — when `fields` is omitted, `company_data` only contains `crustdata_company_id` and `basic_info`. Request additional sections such as `headcount`, `funding`, or `people` explicitly via `fields` (see [Using the `fields` parameter](/company-docs/enrichment/reference#using-the-fields-parameter)). ### Understanding the response The Enrich API returns a **top-level array** — one entry per identifier you submitted. Each entry has three fields: - **`matched_on`** — the identifier you submitted (the domain, URL, name, or ID). - **`match_type`** — which identifier type was used. Possible values: `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url`. - **`matches`** — an array of candidate companies. Each match includes a `confidence_score` and the full `company_data` object. Domain lookups may return multiple matches if the domain is ambiguous. The highest `confidence_score` indicates the best match. Use `exact_match: true` to restrict results to companies whose `primary_domain` exactly matches your input (see the [exact match recipe](#use-exact-match-for-stricter-domain-matching) below). ### How to interpret results - **Multiple matches:** If `matches` contains more than one entry, check `confidence_score` — the highest score is the best match. Use `primary_domain` to verify. - **Empty `matches` array:** The identifier did not match any company. Check for typos or try a different identifier type. - **`confidence_score`:** Higher is better. A score of `1.0` is common for direct identifier lookups such as profile URLs or company IDs. --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request. For the full list of request and response fields, see [Enrich reference](/company-docs/enrichment/reference). If you have a company profile URL, pass it in `professional_network_profile_urls`. This gives you a direct match. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/company/serverobotics" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/company/serverobotics", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 628895, "basic_info": { "name": "Serve Robotics", "primary_domain": "serverobotics.com", "all_domains": ["serverobotics.com"], "website": "https://www.serverobotics.com/", "company_type": "Public Company", "year_founded": 2021, "employee_count_range": "51-200", "markets": ["PRIVATE", "NASDAQ"] } } } ] } ] ``` Response trimmed for clarity. Profile URL lookups are direct matches — they typically return a single match with high confidence. You can also enrich by company name. This is useful when you only have a name from a form submission or event badge scan. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "names": ["Retool"] }' ``` Name-based enrichment may return multiple candidates. Check `confidence_score` and `primary_domain` to pick the right match. If you already have a Crustdata company ID (from a previous search call), pass it in `crustdata_company_ids` for an exact lookup. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [633593] }' ``` Company ID lookups typically return a single exact match, making this the most precise enrichment method. By default, domain-based enrichment can return multiple candidates. Set `exact_match: true` to restrict results to companies whose `primary_domain` exactly matches your input. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["cashfree.com"], "exact_match": true }' ``` ```json Response [ { "matched_on": "cashfree.com", "match_type": "domain", "matches": [ { "confidence_score": 15.0, "company_data": { "basic_info": { "name": "Cashfree Payments", "primary_domain": "cashfree.com" } } }, { "confidence_score": 4.0, "company_data": { "basic_info": { "name": "Cashfree Tech", "primary_domain": "cashfree.com" } } } ] } ] ``` Response trimmed for clarity. With `exact_match: true`, results are limited to records whose `primary_domain` exactly matches your input. You may still receive multiple matches when more than one company record shares that same domain. The same endpoint supports multiple identifiers in a single request, so multi-company enrich stays on this page rather than as a separate API. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [633593, 628895] }' ``` ### Multi-company enrich tips - Submit **one identifier type** per request. Mixing identifier types (e.g., sending both `domains` and `names`) is not supported. - Each entry in the response corresponds to the input at the same position, so you can match results back to your input list by index. - If some identifiers fail to match, their `matches` array will be empty, but the request still succeeds for the others. Each identifier type has trade-offs in precision and convenience. | | Domain | Profile URL | Company Name | Company ID | | ------------------- | ----------------------------------- | ---------------------- | ------------------- | --------------------------------------- | | **Precision** | High | Highest | Medium | Highest | | **Best for** | CRM cleanup, inbound leads | Known company profiles | Fuzzy matching | Internal pipelines and search follow-up | | **Typical matches** | One or more exact-domain candidates | 1 | Multiple candidates | 1 | The most powerful pattern combines [Company Search](/company-docs/search/introduction) with Company Enrich. Search finds companies matching your criteria; Enrich gets the full profile for each match. **Step 1:** Search for well-funded software companies. ```bash curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.industries", "type": "in", "value": ["Software Development"] }, { "field": "funding.total_investment_usd", "type": ">", "value": 10000000 } ] }, "limit": 5, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain"] }' ``` **Step 2:** Take the `crustdata_company_id` values from the search results and pass them in `crustdata_company_ids` to enrich. ```bash curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [633593, 628895] }' ``` This two-step pattern is the foundation for sales, research, and investment workflows. Search narrows the universe; Enrich fills in the details. Because `crustdata_company_ids` is an array, the same endpoint works for one company or many companies. Request every standard section in one call to build a complete profile — the equivalent of the legacy endpoint's return-everything default. Omitted sections come back as `null` placeholders, so list each section you want. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["retool.com"], "fields": [ "basic_info", "revenue", "headcount", "funding", "hiring", "locations", "taxonomy", "people", "competitors", "followers", "web_traffic", "seo", "news", "social_profiles", "software_reviews", "employee_reviews", "reviews", "public_launches", "market_intel" ] }' ``` ```json Response [ { "matched_on": "retool.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "year_founded": 2017, "employee_count_range": "201-500" }, "headcount": { "total": 416, "largest_headcount_country": "USA", "growth_percent": { "mom": 0.0, "qoq": -3.48, "six_months": -7.76, "yoy": -10.54, "two_years": 11.83 } }, "funding": { "total_investment_usd": 141000000.0, "last_round_type": "series_c", "last_fundraise_date": "2022-07-27", "last_round_amount_usd": 45000000.0, "investors": ["Liquid 2 Ventures", "SV Angel", "John Collison", "Pedro Franceschi"] }, "locations": { "country": "USA", "headquarters": "San Francisco, California, United States", "all_office_addresses": ["915 Broadway, New York, NY, 10010, US", "221 Pentonville Road, London, England, N1 9, GB"] }, "taxonomy": { "professional_network_industry": "Software Development" }, "revenue": { "estimated": { "lower_bound_usd": 20000000, "upper_bound_usd": 50000000 } }, "hiring": { "openings_count": 6, "openings_growth_percent": { "mom": -0.45, "qoq": 0.0, "yoy": -0.67 } }, "followers": { "count": 41310, "yoy_percent": 25.44 }, "people": { "founders": [ { "crustdata_person_id": 14540, "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" } } ] } } } ] } ] ``` Response trimmed for clarity — only a few of the requested sections are shown. The full response also includes per-section timeseries, role/region breakdowns, and `people.decision_makers` and `people.cxos` arrays. Each entry in `fields` is a section group — see [Valid `fields` values](/company-docs/enrichment/reference#valid-fields-values). Add-on sections such as `technographics` require a field grant on your account. `/company/enrich` keys the headcount growth maps by **period alias** (`mom`, `qoq`, `six_months`, `yoy`, `two_years`), whereas [`/company/search`](/company-docs/search/introduction) uses `1m`, `3m`, `6m`, `12m`. The values are equivalent; map the keys explicitly. The product-and-review intelligence formerly returned as a separate analyst dataset is now the `market_intel` field group. It includes products, categories, and detailed reviews. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["builder.io"], "fields": ["market_intel"], "exact_match": true }' ``` ```json Response [ { "matched_on": "builder.io", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 698873, "market_intel": { "slug": "builder-io", "company_name": "Builder.io", "year_founded": 2018, "head_office_city": "San Francisco", "head_office_country": "US", "num_employees_min": 51, "num_employees_max": 200, "products": [ { "slug": "builder-io", "name": "Builder.io", "category": "landing-page-software" } ], "reviews": [ { "review_id": 5433620, "headline": "Builder.io: Drag-and-Drop Bliss with A/B Testing Power (But Onboard with Care!)", "overall_rating": 4 } ] } } } ] } ] ``` Response trimmed for clarity. Each review also carries rating breakdowns, purchase reasons, and competitors considered. Product-launch data (launches, makers, ratings, and reviews) is now the `public_launches` field group. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["builder.io"], "fields": ["public_launches"], "exact_match": true }' ``` ```json Response [ { "matched_on": "builder.io", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 698873, "public_launches": { "slug": "builder-io", "company_name": "Builder.io", "producthunt_url": "https://www.producthunt.com/products/builder-io", "rating": 4.62, "num_upvotes": 1803, "num_reviews": 28, "num_followers": 1175, "categories": ["Headless CMS software", "No-code platforms", "Website builders"], "makers": [ { "username": "steve_sewell", "name": "Steve Sewell", "headline": "Founder, CEO of Builder.io" } ] } } } ] } ] ``` Response trimmed for clarity. The `technographics` field group returns the technologies detected for a company — a count, the most notable names, and the full list with categories and detection sources. It is **never included by default**: request it explicitly via `fields`. It also requires field-level permission on your account — contact Crustdata to enable it. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["hubspot.com"], "fields": ["technographics"] }' ``` ```json Response [ { "matched_on": "hubspot.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "technographics": { "total_technologies": 32, "top_technologies": ["AWS", "AngularJS", "Cloudflare DNS"], "technologies": [ { "name": "AWS", "sources": ["web signals", "job posting"], "posting_count": 3, "categories": [ { "category_slug": "cloud-platforms-iaas", "super_slug": "cloud-infrastructure", "score": 1.0 } ], "description": "Cloud platform.", "evidence": [ { "source": "job posting", "posting_id": "p-1", "confidence": 0.9, "url": "https://jobs.example/p-1", "title": "DevOps Engineer", "posted_at": "2026-06-01T09:30:00Z" } ] } ], "updated_at": "2026-07-03T04:19:03.112000Z" } } } ] } ] ``` Response trimmed for clarity. `technologies` contains one entry per detected technology (`total_technologies` of them). Each entry has a `name`, its detection `sources` (`"web signals"` for the company's web presence, `"job posting"` for the company's job postings), its catalog `categories` and `description`, and its job-posting signal: a `posting_count` and `evidence` entries. See [`technographics` fields](/company-docs/enrichment/reference#technographics-fields) for the full field reference. Add-on pricing: requesting technographics adds +2 credits per company that returns technographics data, on top of the endpoint's base cost. Companies with no technographics data are not charged the add-on. The same add-on pricing applies to [Batch Company Enrich](/company-docs/enrichment/batch). Company Search can **filter** on technographics (`technographics.total_technologies`, `technographics.technologies.name`, and more) but does not return technographics values in its responses — filter with [Company Search](/company-docs/search/reference#technographics-filter-only), then enrich the matches here to get the values. The `locations` section returns the headquarters as a raw string **and** as parsed `country` / `state` / `street_address` fields, plus every office address. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["stripe.com"], "fields": ["basic_info", "locations"] }' ``` ```json Response [ { "matched_on": "stripe.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "basic_info": { "name": "Stripe", "primary_domain": "stripe.com" }, "locations": { "country": "USA", "state": "California", "headquarters": "South San Francisco, California, United States", "street_address": "354 Oyster Point Blvd, South San Francisco, California, United States", "all_office_addresses": [ "1 Wilton Park, Wilton Terrace, Dublin, County Dublin, D02 FX04, IE", "920 5th Ave, Seattle, Washington, 98104, US", "201 Bishopsgate, London, England, EC2M 3NS, GB" ] } } } ] } ] ``` Response trimmed for clarity. Request the **full section name** (`"locations"`) to get the parsed fields. Requesting only a subfield (for example `"locations.headquarters"`) returns the sibling keys (`country`, `state`, `street_address`) as `null` — they are placeholders, not missing data. The same applies to every other section: pass the section name from the [field reference](/company-docs/enrichment/reference) to get all of its fields populated. --- ## What to do next - **Look up request/response details and `fields`** — see [Enrich reference](/company-docs/enrichment/reference) for request parameters, `company_data` sections, valid `fields` values, validation, and errors. - **Search for companies first** — use [Company Search](/company-docs/search/introduction) to find companies by industry, funding, headcount, and more, then enrich the matches. - **Discover filter values** — use [Company Autocomplete](/company-docs/autocomplete/introduction) to find valid values before building search filters. # Batch Company Enrich Source: https://docs.crustdata.com/company-docs/enrichment/batch Enrich up to 10,000 companies in a single asynchronous job from a list of names, domains, profile URLs, or Crustdata IDs. Batch enrichment takes a list of identifiers you already have and returns the enriched record for each as a single results file, instead of one [Company Enrich](/company-docs/enrichment/introduction) call per company. ``` POST https://api.crustdata.com/batch/company/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ## Identifiers: exactly one type per job Each job takes **exactly one** identifier type with up to **10,000 identifiers** — providing none, or more than one type, returns `400`; over-cap submissions are rejected. String-list identifiers also accept a single comma-separated string. | Identifier type | Example values | | ----------------------------------- | --------------------------------------------- | | `domains` | `["stripe.com", "openai.com"]` | | `names` | `["Stripe", "OpenAI"]` | | `professional_network_profile_urls` | `["https://www.linkedin.com/company/stripe"]` | | `crustdata_company_ids` | `[631394, 631466]` | --- ## Your first batch enrichment: companies by domain ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["stripe.com", "openai.com"], "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "pending", "entity": "company", "action": "enrich", "identifier_count": 2, "entities_requested": 2, "status_url": "/batch/53ab686b-c054-496b-8baf-baff5ecc85cf" } ``` When the job completes, each line in the downloaded file is one company wrapped in the enrichment envelope: ```json One record from the results file { "original_identifier": "stripe.com", "internal_id": 631394, "data": { "crustdata_company_id": 631394, "basic_info": { "name": "Stripe", "primary_domain": "stripe.com", "company_type": "Privately Held", "year_founded": 2010 }, "headcount": { "total": 15554 } } } ``` Record trimmed for clarity. ### The record envelope Every enrichment record in the results file has three keys: - **`original_identifier`** — the exact value you submitted, so you can join results back to your input list. - **`internal_id`** — the resolved Crustdata ID. (Live person enrichment instead echoes the submitted profile URL here.) - **`data`** — the enriched record, in the same shape as the corresponding non-batch enrich response. Identifiers that cannot be resolved are **silently dropped** — they produce no record and are not charged. Detect them by comparing `entities_requested` with `entities_fulfilled` on the job object, or by diffing the `original_identifier` values in the file against your input list: ```bash Find inputs that produced no record comm -23 <(sort inputs.txt) <(jq -r '.original_identifier' results.jsonl | sort) ``` --- ## Enrich by Crustdata ID If you already hold `crustdata_company_id` values — for example from a previous search — they are the most precise identifier type. In the results file, `original_identifier` echoes the ID you submitted as a string: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [631394, 631466], "fields": ["basic_info.name", "basic_info.primary_domain"] }' ``` ```json One record from the results file { "original_identifier": "631394", "internal_id": 631394, "data": { "crustdata_company_id": 631394, "basic_info": { "name": "Stripe", "primary_domain": "stripe.com" } } } ``` Record trimmed for clarity. --- ## Fields: requested plus defaults `fields` accepts dotted leaf paths (`headcount.total`), whole family names (`headcount`), or a comma-separated string of either. A family name expands to its entire subtree. The response contains the **requested fields plus the default `basic_info` family** — matching the non-batch [Company Enrich](/company-docs/enrichment/reference) exactly, including the selectable field list. An unsupported value returns `400` with every selectable field in `metadata.available_fields`. This request also shows the comma-separated string identifier form: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": "stripe.com,openai.com", "fields": ["headcount"] }' ``` ```json One record, trimmed — the full headcount family comes back { "original_identifier": "stripe.com", "internal_id": 631394, "data": { "basic_info": { "name": "Stripe", "primary_domain": "stripe.com" }, "crustdata_company_id": 631394, "headcount": { "by_function_timeseries": { "CURRENT_FUNCTION": { "Accounting": [ { "date": "2024-12-01 00:00:00", "employee_count": 222 } ] } } } } } ``` ### `chunk_size` Optional internal processing chunk size (identifiers per processing unit), 10–1000, default 100. Out-of-range values return `400`. --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. --- ## Errors ```json 400 — no identifier { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: names, domains, professional_network_profile_urls, or crustdata_company_ids", "metadata": [] } } ``` ```json 400 — two identifier types { "error": { "type": "invalid_request", "message": "Only one identifier type can be provided. Found: domains, crustdata_company_ids", "metadata": [] } } ``` ```json 400 — invalid field (with available_fields) { "error": { "type": "invalid_request", "message": "Invalid fields: bogus.field", "metadata": [ { "available_fields": [ "basic_info.name", "basic_info.primary_domain", "headcount.total" ] } ] } } ``` ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## What to do next - **Find companies first** — see [Batch Company Search](/company-docs/search/batch-search) to turn one query into an input list. - **Single-company enrichment** — see [Company Enrich](/company-docs/enrichment/introduction). - **Resolve a domain to a company first** — see [Company Identify](/company-docs/identify/introduction). - **Valid `fields` values** — see the [enrich reference](/company-docs/enrichment/reference). - **Enrich people in batch** — see [Batch Person Enrich](/person-docs/enrichment/batch-enrich). - **Full schema** — see the [API reference](/openapi-specs/2025-11-01/introduction). # Employee Review API Source: https://docs.crustdata.com/company-docs/employee_review/introduction Enrich a company with its full employee review profile — aggregate star ratings, individual reviews, CEO approval, and firmographic context — from a Crustdata company ID. The Employee Review API returns the employee review profile for a company: the overall star rating and distribution, category ratings (culture, work/life balance, compensation, management, diversity, career opportunities), CEO approval, review/salary/interview/benefit counts, company firmographics (HQ, size, revenue, stock ticker), office locations, awards, and the underlying individual reviews with their pros, cons, advice, and reviewer context. Use it when you want employee-experience signals on a company you already have in your pipeline — a target account, a competitor, or a prospective employer — without writing custom matching logic. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/employee_review/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: Employee review data is 1 credit per company returned. See [Pricing](/general/pricing) for how this stacks with company enrich. --- ## When to use Employee Review - You have a `crustdata_company_id` and want aggregate employee-experience metrics (overall rating, culture, management, compensation). - You want the individual reviews — pros, cons, advice, and reviewer role — to use as training data, summaries, or qualitative signals. - You want CEO approval and company-level firmographics (HQ, size, revenue, awards) in a single payload. If you only have a domain, name, or profile URL, resolve it first with [Company Identify](/company-docs/identify/introduction) to get the `crustdata_company_id`, then call this endpoint. --- ## Your first enrichment The request takes a single `crustdata_company_id` and returns the full employee review profile for that company. Intel (`680974`) is a good starting identifier because the profile is well-populated. ```bash Request curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 680974 }' ``` ```json Response { "crustdata_company_id": 680974, "company_name": "Intel Corporation", "website": "https://intel.com/life", "headquarters": "Santa Clara, United States", "company_size": "10000+ Employees", "company_type": "Company - Public", "revenue": "$10+ billion (USD)", "stock_ticker": "INTC", "founded_year": 1968, "active_status": "ACTIVE", "approval_status": "APPROVED", "mission": "Create world-changing technology that enriches the lives of every person on earth.", "glassdoor_profile_url": "https://www.glassdoor.com/Overview/Working-at-Intel-Corporation-EI_IE1519.11,28.htm", "primary_industry": { "name": "Computer Hardware Development", "sector_name": "Information Technology" }, "company_ceo": { "name": " Lip-Bu Tan", "title": "CEO", "ceo_rating": 0.63, "ceo_ratings_count": 996 }, "review_count": 39077, "salary_count": 52131, "interview_count": 11042, "benefit_count": 5186, "photo_count": 1214, "global_job_count": 412, "overall_rating": { "rating": 3.9, "total_count": 39076, "rating_1_count": 330, "rating_2_count": 486, "rating_3_count": 2259, "rating_4_count": 4366, "rating_5_count": 3899 }, "culture_and_values_rating": 3.9, "work_life_balance_rating": 4.0, "compensation_and_benefits_rating": 4.2, "senior_management_rating": 3.4, "career_opportunities_rating": 3.7, "diversity_and_inclusion_rating": 4.2, "recommend_to_friend_rating": 0.71, "business_outlook_rating": 0.52, "office_locations": [ { "city_name": "Scottsdale", "city_long_name": "Scottsdale (United States)", "state": "AZ", "country": "US", "continent_name": "North America", "latitude": 33.5091705, "longitude": -111.8983307 } ], "awards": [ { "name": "America's Most Iconic Companies", "source": "TIME Magazine and Statista", "year": "2026" } ], "reviews": [ { "id": 103562586, "summary": "Best place to work with", "pros": "Great Team. Work Life Balance.", "cons": "Repeating work. Needs to rely on cross functional teams", "advice": null, "reviewer_job_title": "Semiconductor research engineer", "reviewer_location_city": "Chandler, AZ", "reviewer_employment_status": "REGULAR", "is_current_job": "1", "length_of_employment": "4", "review_date": "2026-04-14", "overall_rating": 5.0, "business_outlook_rating": null, "ceo_rating": null, "recommend_to_friend_rating": null } ], "metadata": { "last_scraped_at": "2026-04-21T10:06:49", "rating_as_of_date": "2026-04-21T10:06:49" } } ``` The live `reviews`, `office_locations`, and `awards` arrays can be long. Trim or paginate them client-side — the API always returns the full set in one call. ### Reading the response - **`crustdata_company_id`** — echoes back the ID you asked about. - **`overall_rating`** — the headline rating (1.0–5.0) plus the 1-star through 5-star bucket counts. Use the bucket counts to detect polarized profiles. - **`review_count` / `salary_count` / `interview_count` / `benefit_count`** — how many of each content type the profile has. A low `review_count` often means ratings are noisy. - **`company_ceo.ceo_rating`** — CEO approval expressed as a ratio between `0.0` and `1.0` (not 0–100). - **`reviews`** — individual reviews with pros, cons, advice, reviewer role, tenure, and category star ratings. Use these for qualitative summaries. - **`primary_industry`** — the industry classification (e.g., `Computer Hardware Development`) and higher-level sector (`Information Technology`). - **`metadata.last_scraped_at`** and **`metadata.rating_as_of_date`** — ISO 8601 timestamps for when the profile data was last refreshed from the web and the effective date of the aggregate rating snapshot. See the [Reference](#reference) section below for every field. --- ## Companies without coverage Not every company in Crustdata has an employee review profile. When the `crustdata_company_id` is valid but no review profile exists, the endpoint returns `500` with a structured error: ```json { "error": { "type": "internal_error", "message": "No glassdoor data found for company_id=662349", "metadata": [] } } ``` Treat this as a "miss" — the company simply isn't covered — and fall back to company firmographics via [Company Enrich](/company-docs/enrichment/introduction) if you still need some context. See the [Examples](#examples) section below for a recipe that handles this gracefully. --- ## Before you start You need: - A **Crustdata API key** — see [Introduction](/general/introduction) for how to obtain one. - A terminal with `curl` (or any HTTP client). - A `crustdata_company_id` — resolve from a domain or name with [Company Identify](/company-docs/identify/introduction) if needed. - The required header: `x-api-version: 2025-11-01`. For the full field list and error codes, see the [Reference](#reference) section below. For real-world recipes, browse the [Examples](#examples) section below. ## Rate limits Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. See [Rate limits](/general/rate-limits) for all per-endpoint limits. --- ## Examples Practical recipes that show how to use `POST /employee_review/enrich` end to end. Every request and response on this page was produced by a live API call — copy them as-is and swap in your own identifiers. **What this does:** pulls the full employee review profile for a well-covered public company. Use it when you already have the Crustdata company ID (from Search, Identify, or your own CRM) and want the employee-experience side of the profile — overall rating, category ratings, CEO approval, and individual reviews. ```bash Request curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 680974 }' ``` ```json Response { "crustdata_company_id": 680974, "company_name": "Intel Corporation", "website": "https://intel.com/life", "headquarters": "Santa Clara, United States", "company_size": "10000+ Employees", "company_type": "Company - Public", "revenue": "$10+ billion (USD)", "stock_ticker": "INTC", "founded_year": 1968, "mission": "Create world-changing technology that enriches the lives of every person on earth.", "glassdoor_profile_url": "https://www.glassdoor.com/Overview/Working-at-Intel-Corporation-EI_IE1519.11,28.htm", "primary_industry": { "name": "Computer Hardware Development", "sector_name": "Information Technology" }, "company_ceo": { "name": " Lip-Bu Tan", "title": "CEO", "ceo_rating": 0.63, "ceo_ratings_count": 996 }, "review_count": 39077, "salary_count": 52131, "interview_count": 11042, "overall_rating": { "rating": 3.9, "total_count": 39076, "rating_1_count": 330, "rating_2_count": 486, "rating_3_count": 2259, "rating_4_count": 4366, "rating_5_count": 3899 }, "culture_and_values_rating": 3.9, "work_life_balance_rating": 4.0, "compensation_and_benefits_rating": 4.2, "senior_management_rating": 3.4, "career_opportunities_rating": 3.7, "diversity_and_inclusion_rating": 4.2, "recommend_to_friend_rating": 0.71, "business_outlook_rating": 0.52, "reviews": [ { "id": 103562586, "summary": "Best place to work with", "pros": "Great Team. Work Life Balance.", "cons": "Repeating work. Needs to rely on cross functional teams", "reviewer_job_title": "Semiconductor research engineer", "reviewer_location_city": "Chandler, AZ", "reviewer_employment_status": "REGULAR", "is_current_job": "1", "length_of_employment": "4", "review_date": "2026-04-14", "overall_rating": 5.0 }, { "id": 103581162, "summary": "Intern at Intel", "pros": "Benefits, pay, PTO and PA, and ESPP", "cons": "Work culture was stagnate, not an organized intern program, never had enough work", "reviewer_job_title": "Systems Integration Intern", "reviewer_location_city": "Beaverton, OR", "reviewer_employment_status": "INTERN", "is_current_job": "0", "length_of_employment": "0", "review_date": "2026-04-15", "overall_rating": 3.0, "business_outlook_rating": "NEUTRAL", "ceo_rating": "NO_OPINION", "recommend_to_friend_rating": "POSITIVE" } ], "office_locations": [ { "city_name": "Scottsdale", "country": "US", "continent_name": "North America", "latitude": 33.5091705, "longitude": -111.8983307 } ], "awards": [ { "name": "America's Most Iconic Companies", "source": "TIME Magazine and Statista", "year": "2026" } ], "metadata": { "last_scraped_at": "2026-04-21T10:06:49", "rating_as_of_date": "2026-04-21T10:06:49" } } ``` **Takeaway:** `overall_rating.rating` is the headline number, but the bucket counts (`rating_1_count` through `rating_5_count`) reveal the shape. A polarized profile — many 1-star and 5-star reviews, few 3-star — looks very different from a unimodal 4-star profile even at the same mean. `recommend_to_friend_rating` and `business_outlook_rating` are ratios between `0.0` and `1.0`, not percentages. **What this does:** runs the same request against a second well-covered company. Use this pattern when you want to compare sentiment across two targets — the response shape is the same, so diff the numbers. ```bash Request curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 681009 }' ``` ```json Response { "crustdata_company_id": 681009, "company_name": "Walmart Inc.", "website": "https://careers.walmart.com", "headquarters": "Bentonville, United States", "company_size": "10000+ Employees", "company_type": "Company - Public", "revenue": "$10+ billion (USD)", "stock_ticker": "WMT", "founded_year": 1962, "glassdoor_profile_url": "https://www.glassdoor.com/Overview/Working-at-Walmart-EI_IE715.11,18.htm", "primary_industry": { "name": "General Merchandise & Superstores", "sector_name": "Retail and wholesale" }, "company_ceo": { "name": " John Furner", "title": "CEO", "ceo_rating": 0.57, "ceo_ratings_count": 91 }, "review_count": 153666, "salary_count": 271448, "interview_count": 14870, "overall_rating": { "rating": 3.4, "total_count": 153649, "rating_1_count": 4203, "rating_2_count": 4468, "rating_3_count": 11348, "rating_4_count": 9698, "rating_5_count": 8163 }, "culture_and_values_rating": 3.1, "work_life_balance_rating": 3.3, "compensation_and_benefits_rating": 3.2, "senior_management_rating": 2.9, "career_opportunities_rating": 3.1, "diversity_and_inclusion_rating": 3.7, "recommend_to_friend_rating": 0.5, "business_outlook_rating": 0.45, "reviews": [ { "id": 103620935, "summary": "Walmart Inc", "pros": "PTO, PPTO, 401k, Health and Dental Benefits", "cons": "Career advancement is stagnant, too much favortisim", "advice": "Recognize the hard working employees for promotion instead of picking one that would be a favorite.", "reviewer_job_title": "Front End Team Associate", "reviewer_location_city": "Prestonsburg, KY", "reviewer_employment_status": "REGULAR", "is_current_job": "1", "length_of_employment": "20", "review_date": "2026-04-18", "overall_rating": 3.0, "business_outlook_rating": "POSITIVE", "ceo_rating": "APPROVE", "recommend_to_friend_rating": "NEGATIVE" } ], "metadata": { "last_scraped_at": "2026-04-21T10:08:15", "rating_as_of_date": "2026-04-21T10:08:15" } } ``` **Takeaway:** the `reviews[]` entries also carry per-reviewer sentiment enums — `business_outlook_rating`, `ceo_rating`, and `recommend_to_friend_rating` — that are separate from the numeric category ratings. A reviewer can approve of the CEO while still not recommending the company. Use these enums as signals for qualitative summaries and segmentation. **What this does:** shows what comes back when the company exists in Crustdata but has no employee review profile. The endpoint returns `HTTP 500` with an `internal_error` response whose message starts with `No ... data found for company_id=`. Treat it as a coverage miss, not as a transient error to retry. ```bash Request curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 662349 }' ``` ```json Response { "error": { "type": "internal_error", "message": "No glassdoor data found for company_id=662349", "metadata": [] } } ``` **Takeaway:** branch on `HTTP 500` combined with `error.type === "internal_error"` and route those requests to your fallback — typically [Company Enrich](/company-docs/enrichment/introduction) for firmographics only. This response does not consume a credit, so it is safe to let through in batch pipelines. Do not retry — the next call for the same company returns the same miss. If you hit a coverage miss on a company you expect to have reviews, resolve the company by its domain with [Company Identify](/company-docs/identify/introduction) first — the `crustdata_company_id` you have may point to a regional subsidiary that is separate from the parent's review profile. **What this does:** demonstrates the two most common client errors — a missing API key and a missing `crustdata_company_id` — and the exact body shape each one returns, so you can branch on them in your client. ```bash 401 — missing API key curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 680974 }' ``` ```json 401 — response { "message": "Missing API key in request" } ``` ```bash 401 — invalid API key curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer invalidkey' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 680974 }' ``` ```json 401 — response { "message": "Invalid API key in request" } ``` ```bash 400 — missing crustdata_company_id curl --request POST \ --url https://api.crustdata.com/employee_review/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{}' ``` ```json 400 — response { "error": { "type": "invalid_request", "message": "crustdata_company_id is required", "metadata": [] } } ``` **Takeaway:** `401` responses use a flat `{message}` shape while validation `400` responses use the nested `{error: {type, message, metadata}}` shape. Branch on HTTP status first, then parse the appropriate body shape. See the [Reference](#errors) section below for every error shape, including the `400` returned for unparseable JSON and the `403` returned for wrong `x-api-version`. --- ## Reference This section is the field-level reference for `POST /employee_review/enrich`. Use it alongside the interactive [API Reference](/openapi-specs/2025-11-01/introduction) when you need to know exactly what to send and what to expect back. ``` POST https://api.crustdata.com/employee_review/enrich ``` --- ### Authentication Every request requires three headers: | Header | Value | Description | | --------------- | --------------------- | ---------------------------------------- | | `authorization` | `Bearer YOUR_API_KEY` | Your Crustdata API key as a Bearer token | | `x-api-version` | `2025-11-01` | API version (required on every request) | | `content-type` | `application/json` | Request body is always JSON | A missing or invalid API key returns `401` with `{"message": "Invalid API key in request"}`. Sending a different `x-api-version` value returns `403 permission_error`. --- ### Request body The request body is a JSON object with a single required field: | Field | Type | Required | Description | | ---------------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `crustdata_company_id` | integer (int64) | Yes | Crustdata internal company identifier. Use [Company Identify](/company-docs/identify/introduction) if you only have a domain, name, or profile URL. Example: `680974`. | ```json Example request { "crustdata_company_id": 680974 } ``` --- ### Response body A successful request returns a single JSON object. The top-level fields fall into four groups — firmographics, content counts, aggregate ratings, and nested objects. #### Firmographics | Field | Type | Description | | ----------------------- | --------------- | ------------------------------------------------------------------------------------------ | | `crustdata_company_id` | integer (int64) | Echoes back the ID you asked about. | | `company_name` | string \| null | Legal or display name of the company. Example: `Intel Corporation`. | | `website` | string \| null | Primary company website as published on the profile. | | `logo_url` | string \| null | URL of the company logo image. | | `headquarters` | string \| null | Free-form "City, Country" string. | | `company_size` | string \| null | Employee count range (bucketed). Example: `5001 to 10000 Employees`, `10000+ Employees`. | | `company_type` | string \| null | Ownership/legal type. Common values: `Company - Public`, `Company - Private`, `Nonprofit`. | | `revenue` | string \| null | Revenue range (bucketed). Example: `$5 to $10 billion (USD)`. | | `stock_ticker` | string \| null | Stock ticker symbol for publicly traded companies. Example: `INTC`. | | `founded_year` | integer \| null | Year the company was founded. | | `active_status` | string \| null | Whether the underlying profile is active. Commonly `ACTIVE` or `INACTIVE`. | | `approval_status` | string \| null | Moderation/approval status of the profile. Commonly `APPROVED`. | | `description` | string \| null | Company description as published on the profile. | | `mission` | string \| null | Company mission statement as published on the profile. | | `glassdoor_profile_url` | string \| null | URL of the employee review profile page on the source site. | #### Content counts | Field | Type | Description | | ------------------ | --------------- | --------------------------------------------------------------- | | `review_count` | integer \| null | Total number of employee reviews indexed for the company. | | `salary_count` | integer \| null | Total number of salary reports indexed. | | `interview_count` | integer \| null | Total number of interview reports indexed. | | `benefit_count` | integer \| null | Total number of benefit reports indexed. | | `photo_count` | integer \| null | Total number of company photos indexed. | | `global_job_count` | integer \| null | Total number of open job listings indexed across all locations. | #### Aggregate ratings Category ratings are on a `1.0–5.0` scale unless otherwise noted. | Field | Type | Description | | ---------------------------------- | -------------- | --------------------------------------------------------------------------------------------------- | | `culture_and_values_rating` | number \| null | Average Culture & Values rating. | | `diversity_and_inclusion_rating` | number \| null | Average Diversity & Inclusion rating. | | `work_life_balance_rating` | number \| null | Average Work/Life Balance rating. | | `senior_management_rating` | number \| null | Average Senior Management rating. | | `compensation_and_benefits_rating` | number \| null | Average Compensation & Benefits rating. | | `career_opportunities_rating` | number \| null | Average Career Opportunities rating. | | `recommend_to_friend_rating` | number \| null | Share of reviewers who would recommend the company to a friend, as a ratio between `0.0` and `1.0`. | | `business_outlook_rating` | number \| null | Share of reviewers with a positive business outlook, as a ratio between `0.0` and `1.0`. | #### Nested objects and arrays | Field | Type | Description | | ------------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------- | | `company_ceo` | object \| null | CEO information. See [`company_ceo`](#company_ceo) below. | | `primary_industry` | object \| null | Primary industry classification. See [`primary_industry`](#primary_industry) below. | | `overall_rating` | object \| null | Overall rating summary with star distribution. See [`overall_rating`](#overall_rating) below. | | `office_locations` | array of objects \| null | Office locations with city, state, country, and geo coordinates. See [`office_locations[]`](#office_locations). | | `awards` | array of objects \| null | Company awards. See [`awards[]`](#awards). | | `reviews` | array of objects \| null | Individual employee reviews. See [`reviews[]`](#reviews). | | `metadata` | object \| null | Refresh timestamps. See [`metadata`](#metadata) below. | #### `company_ceo` | Field | Type | Description | | --------------------- | --------------- | --------------------------------------------------------- | | `name` | string \| null | CEO name, or comma- or `and`-separated names for co-CEOs. | | `title` | string \| null | CEO title (for example `CEO`, `Co-CEOs`). | | `ceo_rating` | number \| null | CEO approval rating as a ratio between `0.0` and `1.0`. | | `ceo_ratings_count` | integer \| null | Number of CEO approval ratings collected. | | `profile_picture_url` | string \| null | URL of the CEO profile picture. | | `award_display_name` | string \| null | Display name of any CEO award (for example `Top CEOs`). | | `award_time_period` | string \| null | Time period the CEO award applies to. | #### `primary_industry` | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------------------ | | `name` | string \| null | Industry name (for example `Computer Hardware Development`). | | `sector_name` | string \| null | Higher-level sector (for example `Information Technology`). | #### `overall_rating` | Field | Type | Description | | ---------------- | --------------- | ----------------------------------------------------- | | `rating` | number \| null | Average overall rating on a `1.0–5.0` scale. | | `total_count` | integer \| null | Total number of ratings that back the overall rating. | | `rating_1_count` | integer \| null | Number of 1-star ratings. | | `rating_2_count` | integer \| null | Number of 2-star ratings. | | `rating_3_count` | integer \| null | Number of 3-star ratings. | | `rating_4_count` | integer \| null | Number of 4-star ratings. | | `rating_5_count` | integer \| null | Number of 5-star ratings. | #### `office_locations[]` | Field | Type | Description | | ---------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------- | | `city_name` | string \| null | City name. | | `city_long_name` | string \| null | City name with a country or state qualifier in parentheses. | | `state` | string \| null | State, province, or region code. | | `country` | string \| null | Country name. | | `continent_name` | string \| null | Continent name. | | `address_line1` | string \| null | First line of the street address. May be an empty string when the underlying record does not publish a street address. | | `address_line2` | string \| null | Second line of the street address. May be an empty string. | | `zip_code` | string \| null | Postal or zip code. May be an empty string. | | `latitude` | number \| null | Latitude in decimal degrees. | | `longitude` | number \| null | Longitude in decimal degrees. | #### `awards[]` | Field | Type | Description | | -------- | --------------- | -------------------------------------------------- | | `name` | string \| null | Award name. | | `source` | string \| null | Publication or organization that issued the award. | | `year` | string \| null | Year the award was granted (as a string, e.g. `"2026"`). | #### `reviews[]` Each entry is a single employee review — summary, pros, cons, advice, reviewer context, and category star ratings. | Field | Type | Description | | ---------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | | `id` | integer (int64) | Unique review identifier. | | `summary` | string \| null | Short headline the reviewer gave the review. | | `pros` | string \| null | Free-form "pros" text. May contain newlines and light HTML entity encoding (for example `&`). | | `cons` | string \| null | Free-form "cons" text. | | `advice` | string \| null | Optional advice to management left by the reviewer. | | `reviewer_job_title` | string \| null | Self-reported job title of the reviewer at the time of review. | | `reviewer_location_city` | string \| null | Self-reported city/region of the reviewer at the time of review. | | `reviewer_employment_status` | string enum \| null | Employment status: `REGULAR`, `PART_TIME`, `CONTRACT`, `FREELANCE`, `INTERN`, or `TEMPORARY`. | | `is_current_job` | string \| null | `"1"` if the reviewer is still employed at the company at the time of review, `"0"` if they are a former employee. | | `length_of_employment` | string \| null | Reviewer's tenure at the company in whole years (as a string). `"0"` means less than one year. | | `review_date` | string \| null | Date the review was posted, typically formatted as `YYYY-MM-DD`. | | `overall_rating` | number \| null | Overall star rating for this review on a `1.0–5.0` scale. | | `career_opportunities_rating` | number \| null | Category star rating. `0.0` means the reviewer did not rate this category. | | `compensation_and_benefits_rating` | number \| null | Category star rating. `0.0` means not rated. | | `culture_and_values_rating` | number \| null | Category star rating. `0.0` means not rated. | | `diversity_and_inclusion_rating` | number \| null | Category star rating. `0.0` means not rated. | | `work_life_balance_rating` | number \| null | Category star rating. `0.0` means not rated. | | `senior_management_rating` | number \| null | Category star rating. `0.0` means not rated. | | `business_outlook_rating` | string enum \| null | Reviewer's sentiment about the company's business outlook: `POSITIVE`, `NEUTRAL`, or `NEGATIVE`. | | `ceo_rating` | string enum \| null | Reviewer's approval of the CEO: `APPROVE`, `DISAPPROVE`, or `NO_OPINION`. | | `recommend_to_friend_rating` | string enum \| null | Whether the reviewer would recommend the company to a friend: `POSITIVE`, `NEUTRAL`, or `NEGATIVE`. | #### `metadata` | Field | Type | Description | | ------------------- | ------------------------ | ------------------------------------------------------------------------------- | | `last_scraped_at` | string date-time \| null | ISO 8601 timestamp when the underlying profile was last refreshed from the web. | | `rating_as_of_date` | string date-time \| null | ISO 8601 effective date of the aggregate rating snapshot. | --- ### Errors The endpoint returns different error response shapes depending on what failed. Use the summary below to know which status to handle and which body shape to parse; field-level tables and real examples follow. | Status | When it happens | Response body | | ------ | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `400` | `crustdata_company_id` is missing, null, or fails type validation. | [Nested error object](#400--validation-error) | | `400` | Request body is not valid JSON. | [Flat detail object](#400--unparseable-body) | | `401` | `authorization` header missing or contains an invalid key. | [Unauthorized](#401--unauthorized) | | `403` | `x-api-version` is set to a value the caller is not allowed to use. | [Permission error](#403--forbidden) | | `500` | The company exists in Crustdata but has no employee review coverage, or another unexpected server failure. | [Nested error object](#500--no-coverage) | #### `400` — validation error Returned when `crustdata_company_id` is missing, `null`, or cannot be parsed as an integer. Response body has a single top-level `error` object: | Field | Type | Description | | ---------------- | ---------------- | ------------------------------------------------------------------ | | `error` | object | Structured error wrapper. | | `error.type` | string | Machine-readable error type. For this endpoint: `invalid_request`. | | `error.message` | string | Human-readable error message. | | `error.metadata` | array of objects | Additional structured context. Often empty (`[]`). | ```json Missing crustdata_company_id { "error": { "type": "invalid_request", "message": "crustdata_company_id is required", "metadata": [] } } ``` Fix: include `crustdata_company_id` in the JSON body. ```json Invalid crustdata_company_id type { "error": { "type": "invalid_request", "message": "Invalid crustdata_company_id: invalid literal for int() with base 10: 'abc'", "metadata": [] } } ``` Fix: pass `crustdata_company_id` as an integer, not a string. #### `400` — unparseable body Returned when the request body is not valid JSON. This shape is different from the validation error above — it comes from the underlying framework. | Field | Type | Description | | -------- | ------ | ----------------------------------------------------------------------- | | `detail` | string | Human-readable description of why the request body could not be parsed. | ```json { "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)" } ``` Fix: send a valid JSON body and keep `content-type: application/json` set. #### `401` — unauthorized Returned when the `authorization` header is missing or the API key is invalid. | Field | Type | Description | | --------- | ------ | --------------------------------------------------------- | | `message` | string | Human-readable explanation of the authentication failure. | ```json Missing Authorization header { "message": "Missing API key in request" } ``` ```json Invalid key { "message": "Invalid API key in request" } ``` Fix: set `authorization: Bearer YOUR_API_KEY` with a valid key. #### `403` — forbidden Returned when the caller does not have access to the endpoint — most commonly when the `x-api-version` header is set to a value that is not enabled for this account. ```json { "error": { "type": "permission_error", "message": "You do not have permission to access /employee_review/enrich.", "metadata": [] } } ``` Fix: set `x-api-version: 2025-11-01`. If you still see this response with the correct version, your account is not enabled for this endpoint — contact Crustdata support. #### `500` — no coverage The most common `500` response from this endpoint is a "no data found" miss. The company exists in the Crustdata graph, but no employee review profile is linked to it. ```json { "error": { "type": "internal_error", "message": "No glassdoor data found for company_id=662349", "metadata": [] } } ``` Fix: treat this as a coverage miss and fall back to [Company Enrich](/company-docs/enrichment/introduction) if you still need firmographic context. This response does not consume a credit. --- ## What to do next - **Pair this with firmographics** — use [Company Enrich](/company-docs/enrichment/introduction) to pull basic company data alongside the employee review profile. - **Resolve an identifier first** — use [Company Identify](/company-docs/identify/introduction) to turn a domain, name, or profile URL into a `crustdata_company_id`. - **Check the spec** — see the full [OpenAPI reference](/openapi-specs/2025-11-01/introduction) for interactive request/response exploration. # Company Enrich reference Source: https://docs.crustdata.com/company-docs/enrichment/reference Reference for Company Enrich: request parameters, response fields, valid field groups, company_data sections, validation rules, and error responses. Reference material for [Company Enrich](/company-docs/enrichment/introduction): request parameters, response fields, valid `fields` values, `company_data` section catalog, validation rules, and error responses. For walk-through examples, see [Company Enrich](/company-docs/enrichment/introduction) and [Examples](/company-docs/enrichment/introduction#examples). --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | ----------------------------------- | --------- | ------------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `domains` | string[] | Exactly one identifier type required | — | Website domains to enrich. | | `professional_network_profile_urls` | string[] | Exactly one identifier type required | — | Company profile URLs to enrich. | | `names` | string[] | Exactly one identifier type required | — | Company names to enrich. | | `crustdata_company_ids` | integer[] | Exactly one identifier type required | — | Crustdata company IDs to enrich. | | `fields` | string[] | No | `["basic_info"]` | Specific field groups to include in the response. Defaults to `basic_info` only (plus `crustdata_company_id`). | | `exact_match` | boolean | No | `null` | Set to `true` to force exact domain matching. | Submit exactly one identifier type per request. ## Response fields reference The response is a top-level array. Each item in the array contains: | Field | Type | Description | | ---------------------------- | ------ | ---------------------------------------------------------------------------- | | `matched_on` | string | The input identifier you submitted | | `match_type` | string | `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url` | | `matches` | array | Array of candidate matches (may be empty for no-match) | | `matches[].confidence_score` | number | How confident the match is. Higher is better. | | `matches[].company_data` | object | Full enriched company profile. See [`company_data` sections](#company_data-sections) below. | --- ## Using the `fields` parameter If you omit `fields`, `company_data` contains only `crustdata_company_id` and `basic_info`. To get `headcount`, `funding`, `people`, `hiring`, or any other section, you must list it explicitly in `fields`. ```bash Request — default fields curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["retool.com"] }' ``` ```bash Request — multiple sections curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["retool.com"], "fields": ["basic_info", "headcount", "funding", "people"] }' ``` Each entry in `fields` is the name of a section group. Include `basic_info` explicitly if you want to keep it alongside other sections. ### Valid `fields` values | Field group | What it returns | | ------------------ | ------------------------------------------------------------------------- | | `basic_info` | Company name, domain, website, profile URL, type, year founded | | `headcount` | Employee count, role/region breakdowns, growth metrics | | `funding` | Total funding, last round details, investor list | | `locations` | HQ country, state, headquarters address, street address, office addresses | | `taxonomy` | Industry, category, NAICS, SIC, and speciality fields | | `revenue` | Revenue estimates (lower/upper bound), public markets, acquisition status | | `hiring` | Open job count, hiring growth rate, recent job titles | | `followers` | Follower count, month-over-month/quarter/year growth | | `seo` | Organic search results, monthly organic clicks, Google Ads budget | | `competitors` | Aggregated competitor domains and paid/organic SEO competitors | | `social_profiles` | Crunchbase, professional network, and Twitter profile links | | `web_traffic` | Monthly visitors, traffic source breakdown | | `employee_reviews` | Overall, culture, and work-life balance ratings | | `people` | Decision makers, founders, C-level executives | | `news` | Recent article URLs, titles, and publish dates | | `software_reviews` | Review count and average rating | | `public_launches` | Product launch data — launches, makers, reviews, and ratings | | `market_intel` | Product and review intelligence — products, reviews, and ratings | | `technographics` | Detected technologies — count, top names, full list with categories and detection sources (gated add-on; see [`technographics` fields](#technographics-fields)) | | `reviews` | Other (non-employee) reviews bucket | ### `company_data` sections The enriched `company_data` object contains the following sections. Each section maps to a `fields` value above. | Section | Key fields | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `crustdata_company_id`| integer | Top-level Crustdata identifier | | `updated_at` | date-time string (nullable) | Top-level last-refresh timestamp | | `indexed_at` | date-time string (nullable) | Top-level last-indexed timestamp | | `basic_info` | `name`, `primary_domain`, `all_domains`, `website`, `professional_network_url`, `professional_network_id`, `profile_name`, `logo_permalink`, `year_founded`, `description`, `company_type`, `employee_count_range`, `markets` | Core identity and classification | | `headcount` | `total`, `by_role_absolute`, `by_role_percent`, `by_region_absolute`, `growth_percent`, `growth_absolute` | Employee footprint and growth | | `funding` | `total_investment_usd`, `last_round_amount_usd`, `last_fundraise_date`, `last_round_type`, `investors`, `funding_rounds`, `milestones`, `acquisitions` | Funding, milestones, and acquisitions | | `locations` | `country`, `state`, `headquarters`, `street_address`, `all_office_addresses` | Headquarters and office locations | | `taxonomy` | `categories`, `professional_network_industry`, `professional_network_industries`, `professional_network_specialities`, `primary_naics_detail`, `sic_detail_list` | Industry and classification data | | `revenue` | `estimated` (`lower_bound_usd`, `upper_bound_usd`), `public_markets`, `acquisition_status` | Revenue estimates and market data | | `hiring` | `openings_count`, `openings_growth_percent`, `recent_openings` | Hiring demand and open roles | | `followers` | `count`, `mom_percent`, `qoq_percent`, `yoy_percent` | Audience and follower metrics | | `seo` | `total_organic_results`, `monthly_organic_clicks`, `monthly_google_ads_budget` | Search visibility metrics | | `competitors` | `all_domains`, `paid_seo`, `organic_seo` | Competitor domains and SEO peers | | `social_profiles` | `crunchbase.url`, `crunchbase.uuid`, `professional_network`, `twitter_url` | External profile links | | `web_traffic` | per-domain `monthly_visitors` and traffic sources | Website traffic and sources | | `employee_reviews` | `overall_rating`, `culture_and_values_rating`, `work_life_balance_rating`, `review_count` | Employee review data | | `people` | `decision_makers`, `founders`, `cxos` | Key people at the company | | `news` | `article_url`, `article_title`, `article_publish_date` | Recent news articles | | `software_reviews` | `review_count`, `average_rating` | Software review metrics | | `public_launches` | `launches`, `makers`, `reviews`, `categories`, `num_upvotes`, `rating` | Product launch data | | `market_intel` | `products`, `reviews`, `year_founded`, `head_office_city`, `head_office_country`, `num_employees_min`, `num_employees_max` | Market-intelligence data | | `technographics` | `total_technologies`, `top_technologies`, `technologies`, `updated_at` | Detected technology stack | | `reviews` | Other non-employee reviews bucket. May be null when not populated. | General review data | | `metadata` | Crustdata processing metadata | Internal metadata | ### `headcount.growth_percent` and `headcount.growth_absolute` keys The two growth maps in the `headcount` object are keyed by **period alias**. Each key holds the change over a trailing window: a percentage for `growth_percent`, an absolute employee delta for `growth_absolute`. | Key | Window | | ------------ | --------------------- | | `mom` | Month over month | | `qoq` | Quarter over quarter | | `six_months` | Trailing six months | | `yoy` | Year over year | | `two_years` | Trailing two years | [`/company/search`](/company-docs/search/introduction) returns the same growth data under **different period keys** — `1m`, `3m`, `6m`, `12m` — and exposes each as a filterable path (`headcount.growth_percent.{1m,3m,6m,12m}`, `headcount.growth_absolute.{1m,3m,6m,12m}`). The values are equivalent, but a client that consumes both endpoints must map the keys explicitly: `mom ↔ 1m`, `qoq ↔ 3m`, `six_months ↔ 6m`, `yoy ↔ 12m`. `two_years` has no counterpart on `/company/search`. ### `technographics` fields The `technographics` section describes the technologies detected for a company. It is **never returned by default** — request it explicitly via `fields` (for example `"fields": ["technographics"]`). | Field | Type | Description | | ------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `total_technologies` | integer | Total count of technologies detected for the company. May exceed the length of `technologies` (see note below). | | `top_technologies` | string[] | The most notable technology names. | | `technologies` | object[] | List of detected technologies, capped at **500 entries** per company. Use `total_technologies` for the true count. | | `technologies[].name` | string | Technology name. | | `technologies[].sources` | string[] | Where the signal was detected: `"web signals"` (the company's web presence) or `"job posting"` (the company's job postings). | | `technologies[].posting_count` | integer, nullable | Number of the company's job postings the technology was detected in. `null` when there is no job-posting signal, never `0`. | | `technologies[].categories` | object[] | Catalog categories for the technology, strongest match first, up to four entries. | | `technologies[].categories[].category_slug` | string | Category slug, a lowercase kebab-case value (for example `frontend-frameworks`, `cloud-platforms-iaas`). The same values power the `technographics.technologies.category` search filter. Enumerate them with the `technology_category` [autocomplete field](/company-docs/autocomplete/introduction#common-fields-to-autocomplete). | | `technologies[].categories[].super_slug` | string | Super-category slug, the broader group the category belongs to (for example `engineering`, `infrastructure`). The same values power the `technographics.technologies.super_category` search filter. | | `technologies[].categories[].score` | number | Strength of the category match. Higher is stronger. | | `technologies[].description` | string, nullable | Short catalog description of the technology. | | `technologies[].evidence` | object[], nullable | Evidence entries behind the detection. Each has a `source`, a `posting_id`, and a `confidence`, plus `url`, `title`, and `posted_at`, which are populated for job-posting evidence and `null` for web-signal evidence. | | `updated_at` | date-time string | Timestamp of the last technographics refresh for the company. | **Access and pricing.** `technographics` requires field-level permission on your account — check your field access with the [permissions endpoint](/general/permissions) or contact Crustdata to enable it. It is a billed add-on: **+2 credits** per company that returns technographics data, on top of the endpoint's base **2 credits** per company. Companies with no technographics data are not charged the add-on. The same add-on pricing applies to [Batch Company Enrich](/company-docs/enrichment/batch). [Company Search](/company-docs/search/reference#technographics-filter-only) can filter and sort on technographics fields but does not return the values — use this endpoint to fetch them. Discover valid filter values with the `technology` and `technology_category` [autocomplete fields](/company-docs/autocomplete/introduction#common-fields-to-autocomplete). --- ## Validation rules These rules reflect current platform behavior. See the [API reference](/openapi-specs/2025-11-01/introduction) for the formal OpenAPI contract. | Rule | Behavior | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | One identifier type per request | Submit `domains`, `names`, `crustdata_company_ids`, or `professional_network_profile_urls` — not a mix. Mixing types is not supported. | | `fields` is optional | Omitting returns `basic_info` only. Pass section group names to include more sections. | | `exact_match` is optional | Default is `null` (auto-detect). Set `true` for strict domain-only matching. | | Multi-company requests | You can submit multiple values in one identifier array. Each is matched independently. | --- ## No-match behavior When enriching, each identifier is matched independently: - **Full match:** All identifiers match — each array entry has populated `matches`. - **Partial match:** Some identifiers match and others do not. Matched identifiers have `company_data`; unmatched identifiers return an empty `matches: []` array. - **No match:** All identifiers fail to match. The API returns `200 OK` with empty `matches: []` for each array entry. The OpenAPI spec also defines a `404` response for this endpoint. The API returns `200` with empty `matches`, but integrations should handle both `200` empty-match and `404` cases. ```json No match — 200 with empty matches [ { "matched_on": "nonexistent-domain.com", "match_type": "domain", "matches": [] } ] ``` ### Partial batch failure When enriching multiple identifiers, some may match and others may not. The request still succeeds with `200`: ```json [ { "matched_on": "hubspot.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "basic_info": { "name": "HubSpot", "primary_domain": "hubspot.com" } } } ] }, { "matched_on": "nonexistent-domain.com", "match_type": "domain", "matches": [] } ] ``` **Action:** Iterate over the top-level array. For each entry, check `matches.length > 0` before accessing `company_data`. Log or retry unmatched identifiers separately. --- ## Errors Common error responses for Enrich: ```json 400 — Bad request { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: crustdata_company_ids, names, domains, or professional_network_profile_urls", "metadata": [] } } ``` ```json 401 — Invalid API key { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` --- ## API reference summary | Detail | Value | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /company/enrich` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | One identifier type: `domains`, `names`, `crustdata_company_ids`, or `professional_network_profile_urls`. Optional: `fields`, `exact_match`. | | **Response** | Top-level array: `[{ matched_on, match_type, matches: [{ confidence_score, company_data }] }]` | | **No match** | `200` with empty `matches: []` for unmatched identifiers. The OpenAPI spec also defines `404`; handle both. | | **Errors** | `400` (bad request), `401` (bad auth), `403` (permission/credits), `404` (per spec), `500` (server error) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. # Company Identify Source: https://docs.crustdata.com/company-docs/identify/introduction Resolve a company from partial information — a name, domain, profile URL, or Crustdata company ID — and get back matched company records ranked by confidence. **Use this when** you have partial company information and need to resolve it to a specific Crustdata company record — for CRM deduplication, lead routing, entity resolution, or pre-enrichment matching. The Company Identify API takes an identifier you have — a website domain, a profile URL, a company name, or a Crustdata company ID — and returns one or more matched companies ranked by confidence score. Identify is designed for entity resolution rather than deep profiling. This page documents the live Identify response directly. Current platform behavior returns match metadata plus `company_data.basic_info`. If you need the broader company sections used in full profiles, see [Enrich reference](/company-docs/enrichment/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/company/identify ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Description | | ----------------------------------------------------------------------------------- | ------- | ---------------- | ------------------------------------------------------------- | | `domains` / `professional_network_profile_urls` / `names` / `crustdata_company_ids` | array | Yes, one only | Submit exactly one identifier type. Max 25 per request. | | `exact_match` | boolean | No | Set to `true` for strict domain matching. | ### Response body The response is a top-level array. Each entry corresponds to one input identifier. | Field | Type | Description | | ---------------------------- | ------ | ---------------------------------------------------------------------------- | | `matched_on` | string | The input identifier you submitted | | `match_type` | string | `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url` | | `matches` | array | Candidate matches ranked by relevance. Empty for no-match inputs. | | `matches[].confidence_score` | number | Higher is better. | | `matches[].company_data` | object | Match metadata plus `basic_info`. | ### Rate limits and pricing Pricing: Free. - **Rate limits:** For current plan-specific limits, see [Rate limits](/general/rate-limits). Use Identify first when your input is ambiguous. You can resolve the right company for free, then call [Enrich](/company-docs/enrichment/introduction) only for the records you want in full detail. Request parameters, validation, no-match behavior, errors. --- ## Your first identification: look up a company by domain Pass a website domain in the `domains` array to find the matching company. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["serverobotics.com"] }' ``` ```json Response [ { "matched_on": "serverobotics.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 628895, "basic_info": { "crustdata_company_id": 628895, "name": "Serve Robotics", "primary_domain": "serverobotics.com", "all_domains": ["serverobotics.com"], "website": "https://www.serverobotics.com/", "professional_network_url": "https://www.linkedin.com/company/serverobotics", "professional_network_id": "72049930", "profile_name": "Serve Robotics", "employee_count_range": "51-200", "industries": [ "Technology, Information and Internet", "Technology, Information and Media" ] } } } ] } ] ``` ### Understanding the response The live Identify endpoint returns a top-level array, not an object with a `results` wrapper. The Identify API returns a top-level array — one entry per identifier you submitted. Each entry has three fields: - **`matched_on`** — the identifier you submitted (the domain, URL, name, or ID). - **`match_type`** — which identifier type was used. Values: `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url`. - **`matches`** — an array of candidate companies ranked by relevance. Each match includes a `confidence_score` and a `company_data` object. Current platform behavior returns `basic_info` here. For detailed behavior when no identifier matches, see [No-match behavior](#no-match-behavior). --- ## Identify vs Enrich | | Identify | Enrich | | ------------ | -------------------------------------------- | ------------------------------ | | **Endpoint** | `/company/identify` | `/company/enrich` | | **Pricing** | **Free** | **2 credits per record** | | **Response** | Match results with `company_data.basic_info` | Full company profile | | **Best for** | Matching, deduplication, entity resolution | Research, scoring, diligence | | **Use when** | You need to resolve "which company is this?" | You need detailed company data | **Common pattern:** Use Identify to resolve ambiguous inputs, then pass the `crustdata_company_id` from the best match into [Enrich](/company-docs/enrichment/introduction) for the full profile. --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request you can copy, paste, and adapt. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Name-based identification often returns multiple candidates. Check `confidence_score` and `primary_domain` to pick the right match. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "names": ["Serve Robotics"] }' ``` ```json Response [ { "matched_on": "Serve Robotics", "match_type": "name", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 628895, "basic_info": { "name": "Serve Robotics", "primary_domain": "serverobotics.com", "employee_count_range": "51-200", "industries": ["Technology, Information and Internet"] } } }, { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 5825197, "basic_info": { "name": "Site Serve Robotics", "primary_domain": "siteserverobotics.co.uk", "employee_count_range": "2-10", "industries": ["Construction"] } } } ] } ] ``` When multiple matches are returned, use `primary_domain` and `employee_count_range` to disambiguate. The first match is not always the right one for name-based lookups. If you have a company profile URL, pass it in `professional_network_profile_urls`. Profile URL lookups are direct matches — they typically return a single match with high confidence. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/company/mintlify" ] }' ``` Pass a Crustdata company ID (for example, from a previous search call) to `crustdata_company_ids` for an exact lookup. ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [628895] }' ``` Company ID lookups return an exact match. An inbound lead arrives from a known domain. Use Identify to resolve the domain to a company record, then use Search to find similar companies for prospecting. ### Step 1: Identify the inbound company ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"domains": ["retool.com"]}' ``` ```json Response [ { "matched_on": "retool.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "employee_count_range": "201-500", "industries": ["Software Development"] } } } ] } ] ``` **Extract:** Take `response[0].matches[0].company_data.basic_info.industries[0]` → `"Software Development"` and `employee_count_range` → `"201-500"`. ### Step 2: Search for similar companies ```bash Request curl --request POST \ --url https://api.crustdata.com/company/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": "basic_info.industries", "type": "in", "value": ["Software Development"]}, {"field": "headcount.total", "type": ">", "value": 200}, {"field": "headcount.total", "type": "<", "value": 1000} ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 10, "fields": ["crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total"] }' ``` ```json Response { "companies": [ { "crustdata_company_id": 67890, "basic_info": { "name": "Retool", "primary_domain": "retool.com" }, "headcount": { "total": 450 } }, { "crustdata_company_id": 12345, "basic_info": { "name": "Notion", "primary_domain": "notion.so" }, "headcount": { "total": 800 } } ], "next_cursor": "H4sIAM_5zGkC...", "total_count": 1543 } ``` **Extract:** Take `companies[].crustdata_company_id` values and pass them to [Enrich](/company-docs/enrichment/introduction) for full profiles of promising matches. **If empty:** If `companies` is `[]`, broaden your filters (for example, wider headcount range or more industries). Use [Autocomplete](/company-docs/autocomplete/introduction) to verify valid filter values. Name-based identification supports two matching modes. With `exact_match: true`, the input must match the stored name **character for character** (case-insensitive) — and the stored name is not always how the company is commonly written. For example, "Tomo Credit" is stored as `"TomoCredit"` (no space), so an exact lookup for the spaced form returns no matches. **`exact_match: true` with a space → no match:** ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "names": ["Tomo Credit"], "exact_match": true }' ``` ```json Response [ { "matched_on": "Tomo Credit", "match_type": "name", "matches": [] } ] ``` **`exact_match: false` (default) tolerates the variation → matches:** ```bash Request curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "names": ["Tomo Credit"], "exact_match": false }' ``` ```json Response [ { "matched_on": "Tomo Credit", "match_type": "name", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 634576, "basic_info": { "name": "TomoCredit", "primary_domain": "tomocredit.ai", "employee_count_range": "51-200", "industries": ["Technology, Information and Internet"] } } } ] } ] ``` **Recommended fallback:** try `exact_match: true` first; if `matches` is empty, retry with `exact_match: false`; then verify the returned `name` or `primary_domain` before using the result. Identify is free, so this two-step resolution costs nothing. --- ## Reference Reference material for Company Identify: the full list of request parameters, validation rules, no-match behavior, and error responses. ### Request parameter reference | Parameter | Type | Required | Description | | ----------------------------------- | --------- | ------------------------------------ | ----------------------------------------- | | `domains` | string[] | Exactly one identifier type required | Website domains to identify. Max 25. | | `professional_network_profile_urls` | string[] | Exactly one identifier type required | Company profile URLs to identify. Max 25. | | `names` | string[] | Exactly one identifier type required | Company names to identify. Max 25. | | `crustdata_company_ids` | integer[] | Exactly one identifier type required | Crustdata company IDs to identify. Max 25.| | `exact_match` | boolean | No | Set to `true` for strict domain matching. | Submit exactly one identifier type per request. Each identifier array accepts a maximum of 25 values per request. Sending 26 or more returns `400`. Split larger lists into batches of 25. `exact_match: true` enforces strict domain matching, but it can still return multiple matches when more than one company record shares that same domain. The OpenAPI model currently reuses the Enrich request schema, but this reference documents the live Identify behavior. Use [Enrich](/company-docs/enrichment/introduction) when you need the broader profile sections. ### No-match behavior When no company matches the identifier, current platform behavior returns `200` with an empty `matches` array: ```json [ { "matched_on": "thisdomaindoesnotexist12345xyz.com", "match_type": "domain", "matches": [] } ] ``` The OpenAPI spec also defines a `404` response for Identify. Current platform behavior returns `200` with empty `matches`, but integrations should handle both. ### Errors | Status | Meaning | | ------ | --------------------------------------------------------------------------- | | `400` | Invalid request — missing or multiple identifier types, or malformed input. | | `401` | Invalid or missing API key. | | `403` | Permission denied or endpoint unavailable for your account. | | `404` | No data found. | | `500` | Internal server error. | ```json 400 — Missing identifier { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: crustdata_company_ids, names, domains, or professional_network_profile_urls", "metadata": [] } } ``` ```json 400 - Too many values { "error": { "type": "invalid_request", "message": "Maximum 25 values allowed for domains. Found 26", "metadata": [] } } ``` ```json 401 — Invalid API key { "message": "Invalid API key in request" } ``` ### API reference summary | Detail | Value | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /company/identify` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Pricing** | Free | | **Request** | One identifier type: `domains`, `names`, `crustdata_company_ids`, or `professional_network_profile_urls`. Max 25 values. Optional: `exact_match`. | | **Response** | `[{ "matched_on", "match_type", "matches": [{ "confidence_score", "company_data" }] }]` | | **Errors** | `400` (bad request), `401` (bad auth), `403` (permission), `404` (no match), `500` (server error) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Get the full profile** — pass the `crustdata_company_id` from Identify into [Enrich](/company-docs/enrichment/introduction) for detailed company data. - **Search for similar companies** — use [Company Search](/company-docs/search/introduction) to find companies matching the same industry or headcount range. - **Verify valid filter values** — use [Autocomplete](/company-docs/autocomplete/introduction) to check filter values before searching. # Company Entity Watcher Source: https://docs.crustdata.com/watcher-docs/company/entity Watch a list of companies you supply and get notified the moment a profile changes — a headcount move, a funding round, a news mention, a rebrand. You supply the list; the watcher delivers the diff. A **Company Entity Watcher** monitors **a list of companies you supply** and notifies you when *their* profiles change. Provide the companies (by domain, name, profile URL, or Crustdata company ID) and describe what to watch for. On each scheduled run, the watcher diffs every company against its last snapshot and delivers only what changed. The list is **yours to [edit any time](#edit-the-watched-list-any-time)** — add or remove companies without recreating the watch. Where a [Company Discovery Watcher](/watcher-docs/company/discovery) **finds new** companies that match a filter, an Entity Watcher **tracks a known set** — a book of accounts, a competitor list, a portfolio — and surfaces *movement* within it: - A tracked company **changes headcount** or **crosses a headcount threshold**. - A tracked company **appears in the news**, **raises funding**, or **adds an investor**. - A tracked company **rebrands**, **relocates its HQ**, or **shifts industry**. Companies have their own create endpoint: ``` POST https://api.crustdata.com/watch/company ``` Every request needs the `x-api-version: 2025-11-01` header and a Bearer token. Replace `YOUR_API_KEY` with your key. This endpoint is **open to all API customers** — no per-endpoint grant needed. Pricing — you pay only for notifications. Every watch's first run is a free baseline: it records each company's starting values internally for diffing (never delivered) and never fires. After that you pay 5 credits per notification — 5 credits for each watched company that changed on a run. A run that surfaces nothing costs nothing, and the price is the same whether you check hourly or monthly. If your balance is too low, the run is suspended rather than partially delivered. See [Pricing](/general/pricing) for the full breakdown. ## How a company entity watcher runs `POST` your `entities` (companies to watch), a `track` (what to watch for), a `config` (schedule + caps), and one or more `notifications` channels. The response returns the full watch object, including its `id`. On the first run, the watcher records an internal **snapshot** of the fields you're tracking — just enough to diff next time. With no prior snapshot to compare, the baseline **never fires and is never charged**, and isn't delivered; it only establishes "before". To read a company's current profile on demand, use the Enrich APIs. On your schedule (`every_hours`), the watcher re-checks each company, diffs it against its last snapshot, and delivers a notification for every company whose `track` condition just became true. You're charged 5 credits per notification. An Entity Watcher fires only on a **transition** — the moment a value crosses from its previous state to a new one. It never fires on the baseline and won't re-notify you about a change it already reported. To read a list's current state on demand instead of watching for change, use the [Company Enrich](/company-docs/enrichment/introduction) API. ## What you can track The `track` describes the change that triggers a notification. It's a tree of condition **leaves**, optionally combined with `and` / `or` groups. A leaf has the shape: ```json { "field": "", "type": "", "value": "" } ``` | Operator | Applies to | Fires when | | ------------ | ---------- | ---------- | | `changed` | a **scalar** field (e.g. `headcount.total`) | the value becomes different from the last snapshot | | `added` | an **array** field (e.g. `news`, `funding.investors`) | a new element appears in the array | | `>`, `<`, `=`, `!=`, `=>` (≥), `=<` (≤) | a scalar, or a predicate on an array element | the comparison flips from false to true (e.g. `headcount.total => 1000`) | Crustdata writes "greater-than-or-equal" as **`=>`** and "less-than-or-equal" as **`=<`** — not `>=` / `<=`. Those reversed forms return `unknown operator`. `value` is required for the comparison operators, and omitted for `changed` and `added`. Combine leaves with a group node — `{ "op": "and" | "or", "conditions": [ … ] }` — to watch for several signals at once. An `or` group fires if **any** child fires; an `and` group fires only when **all** its conditions hold and at least one just became true. ### Common company signals These are examples — a company profile has **500+ addressable fields**, and you can track any of them (any scalar with `changed`, any array with `added`). | Signal | `field` | `type` | | --- | --- | --- | | Headcount changed | `headcount.total` | `changed` | | Crossed a headcount threshold | `headcount.total` | `=>` (with `value`) | | In the news | `news` | `added` | | Raised funding | `funding.total_investment_usd` | `changed` | | New funding round | `funding.last_round_type` | `changed` | | New investor | `funding.investors` | `added` | | Made an acquisition | `funding.acquisitions` | `added` | | Funding milestone | `funding.milestones` | `added` | | Follower count changed | `followers.count` | `changed` | | Hiring activity | `hiring.openings_count` | `changed` | | Industry changed | `taxonomy.professional_network_industry` | `changed` | | HQ relocated | `locations.country` | `changed` | | Name change / rebrand | `basic_info.name` | `changed` | `field` paths address the same profile structure the Enrich API returns, so you can browse every trackable field in the [Company Enrich](/company-docs/enrichment/reference) reference. `changed` requires a **scalar** path, `added` requires an **array** path — mixing them returns a `400` (e.g. `"changed"` on the array `news` is rejected; use `"added"`). ### Recipes: combine signals Group leaves with `or` to fire on **any** of several signals, or `and` to require them together. These `track` blocks are all live-verified: ```json Company momentum { "op": "or", "conditions": [ { "field": "headcount.total", "type": "changed" }, { "field": "funding.total_investment_usd", "type": "changed" }, { "field": "news", "type": "added" } ] } ``` ```json Funding activity { "op": "or", "conditions": [ { "field": "funding.last_round_type", "type": "changed" }, { "field": "funding.investors", "type": "added" }, { "field": "funding.acquisitions", "type": "added" } ] } ``` ```json Scaling past 1,000 { "op": "and", "conditions": [ { "field": "headcount.total", "type": "=>", "value": 1000 }, { "field": "hiring.openings_count", "type": "changed" } ] } ``` Drop any of these into the `track` field of a create request. Groups can nest, so you can mix `and`/`or` — e.g. *"a headcount change **and** the company crossed 1,000 employees."* ## `track` and `fields` are different These are two independent knobs, and the distinction matters: | Key | Controls | Answers | | -------- | -------- | ------- | | `track` | **When** the watch fires | *"What change do I want to be notified about?"* | | `fields` | **What** the notification delivers | *"What data about the company do I want in the payload?"* | Set them independently. You can track one field but deliver many (watch for a headcount change, yet receive full funding history and news in the payload), or track many and deliver few. `track` decides *whether* you get a notification; `fields` decides *what's inside* it. **`fields` defaults to a minimal projection.** Omit `fields` and the delivered `record` contains only `basic_info`. The field you're *tracking* is **not** automatically added to the payload. The fired `changes` array always tells you exactly what moved, but to get the surrounding profile data in `record`, request it in `fields`. ### Requesting all fields `fields` does **not** affect pricing — an entity watch is a flat **5 credits per notification** no matter how many field groups you deliver (see [Pricing](/general/pricing)). There's no cost reason to keep the payload thin; request whatever your workflow needs. There's no wildcard — to receive the complete record, list every field group your API key is entitled to. `fields` is a top-level key (sibling of `track`), fixed at create time. The delivered `record` uses the **same schema as [Company Enrich](/company-docs/enrichment/reference)** — identical field-group names and nesting — so the example below mirrors the full Company Enrich `fields` set. ```json Company — all fields { "entities": { "domains": ["stripe.com"] }, "track": { "field": "headcount.total", "type": "changed" }, "fields": [ "basic_info", "revenue", "headcount", "funding", "hiring", "locations", "taxonomy", "people", "competitors", "followers", "web_traffic", "seo", "news", "social_profiles", "software_reviews", "employee_reviews", "reviews", "public_launches", "market_intel" ] } ``` Some groups require a field-level entitlement on your key (e.g. `technographics`). Requesting one you aren't entitled to returns a field-permission error, so list only the groups your key can access. Group names match the Enrich API — see the [Company Enrich](/company-docs/enrichment/reference) `fields` reference. ## Choosing the companies `entities` is an object mapping an **identifier type** to a list of values. You can mix identifier types in one watch. A single watch holds up to **10,000 companies**. | Accepted identifier keys | | --- | | `domains` | | `names` | | `professional_network_profile_urls` | | `crustdata_company_ids` | ```json { "entities": { "domains": ["netflix.com", "stripe.com"] } } ``` ## Schedule and limits The `config` block controls timing and result caps: | Field | Required | Meaning | | --- | --- | --- | | `trigger.type` | yes | Must be `"interval"`. | | `trigger.every_hours` | yes | How often the watch runs, in hours (integer ≥ 1). Because you pay per notification, a higher frequency only makes changes surface sooner — it doesn't raise the price. | | `max_results_per_run` | no | Cap on notifications per run (1–1000, default 25). | | `preferred_hour` | no | Hour of day (0–23) to prefer for the run. | | `expires_at` | no | Auto-expire date, `YYYY-MM-DD`. | ## Delivery channels Add one or more channels to `notifications` and every change fans out to all of them. | Channel | Shape | | --- | --- | | Webhook | `{ "type": "webhook", "url": "https://…", "headers": { … } }` | | Slack | `{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/…" }` | | Google Chat | `{ "type": "google_chat", "url": "https://chat.googleapis.com/…" }` | At least one notification channel is required. Every delivery is also recorded, so beyond the live push you can re-read a watch's past runs from the [run-history endpoint](#pulled-from-run-history). ## Quick start Watch two companies and get a Slack message whenever either changes headcount **or** appears in the news. This request and response are real: ```bash Request curl --request POST \ --url https://api.crustdata.com/watch/company \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "entities": { "domains": ["netflix.com", "stripe.com"] }, "track": { "op": "or", "conditions": [ { "field": "headcount.total", "type": "changed" }, { "field": "news", "type": "added" } ] }, "config": { "trigger": { "type": "interval", "every_hours": 168 }, "max_results_per_run": 100 }, "notifications": [ { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxxxxxx" } ] }' ``` ```json Response { "id": 46941, "kind": "entity", "dataset": "company", "api_version": "2025-11-01", "config_version": "2025-11-01", "status": "active", "entities": { "domains": ["netflix.com", "stripe.com"] }, "track": { "op": "or", "conditions": [ { "field": "headcount.total", "type": "changed" }, { "field": "news", "type": "added" } ] }, "fields": null, "config": { "trigger": { "type": "interval", "every_hours": 168 }, "max_results_per_run": 100, "expires_at": null, "preferred_hour": null }, "notifications": [ { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxxxxxx" } ], "created_at": "2026-07-16T03:16:38.351849Z", "last_run_at": null } ``` ## Edit the watched list any time Unlike a saved search, an Entity Watcher's list is **mutable** — add or remove companies as your account book or portfolio changes, without recreating the watch. `PATCH` the watch with a new `entities` object: ```bash Request curl --request PATCH \ --url https://api.crustdata.com/watch/company/46941 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "entities": { "domains": ["netflix.com", "stripe.com", "airbnb.com"] } }' ``` ```json Response { "id": 46941, "status": "active", "entities": { "domains": ["netflix.com", "stripe.com", "airbnb.com"] } } ``` Newly added companies are **baselined silently** on the next run — they establish their "before" snapshot first, so adding one never fires a spurious notification. You can also `PATCH` `status` (`active` / `paused`), `config`, and `notifications`. A watch's `track` and `fields` are fixed once created; to change what you watch for, create a new watch. ## Manage your watches | Action | Request | | --- | --- | | List your watches | `GET /watch/company?status=active&limit=50&offset=0` | | Get one | `GET /watch/company/{id}` | | Pause / resume | `PATCH /watch/company/{id}` with `{ "status": "paused" }` | | Add/remove companies | `PATCH /watch/company/{id}` with a new `entities` | | Delete | `DELETE /watch/company/{id}` → `204` | Full request/response for each, verified live: ```bash Request curl --request GET \ --url 'https://api.crustdata.com/watch/company?status=active&limit=50&offset=0' \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response [ { "id": 46941, "kind": "entity", "dataset": "company", "status": "active", "entities": { "domains": ["netflix.com", "stripe.com"] }, "track": { "type": "changed", "field": "headcount.total" }, "config": { "trigger": { "type": "interval", "every_hours": 168 }, "max_results_per_run": 25 }, "notifications": [{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxxxxxx" }], "created_at": "2026-07-16T13:39:18.247195Z", "last_run_at": "2026-07-16T13:39:18.598964Z" } ] ``` Returns an array of your company watches. Filter with `status`, page with `limit` and `offset`. ```bash Request curl --request GET \ --url https://api.crustdata.com/watch/company/46941 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response { "id": 46941, "kind": "entity", "dataset": "company", "api_version": "2025-11-01", "config_version": "2025-11-01", "status": "active", "entities": { "domains": ["netflix.com", "stripe.com"] }, "track": { "type": "changed", "field": "headcount.total" }, "fields": null, "config": { "trigger": { "type": "interval", "every_hours": 168 }, "max_results_per_run": 25, "expires_at": null, "preferred_hour": null }, "notifications": [{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxxxxxx" }], "created_at": "2026-07-16T13:39:18.247195Z", "last_run_at": "2026-07-16T13:39:18.598964Z" } ``` `last_run_at` is `null` until the baseline run completes, then carries the timestamp of the most recent run. ```bash Request curl --request PATCH \ --url https://api.crustdata.com/watch/company/46941 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "status": "paused" }' ``` ```json Response { "id": 46941, "status": "paused" } ``` A paused watch stops running until you resume it. Send `{ "status": "active" }` to resume. ```bash Request curl --request DELETE \ --url https://api.crustdata.com/watch/company/46941 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` Returns `204 No Content`. Deletion is terminal — the watch cannot be resumed. ## What a notification looks like Every fired company carries two things: the **`changes`** array (exactly what moved, independent of `fields`) and a **`record`** (the company's current data projected to the `fields` you requested — the same shape the Enrich API returns, so with the default `fields` it's just `basic_info`). The company's identity travels **inside** the record as `crustdata_company_id`. There are two ways to receive these, and they use **different envelopes**: ### Pushed to your webhook When a watch fires, we `POST` this body to each channel. The example below is from a watch created with `fields: ["basic_info", "headcount", "news"]`: ```json Webhook POST body { "metadata": { "watch_id": 46941, "kind": "entity", "dataset": "company", "api_version": "2025-11-01", "run_id": 64210, "notification_id": "ntf_64210", "delivered_at": "2026-07-16T03:20:00.000000Z", "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 100, "truncated": false } }, "results": [ { "changes": [ { "field": "headcount.total", "type": "changed", "from": 8200, "to": 8460 } ], "record": { "basic_info": { "name": "…", "domain": "…" }, "headcount": { "total": 8460 }, "news": [ { "title": "…", "url": "…", "published_at": "…" } ], "crustdata_company_id": 631480 } } ] } ``` For an entity watch, `results` is a **flat list** — one object per fired company, each with its own `changes` and `record`. A `changed` scalar reports as `{ "field": …, "type": "changed", "from": …, "to": … }`; an `added` array reports as `{ "field": …, "type": "added", "new_elements": [ … ] }`. ### Pulled from run history To re-read a past run — or audit exactly what was delivered — the run-history endpoint returns the same content under a different envelope: ```json GET /watcher/watches/{id}/runs/{run_id}/summary { "id": 64210, "status": "SUCCESS", "new_records_count": 1, "notifications": [ { "sent_at": "2026-07-16T03:20:00Z", "http_status": 200, "payload": { "subscription_id": 46941, "event_type": "indb:entity:company", "timestamp": "2026-07-16T03:20:00Z", "notifications": [ { "uid": "company_631480_64210", "changes": [ { "field": "headcount.total", "type": "changed", "from": 8200, "to": 8460 } ], "record": { "basic_info": { … }, "headcount": { … }, "news": [ … ], "crustdata_company_id": 631480 } } ] } } ] } ``` ## Smoke-test your webhook Before a real change ever fires, push **one sample notification** through a watch to verify your receiver end-to-end — signature check, JSON parsing, routing — without waiting for a company to actually move. The test delivers the **exact envelope a real run sends** (same shape, same signature headers), flagged with `metadata.test: true`. It runs no diff, records no snapshot, **costs no credits**, and persists nothing. ``` POST https://api.crustdata.com/watch/company/{watch_id}/test ``` All three body fields are optional: | Field | Type | Description | | --- | --- | --- | | `notification_endpoint` | string | Deliver this one test to a URL you control instead of the watch's configured channels — handy with a request-inspection service. Must be a **public** `http(s)` URL; endpoints resolving to private, loopback, or link-local addresses are rejected. | | `fields` | string[] | Project the sample `record` to a specific field set for this test (defaults to the watch's own `fields`). | | `count` | integer | Deliver `N` sample companies (`1`–`25`) in one envelope to exercise multi-record handling. Each gets a distinct `crustdata_company_id`. Default `1`. | This request and response are real: ```bash Request curl --request POST \ --url https://api.crustdata.com/watch/company/47362/test \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "notification_endpoint": "https://your-server.example.com/webhook", "fields": ["basic_info", "funding"] }' ``` ```json Response { "delivered": [ { "type": "webhook", "endpoint": "https://your-server.example.com/webhook", "http_status": 200, "ok": true, "response": "…your receiver's response body, truncated to 500 chars…" } ], "envelope": { "…the exact body delivered to your webhook, shown below…" } } ``` Your receiver gets this `POST`. It is the shape of a real notification — only `metadata.test: true` and the **sample values** (`"sample_text"`, `1234`, …) mark it as a test: ```http Request headers Content-Type: application/json x-api-version: 2025-11-01 x-crustdata-watch-id: 47362 x-crustdata-event-id: ntf_test_1785211468 x-crustdata-signature: t=1785211468,v1= ``` ```json Webhook POST body { "metadata": { "watch_id": 47362, "kind": "entity", "dataset": "company", "api_version": "2025-11-01", "run_id": 1785211468, "notification_id": "ntf_test_1785211468", "delivered_at": "2026-07-28T04:04:28.168658Z", "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 25, "truncated": false }, "test": true }, "results": [ { "changes": [ { "field": "headcount.total", "type": "changed", "value": null, "from": null, "to": 1234 } ], "record": { "crustdata_company_id": 814313, "basic_info": { "name": "sample_text", "primary_domain": "sample_text", "website": "sample_text", "company_type": "sample_text", "year_founded": 1234, "employee_count_range": "sample_text", "industries": ["sample_text"] }, "funding": { "total_investment_usd": 12.5, "last_round_type": "sample_text", "last_round_amount_usd": 12.5, "last_fundraise_date": "2026-06-01", "investors": ["sample_text"], "milestones": [ { "date": "2026-06-01", "amount_usd": 12.5, "round_type": "sample_text" } ] } } } ] } ``` The sample `record` carries **placeholder values** with the exact **shape** of a real notification (every field in the `fields` groups you requested is present), so your parser, signature check, and routing see production structure — just not real company data. The `changes` array mirrors your watch's `track`: a `changed` scalar reports `{ "type": "changed", "value": …, "from": …, "to": … }` (`value` is the threshold target for `>`/`<` operators, `null` for a plain `changed`), and an `added` array reports `{ "type": "added", "new_elements": [ … ] }`. Pass `"count": 3` to receive three sample companies in one envelope — distinct `crustdata_company_id`s (`814313`, `814314`, `814315`) and `summary.delivered: 3` — so you can exercise how your receiver iterates `results`. The signature is computed exactly as for a real notification — HMAC-SHA256 over `.` keyed by your API key, delivered as `x-crustdata-signature: t=,v1=`. A test that verifies here verifies real deliveries. ## Rate limits Watch-management requests are rate-limited to **10 requests per minute** per API key. This bounds bursty create/update loops; steady use is unaffected. Need to *find* new matching companies instead of watching a known list? Use the Company Discovery Watcher. Browse the full set of trackable `field` paths for companies. # Company Discovery Watcher Source: https://docs.crustdata.com/watcher-docs/company/discovery Turn a company search into a recurring feed. A Company Discovery Watcher re-runs your filters on a schedule and pushes new matching companies to a webhook or Slack. A **Company Discovery Watcher** turns a [Company Search](/company-docs/search/introduction) filter into a recurring feed. Each run re-evaluates your filters against Crustdata's database and delivers only the **new** matches to a webhook or Slack — a continuous, deduplicated feed, with no manual re-running. ``` POST https://api.crustdata.com/watch/company/search ``` Pricing: the first run is a free baseline (a sample of up to 5 matches), then 2 credits per new company delivered. Common uses: - **Sales / GTM** — new accounts entering your ICP (e.g. *Brazilian fintechs that crossed 30% headcount growth*). - **Market intelligence** — companies that newly match a segment, funding stage, or growth band. - **Sourcing** — a fresh pipeline of recently founded or fast-growing companies in your target market. A Discovery Watcher tracks companies that **newly match a filter**. To watch a **known list** of companies for profile changes instead, use the [Company Entity Watcher](/watcher-docs/company/entity). ## How a watcher runs `POST` your `filters`, a `config` (schedule + result cap), and one or more `notifications` channels. The response returns a watch `id`. The first run happens within seconds, delivering a **free baseline sample of up to 5 matches** so you can confirm the setup and payload shape. No credits charged. On the schedule you set (`every_hours`), the watcher re-runs your filters and delivers companies that are **new or refreshed since the previous run**, up to `max_results_per_run`. You're charged per delivered company. Each run delivers companies **added or updated since the last run** — a feed of *movement* within your filter set, not a one-time export. For the full current match set, use [Company Search](/company-docs/search/introduction). ## Request body | Field | Required | Description | | ----- | -------- | ----------- | | `filters` | Yes | A filter tree, identical to [Company Search](/company-docs/search/reference). `{ "op": "and"\|"or", "conditions": [ { "field", "type", "value" } ] }`. | | `config.trigger` | Yes | `{ "type": "interval", "every_hours": N }` — how often the watch runs (e.g. `1`, `6`, `24`, `168`). | | `config.max_results_per_run` | No | Max records delivered per run. Default `25`. The first (baseline) run is always capped at 5. | | `config.expires_at` | No | ISO date (`"2027-01-01"`). The watch auto-stops after this date. | | `notifications` | Yes | One or more delivery channels (see [below](#delivery-channels)). | The response returns the watch `id`: ```json { "id": 46849 } ``` All examples require the headers `authorization: Bearer YOUR_API_KEY`, `content-type: application/json`, and `x-api-version: 2025-11-01`. For the full list of `field` values and operators, see the [Company Search reference](/company-docs/search/reference). ## Company filter format Watcher filters use the **exact same syntax and fields** as [Company Search](/company-docs/search/reference) — no new filter language to learn. Each filter is a tree of leaf conditions combined with `and` / `or` operator groups: - **Leaf condition** — `{ "field": "...", "type": "...", "value": ... }`, where `field` is a company attribute (e.g. `headcount.total`), `type` is the operator (e.g. `=>`, `in`, `>`), and `value` is what to match. - **Operator group** — `{ "op": "and" | "or", "conditions": [ ... ] }`, where `conditions` is a list of leaf conditions or nested groups. `and` requires every condition, `or` any one; nest groups for compound logic. ## Recipes ### Segment + headcount growth New **Brazilian software companies (50–500 employees) that grew headcount 30%+**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/company/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": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" }, { "field": "locations.country", "type": "in", "value": ["Brazil"] }, { "field": "headcount.total", "type": "=>", "value": 50 }, { "field": "headcount.total", "type": "=<", "value": 500 }, { "field": "headcount.growth_percent.6m", "type": "=>", "value": 30 } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25 }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### Well-funded companies in a country New **US companies that have raised more than $10M**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/company/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": "locations.country", "type": "in", "value": ["USA"] }, { "field": "funding.total_investment_usd", "type": ">", "value": 10000000 } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ] }' ``` ### Recently founded companies New **companies founded after 2020**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/company/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": "basic_info.year_founded", "type": ">", "value": 2020 }, { "field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" } ] }, "config": { "trigger": { "type": "interval", "every_hours": 168 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### High-growth mid-market Software companies **growing fast in the 200–2,000 range**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/company/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": "headcount.growth_percent.6m", "type": ">", "value": 15 }, { "field": "headcount.total", "type": "=>", "value": 200 }, { "field": "headcount.total", "type": "=<", "value": 2000 } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ## Delivery channels Every watch needs at least one channel in `notifications`. Add one or more; matches fan out to all of them. ```json Webhook { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata", "headers": { "X-Watch-Name": "brazil-software-growth" } } ``` ```json Slack { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ``` ```json Multiple [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }, { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ] ``` A Slack channel must be a genuine Slack incoming webhook (`https://hooks.slack.com/services/…`). Any other URL will fail delivery. ## Manage a watch ```bash List runs curl --request GET \ --url 'https://api.crustdata.com/watcher/watches/46849/runs?limit=20' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```bash Run detail (delivered records) curl --request GET \ --url https://api.crustdata.com/watcher/watches/46849/runs/54811/summary \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```bash Cancel a watch curl --request DELETE \ --url https://api.crustdata.com/watcher/watches/46849 \ --header 'authorization: Bearer YOUR_API_KEY' ``` ## Pricing | | Credits | | --- | --- | | First run (baseline, up to 5 matches) | **Free** | | Each new company delivered on later runs | **2** | You're charged only for companies actually delivered — never for a run that finds nothing new. See [Pricing](/general/pricing) for full details. ## Related Watch a known list of companies for profile changes. Pull the full current set of matches in one call. Every filter `field`, operator, and value. Credit costs across all Crustdata endpoints. # Person Search Source: https://docs.crustdata.com/person-docs/search/introduction Learn how to search for people using the Person Search API, from simple name lookups to multi-filter queries. The Person Search API lets you find professionals by name, title, company, location, and more. This page walks you through the basics: your first search, the response shape, and combining filters — plus worked example recipes you can copy, paste, and adapt. For the operator list, field catalog, and validation rules, see [Search reference](/person-docs/search/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/person/search ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: 0.03 credits per result returned. **Looking for the list of fields you can filter on?** See [Searchable fields](/person-docs/search/reference#searchable-fields) in the search reference for the full table of `filters.field` values grouped by family, plus a one-line trick to fetch the live list from the API. Employer + title, geo radius, country, and post-processing exclusions. Cursor-based pagination and sort rules for stable ordering. Operators, searchable fields, response fields, preview mode, errors. --- ## Your first search: find a person by name The simplest search finds a person by their exact name. You pass a single filter with the `=` operator. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_profile.name", "type": "=", "value": "Abhilash Chowdhary" }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 1068035, "basic_profile": { "name": "Abhilash Chowdhary", "headline": "Co-founder at Crustdata (YC F24) | Real-time B2B data for AI agents", "location": { "raw": "San Francisco, California, United States", "city": "San Francisco", "state": "California", "country": "United States of America", "continent": "North America" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/abhilashchowdhary" } }, "experience": { "employment_details": { "current": [ { "name": "Crustdata (YC F24)", "title": "Co-Founder & CEO" } ], "past": [ { "name": "Serve Robotics", "title": "Engineering Manager, Motion Planning and Controls" }, { "name": "Postmates by Uber", "title": "Robotics Lead, Motion Planning and Controls" } ] } }, "education": { "schools": [ { "school": "Virginia Tech", "degree": "Master’s Degree" }, { "school": "IIIT Hyderabad", "degree": "Bachelor of Technology (B.Tech.)" }, { "school": "Y Combinator", "degree": "F24 Batch" } ] } } ], "next_cursor": "H4sIAG6-oWkC_xXMMQrDMAxA0a...", "total_count": 8 } ``` Response trimmed for clarity. ### Understanding the response Every search response has three fields: - **`profiles`** — an array of matching people. Each profile contains identity fields, education, profile handles, and contact availability flags for the fields you requested. - **`total_count`** — how many people match your filters across the full database. Here, 8 people named "Abhilash Chowdhary" exist. - **`next_cursor`** — a pagination token. Pass it in the next request to get the next page of results. `null` means there are no more pages. --- ## Combine filters with `and` Real searches need more than one criterion. Wrap multiple conditions inside an `op: "and"` group to require all of them. This search finds Co-Founders located in San Francisco. The `(.)` operator does a fuzzy token match instead of an exact match. This makes it tolerant to typos. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.title", "type": "(.)", "value": "Co-Founder" }, { "field": "basic_profile.location.full_location", "type": "(.)", "value": "San Francisco" } ] }, "limit": 2 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 1279, "basic_profile": { "name": "Dipesh Garg", "headline": "CEO at Truelancer | 2 Million+ Professionals", "location": { "raw": "San Francisco, California, United States" } }, "experience": { "employment_details": { "current": [ { "name": "Truelancer.com", "title": "CEO & Founder" } ], "past": [ { "name": "MyRemoteTeam Inc", "title": "Founder" }, { "name": "MyRemoteTeam Inc", "title": "Lead Developer" } ] } } } ], "next_cursor": "H4sIAHC-oWkC_xWMMQ7CMAwAv...", "total_count": 95577 } ``` Response trimmed for clarity. The key difference from the first example: instead of a single `filters` object, you now have a group with `op: "and"` and a `conditions` array. Every condition must match for a profile to be included. For more filter patterns (employer + title, geo radius, excludes), see the [example requests](#example-requests) below. To walk through large result sets, see [Pagination and sorting](/person-docs/search/reference#paginate-through-results). --- ## Read normalized titles and education details Each profile also returns a normalized title classification and structured education — including school location and a Crustdata-hosted institution logo. Request the sections you need with `fields`, and filter by `crustdata_person_id` to fetch a single person. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "crustdata_person_id", "type": "=", "value": 14540 }, "fields": ["crustdata_person_id", "basic_profile", "education"], "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 14540, "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO", "normalized_title": { "matched_title": "Co-Founder, CEO, CTO", "department": "Executive Leadership", "sub_department": "Founder & Entrepreneurship Leadership", "similarity": 0.6012, "confident": true } }, "education": { "schools": [ { "school": "University of Oxford", "degree": "Bachelor of Arts (B.A.)", "location": { "raw": "South Hinksey, Wootton, Kennington, Oxford, Boars Hill, England, United Kingdom", "city": "Boars Hill", "state": "England", "country": "United Kingdom", "continent": "Europe" }, "institute_logo_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/company/3d2093a16f7cf7b459a0d30d4e795d0be5a41a3396f21dee899e43e8a8b6de8d.jpg" } ] } } ], "next_cursor": "H4sIAAFsGWoC_x...", "total_count": 1 } ``` `basic_profile.normalized_title` and `education.schools.location` are **filterable** but not sortable. `professional_network.followers` and `professional_network.connections` are **filterable and sortable**, but they are **not returned** in the search response — use them to narrow or rank results, not to read counts. School `description` and `institute_logo_permalink` are returned for display only. --- ## Example Requests Common filter patterns for Person Search. Each is a full, tested request against `POST /person/search` that you can copy, paste, and adapt. Click any row to expand it. Find people by exact title match: ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.current.title", "type": "=", "value": "Chief Executive Officer" }, "limit": 100 }' ``` **`=` and `in` match the full title string exactly.** `"Chief Information Security Officer"` will not match people titled "Deputy Chief Information Security Officer" or "SVP and CISO". For substring or word matching — the common case for titles — use the `(.)` operator, as in the next example. Find people whose headline contains "founder" (case-insensitive contains match): ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_profile.headline", "type": "(.)", "value": "founder" }, "limit": 100 }' ``` Find people with more than 10 years of experience: ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "years_of_experience_raw", "type": ">", "value": 10 }, "limit": 100 }' ``` Find well-connected people who recently changed jobs: ```bash curl --request POST \ --url https://api.crustdata.com/person/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": "professional_network.connections", "type": ">", "value": 500 }, { "field": "recently_changed_jobs", "type": "=", "value": true } ] }, "limit": 50 }' ``` Find professionals in a region, excluding certain employers and titles: ```bash curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.company_name", "type": "not_in", "value": ["Google", "Meta", "Amazon"] }, { "field": "experience.employment_details.current.title", "type": "!=", "value": "Intern" }, { "field": "professional_network.location.raw", "type": "=", "value": "San Francisco Bay Area" } ] }, "limit": 100 }' ``` Find senior people (VP, Director, or CXO) with 10+ years of experience at companies under 1,000 employees: ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "op": "and", "conditions": [ { "op": "or", "conditions": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "VP" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Director" }, { "field": "experience.employment_details.current.seniority_level", "type": "=", "value": "CXO" } ] }, { "field": "years_of_experience_raw", "type": "=>", "value": 10 }, { "field": "experience.employment_details.current.company_headcount_latest", "type": "<", "value": 1000 } ] }, "limit": 50 }' ``` Find people who started a role at a public company since 2023: ```bash curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.start_date", "type": "=>", "value": "2023-01-01" }, { "field": "experience.employment_details.current.company_type", "type": "=", "value": "Public Company" }, { "field": "years_of_experience_raw", "type": "=<", "value": 15 } ] }, "limit": 50 }' ``` Find Stanford alumni (non-Bachelor degree) skilled in machine learning: ```bash curl --request POST \ --url https://api.crustdata.com/person/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": "education.schools.school", "type": "(.)", "value": "Stanford" }, { "field": "education.schools.degree", "type": "!=", "value": "Bachelor" }, { "field": "skills.professional_network_skills", "type": "(.)", "value": "machine learning" } ] }, "limit": 100 }' ``` --- ## Semantic search (natural language) Instead of hand-building filters, pass a natural-language `search.query` to rank people by overall profile meaning. `search.mode` controls how matching works — including a **keyword (lexical)** mode and a **semantic** mode: | `search.mode` | What it does | | ------------------ | ------------------------------------------------------------------ | | `hybrid` (default) | Combines keyword (lexical) and semantic vector matching | | `lexical` | Keyword matching only — exact terms, acronyms, tools, names | | `semantic` | Vector similarity only — concept matching across different wording | Every result carries a relevance `fit` tier (`strong`, `possible`, or `weak`). Responses use the standard Person Search shape plus `fit` and `total_count_relation`: ```json Response shape { "profiles": [ { "crustdata_person_id": 123, "fit": "strong", "basic_profile": { "name": "Example Person", "current_title": "Founding Engineer" } } ], "next_cursor": "H4sIA...", "total_count": 1250, "total_count_relation": "eq" } ``` Semantic search is in **beta**. Don't send `sorts` — results are already rank-ordered by relevance. For recall modes (`managed` vs `exact`) and the full behavior, see the [Person Semantic Search guide](/guides/person-semantic-search). The default. Describe who you want in plain language; `hybrid` blends keyword and semantic matching for the best general-purpose recall. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "founding engineers at developer tools startups", "mode": "hybrid" }, "fields": ["fit", "basic_profile", "experience.employment_details.current"], "limit": 5 }' ``` Use `lexical` when exact terms, acronyms, tools, or names should dominate — keyword matching only, no vector similarity. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "Golang Kubernetes platform engineer", "mode": "lexical" }, "fields": ["fit", "basic_profile"], "limit": 5 }' ``` For precise keyword search, set `search.query_syntax: "boolean"` to turn the query into a boolean expression: a space means **AND**, `|` means OR, `+` requires a term, `-` excludes a term, `"…"` matches an exact phrase, `*` matches a prefix, and `()` groups operators. Honored only with `search.mode: "lexical"` and the top-level recall `mode: "exact"`. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "\"site reliability\" +kubernetes (golang | rust) -recruiter", "mode": "lexical", "query_syntax": "boolean" }, "mode": "exact", "fields": ["fit", "basic_profile"], "limit": 5 }' ``` See the [Person Semantic Search guide](/guides/person-semantic-search#boolean-keyword-operators) for the full operator reference. Use `semantic` to match the meaning of a query even when profiles use different words — vector similarity only. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "people who scaled infrastructure at hypergrowth startups", "mode": "semantic" }, "fields": ["fit", "basic_profile"], "limit": 5 }' ``` Enforce explicit `filters` as hard constraints, then rank inside that set with the query using the top-level `mode: "exact"`. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "machine learning engineers who have built recommender systems", "mode": "hybrid" }, "mode": "exact", "filters": { "field": "basic_profile.location.full_location", "type": "(.)", "value": "San Francisco" }, "fields": ["fit", "basic_profile"], "limit": 5 }' ``` --- ## More recipes Each recipe below is a full walkthrough with a tested request. Expand any one to see the pattern, the response, and how the operators work. This is the most common pattern for sales and recruiting: find people with a specific title at a specific company. This search finds VPs, Directors, and Heads of department at Retool. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.company_name", "type": "in", "value": ["Retool"] }, { "op": "or", "conditions": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "VP" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Vice President" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Director" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Head of" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Head" } ] } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 97567, "basic_profile": { "name": "Krithika S.", "headline": "Marketing at Thrive Capital", "location": { "country": "United States of America", "raw": "United States" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/krithix" } }, "experience": { "employment_details": { "current": [ { "name": "Thrive Capital", "title": "Executive in Residence, Marketing" } ], "past": [ { "name": "Stripe", "title": "Head of Marketing" }, { "name": "Retool", "title": "VP Marketing" }, { "name": "OpenAI", "title": "" }, { "name": "Google", "title": "" }, { "name": "Dropbox", "title": "" } ] } } } ], "next_cursor": "H4sIAJO-oWkC_xXMMQ6DMAwAw...", "total_count": 88 } ``` Response trimmed for clarity. ### How the operators work There are two different operators at play here: - **`in`** on `experience.employment_details.company_name` checks if the person has worked at any of the listed companies (current or past). Pass an array even for a single company. To search only current employers, use `experience.employment_details.current.company_name` instead. - **`(.)`** on `experience.employment_details.title` does a regex match. The pipe `|` means "or", so `VP|Director|Head of` matches any title containing "VP", "Director", or "Head of". To search only current titles, use `experience.employment_details.current.title` instead. The `experience.employment_details.company_name` field includes **all** employers (current and past). If you see someone whose current role is at a different company, it means they previously worked at your target company. When you know a company's profile URL but not its exact name, filter on the employer's profile URL. Names can be ambiguous; the profile URL is exact, so this is the most reliable way to target one specific company. Lead with the current-employer field to find people who **currently** work there: ```bash Request — current employees curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.current.company_professional_network_profile_url", "type": "=", "value": "https://www.linkedin.com/company/stripe" }, "limit": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Lucas Dickey", "headline": "Day Zero Things" }, "experience": { "employment_details": { "current": [{ "name": "Stripe", "title": "Builder" }] } } } ], "next_cursor": "H4sIABi5FmoC_xXMMQ7CMAwA...", "total_count": 11528 } ``` Response trimmed for clarity. The value must be the **exact, full** profile URL — for example `https://www.linkedin.com/company/stripe`. A trailing slash (`.../stripe/`), a missing scheme (`linkedin.com/company/stripe`), or a bare slug (`stripe`) all return zero results. ### Current, former, or either - **Current employees** — `experience.employment_details.current.company_professional_network_profile_url` - **Former employees** — `experience.employment_details.past.company_professional_network_profile_url` - **Anyone who has ever worked there** — combine both with an `or` group: ```json { "filters": { "op": "or", "conditions": [ { "field": "experience.employment_details.current.company_professional_network_profile_url", "type": "=", "value": "https://www.linkedin.com/company/stripe" }, { "field": "experience.employment_details.past.company_professional_network_profile_url", "type": "=", "value": "https://www.linkedin.com/company/stripe" } ] }, "limit": 25 } ``` To target several companies at once, use the `in` operator with an array of profile URLs. The bare `experience.employment_details.company_professional_network_profile_url` path (all employers) is not filterable — use the `current.` or `past.` variants above. The accepted alias `...company_linkedin_profile_url` resolves to the same data. Sometimes you want everyone at a company _except_ certain roles. Use the `not_in` operator to exclude titles. This search finds people at OpenAI or Retool but excludes interns and students. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.company_name", "type": "in", "value": ["OpenAI", "Retool"] }, { "field": "experience.employment_details.title", "type": "not_in", "value": ["Intern", "Student"] } ] }, "limit": 2 }' ``` The `not_in` operator removes any profile where one of the listed values appears in their title history. This is useful for cleaning up results in recruiting or sales workflows. The `geo_distance` filter finds people within a specific distance of a city. This is powerful for territory-based sales or local recruiting. This search finds CTOs within 10 miles of San Francisco. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "professional_network.location.raw", "type": "geo_distance", "value": { "location": "San Francisco", "distance": 10, "unit": "mi" } }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "CTO|Chief Technology" } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 1188, "basic_profile": { "name": "Matthew Trentini", "headline": "-", "location": { "city": "San Francisco", "state": "California", "country": "United States of America", "raw": "San Francisco Bay Area" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/matthew-trentini-b339bb5" } }, "experience": { "employment_details": { "current": [ { "name": "Farallon Capital Management", "title": "Chief Technology Officer" } ], "past": [ { "name": "Farallon Capital Management", "title": "Lead Software Engineer" } ] } } } ], "next_cursor": "H4sIAJi-oWkC_xXMMQ7CMAxA0a...", "total_count": 104310 } ``` Response trimmed for clarity. ### How geo_distance works The `geo_distance` filter uses the `professional_network.location.raw` field. The `value` is an object whose centre is given as **either** a `location` string (geocoded server-side) **or** an explicit `lat_lng` pair (which skips geocoding). If both are supplied, `lat_lng` wins. | Field | Required | Description | | ---------- | ----------------- | -------------------------------------------------------------------------------------------- | | `location` | one of | City or region name geocoded server-side (e.g., `"San Francisco"`, `"London"`, `"New York"`) | | `lat_lng` | one of | Explicit `[lat, lng]`. Lat in `[-90, 90]`, lng in `[-180, 180]`. Bypasses geocoding. | | `distance` | Yes | Radius from the centre point. Must be positive. | | `unit` | No | One of `km`, `mi`, `miles`, `m`, `meters`, `ft`, `feet`. Defaults to `km`. | ### Search by explicit coordinates Use `lat_lng` when you already have coordinates (for example, from a map picker) or you want to skip the geocoding step. The example below finds people within 5 km of latitude `37.7749`, longitude `-122.4194` (downtown San Francisco). ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "professional_network.location.raw", "type": "geo_distance", "value": { "lat_lng": [37.7749, -122.4194], "distance": 5, "unit": "km" } }, "limit": 5 }' ``` The `geo_exclude` filter is the inverse of `geo_distance` — it removes people **inside** the radius and keeps everyone else. Use it to carve out a metro you already cover, or to find candidates outside a relocation zone. This search finds engineers in the United States who are **not** within 50 km of San Francisco. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "professional_network.location.raw", "type": "geo_exclude", "value": { "location": "San Francisco", "distance": 50, "unit": "km" } }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "engineer" } ] }, "limit": 1 }' ``` `geo_exclude` accepts the same value object as `geo_distance` (`location` or `lat_lng`, a required `distance`, and an optional `unit`), so you can also exclude a radius around explicit coordinates. Use `(!)` when you want to drop profiles whose value contains a particular phrase — useful when `not_in` is too rigid (it requires exact values) and you want a substring-style exclusion instead. This search finds VP-level people at Retool, then drops anyone whose headline mentions "Investor" or "Advisor". ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.company_name", "type": "=", "value": "Retool" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "VP" }, { "field": "basic_profile.headline", "type": "(!)", "value": "Investor" }, { "field": "basic_profile.headline", "type": "(!)", "value": "Advisor" } ] }, "limit": 5 }' ``` `(!)` matches a multi-word value as a literal phrase. `(!) "New York"` excludes only profiles that literally contain `"New York"` — it does **not** exclude `"New Yorker"`. To exclude on each word independently, send a separate `(!)` condition for each word inside an `and` group, as shown above. For broader geographic targeting, filter by country directly. `basic_profile.location.country` uses full country names, such as `"United States"` or `"India"`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "basic_profile.location.country", "type": "=", "value": "United States" }, "limit": 2 }' ``` This returns all people located in the United States. With 125M+ matching profiles, you will want to combine this with title or employer filters to narrow results. Use `company_headquarters_country` when you want to filter by where a person's current or past employer is headquartered. Note: `company_headquarters_country` uses ISO-3 codes (`USA`, `IND`, `GBR`), unlike `basic_profile.location.country` which uses full names. Use ISO 3166-1 alpha-3 codes for the current, past, and all-role headquarters country fields. See the [ISO 3166-1 alpha-3 country code list](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for accepted codes. This search finds founders or co-founders whose current employer is headquartered in the United States and who previously worked at an employer headquartered in India. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "op": "and", "conditions": [ { "op": "or", "conditions": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Founder" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Co-Founder" } ] }, { "field": "experience.employment_details.current.company_headquarters_country", "type": "=", "value": "USA" }, { "field": "experience.employment_details.past.company_headquarters_country", "type": "=", "value": "IND" } ] }, "limit": 10 }' ``` Use `post_processing` to remove known profiles from results. This is useful when re-running searches and you want to skip people you have already contacted. ```bash Exclude specific people curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.title", "type": "(.)", "value": "Founder" }, "limit": 5, "post_processing": { "exclude_names": ["Ali Kashani"], "exclude_profiles": ["https://www.linkedin.com/in/alikashani"] } }' ``` You can exclude by name, by profile URL, or both. Person Search returns stable Crustdata-hosted logo permalinks for employers (`company_profile_picture_permalink`) and schools (`institute_logo_permalink`), so you can render a profile card without resolving image URLs yourself. Request the `experience` and `education` sections for the person you want. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "crustdata_person_id", "type": "=", "value": 14540 }, "fields": ["basic_profile", "experience", "education"], "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 14540, "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "company_profile_picture_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/company/72f60d0ccad488216922fb784abc89890b49eeed8ab1eca1a0a12c72a68a0620.jpg" } ] } }, "education": { "schools": [ { "school": "University of Oxford", "institute_logo_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/company/3d2093a16f7cf7b459a0d30d4e795d0be5a41a3396f21dee899e43e8a8b6de8d.jpg" } ] } } ], "next_cursor": "H4sIAFNsGWoC_xXM...", "total_count": 1 } ``` `company_profile_picture_permalink` and `institute_logo_permalink` are returned for display only — they are not searchable fields. When you have a company's website domain but not its exact display name, filter on the employer's domain. Lead with the current-employer field to find people who **currently** work there. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.current.company_website_domain", "type": "=", "value": "stripe.com" }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 13183, "basic_profile": { "name": "Lucas Dickey", "headline": "Day Zero Things", "location": { "raw": "Greater Seattle Area", "country": "United States" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/lucasdickey" } }, "experience": { "employment_details": { "current": [ { "name": "Stripe", "title": "Builder", "company_website": "https://stripe.com" } ] } } } ], "next_cursor": "H4sIADnRQWoC_xXMOw...", "total_count": 11897 } ``` Response trimmed for clarity. The response carries the employer's full website URL under `company_website` — the `company_website_domain` paths are filter-side names and do not appear in search responses. To target several companies at once, switch to the `in` operator with an array of domains. For current employees use the `current.` field above; for former employees use `experience.employment_details.past.company_website_domain`; for anyone who has ever worked there, use the all-roles field `experience.employment_details.company_website_domain`. Combine the `recently_changed_jobs` flag with a current-employer filter to scope the job-change signal to a target company, a useful starting point for sales and recruiting outreach. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.company_name", "type": "in", "value": ["OpenAI"] }, { "field": "recently_changed_jobs", "type": "=", "value": true } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 13535, "basic_profile": { "name": "Gary Lin", "headline": "Deployed at OpenAI (we’re hiring!)", "location": { "raw": "New York, New York, United States" } }, "experience": { "employment_details": { "current": [ { "name": "OpenAI", "title": "Member of Forward Deployed Staff" } ] } } } ], "next_cursor": "H4sIADrRQWoC_xXMPQ...", "total_count": 1051 } ``` Response trimmed for clarity. `recently_changed_jobs` is a boolean. Pair it with a `current.` employer or title filter to scope the signal to the population you care about. It is filterable and sortable, but it is not returned in the response. The flag marks a past role that **ended** within 90 days of the last index rebuild, not a new role that started. To target people who recently started a role, filter `experience.employment_details.current.start_date` against a date window. To find individual contributors only, use `not_in` on the **all-roles** career-history paths (`experience.employment_details.seniority_level` and `experience.employment_details.title`). On these all-employers fields, `not_in` drops anyone who has **ever** held that seniority or title at any point in their career — not just in their current role. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.seniority_level", "type": "not_in", "value": ["Owner / Partner", "CXO", "Vice President", "Director"] }, { "field": "experience.employment_details.title", "type": "not_in", "value": ["CEO", "President", "Chairman", "Founder", "Co-Founder"] }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "engineer" } ] }, "limit": 50 }' ``` This keeps people whose current title contains "engineer" but excludes anyone who has ever been a VP/Director/CXO or held a founder/CEO-level title. Because the exclusions are on the all-roles paths, a single past executive stint is enough to remove a profile. Use `not_in` on the all-roles `experience.employment_details.company_name` path to remove anyone who has **ever** worked at a named company — current or past. This is the cleanest way to screen out competitor alumni from a candidate or prospect list. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "Software Engineer" }, { "field": "experience.employment_details.company_name", "type": "not_in", "value": ["Google", "Meta"] } ] }, "limit": 50 }' ``` The all-roles `company_name` path covers current and past employers, so this excludes a profile if Google or Meta appears anywhere in their work history. For exact-company precision, prefer the company profile-URL paths (`...company_professional_network_profile_url`) over names. Stack filters to source founders for an investor pipeline: a founder/CEO title, a target school, a recent current-role start date (a proxy for a recently founded company), and a country. Each layer narrows the universe. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "op": "and", "conditions": [ { "op": "or", "conditions": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Founder" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "CEO" } ] }, { "field": "education.schools.school", "type": "(.)", "value": "Stanford" }, { "field": "experience.employment_details.current.start_date", "type": "=>", "value": "2023-01-01" }, { "field": "basic_profile.location.country", "type": "=", "value": "United States" } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 1102, "basic_profile": { "name": "François Locoh-Donou", "headline": "Chairman, President, & CEO of F5", "location": { "raw": "Seattle, Washington, United States", "country": "United States" } }, "education": { "schools": [ { "school": "Stanford GSB", "degree": "Master of Business Administration (MBA)" } ] } } ], "next_cursor": "H4sIADvRQWoC_xXMMQ...", "total_count": 10046 } ``` Response trimmed for clarity. `basic_profile.location.country` uses full country names; `current.start_date` accepts ISO date strings with the `=>` (greater-than-or-equal) operator. Filter on where a person's **past** and **current** employers are headquartered to find cross-border movers — for example, engineers who once worked at India-headquartered companies and are now at US-headquartered ones. Headquarters-country fields use ISO 3166-1 alpha-3 codes (`IND`, `USA`). ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.past.company_headquarters_country", "type": "=", "value": "IND" }, { "field": "experience.employment_details.current.company_headquarters_country", "type": "=", "value": "USA" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Engineer" } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 1394, "basic_profile": { "name": "Harbin Lee", "location": { "raw": "Amsterdam, North Holland, Netherlands" } }, "experience": { "employment_details": { "current": [ { "name": "OSIsoft, LLC.", "title": "Senior Software Engineer", "company_headquarters_country": "USA" } ], "past": [ { "name": "InMobi", "title": "Senior Software Engineer", "company_headquarters_country": "IND" } ] } } } ], "next_cursor": "H4sIADzRQWoC_xXMMQ...", "total_count": 640192 } ``` Response trimmed for clarity. Note that the **person's** location and the **employer's** headquarters country are independent — this person is based in the Netherlands while their employers are US- and India-headquartered. For the broadest geographic targeting, filter on `basic_profile.location.continent`. It takes a full continent name such as `"Europe"`, `"Asia"`, or `"North America"`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "basic_profile.location.continent", "type": "=", "value": "Europe" }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Founder" } ] }, "limit": 2 }' ``` This returns founders located anywhere in Europe (total_count 1,055,157 at time of writing). Combine continent with title, employer, or country filters to narrow a large pool. `basic_profile.location.continent` is filterable but not sortable. To find people who worked at **both** of two named companies, target one as the current employer and the other as a past employer. Use the slot-specific paths `experience.employment_details.current.company_name` and `experience.employment_details.past.company_name`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.company_name", "type": "=", "value": "Stripe" }, { "field": "experience.employment_details.past.company_name", "type": "=", "value": "Google" } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 153784, "basic_profile": { "name": "Vadim Jelezniakov", "headline": "Head of Engineering for Data Infrastructure @ Stripe" }, "experience": { "employment_details": { "current": [ { "name": "Stripe", "title": "Head of Data Infrastructure" } ], "past": [ { "name": "Meta", "title": "Director of Engineering, Core Services PE" }, { "name": "Google", "title": "" } ] } } } ], "next_cursor": "H4sIADzRQWoC_xXMMQ...", "total_count": 754 } ``` Response trimmed for clarity. This pattern works because each company is matched against a **different** employment slot (`current.` vs `past.`). Putting two different company names in two `=` conditions on the **same** all-roles path (`experience.employment_details.company_name`) returns zero results — a single employment record can't equal both names at once. Filter on `experience.employment_details.current.business_email_verified` to keep only profiles where a business email on the current role has been verified — useful when you plan to enrich and reach out by email. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.business_email_verified", "type": "=", "value": true }, { "field": "experience.employment_details.current.company_name", "type": "in", "value": ["Stripe"] } ] }, "limit": 1 }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 44532, "basic_profile": { "name": "Maureen Aguilar", "headline": "Co-Founder at Stripe", "location": { "raw": "White Post, Virginia, United States" } }, "experience": { "employment_details": { "current": [ { "name": "Stripe", "title": "Co-Founder", "business_email_verified": true } ] } } } ], "next_cursor": "H4sIAD3RQWoC_xXMSw...", "total_count": 2858 } ``` Response trimmed for clarity. `business_email_verified` is filterable on the current, past, and all-roles paths but is not returned in standard search results — retrieve the actual email with [Contact Enrich](/person-docs/contact/enrich). Add a `sorts` array to order results. This finds everyone associated with Retool and ranks them by professional-network followers, highest first. Request the fields you want to read alongside the sort key. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.company_name", "type": "in", "value": ["Retool"] }, "sorts": [{ "field": "professional_network.followers", "order": "desc" }], "limit": 3, "fields": ["crustdata_person_id", "basic_profile.name"] }' ``` ```json Response { "profiles": [ { "crustdata_person_id": 193594, "basic_profile": { "name": "Jeff An" } }, { "crustdata_person_id": 3408565, "basic_profile": { "name": "Jake Fox (on leave)" } }, { "crustdata_person_id": 1061161, "basic_profile": { "name": "Cailen DSa" } } ], "next_cursor": "H4sIAL8XRmoC_x...", "total_count": 952 } ``` Response trimmed for clarity. `professional_network.followers` is filterable and sortable, but it is **not returned** in results — the sort ranks profiles by follower count without exposing the count itself. Always include a `sorts` array when paginating so ordering stays stable across pages — see [Pagination and sorting](/person-docs/search/reference#paginate-through-results). Preview mode returns lightweight results so you can sanity-check a filter and read `total_count` before committing to a full search. Set `preview: true` alongside your normal filters. Preview is a premium feature. To enable it for your account, reach out to [gtm@crustdata.co](mailto:gtm@crustdata.co). If preview is not enabled, the API returns `400 invalid_request` with the message `PersonDB preview feature is not available for your account.` ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.title", "type": "(.)", "value": "Founder" }, "preview": true, "limit": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "headline": "CEO & Co-Founder, Noise (Hiring at all levels)", "location": { "raw": "Gurgaon, Haryana, India" }, "name": "Gaurav Khatri", "professional_network_name": "Gaurav Khatri", "profile_picture_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/person/1cf520715028fad9e20ae5a7dfda3a45cd5eaec540e944f873ce7d02e22316cb.jpg" }, "crustdata_person_id": 958, "social_handles": { "dev_platform_identifier": { "profile_url": null }, "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/gauravkhatrigonoise" } } } ], "next_cursor": "H4sIAHyEXmoC_xXMMQ6DMAxA0asgzx3i2CSkV6kq5DiOGKpGhTBUiLuXjv8N_4DPbut3XmRb4D5AThktc1R0rqBjRKU8GYfiiYtXKYEkUBWKtapZ8sYUU6nRVSVhuA2wtbVfr0cap-eVvXV5zdr291_HFNB7Pn_nuKkBeQAAAA==", "total_count": 5961224, "total_count_relation": null } ``` Response trimmed for clarity. Preview responses keep the same top-level shape as a normal search — `profiles`, `next_cursor`, and `total_count` — but each profile carries a reduced field set. See [Preview mode](/person-docs/search/reference#preview-mode) in the search reference. --- ## What to do next - **Try more filter patterns** — see the [example requests](#example-requests) above for employer + title, geo radius, and exclude patterns. - **Paginate and sort** — see [Pagination and sorting](/person-docs/search/reference#paginate-through-results) to walk through every matching profile in a stable order. - **Look up operators and fields** — see [Search reference](/person-docs/search/reference) for operators, searchable fields, response fields, request parameters, and errors. - **Enrich a profile** — once you have a profile URL from search, use [Person Enrich](/person-docs/enrichment/introduction) to get the full cached profile. - **Discover filter values** — use [Person Autocomplete](/person-docs/autocomplete/introduction) to find exact indexed values for search filters before building a filter. - **Check the API reference** — see the [OpenAPI spec](/openapi-specs/2025-11-01/introduction) for the full schema. # Person Search reference Source: https://docs.crustdata.com/person-docs/search/reference Reference for Person Search: filter operators, searchable fields, response fields, request parameters, preview mode, and errors. Reference material for [Person Search](/person-docs/search/introduction): the full list of filter operators, searchable fields with sortable flags, response fields, request parameters, preview mode, and error responses. For walk-through examples, see [Person Search](/person-docs/search/introduction) and [Examples](/person-docs/search/introduction). --- ## Filter operator reference Person Search accepts the following `filters.type` operators. | Operator | Value shape | Meaning | Example use | | -------------- | ------------------------------ | -------------------------------------------------- | -------------------------------------------------------------------- | | `=` | scalar | Exact match | `basic_profile.name` = "David Hsu" | | `!=` | scalar | Not equal | Exclude a specific country | | `<` | scalar (number or date string) | Less than | Profiles updated before a timestamp | | `=<` | scalar (number or date string) | Less than or equal | `professional_network.connections` at or below a threshold | | `>` | scalar (number or date string) | Greater than | `years_of_experience_raw` above a threshold | | `=>` | scalar (number or date string) | Greater than or equal | `professional_network.followers` at or above a threshold | | `in` | array | Value is in list | `experience.employment_details.company_name` in ["Retool", "OpenAI"] | | `not_in` | array | Value is not in list | Exclude titles like "Intern" | | `is_null` | `null` | Field has no value (see [null checks](#null-checks-is_null-and-is_not_null)) | People with no current employment | | `is_not_null` | `null` | Field has at least one non-null value | People with at least one past role | | `has_all` | array | Each listed value is matched by some nested-array element, possibly different elements (see [Nested-array matching](#nested-array-matching-all_of-and-has_all)) | `experience.employment_details.company_id` `has_all` [629097, 632500] | | `(.)` | string | Case-insensitive all-words match — every query word must appear, in any order (not regex); a piped value is split into separate words that must ALL match (AND), not OR | Match `engineer` in any title; for VP **or** Director use `any_of` | | `(!)` | string | Fuzzy negation — exclude substring matches | Exclude titles containing "Intern" (see notes below) | | `[.]` | string | Case-insensitive exact-phrase match — words must be contiguous and in order (no partial words or typos) | `[.]` "Software Engineer" matches "Senior Software Engineer", not "Engineer Software" | | `geo_distance` | object | Within radius of a location or explicit coordinate | People within 50 km of San Francisco | | `geo_exclude` | object | Outside radius of a location or explicit coordinate | People **not** within 50 km of San Francisco | Use `=>` for greater-than-or-equal and `=<` for less-than-or-equal. The operators `>=` and `<=` are not supported. **Operator sets differ per endpoint.** `contains` is **not** supported on `/person/search` (use `(.)` for substring/word matching) — it is accepted by Company Search and by the autocomplete endpoints' `filters`. `has_all` works only on nested array fields (for example `experience.employment_details.*`); non-array fields return `400`. ### `(!)` — fuzzy negation `(!)` excludes profiles whose value contains the given substring, case-insensitive. It is the opposite of `(.)`. Multi-word values are matched as a literal phrase — they are **not** word-split: - `{ "type": "(!)", "value": "New York" }` excludes only profiles that literally contain the phrase `"New York"`. A profile whose location is `"New Yorker"` is **not** excluded by this filter; `"New York City"` **is** excluded because it contains the full phrase. - To exclude on each word independently, wrap multiple `(!)` conditions in an `and` group: ```json { "op": "and", "conditions": [ { "field": "basic_profile.headline", "type": "(!)", "value": "Intern" }, { "field": "basic_profile.headline", "type": "(!)", "value": "Student" } ] } ``` ### `geo_distance` — radius around a point Supply the centre of the radius using **one** of: - `location` — a string that is geocoded server-side (e.g. `"San Francisco, CA"`). - `lat_lng` — explicit coordinates as `[lat, lng]`. Skips geocoding. If both are supplied, `lat_lng` wins. `distance` is required; `unit` defaults to `km`. | Field | Type | Required | Notes | | ---------- | -------- | -------- | -------------------------------------------------------------------------- | | `location` | string | One of | Geocoded server-side. Ignored when `lat_lng` is also set. | | `lat_lng` | number[] | One of | Two-element `[lat, lng]`. Lat in `[-90, 90]`, lng in `[-180, 180]`. | | `distance` | number | Yes | Radius around the centre. Must be positive. | | `unit` | string | No | One of `km`, `mi`, `miles`, `m`, `meters`, `ft`, `feet`. Defaults to `km`. | See [`geo_distance` examples](/person-docs/search/introduction#search-within-a-geographic-radius) for end-to-end requests using both `location` and `lat_lng`. ### `geo_exclude` — exclude a radius `geo_exclude` is the inverse of `geo_distance`: it removes profiles **inside** the radius and keeps everyone else. It takes the same value object — `location` or `lat_lng`, a required `distance`, and an optional `unit` (defaults to `km`) — and works on the same location fields. Use it to carve out a metro you already cover, or to target candidates outside a region. ```json { "field": "professional_network.location.raw", "type": "geo_exclude", "value": { "location": "San Francisco", "distance": 50, "unit": "km" } } ``` See [`geo_exclude` examples](/person-docs/search/introduction#exclude-a-geographic-radius) for an end-to-end request. ### Null checks: `is_null` and `is_not_null` Both operators take a literal `null` value. The `value` key is required — omitting it returns `400`: ```json { "field": "experience.employment_details.current.name", "type": "is_null", "value": null } ``` On nested-array fields the check is element-based: `is_null` matches profiles where **no element** carries the field (for example, an empty `experience.employment_details.current` array), and `is_not_null` matches profiles where **at least one element** has a non-null value. Combine the two to express presence patterns — for example, people who left a job and have not started a new one: ```json { "op": "and", "conditions": [ { "field": "experience.employment_details.current.name", "type": "is_null", "value": null }, { "field": "experience.employment_details.past.name", "type": "is_not_null", "value": null } ] } ``` ## Nested-array matching: `all_of` and `has_all` Some fields are **arrays of nested objects** — for example a person's employment history (`experience.employment_details.*`), where each entry has its own `title`, `company_id`, `company_name`, and dates. When you put several conditions on one such field, there are two distinct things you might mean: - **Same element** — one array entry satisfies all the conditions (was an _Engineer_ **at** company X — one job). - **Cross element** — different entries each satisfy a condition (was an _Engineer_ at A **and** a _Manager_ at B — two separate jobs). A plain `and` group over one nested field means **same element**: all conditions must match within a single entry. To express **cross element**, use an `all_of` group. ### `all_of` — each condition matched by some element `all_of` is a group operator (like `and`/`or`). Each condition inside it must be satisfied by **at least one array element**, and each condition is evaluated **independently** — so different conditions can be matched by different elements. A condition can be a single filter or an `and`/`or` group; a group is matched **within one element**. Existential — some job is both `Engineer` and at company `629097`: ```json { "filters": { "op": "all_of", "conditions": [ { "op": "and", "conditions": [ { "field": "experience.employment_details.title", "type": "(.)", "value": "Engineer" }, { "field": "experience.employment_details.company_id", "type": "=", "value": 629097 } ] } ] } } ``` Cross element — `Engineer` at one company **and** `Manager` at another, in different jobs: ```json { "filters": { "op": "all_of", "conditions": [ { "op": "and", "conditions": [ { "field": "experience.employment_details.title", "type": "(.)", "value": "Engineer" }, { "field": "experience.employment_details.company_id", "type": "=", "value": 629097 } ] }, { "op": "and", "conditions": [ { "field": "experience.employment_details.title", "type": "(.)", "value": "Manager" }, { "field": "experience.employment_details.company_id", "type": "=", "value": 632500 } ] } ] } } ``` `all_of` groups nest freely inside `and`/`or` groups, so you can combine cross-element requirements with document-level filters (like location). Conditions inside `all_of` accept any **positive** operator (`=`, `(.)`, `[.]`, `in`, `<`, `>`, `=<`, `=>`). ### `has_all` — every value matched by some element `has_all` is shorthand for the common cross-element case: a value list where **each value is matched by some element** of the array. Worked at **both** company `629097` and `632500` (two separate roles): ```json { "filters": { "field": "experience.employment_details.company_id", "type": "has_all", "value": [629097, 632500] } } ``` This expands to exactly `all_of` with one equality condition per value: `{ "op": "all_of", "conditions": [company_id = 629097, company_id = 632500] }`. ### Rules and validation - Send the operator in lowercase: `all_of` (like `and`/`or`; `allOf` and other casings are rejected). - All fields inside one `all_of` condition (or its `and`/`or` group) must resolve to a **single** nested-array path. Mixing paths (for example an employment field and an education field in the same group) is rejected — split them into separate `all_of` conditions. - `all_of` and `has_all` are valid only on **nested-array fields** (employment, education, certifications, honors). Using them on a scalar field (such as a name) is rejected. - Conditions inside `all_of` allow **positive** predicates only. Negation operators (`!=`, `not_in`, `(!)`, `geo_exclude`) are not allowed — apply negation at the document level instead. - Do not put a `has_all` condition (or another `all_of`) **inside** an `all_of` group — expand it into `=` conditions instead. - `has_all` requires a **non-empty** list value. ## Searchable fields - Some returned fields use a different filter path. For example, the returned `basic_profile.current_title` is searched with `experience.employment_details.current.title`. - Contact availability flags such as `contact.has_business_email` are response-only convenience fields. For search filters, use `experience.employment_details.current.business_email_verified`, `experience.employment_details.past.business_email_verified`, or `experience.employment_details.business_email_verified`. - `social_handles.professional_network_identifier.profile_url` is returned in search results but is rejected as a search filter. Use [Person Enrich](/person-docs/enrichment/introduction) for direct profile URL lookups. - Some searchable fields, such as `certifications.*` and `honors.title`, may not appear in the response summary below. - Filter by employer profile ID with `experience.employment_details.professional_network_id`, or the `current.` / `past.` variants to pin the tense. Get the value from `basic_info.professional_network_id` on [Company Identify](/company-docs/identify/introduction) or [Company Enrich](/company-docs/enrichment/introduction), and pass it as a string. Use this when you hold a profile ID; `experience.employment_details.company_id` remains the path when you hold a Crustdata company ID. Country filters do not all use the same value format. Use full country names for person location fields, and use ISO 3166-1 alpha-3 codes for employer headquarters country fields. ### Country and region value formats | Field | Value format | Examples | | -------------------------------------------------------------------- | ------------------------------- | ----------------------------- | | `basic_profile.location.country` | Full country name | `"United States"`, `"India"` | | `basic_profile.location.state` | Full state or region name | `"California"`, `"Ontario"` | | `basic_profile.location.continent` | Full continent name | `"North America"`, `"Asia"` | | `professional_network.location.raw` | Raw location string | `"San Francisco Bay Area"` | | `experience.employment_details.company_headquarters_country` | ISO 3166-1 alpha-3 country code | `"USA"`, `"IND"` | | `experience.employment_details.current.company_headquarters_country` | ISO 3166-1 alpha-3 country code | `"USA"`, `"GBR"` | | `experience.employment_details.past.company_headquarters_country` | ISO 3166-1 alpha-3 country code | `"IND"`, `"USA"` | | `experience.employment_details.company_hq_location` | Employer HQ location string | `"San Francisco, California"` | For headquarters country filters, use ISO-3 codes such as `USA`, `IND`, and `GBR`. For the full code list, see the [ISO 3166-1 alpha-3 country code list](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3). For `basic_profile.location.country`, use [Autocomplete](/person-docs/autocomplete/introduction) to discover indexed full-country labels before filtering. ### Website value formats | Field | Value format | Examples | | --------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------ | | `experience.employment_details.current.company_website_domain` | Bare website domain | `"stripe.com"` | | `experience.employment_details.past.company_website_domain` | Bare website domain | `"stripe.com"` | | `experience.employment_details.company_website_domain` | Bare website domain | `"stripe.com"` | | `experience.employment_details.company_website` | Exact stored website URL (scheme + any query string) | `"http://www.chetnetwork.com"` | Filter websites with the `company_website_domain` fields — a bare domain, no scheme. The all-roles `company_website` filter matches the stored URL **exactly**, including scheme and any query string (stored values can look like `https://example.com?utm_source=profile`), so prefer the domain fields unless you need an exact URL match. In search responses the website comes back as a full URL under `experience.employment_details.current.company_website` / `past.company_website`; the `company_website_domain` paths are filter-side names and never appear in the search response. [Person Enrich](/person-docs/enrichment/introduction) is the reverse: it returns the bare domain under `company_website_domain`. ### Identity & metadata | Field | Type | Filterable | Sortable | Description | | ------------------------------ | -------- | ---------- | -------- | ----------------------------- | | `crustdata_person_id` | integer | Yes | Yes | Crustdata person ID | | `metadata.updated_at` | datetime | Yes | Yes | Last profile update timestamp | | `metadata.last_scraped_source` | string | Yes | No | Last profile refresh source | ### Basic profile | Field | Type | Filterable | Sortable | Description | | ----------------------------------------------- | -------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `basic_profile.name` | string | Yes | Yes | Full name | | `basic_profile.first_name` | string | Yes | No | First name | | `basic_profile.last_name` | string | Yes | No | Last name | | `basic_profile.headline` | string | Yes | No | Profile headline | | `basic_profile.summary` | string | Yes | No | Profile summary / about | | `basic_profile.languages` | string[] | Yes | No | Spoken languages | | `basic_profile.last_updated` | datetime | Yes | No | Last update timestamp on the basic profile | | `basic_profile.location` | string | Yes | Yes | Location summary | | `basic_profile.location.full_location` | string | Yes | Yes | Full location string | | `basic_profile.location.city` | string | Yes | Yes | City | | `basic_profile.location.state` | string | Yes | Yes | State / region as a full name | | `basic_profile.location.country` | string | Yes | Yes | Country as a full name | | `basic_profile.location.continent` | string | Yes | No | Continent as a full name | | `basic_profile.normalized_title.department` | string | Yes | No | [Job Title Normalization](/guides/job-title-normalization) (beta) — high-level department. | | `basic_profile.normalized_title.sub_department` | string | Yes | No | [Job Title Normalization](/guides/job-title-normalization) (beta) — sub-department / category. | | `basic_profile.normalized_title.matched_title` | string | Yes | No | [Job Title Normalization](/guides/job-title-normalization) (beta) — standardized canonical title. | | `basic_profile.normalized_title.similarity` | float | No | No | [Job Title Normalization](/guides/job-title-normalization) (beta) — similarity score between raw and matched title. | | `basic_profile.normalized_title.confident` | boolean | No | No | [Job Title Normalization](/guides/job-title-normalization) (beta) — whether the mapping is high-confidence. | | `basic_profile.professional_network_name` | string | Yes | No | Display name on the professional-network profile | ### Professional network | Field | Type | Filterable | Sortable | Description | | --------------------------------------------------- | -------- | ---------- | -------- | ------------------------------------------------------------------------- | | `professional_network.location.raw` | string | Yes | No | Raw location string from profile | | `professional_network.location` | string | Yes | No | Network location summary | | `professional_network.location.city` | string | Yes | No | Network location city | | `professional_network.location.state` | string | Yes | No | Network location state / region | | `professional_network.location.country` | string | Yes | No | Network location country | | `professional_network.location.continent` | string | Yes | No | Network location continent | | `professional_network.connections` | integer | Yes | Yes | Connection count | | `professional_network.followers` | integer | Yes | Yes | Follower count (requires API key access) | | `professional_network.open_to_cards` | string[] | Yes | No | Open-to signal codes. See [Open-to signal values](#open-to-signal-values) | | `professional_network.metadata.last_scraped_source` | string | Yes | No | Last profile refresh source | ### Skills | Field | Type | Filterable | Sortable | Description | | ------------------------------------ | -------- | ---------- | -------- | ------------- | | `skills.professional_network_skills` | string[] | Yes | No | Listed skills | ### Experience — all employers | Field | Type | Filterable | Sortable | Description | | --------------------------------------------------------------------- | -------- | ---------- | -------- | ---------------------------------------- | | `experience.employment_details.company_name` | string | Yes | No | Company name across all roles | | `experience.employment_details.title` | string | Yes | No | Job title across all roles | | `experience.employment_details.description` | string | Yes | No | Role description across all roles | | `experience.employment_details.seniority_level` | string | Yes | No | Seniority level across all roles | | `experience.employment_details.function_category` | string | Yes | No | Function category across all roles | | `experience.employment_details.start_date` | date | Yes | Yes | Role start date across all roles | | `experience.employment_details.end_date` | date | Yes | No | Role end date across all roles | | `experience.employment_details.location` | string | Yes | No | Role location across all roles | | `experience.employment_details.company_id` | integer | Yes | Yes | Company ID across all roles | | `experience.employment_details.professional_network_id` | string | Yes | No | Employer profile ID across all roles | | `experience.employment_details.company_website_domain` | string | Yes | No | Employer website domain | | `experience.employment_details.company_headcount_latest` | integer | Yes | Yes | Employer latest headcount | | `experience.employment_details.company_headcount_range` | string | Yes | No | Employer headcount range | | `experience.employment_details.company_industries` | string[] | Yes | No | Employer industries | | `experience.employment_details.company_professional_network_industry` | string | Yes | No | Employer primary industry label | | `experience.employment_details.company_type` | string | Yes | No | Employer company type | | `experience.employment_details.company_headquarters_country` | string | Yes | No | Employer HQ country as ISO-3 code | | `experience.employment_details.company_hq_location` | string | Yes | No | Employer HQ location string | | `experience.employment_details.company_website` | string | Yes | No | Employer website across all roles — exact match against the stored full URL (scheme and any query string included); prefer `company_website_domain` | | `experience.employment_details.employment_type` | string | Yes | No | Employment type across all roles | | `experience.employment_details.years_at_company_raw` | number | Yes | Yes | Whole years in the role, floored. Counts from the role's start date, so a promotion starts it over. For precise tenure, filter `start_date` instead | | `experience.employment_details.business_email_verified` | boolean | Yes | No | Verified business email across all roles | ### Experience — current employer | Field | Type | Filterable | Sortable | Description | | -------------------------------------------------------------------------------- | -------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `experience.employment_details.current.company_name` | string | Yes | No | Current company name (filter alias of `current.name`) | | `experience.employment_details.current.title` | string | Yes | No | Current job title | | `experience.employment_details.current.description` | string | Yes | No | Current role description | | `experience.employment_details.current.seniority_level` | string | Yes | No | Current seniority level | | `experience.employment_details.current.function_category` | string | Yes | No | Current function category | | `experience.employment_details.current.start_date` | date | Yes | No | Current role start date | | `experience.employment_details.current.name` | string | Yes | No | Current company name (returned response field) | | `experience.employment_details.current.years_at_company_raw` | number | Yes | No | Years at current company | | `experience.employment_details.current.company_headquarters_country` | string | Yes | No | Current employer HQ country as ISO-3 code | | `experience.employment_details.current.company_id` | integer | Yes | No | Current employer company ID (filter alias of `current.crustdata_company_id`) — the response returns the id under `current.crustdata_company_id`, not `company_id` | | `experience.employment_details.current.professional_network_id` | string | Yes | No | Current employer profile ID. Returned under the same name on each role, so this one is not an alias. | | `experience.employment_details.current.position_id` | string | Yes | No | Per-role identifier for the current position (one per role, distinct from company ID) | | `experience.employment_details.current.company_industries` | string[] | Yes | No | Current employer industries | | `experience.employment_details.current.company_type` | string | Yes | No | Current employer company type | | `experience.employment_details.current.company_headcount_latest` | integer | Yes | No | Current employer latest headcount | | `experience.employment_details.current.company_headcount_range` | string | Yes | No | Current employer headcount range | | `experience.employment_details.current.company_hq_location` | string | Yes | No | Current employer HQ location string | | `experience.employment_details.current.company_website_domain` | string | Yes | No | Current employer website domain (filter alias of `current.company_website`) — filter with a bare domain like `"stripe.com"`; the response returns the full URL under `current.company_website` | | `experience.employment_details.current.company_professional_network_industry` | string | Yes | No | Current employer primary industry label | | `experience.employment_details.current.company_professional_network_profile_url` | string | Yes | No | Current employer profile URL. Exact full URL — see [Find people at a company](/person-docs/search/introduction#find-people-at-a-company-by-its-profile-url) | | `experience.employment_details.current.company_linkedin_profile_url` | string | Yes | No | Current employer profile URL (accepted alias) | | `experience.employment_details.current.employment_type` | string | Yes | No | Current employment type | | `experience.employment_details.current.business_email_verified` | boolean | Yes | No | Verified business email on current role | ### Experience — past employer | Field | Type | Filterable | Sortable | Description | | ----------------------------------------------------------------------------- | -------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `experience.employment_details.past.company_name` | string | Yes | No | Past company name (filter alias of `past.name`) | | `experience.employment_details.past.title` | string | Yes | No | Past job title | | `experience.employment_details.past.description` | string | Yes | No | Past role description | | `experience.employment_details.past.seniority_level` | string | Yes | No | Past seniority level | | `experience.employment_details.past.function_category` | string | Yes | No | Past function category | | `experience.employment_details.past.start_date` | date | Yes | No | Past role start date | | `experience.employment_details.past.name` | string | Yes | No | Past company name (returned response field) | | `experience.employment_details.past.years_at_company_raw` | number | Yes | No | Years at past company | | `experience.employment_details.past.company_headquarters_country` | string | Yes | No | Past employer HQ country as ISO-3 code | | `experience.employment_details.past.company_id` | integer | Yes | No | Past employer company ID (filter alias of `past.crustdata_company_id`) — the response returns the id under `past.crustdata_company_id`, not `company_id` | | `experience.employment_details.past.professional_network_id` | string | Yes | No | Past employer profile ID. Returned under the same name on each role, so this one is not an alias. | | `experience.employment_details.past.position_id` | string | Yes | No | Per-role identifier for the past position (one per role, distinct from company ID) | | `experience.employment_details.past.company_industries` | string[] | Yes | No | Past employer industries | | `experience.employment_details.past.company_type` | string | Yes | No | Past employer company type | | `experience.employment_details.past.company_headcount_latest` | integer | Yes | No | Past employer latest headcount | | `experience.employment_details.past.company_headcount_range` | string | Yes | No | Past employer headcount range | | `experience.employment_details.past.company_hq_location` | string | Yes | No | Past employer HQ location string | | `experience.employment_details.past.company_website_domain` | string | Yes | No | Past employer website domain (filter alias of `past.company_website`) — filter with a bare domain like `"stripe.com"`; the response returns the full URL under `past.company_website` | | `experience.employment_details.past.company_professional_network_industry` | string | Yes | No | Past employer primary industry label | | `experience.employment_details.past.company_professional_network_profile_url` | string | Yes | No | Past employer profile URL. Exact full URL — see [Find people at a company](/person-docs/search/introduction#find-people-at-a-company-by-its-profile-url) | | `experience.employment_details.past.company_linkedin_profile_url` | string | Yes | No | Past employer profile URL (accepted alias) | | `experience.employment_details.past.employment_type` | string | Yes | No | Past employment type | | `experience.employment_details.past.business_email_verified` | boolean | Yes | No | Verified business email on past role | ### Education | Field | Type | Filterable | Sortable | Description | | ------------------------------------------ | ------ | ---------- | -------- | ------------------------------ | | `education.schools.school` | string | Yes | No | School name | | `education.schools.degree` | string | Yes | No | Degree | | `education.schools.field_of_study` | string | Yes | No | Field of study | | `education.schools.location` | string | Yes | No | School location summary | | `education.schools.location.city` | string | Yes | No | School location city | | `education.schools.location.state` | string | Yes | No | School location state / region | | `education.schools.location.country` | string | Yes | No | School location country | | `education.schools.location.continent` | string | Yes | No | School location continent | | `education.schools.location.full_location` | string | Yes | No | School full location string | ### Certifications & honors | Field | Type | Filterable | Sortable | Description | | ------------------------------------- | ------ | ---------- | -------- | ----------------------------- | | `certifications.name` | string | Yes | No | Certification name | | `certifications.issuing_organization` | string | Yes | No | Certification issuer | | `certifications.issue_date` | date | Yes | No | Certification issue date | | `certifications.expiration_date` | date | Yes | No | Certification expiration date | | `certifications.credential_id` | string | Yes | No | Certification credential ID | | `certifications.credential_url` | string | Yes | No | Certification credential URL | | `honors.title` | string | Yes | No | Honor or award title | ### Social handles | Field | Type | Filterable | Sortable | Description | | ------------------------------- | ------ | ---------- | -------- | ---------------- | | `social_handles.twitter_handle` | string | Yes | No | Twitter/X handle | ### GitHub dev platform (filter-only) Filter and sort on the person's best-matched GitHub account. The field paths mirror the `dev_platform_profiles` shape returned by [Dev Platform Enrich](/person-docs/dev_platform/introduction), and they are **search-only**: none of these fields are returned in the search response. Filter here, then fetch the values with Dev Platform Enrich. Account-level fields describe the whole account. The `all_languages`, `all_topics`, and `total_*` / `max_stars` / `median_stars` rollups are computed across **all** of the person's owned repositories. | Field | Type | Filterable | Sortable | Description | | ------------------------------------------- | ------- | ---------- | -------- | ----------------------------------------------- | | `dev_platform_profiles.name` | string | Yes | No | Name on the GitHub profile | | `dev_platform_profiles.bio` | string | Yes | No | Profile bio | | `dev_platform_profiles.location` | string | Yes | No | Self-reported location | | `dev_platform_profiles.website_url` | string | Yes | No | Blog / website URL | | `dev_platform_profiles.company_text` | string | Yes | No | Company text on the profile | | `dev_platform_profiles.is_hireable` | boolean | Yes | No | GitHub "available for hire" flag | | `dev_platform_profiles.followers` | number | Yes | Yes | Follower count | | `dev_platform_profiles.following` | number | Yes | Yes | Following count | | `dev_platform_profiles.public_repo_count` | number | Yes | Yes | Public repositories owned | | `dev_platform_profiles.metadata.created_at` | date | Yes | Yes | Account creation date | | `dev_platform_profiles.total_repos` | number | Yes | Yes | Owned repositories tracked | | `dev_platform_profiles.total_stars` | number | Yes | Yes | Stars across all owned repositories | | `dev_platform_profiles.max_stars` | number | Yes | Yes | Stars on the most-starred repository | | `dev_platform_profiles.median_stars` | number | Yes | Yes | Median stars across owned repositories | | `dev_platform_profiles.all_languages` | array | Yes | No | Primary languages across all owned repositories | | `dev_platform_profiles.all_topics` | array | Yes | No | Topic tags across all owned repositories | Repository-level fields match against the person's **top 25 most recently pushed owned repositories**: | Field | Type | Filterable | Sortable | Description | | ----------------------------------------------- | ------- | ---------- | -------- | ---------------------------------- | | `dev_platform_profiles.repos.full_name` | string | Yes | No | `owner/repo` slug | | `dev_platform_profiles.repos.owner` | string | Yes | No | Owning account login | | `dev_platform_profiles.repos.description` | string | Yes | No | Repository description | | `dev_platform_profiles.repos.primary_language` | string | Yes | No | Primary language of the repository | | `dev_platform_profiles.repos.topics` | array | Yes | No | Topic tags | | `dev_platform_profiles.repos.license_key` | string | Yes | No | License key (for example `mit`) | | `dev_platform_profiles.repos.is_fork` | boolean | Yes | No | Whether the repository is a fork | | `dev_platform_profiles.repos.parent` | string | Yes | No | Parent repository slug (if a fork) | | `dev_platform_profiles.repos.archived` | boolean | Yes | No | Archived flag | | `dev_platform_profiles.repos.disabled` | boolean | Yes | No | Disabled flag | | `dev_platform_profiles.repos.is_template` | boolean | Yes | No | Template repository flag | | `dev_platform_profiles.repos.has_issues` | boolean | Yes | No | Issues enabled | | `dev_platform_profiles.repos.has_projects` | boolean | Yes | No | Projects enabled | | `dev_platform_profiles.repos.has_wiki` | boolean | Yes | No | Wiki enabled | | `dev_platform_profiles.repos.stars` | number | Yes | No | Star count | | `dev_platform_profiles.repos.forks_count` | number | Yes | No | Fork count | | `dev_platform_profiles.repos.open_issues_count` | number | Yes | No | Open issue count | | `dev_platform_profiles.repos.size` | number | Yes | No | Repository size (KB) | | `dev_platform_profiles.repos.github_created_at` | date | Yes | No | Repository creation date | | `dev_platform_profiles.repos.github_updated_at` | date | Yes | No | Last metadata update | | `dev_platform_profiles.repos.github_pushed_at` | date | Yes | No | Last code push | ```json Filter example — active Rust developers { "filters": { "op": "and", "conditions": [ { "field": "dev_platform_profiles.all_languages", "type": "=", "value": "Rust" }, { "field": "dev_platform_profiles.repos.github_pushed_at", "type": "=>", "value": "2025-07-01" } ] }, "sorts": [{ "field": "dev_platform_profiles.followers", "order": "desc" }], "limit": 10 } ``` ### Other | Field | Type | Filterable | Sortable | Description | | ------------------------- | ------- | ---------- | -------- | ----------------------------------------- | | `recently_changed_jobs` | boolean | Yes | Yes | True when a past role **ended** within 90 days of the last index rebuild. It marks a role ending, not a new role starting. To find people who recently started somewhere, filter `current.start_date` | | `years_of_experience_raw` | number | Yes | Yes | Total years of experience (precise) | | `years_of_experience` | number | Yes | No | Total years of experience (rounded) | ### Open-to signal values `professional_network.open_to_cards` is a closed enum of exactly three code strings. Filter with the `in` operator using one or more of these values: | Code | Meaning | | ----------------- | ------------------------------------------- | | `CAREER_INTEREST` | Profile is open to new career opportunities | | `HIRING_MANAGER` | Profile is actively hiring | | `VOLUNTEERING` | Profile is open to volunteer work | Only the three uppercase code strings above are indexed. Human-readable strings like `"open_to_work"` or `"Open to Work"` return **zero results**. Always use the codes as-is. ```json Filter example { "filters": { "op": "and", "conditions": [ { "field": "professional_network.open_to_cards", "type": "in", "value": ["CAREER_INTEREST"] } ] }, "limit": 5 } ``` ## Response fields Each profile in the response can include these sections, depending on `fields`. This table summarizes returned sections only. It is not a complete filter reference. | Section | Key fields | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------- | | `basic_profile` | `name`, `headline`, `current_title`, `normalized_title`, `professional_network_name`, `location` | Identity and location | | `experience` | `employment_details.current`, `employment_details.past` (each role includes `company_profile_picture_permalink`) | Full work history | | `education` | `schools` (each includes `school`, `degree`, `location`, `description`, `institute_logo_permalink`) | Education background | | `contact` | `has_business_email` | Contact availability flag | | `social_handles` | `professional_network_identifier.profile_url`, `dev_platform_identifier.profile_url`, `twitter_identifier.slug` | Available profile handles | **`skills` and `dev_platform_profiles` are not returned by Person Search.** Search returns a lightweight subset, so these sections are absent from the response for every account regardless of `fields` — they are not gated, just not part of search. Fetch skills with [Person Enrich](/person-docs/enrichment/introduction) and GitHub data with [Dev Platform Enrich](/person-docs/dev_platform/introduction). Both `skills.professional_network_skills` and the `dev_platform_profiles.*` GitHub fields still work as filters (see [Filterable but not returned](#filterable-but-not-returned)). ### Filterable but not returned Some fields can be used in `filters` to narrow results but are **not returned** in the search response — search is lightweight discovery, not full enrichment. Filter on them, then fetch their values with [Person Enrich](/person-docs/enrichment/introduction). `skills.professional_network_skills` is the most common example — filterable here, but only returned by Enrich. | Field | Filterable | Returned | | --------------------------------------------------------- | ---------- | -------- | | `basic_profile.summary` | Yes | No | | `certifications.*` (name, issuer, dates, credential) | Yes | No | | `honors.title` | Yes | No | | `skills.professional_network_skills` | Yes | No | | `dev_platform_profiles.*` (GitHub account, rollups, repos) | Yes | No | | `recently_changed_jobs` | Yes | No | The reverse also holds for a few fields: the person's profile URL (`social_handles.professional_network_identifier.profile_url`) is **returned but not filterable**. To find people at a company, filter on the employer's company URL (`experience.employment_details.current.company_professional_network_profile_url`) instead. --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | ----------------- | -------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `filters` | object | Yes | — | Filter condition or condition group. See [operators](#filter-operator-reference) above. | | `fields` | string[] | No | Default set | Dot-path fields or section groups to return (e.g., `["basic_profile.name", "experience.employment_details.current.title"]`). When omitted, the default profile sections are returned. `skills` and `dev_platform_profiles` are **not returned by search** for any account — fetch skills via [Person Enrich](/person-docs/enrichment/introduction). | | `sorts` | array | No | `[]` | Sort specifications as an array of `{ field, order }` objects. Use `asc` or `desc` for `order`. Required for stable pagination. | | `limit` | integer | No | 20 | Max profiles per page (1–1000). | | `count` | integer | No | — | Alias for `limit`. | | `cursor` | string | No | `null` | Pagination cursor from previous response's `next_cursor`. | | `post_processing` | object | No | — | `exclude_profiles` (URL array) and `exclude_names` (name array). | | `preview` | boolean | No | `false` | Premium feature — see [Preview mode](#preview-mode). | | `return_query` | boolean | No | `false` | Debug flag accepted by the API. The response does not include a top-level `query` field. | ## Preview mode Preview search is a premium feature. Book a demo to enable it for your account. If preview access is enabled for your account, use `preview: true` to get lightweight results before running a full search. Preview responses keep the same top-level shape but may return fewer profile fields. If preview is not enabled for your account, the API returns `400 invalid_request` with the message `error: PersonDB preview feature is not available for your account.` ```bash Preview search curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.title", "type": "(.)", "value": "Founder" }, "preview": true, "limit": 1 }' ``` --- ## Errors | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------------------- | | `400` | Invalid request — unsupported field, wrong operator, malformed filters, or preview not enabled for your account. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `500` | Internal server error. Retry with exponential backoff. | ### No results When no people match the filters, the API returns `200` with an empty `profiles` array: ```json { "profiles": [], "next_cursor": null, "total_count": 0 } ``` **Action:** Broaden filters or check field values with [Autocomplete](/person-docs/autocomplete/introduction). **Filter sizing.** To match many values of a field, put them all in a single `in` condition rather than many separate conditions — `in` compiles to one efficient query. A single `in` list of up to ~5,000–10,000 values returns in a few seconds; larger lists get progressively slower, and very large lists (roughly 50,000+) are rejected. For bigger sets, split the values into ~5,000–10,000 chunks across multiple requests and merge. The request body is hard-capped at **10 MB** (larger bodies return `413 request_too_large`), though with typical payloads you reach the query-size limit first. --- ## API reference summary | Detail | Value | | -------------- | -------------------------------------------------------------------------------- | | **Endpoint** | `POST /person/search` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Response** | `{ "profiles": [...], "next_cursor": "...", "total_count": N }` | | **Pagination** | Cursor-based. Pass `next_cursor` as `cursor`. Stop when `next_cursor` is `null`. | | **Errors** | `400`, `401`, `403`, `500` | ## Paginate through results When your search matches more profiles than your `limit`, use cursor-based pagination to walk through all pages. **First page:** send your normal search request. ```bash First page curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.company_name", "type": "in", "value": ["Retool"] }, "limit": 100 }' ``` **Next page:** take the `next_cursor` value from the response and pass it in your next request. Keep the same `filters` and `limit`. ```bash Next page curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.company_name", "type": "in", "value": ["Retool"] }, "limit": 100, "cursor": "PASTE_NEXT_CURSOR_VALUE_HERE" }' ``` Continue until `next_cursor` is `null`, which means you have reached the last page. Always include `sorts` when paginating to ensure stable ordering across pages. --- ## Sort results Use the `sorts` parameter to order results by a specific field. This is important for stable pagination. ```bash Sort by connections (descending) curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": { "field": "experience.employment_details.current.title", "type": "=", "value": "CEO" }, "sorts": [{"field": "professional_network.connections", "order": "desc"}], "limit": 5, "fields": ["crustdata_person_id", "basic_profile.name"] }' ``` The 24 sortable fields are: `crustdata_person_id`, `metadata.updated_at`, `basic_profile.name`, `basic_profile.location`, `basic_profile.location.city`, `basic_profile.location.state`, `basic_profile.location.country`, `basic_profile.location.full_location`, `professional_network.connections`, `professional_network.followers`, `experience.employment_details.start_date`, `experience.employment_details.company_id`, `experience.employment_details.company_headcount_latest`, `experience.employment_details.years_at_company_raw`, `recently_changed_jobs`, `years_of_experience_raw`, `dev_platform_profiles.followers`, `dev_platform_profiles.following`, `dev_platform_profiles.metadata.created_at`, `dev_platform_profiles.public_repo_count`, `dev_platform_profiles.total_repos`, `dev_platform_profiles.total_stars`, `dev_platform_profiles.max_stars`, and `dev_platform_profiles.median_stars`. Sorting on any other field returns `400`. Profiles without a linked GitHub account sort **last** on the `dev_platform_profiles.*` sort fields, for both `asc` and `desc`. The **Sortable** column in [Searchable fields](#searchable-fields) marks these per field, alongside whether each field is **Filterable**. See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema # Person Autocomplete Source: https://docs.crustdata.com/person-docs/autocomplete/introduction Discover valid field values for Person Search filters using the autocomplete API. **Use this when** you need to discover exact indexed values before building a Person Search query — for filter dropdowns, input validation, dataset exploration, or guided query builders. The Person Autocomplete API returns ranked field-value suggestions so you can feed the result straight into a [Person Search](/person-docs/search/introduction) filter without guessing at valid values. This endpoint returns **field values, not person profiles**. It also has **no pagination or cursor** — the `limit` parameter (max `100`) is the only way to control result size. For a full list of distinct values you need to combine Autocomplete with progressively narrower `filters` scopes. To fetch person records, use [Person Search](/person-docs/search/introduction) or [Person Enrich](/person-docs/enrichment/introduction) instead. **Mental model.** The top-level **`field`** is the field whose values you want suggested — the autocomplete target. **`filters.field`** is different: it's any Person Search filter field you use to narrow the population that autocomplete runs over. Autocomplete targets come from a fixed allowlist; filter fields come from the broader Person Search filter vocabulary. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### At a glance | Detail | Value | | --------------- | -------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/person/search/autocomplete` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header (required) | | **Required** | `field` (string) · `query` (string, may be empty) | | **Optional** | `limit` (integer, 1–100, default 20) · `filters` (single condition or condition group) | | **Response** | `{ "suggestions": [ { "value": string } ] }` | | **Errors** | `400` invalid request · `401` unauthorized · `500` internal | | **Pricing** | Free — see [Pricing](/general/pricing#person-apis). | Default `rate-limit` is 45 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. Most-common values, narrow with filters, multi-value, numeric comparisons, Autocomplete → Search workflow. Contract, operators, request parameters, autocomplete-enabled fields, implementation tips, and errors. --- ## When to use Autocomplete vs Search | You want to… | Use | | ------------------------------------------------------ | ---------------------------------------- | | Discover valid filter values for a field | **Autocomplete** (this page) | | See the distinct values a field takes, ranked by count | **Autocomplete** with an empty `query` | | Return actual person profiles matching filters | [**Person Search**](/person-docs/search/introduction) | | Build a type-ahead dropdown for a filter UI | **Autocomplete** with a partial `query` | | Build a live multi-field query that returns full rows | [**Person Search**](/person-docs/search/introduction) | --- ## Quick start: discover job title values Type-ahead lookup on a single field. Pass the user's partial input as `query` and cap the dropdown size with `limit`. ```bash curl curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "VP", "limit": 5 }' ``` ```python Python import os import requests response = requests.post( "https://api.crustdata.com/person/search/autocomplete", headers={ "Authorization": f"Bearer {os.environ['CRUSTDATA_API_KEY']}", "Content-Type": "application/json", "x-api-version": "2025-11-01", }, json={ "field": "experience.employment_details.current.title", "query": "VP", "limit": 5, }, ) response.raise_for_status() suggestions = response.json()["suggestions"] ``` ```javascript Node.js const response = await fetch( "https://api.crustdata.com/person/search/autocomplete", { method: "POST", headers: { Authorization: `Bearer ${process.env.CRUSTDATA_API_KEY}`, "Content-Type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify({ field: "experience.employment_details.current.title", query: "VP", limit: 5, }), }, ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { suggestions } = await response.json(); ``` ```json Response { "suggestions": [ { "value": "VP" }, { "value": "VP of Sales" }, { "value": "vp" }, { "value": "VP Sales" }, { "value": "VP Operations" } ] } ``` Each suggestion includes: - **`value`** — the exact **indexed value**: the raw string stored against `field` in Crustdata's person index. Use it verbatim as a Person Search filter value — two distinct indexed values (for example `"VP"` and `"vp"`) are different filter keys, and substituting one for the other will return different results. Suggestions are returned ranked by internal frequency within the (optionally filtered) population. The ranking signal itself is not returned in the response. When no values match the `query` (or no values exist within the `filters` scope), the response returns an empty array — not a 404: ```json No results { "suggestions": [] } ``` --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request. For operators, request parameters, autocomplete-enabled fields, implementation tips, and errors, see the [Reference](#reference) below. Pass an empty `query` to retrieve the top values for the field by frequency. Useful for seeding filter dropdowns or showing popular options. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "", "limit": 5 }' ``` ```json Response { "suggestions": [ { "value": "Owner" }, { "value": "Manager" }, { "value": "Project Manager" }, { "value": "Intern" }, { "value": "Software Engineer" } ] } ``` Empty-query autocomplete can occasionally return blank string values when a field has many empty indexed records. Filter those out in your UI if you do not want a blank option. Some fields (for example, `open_to_cards` and `employment_type`) have a **fixed, known value space**. An empty `query` returns the full set in declaration order — useful for populating a static dropdown or chip group without an extra round-trip. `open_to_cards` is a closed enum of exactly three codes. The alias `professional_network.open_to_cards` returns the same suggestions and is the spelling used by the `/person/search` filter. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "open_to_cards", "query": "", "limit": 10 }' ``` ```json Response { "suggestions": [ { "value": "CAREER_INTEREST" }, { "value": "HIRING_MANAGER" }, { "value": "VOLUNTEERING" } ] } ``` Feed any returned `value` back into a `professional_network.open_to_cards` filter on `/person/search` to find people who have surfaced that open-to signal on their profile. `employment_type` returns canonical employment labels by frequency. Use any returned `value` as an exact-match filter on the corresponding employment field — flat (`experience.employment_details.employment_type`), current-only (`experience.employment_details.current.employment_type`), or past-only (`experience.employment_details.past.employment_type`). ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "employment_type", "query": "", "limit": 10 }' ``` ```json Response { "suggestions": [ { "value": "Full-time" }, { "value": "Part-time" }, { "value": "Internship" } ] } ``` Scope the autocomplete to a subset of the dataset with the optional `filters` field. The suggestions are then computed against the filtered population. `filters` accepts either a single `AutocompleteFilterCondition` or a nested `AutocompleteFilterConditionGroup` combined with `and`/`or` logic. Use a single `AutocompleteFilterCondition` to filter on one field — for example, top "VP" titles among current Google employees. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "VP", "limit": 5, "filters": { "field": "experience.employment_details.current.company_name", "type": "=", "value": "Google" } }' ``` ```json Response { "suggestions": [ { "value": "VP" }, { "value": "VP of Engineering" }, { "value": "VP Engineering" }, { "value": "VP Sales" }, { "value": "vp" } ] } ``` Use an `AutocompleteFilterConditionGroup` to combine multiple conditions with `op: "and"` or `op: "or"` — for example, top titles matching `"engineer"` at Google in the United States. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "engineer", "limit": 5, "filters": { "op": "and", "conditions": [ { "field": "experience.employment_details.current.company_name", "type": "=", "value": "Google" }, { "field": "basic_profile.location.country", "type": "=", "value": "United States" } ] } }' ``` ```json Response { "suggestions": [ { "value": "Software Engineer" }, { "value": "Senior Software Engineer" }, { "value": "Staff Software Engineer" }, { "value": "Senior Staff Software Engineer" }, { "value": "Software Engineer III" } ] } ``` Groups can be nested — pass another `AutocompleteFilterConditionGroup` inside `conditions` to express arbitrarily complex boolean expressions. Use `in` or `not_in` to match any value in a list. Pass a JSON **array** for `value` — a comma-separated string will return a 400. Top engineer titles across the United States, United Kingdom, and Canada: ```bash Request (in) curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "Engineer", "limit": 5, "filters": { "field": "basic_profile.location.country", "type": "in", "value": ["United States", "United Kingdom", "Canada"] } }' ``` ```json Response { "suggestions": [ { "value": "Software Engineer" }, { "value": "Senior Software Engineer" }, { "value": "Engineer" }, { "value": "Project Engineer" }, { "value": "Mechanical Engineer" } ] } ``` Top seniority levels **excluding** the United States and United Kingdom: ```bash Request (not_in) curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.seniority_level", "query": "", "limit": 5, "filters": { "field": "basic_profile.location.country", "type": "not_in", "value": ["United States", "United Kingdom"] } }' ``` ```json Response { "suggestions": [ { "value": "Entry Level" }, { "value": "Entry Level Manager" }, { "value": "Senior" }, { "value": "Director" }, { "value": "Owner / Partner" } ] } ``` Pass numeric values as JSON numbers. Filter `value` accepts string, number, integer, or boolean scalars; for `in` and `not_in`, use arrays of strings, numbers, or integers that match the underlying field's type. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "experience.employment_details.current.title", "query": "", "limit": 3, "filters": { "field": "experience.employment_details.current.company_headcount_latest", "type": ">", "value": 10000 } }' ``` ```json Response { "suggestions": [ { "value": "Owner" }, { "value": "Manager" }, { "value": "Project Manager" } ] } ``` This is the canonical end-to-end Person API workflow. Autocomplete discovers the exact title value the Search API expects, Search finds matching people, and Enrich fills in the details. ### Step 1: Discover valid title values ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"field": "experience.employment_details.current.title", "query": "VP Sales", "limit": 3}' ``` ```json Response { "suggestions": [ { "value": "VP Sales" }, { "value": "VP of Sales" }, { "value": "VP, Sales" } ] } ``` **Extract:** Take `suggestions[0].value` → `"VP Sales"`. Use this exact string in your Search filter. **If empty:** Try a broader query (for example, `"VP"` instead of `"VP Sales"`). ### Step 2: Search for matching people ```bash Request curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.title", "type": "in", "value": ["VP Sales", "VP of Sales"]}, {"field": "experience.employment_details.current.company_headcount_range", "type": "in", "value": ["51-200", "201-500"]} ] }, "limit": 3, "fields": ["basic_profile.name", "experience.employment_details.current.title", "experience.employment_details.current.company_name", "social_handles.professional_network_identifier.profile_url"] }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Jane Smith" }, "experience": { "employment_details": { "current": [{ "title": "VP of Sales", "name": "Acme Corp" }] } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/janesmith" } } } ], "next_cursor": "H4sIACIdzWkC...", "total_count": 15420 } ``` **Extract:** Take `social_handles.professional_network_identifier.profile_url` → `"https://www.linkedin.com/in/janesmith"`. Pass the profile URL to Enrich. **If empty:** Broaden filters or verify values with an earlier autocomplete call. ### Step 3: Enrich the top match ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"professional_network_profile_urls": ["https://www.linkedin.com/in/janesmith"]}' ``` **Result:** Full person profile with employment history, education, skills, and more. See [Person Enrich](/person-docs/enrichment/introduction) for the full response shape. See [Person Search](/person-docs/search/introduction) for the full filter grammar. --- ## Reference Reference material for Person Autocomplete: contract vs current behavior, supported filter operators, request parameters, autocomplete-enabled fields, implementation tips for UI builders, and errors. For walk-through examples, see the [Examples](#examples) above. ### Guaranteed contract vs current behavior Use this table to separate the parts you can build against with confidence from the observed behavior that may evolve. | Topic | Kind | What it means | | ----------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint, HTTP method, auth headers | **Contract** | `POST /person/search/autocomplete`, bearer auth, `x-api-version: 2025-11-01`. | | Request body shape | **Contract** | `field` and `query` required, `limit` and `filters` optional, `filters` uses conditions / condition groups with the documented operators. | | Response body shape | **Contract** | `{ "suggestions": [ { "value" } ] }`. Empty results return `{"suggestions": []}` with status `200`. | | Supported operators | **Contract** | `=`, `!=`, `<`, `=<`, `>`, `=>`, `in`, `not_in`, `contains` — see [Supported operators](#supported-operators). | | `limit` bounds and default | **Contract** | Minimum `1`, maximum `100`, default `20`. | | Error status codes | **Contract** | `400` (invalid request), `401` (unauthorized), `500` (internal). | | Suggestion ranking | Current behavior | Suggestions are ranked by internal frequency within the (optionally filtered) population — the ranking signal is not exposed in the response. | | Case-insensitive query matching | Current behavior | `"vp"` and `"VP"` currently return the same suggestions. | | Multi-token query loose matching | Current behavior | A multi-word `query` may return values containing only one of the tokens. | | Blank-string suggestions | Current behavior | Empty-`query` calls can return `""` as the top suggestion when a field has many empty indexed records. | ### Supported operators The `type` field on every `AutocompleteFilterCondition` accepts the same operators as Person Search filters. The **greater-than-or-equal** operator is `=>` (not `>=`) and **less-than-or-equal** is `=<` (not `<=`). This is intentional — do not mistype them as the more common `<=` and `>=`. | Operator | `value` shape | Meaning | | ---------- | ------------------------------------------ | -------------------------------------------------------------------------- | | `=` | string, number, integer, or boolean | Exact match — the field value equals `value`. | | `!=` | string, number, integer, or boolean | Not equal — the field value differs from `value`. | | `<` | number or ISO date string | Less than — numeric or date comparison. | | `=<` | number or ISO date string | Less than or equal — numeric or date comparison. | | `>` | number or ISO date string | Greater than — numeric or date comparison. | | `=>` | number or ISO date string | Greater than or equal — numeric or date comparison. | | `in` | **array** of strings, numbers, or integers | Membership — field value matches any entry in the array. | | `not_in` | **array** of strings, numbers, or integers | Negated membership — field value matches none of the entries in the array. | | `contains` | string | Substring match — field value contains `value`. | This operator set applies to **this autocomplete endpoint's `filters` parameter only**. `POST /person/search` uses a different set — it rejects `contains`; use `(.)` (all-words), `[.]` (exact phrase), `(!)` (negation), or `has_all` there. See the [Person Search operator reference](/person-docs/search/reference#filter-operator-reference). Pass values with the JSON type that matches the underlying field: `"value": 10000` for numeric fields, `"value": true` for booleans, and strings for text fields. `in` and `not_in` require a JSON array — a comma-separated string will return a 400. ### Request parameters **Operator footguns.** Use `=>` for greater-than-or-equal and `=<` for less-than-or-equal — these are **not** `>=` and `<=`. `in` / `not_in` require JSON arrays (not comma-separated strings). | Parameter | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `field` | string | Yes | Dataset field whose values you want suggested. Must be an **autocomplete-enabled field** — see [Autocomplete-enabled fields](#autocomplete-enabled-fields). Unsupported fields return `400`. | | `query` | string | Yes | Partial text to match against indexed values. Pass `""` to retrieve the top values for the field by frequency. | | `limit` | integer | No | Maximum number of suggestions to return. Minimum `1`, maximum `100`, default `20`. | | `filters` | object | No | Optional scope for the autocomplete computation. Pass either a single `AutocompleteFilterCondition` or a nested `AutocompleteFilterConditionGroup`. | #### `AutocompleteFilterCondition` | Field | Type | Required | Description | | ------- | ------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `field` | string | Yes | Dataset field to filter on. Can be any [Person Search](/person-docs/search/introduction) filter field — **this scope is broader than the autocomplete allowlist** for the top-level `field`. | | `type` | string (enum) | Yes | One of [the supported operators](#supported-operators): `=`, `!=`, `<`, `=<`, `>`, `=>`, `in`, `not_in`, `contains`. | | `value` | string, number, integer, boolean, or array | Yes | Scalar for comparison operators; JSON array of strings/numbers/integers for `in` and `not_in`. Match the JSON type to the underlying field's type. | #### `AutocompleteFilterConditionGroup` | Field | Type | Required | Description | | ------------ | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `op` | string (enum) | Yes | `"and"` or `"or"`. | | `conditions` | array | Yes | One or more `AutocompleteFilterCondition` items or nested `AutocompleteFilterConditionGroup` items. Must contain at least one. | ### Autocomplete-enabled fields The top-level `field` in a Person Autocomplete request (the field whose values you want suggested) must come from a **fixed allowlist** of autocomplete-enabled dataset fields. Not every Person Search field is autocomplete-enabled. The `filters.field` is different: it can be any dataset field that [Person Search](/person-docs/search/introduction) accepts as a filter, not only the autocomplete allowlist. So you can filter autocomplete results on a broader set of fields than you can autocomplete on directly. The tables below are a **documented subset** of the autocomplete allowlist. They cover the fields most useful for building filter dropdowns, but they are **not exhaustive**. If a field you need is not listed, treat its support as unknown until you verify it — the safest way to confirm is to call the endpoint with that field and check whether you get a `400` error. See [Verify the live list](#verify-the-live-list) below for the debug-only live source. **`company_name` vs `name` for employers (current behavior).** For autocomplete, use `experience.employment_details.current.name` — the `company_name` variant is rejected in autocomplete scope. For `/person/search` filters, both `current.name` and `current.company_name` are accepted as aliases. In response payloads, the current employer appears under the `name` key. #### Common supported fields | Field | What it discovers | | -------------------------------------------------------------- | ------------------------------- | | `experience.employment_details.current.title` | Current job titles | | `experience.employment_details.current.name` | Current employer names | | `experience.employment_details.current.seniority_level` | Seniority level buckets | | `experience.employment_details.current.function_category` | Job function | | `experience.employment_details.current.company_industries` | Current employer industries | | `experience.employment_details.current.company_type` | Current employer type | | `experience.employment_details.current.company_hq_location` | Current employer HQ location | | `experience.employment_details.current.company_website_domain` | Current employer website domain | | `experience.employment_details.current.employment_type` | Current employment type | | Field | What it discovers | | ----------------------------------------------------------- | ---------------------------- | | `experience.employment_details.past.title` | Past job titles | | `experience.employment_details.past.name` | Past employer names | | `experience.employment_details.past.seniority_level` | Past seniority level buckets | | `experience.employment_details.past.function_category` | Past job function | | `experience.employment_details.past.company_type` | Past employer type | | `experience.employment_details.past.company_hq_location` | Past employer HQ location | | `experience.employment_details.past.company_website_domain` | Past employer website domain | | `experience.employment_details.past.employment_type` | Past employment type | | Field | What it discovers | | ----------------------------------------- | -------------------------- | | `basic_profile.name` | Person names | | `basic_profile.first_name` | Person first names | | `basic_profile.last_name` | Person last names | | `basic_profile.headline` | Profile headlines | | `basic_profile.languages` | Spoken languages | | `basic_profile.location.raw` | Raw location strings | | `basic_profile.location.full_location` | Full location strings | | `basic_profile.location.city` | Cities | | `basic_profile.location.state` | States / regions | | `basic_profile.location.country` | Countries | | `basic_profile.location.continent` | Continents | | `professional_network.location.city` | Profile network cities | | `professional_network.location.state` | Profile network states | | `professional_network.location.country` | Profile network countries | | `professional_network.location.continent` | Profile network continents | | `professional_network.open_to_cards` | Open-to signal codes | | Field | What it discovers | | ------------------------------------- | ---------------------- | | `education.schools.school` | Schools / universities | | `education.schools.degree` | Degrees | | `education.schools.field_of_study` | Fields of study | | `skills.professional_network_skills` | Skills | | `certifications.name` | Certification names | | `certifications.issuing_organization` | Certification issuers | | `honors.title` | Honors and awards | These fields have a **fixed, known value space**. An empty `query` returns the full enum in declaration order. They're useful when you need the exact set of accepted values for a filter dropdown — for example, populating an "open to" picker or an employment-type chip group. | Field | What it discovers | Returned values | | ----------------- | ---------------------- | ---------------------------------------------------------------- | | `open_to_cards` | Open-to signal codes | `CAREER_INTEREST`, `HIRING_MANAGER`, `VOLUNTEERING` | | `employment_type` | Employment type labels | `Full-time`, `Part-time`, `Internship`, and other indexed labels | `professional_network.open_to_cards` is an alias of `open_to_cards` — both return the same suggestions. Use whichever spelling matches the filter path on `/person/search`. #### Verify the live list Call the endpoint with a deliberately invalid `field`. The `400` response lists every currently accepted field in its error `message`: ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "current_title", "query": "", "limit": 1 }' ``` ```json Response (abbreviated) { "error": { "type": "invalid_request", "message": "Field 'current_title' is not supported on scope 'person'. Valid fields: basic_profile.city, basic_profile.continent, basic_profile.country, ..., experience.employment_details.current.function_category, experience.employment_details.current.title, ..., skills.professional_network_skills, state", "metadata": [] } } ``` ### Implementation tips for UI builders - **Debounce** autocomplete calls to avoid one request per keystroke — 150–300 ms on input idle works well for typeahead UIs. - **Drop blank values.** If `""` is returned as the top suggestion, remove it before rendering the dropdown. - **Handle casing variants carefully.** Suggestions like `"VP"` and `"vp"` are **distinct indexed values**. Only merge them into a single UI option if you intentionally want normalized grouping — and if you do, preserve the underlying raw values and expand them in the eventual Person Search filter using `in`, for example `{"type": "in", "value": ["VP", "vp"]}`. If the UI needs exact-value selection, keep them as separate options. - **Cap `limit`.** Most dropdowns need 5–15 options. Lower `limit` reduces payload size and gives faster responses. - **Cache top-values lookups.** Empty-`query` calls that seed filter dropdowns rarely change — cache them client-side or at the edge. ### Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | Invalid request — unsupported `field`, missing required field, a wrong `value` shape (for example a comma string for `in`), or a malformed body. | | `401` | Unauthorized — the `Authorization` header is missing, malformed, or contains an invalid API key. | | `500` | Internal server error — retry after a short delay. | Note: a `query` that matches nothing is **not** an error — the endpoint returns `{"suggestions": []}` with a 200 status. Only use 4xx/5xx handling for actual request or server failures. ```json 400 — Unsupported field { "error": { "type": "invalid_request", "message": "Field 'current_title' is not supported on scope 'person'. Valid fields: basic_profile.city, basic_profile.continent, ..., experience.employment_details.current.function_category, experience.employment_details.current.title, ..., skills.professional_network_skills, state", "metadata": [] } } ``` ```json 400 — Wrong value shape for in operator { "error": { "type": "invalid_request", "message": "Failed to get autocomplete suggestions: IN operator requires list or tuple value, got: ", "metadata": [] } } ``` ```json 401 — Invalid API key { "message": "Invalid API key in request" } ``` ### API reference summary | Detail | Value | | ------------------- | ------------------------------------------------------------------------------ | | **Endpoint** | `POST /person/search/autocomplete` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Required params** | `field`, `query` | | **Optional params** | `limit` (default: 20, max: 100), `filters` | | **Response** | `{ "suggestions": [{ "value": "..." }] }` | | **Empty result** | `200` with `"suggestions": []` | | **Errors** | `400` (unsupported field or bad value), `401` (bad auth), `500` (server error) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Try more patterns** — see [Examples](#examples) for most-common values, filtered suggestions, and the Autocomplete → Search workflow. - **Reference** — see [Reference](#reference) for operators, request parameters, autocomplete-enabled fields, implementation tips, and errors. - **Search for people** — use discovered values in [Person Search](/person-docs/search/introduction). - **Enrich matches** — use [Person Enrich](/person-docs/enrichment/introduction) to get full profiles after you find people. # Filter value enumeration Source: https://docs.crustdata.com/person-docs/filter-values Downloadable lists of the valid values for person search filter fields — skills, titles, regions, and employer industries. Several person-search filter fields accept values from a fixed vocabulary. The [autocomplete API](/person-docs/autocomplete/introduction) is always the current source of truth, but these downloadable snapshots are convenient when you build filters programmatically against a large set. These files are point-in-time snapshots. For always-current values, use the [autocomplete API](/person-docs/autocomplete/introduction). ## Person filter values | Field | Used by | Values | Download | | ------------------------------------ | ----------------------------------------------------------- | ------ | --------------------------------------------------- | | `skills.professional_network_skills` | [Person Search](/person-docs/search/introduction) | 40,098 | [skills.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/people-search/skills.json) | | `CURRENT_TITLE` | [Person Live Search](/person-docs/search/live-search) | 25,204 | [titles.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/people-search/linkedin_titles.json) | | `REGION` | [Person Live Search](/person-docs/search/live-search) | 58,221 | [regions.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/people-search/static-region-filter-values.json) | | Employer industry | both (`INDUSTRY` live / `company_industries` indexed) | 433 | [industries.json](https://s3.us-east-2.amazonaws.com/fulldocs.crustdata.com/examples/people-search/static-linkedin-industries.json) | **Indexed vs live.** `skills` is filterable on the indexed [Person Search](/person-docs/search/introduction). `CURRENT_TITLE` and `REGION` are filter fields on the real-time [Person Live Search](/person-docs/search/live-search) (the uppercase, real-time filter fields). On indexed Person Search, titles are matched as **text** with the `(.)` operator rather than chosen from this list. **Industries** are shared with company search — the same 433-value taxonomy applies to the employer-industry filters on both sides. ## What to do next - **Discover values interactively** — [Person Autocomplete](/person-docs/autocomplete/introduction) - **Build a search** — [Person Search](/person-docs/search/introduction) - **Company values** — [Company filter values](/company-docs/filter-values) # Person Batch Search Source: https://docs.crustdata.com/person-docs/search/batch-search Run one person search query asynchronously and receive the whole result set as a single file — from the indexed database (up to 10,000 records) or retrieved from the web in real time (up to 1,000 records). Batch search runs one query asynchronously and delivers the **entire result set** as a single file. Where [Person Search](/person-docs/search/introduction) returns one cursor page per call, a batch job walks every page for you. This page covers two batch endpoints: - [**Batch search**](#batch-search) — results served from the indexed database, with higher caps and faster turnaround. - [**Batch live search**](#batch-live-search) — results retrieved from the web in real time, fresher but with lower caps and slower. Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ## Batch search ``` POST https://api.crustdata.com/batch/person/search ``` Batch search runs one query asynchronously and delivers the **entire result set** as a single file. Where [Person Search](/person-docs/search/introduction) returns one cursor page per call, a batch job walks every page for you. For fresher results retrieved from the web in real time, see [Batch live search](#batch-live-search) below. ### How batch search differs from non-batch search - **One query, whole result set.** You submit a single query; the job paginates server-side until `max_results` is reached or the matches run out. - **`max_results` is the only volume control.** It clamps to **10,000** and defaults to that cap when omitted. Values above the cap are silently clamped; zero or negative values return `400`. The non-batch paging knobs — `limit`, `page`, `preview` — are silently ignored. - **Flat records.** Each line in the results file has exactly the non-batch search record shape. No envelope. - **Exact field projection.** When you pass `fields`, each record contains exactly those fields — nothing more. Omit `fields` to get every field your account can read. ### Database batch search `POST /batch/person/search` takes the same filter fields and operators as [Person Search](/person-docs/search/reference), with one extra rule: the top level of `filters` **must** be an `{op, conditions}` group — a bare `{field, type, value}` condition is rejected with `400`. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "engineer"} ] }, "max_results": 2, "fields": ["basic_profile.name", "basic_profile.current_title"] }' ``` ```json Response { "batch_id": "a9afc353-8876-4e64-8b84-02a599561021", "status": "pending", "entity": "person", "action": "search", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/a9afc353-8876-4e64-8b84-02a599561021" } ``` Search jobs always report `identifier_count: 1` — the one query. When the job completes, the downloaded file contains flat records with exactly the requested fields: ```json The results file (all records) {"basic_profile": {"current_title": "Principal Software Engineer", "name": "Man Sum Simon Yuen"}} {"basic_profile": {"current_title": "Knowledge Systems Architect · Context Engineer", "name": "Emily Anderson"}} ``` #### Nest groups for or-logic Groups nest inside `conditions`. This finds engineers in either the United States or Canada: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "engineer"}, { "op": "or", "conditions": [ {"field": "basic_profile.location.country", "type": "=", "value": "United States"}, {"field": "basic_profile.location.country", "type": "=", "value": "Canada"} ] } ] }, "max_results": 2, "fields": ["basic_profile.name", "basic_profile.location.country"] }' ``` ```json The results file (all records) {"basic_profile": {"location": {"country": "United States"}, "name": "Emily Anderson"}} {"basic_profile": {"location": {"country": "United States"}, "name": "Michael Phillips"}} ``` Exact-match values must match the **indexed** value — both `"United States"` and `"United States of America"` exist as `basic_profile.location.country` values. Use an `or` group or [Person Autocomplete](/person-docs/autocomplete/introduction) to discover indexed values first. #### Two warnings before you submit large jobs Unknown filter field names are **not rejected at submit time** the way non-batch search rejects them — a typo in `field` simply produces a job that completes with 0 results. Double-check field names against the [search reference](/person-docs/search/reference) first. Do not pass a non-empty `sorts` array — the job will complete with **0 results**. Sort the downloaded file instead, for example `jq -s 'sort_by(.basic_profile.name) | .[]' results.jsonl`. ### Errors ```json 400 — no filters { "error": { "type": "invalid_request", "message": "`filters` must be provided for search", "metadata": [] } } ``` ```json 400 — bare condition instead of a group { "error": { "type": "invalid_request", "message": "Invalid filter shape: [{'type': 'missing', 'loc': ('op',), 'msg': 'Field required', ...}]", "metadata": [] } } ``` ```json 400 — search URL on a database search { "error": { "type": "invalid_request", "message": "`professional_network_search_url` is only valid for live search", "metadata": [] } } ``` --- ## Batch live search Batch live search is available to accounts with the `live` entitlement. The [Batch search](#batch-search) endpoint above is generally available. ``` POST https://api.crustdata.com/batch/person/professional_network/search/live ``` Batch live search runs one query asynchronously and delivers the **entire result set** as a single file, with results retrieved from the web in real time. Where [Person Live Search](/person-docs/search/live-search) returns one cursor page per call, a batch job walks every page for you. For results served from the indexed database — higher caps and faster — see [Batch search](#batch-search) above. ### How batch live search differs from non-batch search - **One query, whole result set.** You submit a single query; the job paginates server-side until `max_results` is reached or the matches run out. - **`max_results` is the only volume control.** It clamps to **1,000** and defaults to that cap when omitted. Values above the cap are silently clamped; zero or negative values return `400`. The non-batch paging knobs — `limit`, `page`, `preview` — are silently ignored. - **Flat records.** Each line in the results file has exactly the non-batch search record shape. No envelope. - **Exact field projection.** When you pass `fields`, each record contains exactly those fields — nothing more. Omit `fields` to get every field your account can read. ### Live batch search `POST /batch/person/professional_network/search/live` retrieves results from the web in real time — fresher than the database, capped at **1,000** records per job, fetched internally in pages of 25, and slower. Live jobs are the most likely to end `failed` with an `error_message`. Live filters use a **flat list** of `{field, type, value}` objects — not the `{op, conditions}` group — and `field` takes an uppercase filter type instead of a dotted path: ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["Software Engineer"]}, {"field": "REGION", "type": "in", "value": ["United States"]} ], "max_results": 2, "fields": ["basic_profile.name", "basic_profile.headline"] }' ``` ```json Response { "batch_id": "65de822c-f7d3-4b4c-af89-ef683704889e", "status": "pending", "entity": "person", "action": "search_live", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/65de822c-f7d3-4b4c-af89-ef683704889e" } ``` ```json The results file (all records) {"basic_profile": {"headline": "Full-Stack Software Developer | Spring Boot | Angular | MySQL | Java | TypeScript | US Citizen", "name": "Sael Torres - Diaz"}} {"basic_profile": {"headline": "Software Engineer - Frontend / Backend / TypeScript / JavaScript / React / Nest.js / Angular / Spring. Authorized to work in the USA without a current/future sponsorship.", "name": "Daniel Ignatyev"}} ``` Valid filter types: `CURRENT_COMPANY`, `PAST_COMPANY`, `CURRENT_TITLE`, `PAST_TITLE`, `SCHOOL`, `COMPANY_HEADQUARTERS`, `COMPANY_HEADCOUNT`, `COMPANY_TYPE`, `FUNCTION`, `INDUSTRY`, `NUM_OF_FOLLOWERS`, `REGION`, `SENIORITY_LEVEL`, `YEARS_AT_CURRENT_COMPANY`, `YEARS_IN_CURRENT_POSITION`, `YEARS_OF_EXPERIENCE`, `FOLLOWER_OF`, `CONNECTION_OF`, `KEYWORD_COMPANY`, `KEYWORD_FIRST_NAME`, `KEYWORD_LAST_NAME`, `KEYWORD_SCHOOL`, `KEYWORD_TITLE`, `NETWORK_DEGREE`, `PROFILE_LANGUAGE`, `SERVICE_CATEGORY`, `CONTACT_INTEREST` — the same filters as the non-batch [Person Live Search](/person-docs/search/live-search), which documents per-filter value formats. An unknown filter type returns `400` listing the valid values. Instead of `filters`, you can replay a saved people search URL: ```json Request body { "professional_network_search_url": "https://www.linkedin.com/sales/search/people?query=...", "max_results": 100 } ``` Provide either `filters` or `professional_network_search_url` — omitting both returns `400`. ### Errors ```json 400 — neither filters nor a search URL { "error": { "type": "invalid_request", "message": "Either `filters` or `professional_network_search_url` must be provided for live search", "metadata": [] } } ``` ```json 400 — group instead of a flat list { "error": { "type": "invalid_request", "message": "`filters` for live search must be a list of {field, type, value} objects", "metadata": [] } } ``` --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## What to do next - **Build queries interactively first** — iterate with [Person Search](/person-docs/search/introduction) or [Person Live Search](/person-docs/search/live-search) page by page, then submit the final query as a batch job. - **Look up operators and fields** — see the [search reference](/person-docs/search/reference). - **Enrich the people you found** — see [Batch Person Enrich](/person-docs/enrichment/batch-enrich) or [Batch Person Live Enrich](/person-docs/enrichment/batch-enrich). - **Search companies in batch** — see [Batch Company Search](/company-docs/search/batch-search) or [Batch Company Live Search](/company-docs/search/batch-search). - **Search jobs in batch** — see [Batch Job Search](/job-docs/search/batch-search). - **Full schema** — see the [API reference](/openapi-specs/2025-11-01/introduction). # Person Realtime Search Source: https://docs.crustdata.com/person-docs/search/live-search Search people in real time using structured live-search filters. **Use this when** you need live people results from the web — for prospecting, hiring signal detection, or finding people that are not yet in the indexed database. The Person Realtime Search API lets you search people in real time from the web. Unlike standard [Person Search](/person-docs/search/introduction), which queries Crustdata's indexed database, this endpoint runs a live search using structured filters. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/person/professional_network/search/live ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Description | | --------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `filters` | array | Yes\* | Array of filter objects. Each has `field`, `type`, and `value`. | | `page` | integer | Yes\* | Page number for synchronous filter-based search. | | `background_job` | boolean | No | Set to `true` to submit an async background job. | | `job_id` | string | No | Retrieve results for a previously submitted background job. | | `limit` | integer | No | Max profiles to fetch for background-job submissions. | | `preview` | boolean | No | Return lightweight realtime profiles. Cannot be combined with `page`, `limit`, `background_job`, or `post_processing`. | | `post_processing` | object | No | Exclusion rules such as `exclude_profiles` and `exclude_names`. | \*For synchronous requests, provide `filters` and `page`. For preview requests, provide `filters` and `preview`. For async job retrieval, provide `job_id`. ### Response body | Field | Type | Description | | --------------------- | ------ | ------------------------------------------------------------------ | | `profiles` | array | Person profiles matching the search. | | `total_display_count` | string | Total matching people (for example, `"2,500+"`). | | `job_id` | string | Background job UUID when async processing is used or polled. | | `message` | string | Background job status message for submitted or still-running jobs. | ### Rate limits and credits Current pricing for this live endpoint: Pricing: 2 credits per profile returned. Availability: Enterprise-only. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. --- ## Search vs Realtime Search | | **Search** | **Realtime Search** | | ---------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | **Data source** | Crustdata indexed database | Live search from the web | | **Filter syntax** | `{ "field": "dotpath", "type": "op", "value": ... }` | `[{ "field": "ENUM_TYPE", "type": "in", "value": [...] }]` | | **Filter field names** | Dot-path fields like `experience.employment_details.current.title` | Enums like `CURRENT_TITLE`, `INDUSTRY`, `REGION` | | **Pagination** | Cursor-based (`cursor` + `next_cursor`) | Page-based (`page`) or background jobs (`background_job`) | | **Field selection** | `fields` parameter to choose response fields | Not supported — returns the live profile payload | | **Best for** | Structured segmentation, repeatable list building | Live prospecting and fresh people discovery | Use [Person Search](/person-docs/search/introduction) when you need precise structured filters, field selection, and cursor-based pagination over indexed data. Use **Realtime Search** when you want live results from the web. --- ## Discover valid live filter values Live search uses a different value vocabulary than indexed search. The dedicated **[Live Search Autocomplete](/person-docs/search/live-autocomplete)** endpoint (`POST /professional_network/search/autocomplete`) returns valid values for the open-ended fields — `industry`, `region`, `title`, and `school`. Use the returned values exactly as-is in your live-search filter `value` arrays. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "title", "query": "technical account", "limit": 5 }' ``` See **[Live Search Autocomplete](/person-docs/search/live-autocomplete)** for all four fields, the field-to-filter mapping, and worked examples. Fields that take a fixed value set (like `SENIORITY_LEVEL` and `FUNCTION`) are not autocompleted — see [Filter fields with fixed values](#filter-fields-with-fixed-values). `/professional_network/search/autocomplete` can be permission-gated. If you receive `403 permission_error`, ask support to enable access for your account. --- ## Search with structured filters Use the `filters` array to define search criteria programmatically. Each filter has a `field`, `type` (operator), and `value`. ### Find CEOs at small companies ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["CEO"]}, {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["11-50"]} ], "page": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Ivan Barnola Gasch", "headline": "Founder & CEO - Oblivion", "current_title": "Founder & Main Director" }, "professional_network": { "connections": 9611, "location": { "raw": "Barcelona, Catalonia, Spain" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/ivanbg2004" } }, "skills": { "professional_network_skills": [ "Red Teaming", "AI & LLM Automation", "Cloud Architecture" ] }, "experience": { "employment_details": { "current": [ { "title": "Founder & Main Director", "name": "Oblivion" } ] } } } ], "total_display_count": "2,500+" } ``` Response trimmed for clarity. Each profile can include basic info, professional-network data, skills, and employment history. ### Understanding the response Every realtime search response has two fields: - **`profiles`** — an array of person profiles matching your search. - **`total_display_count`** — the total number of matching people across all pages (as a string, for example `"2,500+"`). This is approximate rather than exact for large result sets. ### How to interpret results - **`total_display_count` is a string:** Values like `"2,500+"` are approximate indicators, not exact counts. - **Empty `profiles` array:** No more results for this page. Stop paginating. - **Filter values are strict:** Use the shared autocomplete endpoint to discover valid enumerated values before building filters. --- ## More filter examples ### Search for sales leaders in the United States ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["VP of Sales", "Head of Sales"]}, {"field": "INDUSTRY", "type": "in", "value": ["Software Development"]}, {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200", "201-500"]}, {"field": "REGION", "type": "in", "value": ["United States"]} ], "page": 1 }' ``` ### Search for engineering candidates by school and region ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "SCHOOL", "type": "in", "value": ["Stanford University"]}, {"field": "CURRENT_TITLE", "type": "in", "value": ["Software Engineer", "Senior Software Engineer"]}, {"field": "REGION", "type": "in", "value": ["United States"]} ], "page": 1 }' ``` --- ## Preview mode Use `preview: true` when you want a lightweight realtime result set before running a fuller live search. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["CEO"]} ], "preview": true }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Justinas Kairys", "current_title": "Founder & CEO", "headline": null }, "professional_network": { "location": { "raw": "Lowestoft, England, United Kingdom" }, "connections": null } } ], "total_display_count": "6M+" } ``` Preview responses keep the same top-level envelope as standard realtime search, but per-profile payloads are much sparser and often contain empty arrays, null values, or blank profile URLs. --- ## Background job mode For larger result sets, use `background_job: true`. The API returns `200` with a `job_id` and `message`. ```bash Submit job curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["CEO"]}, {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"]} ], "background_job": true, "limit": 2000 }' ``` ```json 200 Response { "job_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Your request is being processed. Please use the job_id to retrieve the results." } ``` ```bash Retrieve results curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "job_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` Polling with `job_id` can also return `200` with `job_id` and `message` while the job is still running. Keep polling until the response includes `profiles`. --- ## Paginate through results Use the `page` parameter to walk through synchronous results. **First page:** set `page` to `1`. **Next page:** increment `page` by 1 and repeat the same request with the same filters. ```bash Page 2 curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["CEO"]}, {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["11-50"]} ], "page": 2 }' ``` Continue incrementing `page` until the `profiles` array comes back empty. --- ## Filter object schema Each filter in the `filters` array must have this shape: | Key | Type | Required | Description | | ------- | ------------------ | -------- | ------------------------------------------------------------------------- | | `field` | string | Yes | A live-search filter type (see table below). | | `type` | string | Yes | Operator: `"in"` or `"not in"`. | | `value` | string or string[] | Yes | A string or array of strings matching the filter field's expected values. | All filters in the array are combined with implicit AND logic. Nested `or` groups are not supported in Realtime Search — use [indexed Search](/person-docs/search/introduction) for complex boolean queries. ## Available filter fields | Field | Description | Operators | | -------------------------- | ------------------------- | -------------- | | `CURRENT_TITLE` | Current job title | `in`, `not in` | | `PAST_TITLE` | Previous job titles | `in`, `not in` | | `CURRENT_COMPANY` | Current employer | `in`, `not in` | | `PAST_COMPANY` | Previous employers | `in`, `not in` | | `INDUSTRY` | Industry | `in`, `not in` | | `COMPANY_HEADCOUNT` | Company size range | `in`, `not in` | | `COMPANY_TYPE` | Employer company type | `in` | | `COMPANY_HEADQUARTERS` | Company HQ location | `in`, `not in` | | `REGION` | Person's region | `in`, `not in` | | `SENIORITY_LEVEL` | Seniority level | `in`, `not in` | | `FUNCTION` | Job function | `in`, `not in` | | `SCHOOL` | Education institution | `in`, `not in` | | `YEARS_OF_EXPERIENCE` | Years of experience | `in`, `not in` | | `YEARS_AT_CURRENT_COMPANY` | Tenure at current company | `in`, `not in` | | `KEYWORD_TITLE` | Title keyword search | `in`, `not in` | | `KEYWORD_COMPANY` | Company keyword search | `in`, `not in` | | `KEYWORD_FIRST_NAME` | First name search | `in`, `not in` | | `KEYWORD_LAST_NAME` | Last name search | `in`, `not in` | Filter values use the Realtime Search vocabulary, not the indexed Search vocabulary. Use `/professional_network/search/autocomplete` to discover current values for the open-ended fields (`CURRENT_TITLE`, `PAST_TITLE`, `CURRENT_COMPANY`, `PAST_COMPANY`, `COMPANY_HEADQUARTERS`, `REGION`, `INDUSTRY`, `SCHOOL`). The fields below take a **fixed set of values** that autocomplete does not enumerate — use these lists. ### Filter fields with fixed values Values are matched as exact strings, and commas can be part of a single value — a range like `1,001-5,000` is one array element. If your client or shell splits list input on commas, quote or escape these values before building the array. | Field | Accepted values | | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `COMPANY_HEADCOUNT` | `Self-employed`, `1-10`, `11-50`, `51-200`, `201-500`, `501-1,000`, `1,001-5,000`, `5,001-10,000`, `10,001+` | | `SENIORITY_LEVEL` | `Owner / Partner`, `CXO`, `Vice President`, `Director`, `Experienced Manager`, `Entry Level Manager`, `Strategic`, `Senior`, `Entry Level`, `In Training` | | `FUNCTION` | `Accounting`, `Administrative`, `Arts and Design`, `Business Development`, `Community and Social Services`, `Consulting`, `Education`, `Engineering`, `Entrepreneurship`, `Finance`, `Healthcare Services`, `Human Resources`, `Information Technology`, `Legal`, `Marketing`, `Media and Communication`, `Military and Protective Services`, `Operations`, `Product Management`, `Program and Project Management`, `Purchasing`, `Quality Assurance`, `Real Estate`, `Research`, `Sales`, `Customer Success and Support` | | `COMPANY_TYPE` | `Public Company`, `Privately Held`, `Non Profit`, `Educational Institution`, `Partnership`, `Self Employed`, `Self Owned`, `Government Agency` | | `YEARS_OF_EXPERIENCE`, `YEARS_AT_CURRENT_COMPANY` | `Less than 1 year`, `1 to 2 years`, `3 to 5 years`, `6 to 10 years`, `More than 10 years` | ## Supported operators | Operator | Description | Use with | | -------- | -------------------- | ------------------------------------------- | | `in` | Value is in list | Most filter fields | | `not in` | Value is not in list | `REGION`, `INDUSTRY`, `CURRENT_TITLE`, etc. | --- ## Response fields Each person in the response can include these sections: | Section | Key fields | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `updated_at` | `string` (ISO 8601 date-time) | When the live profile was retrieved. Top-level field. | | `basic_profile` | `name`, `headline`, `summary`, `current_title`, `languages` | Core profile identity | | `professional_network` | `connections`, `location.raw`, `profile_picture_url`, `pronoun` | Profile-level network metadata, including pronoun | | `social_handles` | `professional_network_identifier.profile_url`, `professional_network_identifier.urn`, `twitter_identifier.slug` | Canonical profile URL, network URN, and Twitter handle | | `contact` | `business_emails[].email`, `websites[]` | Contact details when surfaced from the live profile | | `skills` | `professional_network_skills` | Listed skills | | `experience` | `employment_details.current`, `employment_details.past` | Work history | | `education` | `schools` | Education background when available | `updated_at`, `professional_network.pronoun`, `social_handles.professional_network_identifier.urn`, `social_handles.twitter_identifier.slug`, and the `contact` block (with `business_emails` and `websites`) are nullable — they appear when the live profile surfaces them, and may be `null` or omitted otherwise. --- ## Validation rules | Rule | Behavior | | ---------------- | ---------------------------------------------------------------------------- | | `filters` | Provide one or more live-search filters for sync or async search submission. | | `page` | Required for synchronous filter-based requests. | | `background_job` | Use `true` to submit an async search job. | | `job_id` | Use it to retrieve results for a submitted background job. | | Empty results | Returns `200` with an empty `profiles` array. | ## Errors ```json 400 — Invalid filter key { "error": { "type": "invalid_request", "message": "'filter_type' is not supported. Use 'field' instead.", "metadata": [] } } ``` ```json 401 — Invalid API key { "message": "Invalid API key in request" } ``` ## API reference summary | Detail | Value | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Endpoint** | `POST /person/professional_network/search/live` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Sync response** | `{ profiles, total_display_count }` | | **Async submit** | `200` with `{ job_id, message }` | | **Request** | `filters` plus `page` for sync, `filters` plus `preview` for lightweight results, `filters` plus `background_job` for async submit, or `job_id` for retrieval | | **Errors** | `400`, `401`, `500` | For credit pricing, see [Pricing](/general/pricing). For throughput guidance, see [Rate limits](/general/rate-limits). See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## Examples Worked recipes for `POST /person/professional_network/search/live`. Each is a full, live-tested request you can copy, paste, and adapt. Filter `value` strings are strict — use the [shared autocomplete endpoint](#discover-valid-live-filter-values) to confirm exact values for `INDUSTRY`, `REGION`, `FUNCTION`, `SENIORITY_LEVEL`, and `COMPANY_HEADCOUNT` before building filters. Click any row to expand it. Combine `CURRENT_TITLE` with `COMPANY_HEADCOUNT` to target a seniority band at a company-size range. `COMPANY_HEADCOUNT` values are fixed buckets — note the comma in `501-1,000`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["Chief Financial Officer"]}, {"field": "COMPANY_HEADCOUNT", "type": "in", "value": ["201-500", "501-1,000"]} ], "page": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Simon Russell", "current_title": "Chief Financial Officer", "headline": "Chief Financial Officer at Moore Barlow LLP" }, "professional_network": { "location": { "raw": "Guildford, England, United Kingdom" } }, "experience": { "employment_details": { "current": [ { "name": "Moore Barlow LLP", "title": "Chief Financial Officer", "start_date": "2020-08-01T00:00:00" } ] } } } ], "total_display_count": "71K+" } ``` Response trimmed for clarity. The accepted `COMPANY_HEADCOUNT` buckets are `Self-employed`, `1-10`, `11-50`, `51-200`, `201-500`, `501-1,000`, `1,001-5,000`, `5,001-10,000`, and `10,001+`. `CURRENT_COMPANY` accepts a company profile URL, a website domain, or a plain company name. A domain is the most reliable identifier when a name is ambiguous. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_COMPANY", "type": "in", "value": ["rippling.com"]} ], "page": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Ayush Sharma", "current_title": "VP Sales, India Site Lead", "headline": "VP - Sales, India Site Lead - Rippling" }, "professional_network": { "connections": 10149, "location": { "raw": "Bengaluru, Karnataka, India" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/ayush90" } }, "experience": { "employment_details": { "current": [ { "name": "Rippling", "title": "VP Sales, India Site Lead" } ] } } } ], "total_display_count": "6.5K+" } ``` Response trimmed for clarity. Pass several companies in the `value` array to search across all of them at once. Stack `FUNCTION` and `SENIORITY_LEVEL` on top of `CURRENT_COMPANY` to reach decision-makers in a specific department. This finds VP- and Director-level people in the Sales function at one company. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_COMPANY", "type": "in", "value": ["rippling.com"]}, {"field": "FUNCTION", "type": "in", "value": ["Sales"]}, {"field": "SENIORITY_LEVEL", "type": "in", "value": ["Vice President", "Director"]} ], "page": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Paul Barber", "current_title": "VP of Sales", "headline": "Growth and innovation at Rippling." }, "professional_network": { "connections": 3011, "location": { "raw": "Phoenix, Arizona, United States" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/pkbarber" } }, "experience": { "employment_details": { "current": [ { "name": "Rippling", "title": "VP of Sales" } ] } } } ], "total_display_count": "38" } ``` Response trimmed for clarity. When the whole result set is small, `total_display_count` is an exact number (here `"38"`) rather than an approximate `"…+"` string. All filters in the array combine with AND. Mix `in` and `not in` operators to carve out regions you do not want. This finds people at Google or Microsoft, at employers headquartered in the United States or Canada, in Software Development, excluding two regions. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_COMPANY", "type": "in", "value": ["Google", "Microsoft"]}, {"field": "COMPANY_HEADQUARTERS", "type": "in", "value": ["United States", "Canada"]}, {"field": "INDUSTRY", "type": "in", "value": ["Software Development"]}, {"field": "REGION", "type": "not in", "value": ["California, United States", "New York, United States"]} ], "page": 1 }' ``` This request returns the standard envelope with `profiles` and `total_display_count` (`"120K+"` at the time of writing). `REGION` values are strict — confirm them with the [shared autocomplete endpoint](#discover-valid-live-filter-values). Combine `CURRENT_TITLE` with `SCHOOL` for talent or investor sourcing. This finds Founders and Co-Founders who attended Stanford University. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_TITLE", "type": "in", "value": ["Founder", "Co-Founder"]}, {"field": "SCHOOL", "type": "in", "value": ["Stanford University"]} ], "page": 1 }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Hannah Farrar", "headline": "Chief Executive and Co-Founder at CF" }, "professional_network": { "connections": 7966, "location": { "raw": "United Kingdom" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/hannah-farrar" } }, "experience": { "employment_details": { "current": [ { "name": "CF", "title": "Chief Executive and Co-Founder" } ] } } } ], "total_display_count": "23K+" } ``` Response trimmed for clarity. Use `preview: true` for a fast, lightweight look at who matches a filter set before you paginate the full search. Preview cannot be combined with `page`, `limit`, `background_job`, or `post_processing`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ {"field": "CURRENT_COMPANY", "type": "in", "value": ["serverobotics.com"]}, {"field": "REGION", "type": "in", "value": ["United States"]} ], "preview": true }' ``` ```json Response { "profiles": [ { "basic_profile": { "name": "Warren Jolly", "current_title": "Investor", "headline": null }, "professional_network": { "location": { "raw": "Los Angeles Metropolitan Area" }, "connections": null } } ], "total_display_count": "340" } ``` Preview responses keep the same top-level envelope, but per-profile payloads are sparser — expect empty arrays, `null` values, and blank profile URLs. ## What to do next - **Fetch fresh profile data** — use [Person Live Enrich](/person-docs/enrichment/live-enrich) when you have a known profile and need a fresh response from the web. - **Use indexed search** — for structured, repeatable queries use [Person Search](/person-docs/search/introduction). - **Read the overview** — see [Person Search](/person-docs/search/introduction) for the main Search entry point. # Live Search Autocomplete Source: https://docs.crustdata.com/person-docs/search/live-autocomplete Discover valid values for real-time (live) search filters — industry, region, title, and school — via the shared /professional_network/search/autocomplete endpoint. Real-time (live) search uses a different value vocabulary than indexed search. Use this endpoint to discover valid values for the open-ended live-search filter fields before you build a [Person Live Search](/person-docs/search/live-search) or [Company Live Search](/company-docs/search/live-search) query. It is **shared** across person and company live search. This is **separate** from [Person Autocomplete](/person-docs/autocomplete/introduction) and [Company Autocomplete](/company-docs/autocomplete/introduction), which serve the **indexed** `/person/search` and `/company/search` endpoints. The field names and the returned values differ — use this page for live search. ## Endpoint ``` POST https://api.crustdata.com/professional_network/search/autocomplete ``` ## Request | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------ | | `field` | string | Yes | One of `industry`, `region`, `title`, `school`. | | `query` | string | Yes | Prefix/substring to match against the field's values. | | `limit` | integer | No | Maximum number of suggestions to return. | ## Which live-search fields support autocomplete? Only the four open-ended fields above. The other live-search filter fields take a **fixed set of values** that this endpoint does not enumerate — see [Person Live Search → Filter fields with fixed values](/person-docs/search/live-search#filter-fields-with-fixed-values) and [Company Live Search → Filter fields with fixed values](/company-docs/search/live-search#filter-fields-with-fixed-values). Company filters (`CURRENT_COMPANY`, `PAST_COMPANY`) take a profile URL, domain, or name directly — no autocomplete needed. Map the autocomplete `field` to the live-search filter field as follows: | Autocomplete `field` | Live-search filter field(s) | | -------------------- | --------------------------------------------- | | `title` | `CURRENT_TITLE`, `PAST_TITLE` | | `industry` | `INDUSTRY` | | `region` | `REGION`, `COMPANY_HEADQUARTERS` | | `school` | `SCHOOL` | ## Examples ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "industry", "query": "tech", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Technical and Vocational Training" }, { "value": "Technology, Information and Internet" }, { "value": "Technology, Information and Media" } ] } ``` Use the returned value verbatim in an `INDUSTRY` filter, e.g. `{"field": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"]}`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "region", "query": "new york", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "New York City Metropolitan Area" }, { "value": "New York County, New York, United States" }, { "value": "New York Mills, Minnesota, United States" } ] } ``` Region values feed both `REGION` (the person's region) and `COMPANY_HEADQUARTERS`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "title", "query": "technical account", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Technical Account Executive" }, { "value": "Technical Account Manager" }, { "value": "Technical Accountant" } ] } ``` Title values feed `CURRENT_TITLE` and `PAST_TITLE`. ```bash Request curl --request POST \ --url https://api.crustdata.com/professional_network/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "school", "query": "harvard univ", "limit": 3 }' ``` ```json Response { "suggestions": [ { "value": "Harvard University" }, { "value": "Harvard University -- Executive Education" }, { "value": "Harvard University Graduate School of Design" } ] } ``` School values feed the `SCHOOL` filter. Default `rate-limit` is 10 requests per minute. Email [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits. This endpoint can be permission-gated. If you receive `403 permission_error`, ask support to enable live-search access for your account. ## What to do next - **Person live search** — use the values in [Person Live Search](/person-docs/search/live-search) - **Company live search** — use the values in [Company Live Search](/company-docs/search/live-search) - **Indexed autocomplete** — for the indexed `/person/search` and `/company/search` endpoints, see [Person Autocomplete](/person-docs/autocomplete/introduction) and [Company Autocomplete](/company-docs/autocomplete/introduction) # Person Enrichment Source: https://docs.crustdata.com/person-docs/enrichment/introduction Enrich person profiles by profile URL — first enrichment, field selection, batch, and the Search-then-Enrich workflow. The Person Enrich API takes a profile URL and returns a rich person profile. This page covers the basics (your first enrichment and the response shape) plus worked example recipes. For the request/response schema and errors, see [Enrich reference](/person-docs/enrichment/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/person/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### Request body | Parameter | Type | Required | Description | | ----------------------------------- | ----- | -------- | -------------------------------------------------- | | `professional_network_profile_urls` | array | Yes | Profile URLs to enrich. Max 25 per request. | | `fields` | array | No | Specific field paths or section groups to include. | ### Response body The response is a top-level array. Each entry corresponds to one input identifier. | Field | Type | Description | | ---------------------------- | ------ | -------------------------------------------------------- | | `matched_on` | string | The input profile URL | | `match_type` | string | `professional_network_profile_url` | | `matches` | array | Candidate matches. Empty for no-match inputs. | | `matches[].confidence_score` | number | 0 to 1. Higher is better. `1.0` is common for direct URL lookups. | | `matches[].person_data` | object | Full enriched profile. | ### Rate limits and pricing Pricing: Base profile is 1 credit. Developer platform data adds 1 credit. Maximum: 2 credits per profile. Need email addresses or phone numbers? Use [Contact Enrich](/person-docs/contact/enrich). Request parameters, response fields, valid `fields` values, `person_data` sections, advanced flags, errors. --- ## Your first enrichment: look up a profile The simplest enrichment takes a single profile URL and returns the person's cached profile. Pass the URL in the `professional_network_profile_urls` array. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary/" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/abhilashchowdhary/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "Abhilash Chowdhary", "headline": "Co-founder at Crustdata (YC F24) | Real-time B2B data for AI agents", "current_title": "Co-Founder & CEO", "summary": "Love building things. A software engineer with vast experience in shipping products spanning robotics, web technologies and quantitative finance.", "location": { "city": null, "continent": null, "country": null, "raw": "San Francisco, California, United States", "state": null }, "languages": ["English", "Hindi"], "last_updated": "2026-03-03T17:09:31.949863Z" }, "crustdata_person_id": 1068035, "social_handles": { "twitter_identifier": {} } } } ] } ] ``` Response trimmed for clarity. The full response can include employment history, education, skills, and developer platform profiles when available. ### Understanding the response The Enrich API returns an **array** — one entry per identifier you submitted. Each entry has three fields: - **`matched_on`** — the profile URL you submitted. - **`match_type`** — `professional_network_profile_url`. - **`matches`** — an array of candidate profiles. Each match includes a `confidence_score` (0 to 1) and the full `person_data` object. For profile URL lookups, you will typically get exactly one match with a `confidence_score` of `1.0`, because the URL is a direct identifier. ### Need fresh data from the web? If the cached enrich response is missing a recent update or you need real-time retrieval from the web, use [Person Live Enrich](/person-docs/enrichment/live-enrich). That endpoint is designed for fresh profile retrieval when cached enrich is not enough. --- ## Examples Worked recipes you can copy, paste, and adapt. ### Selecting fields: education, employers, and more By default, `/person/enrich` returns only the `basic_profile` and `social_handles` sections. Employment history, education, and skills are **not** returned unless you request them with the `fields` parameter. Request any of these sections (or dot-paths within them) in `fields`: | Section | Returns | | -------------------------- | --------------------------------------------------------------------------------------------------------- | | `experience` | Employment history — current and past roles, each with company, `crustdata_company_id`, title, and dates | | `education` | Schools, degrees, and fields of study | | `skills` | Professional skills | | `professional_network` | Connections, followers, and profile metadata | | `dev_platform_profiles` | Developer platform context (extra credit) | | `certifications`, `honors` | Certifications and awards (gated — require field-level permission on your key) | `experience`, `education`, `skills`, and `professional_network` are part of the base profile (**1 credit**) — requesting them adds **no extra credits**. `dev_platform_profiles` costs more. See [Pricing](/general/pricing) and the full [field catalog](/person-docs/enrichment/reference#person_data-sections). Pass `experience` to get each role's company, `crustdata_company_id`, title, and dates. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "experience"] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "crustdata_company_id": 633593, "start_date": "2017-01-01T00:00:00+00:00" } ], "past": [] } } } } ] } ] ``` Pass `education` for each school, degree, and field of study. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "education"] }' ``` ```json Response (trimmed to the education section) { "education": { "schools": [ { "school": "University of Oxford", "degree": "Bachelor of Arts (B.A.)", "field_of_study": "Philosophy and Computer Science" } ] } } ``` Name every section you want in one call. `experience`, `education`, `skills`, and `professional_network` are part of the base profile (no extra credits). `dev_platform_profiles` adds a credit. `certifications` and `honors` are omitted here — they require field-level permission on your key and otherwise return an error (see the gated example below). ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": [ "basic_profile", "professional_network", "experience", "education", "skills", "social_handles", "dev_platform_profiles" ] }' ``` ```json Response (trimmed) { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" }, "professional_network": { "connections": 700, "followers": 14319, "joined_date": "2007-12-01T00:00:00+00:00", "open_to_cards": [] }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "crustdata_company_id": 633593 } ], "past": [] } }, "education": { "schools": [ { "school": "University of Oxford", "degree": "Bachelor of Arts (B.A.)", "field_of_study": "Philosophy and Computer Science" } ] }, "skills": { "professional_network_skills": [] }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } }, "dev_platform_profiles": [] } ``` To also include `certifications` and `honors`, your API key needs field-level permission — contact Crustdata to enable them. `skills` and `dev_platform_profiles` may be empty for some profiles. Use dot-paths to pull just the sub-fields you need instead of whole sections. ```bash curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile.name", "experience.employment_details.current.title", "education.schools.degree"] }' ``` Request the `social_handles` section to get the person's canonical profile URLs — useful for keying records or linking out to the profile. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile.name", "social_handles"] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu" }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu", "urn_url": "https://www.linkedin.com/in/ACoAAAEnBBoBUwCVyafB-XsEBTeHaFN8ScMKh6U" }, "dev_platform_identifier": { "profile_url": null }, "twitter_identifier": { "slug": "" } } } } ] } ] ``` - **`profile_url`** — the canonical vanity URL. The slug can change if the person edits it. - **`urn_url`** — the permanent URN-based URL, which never changes. Use it (or `crustdata_person_id`) as a stable per-person key. `urn_url` requires **field-level permission** on your API key and is absent from the response by default — contact support to enable it. Both URL forms are accepted as input identifiers in `professional_network_profile_urls`. Set `preview: true` to get a lightweight response at **0 credits**: the `crustdata_person_id`, `basic_profile`, `social_handles.professional_network_identifier`, and the person's connection count and profile picture. Any `fields` you pass are ignored in preview mode, and `preview` cannot be combined with `enrich_realtime`. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "preview": true }' ``` Preview is **enterprise only** and must be enabled for your account — contact the Crustdata team to turn it on. Without it, the API returns `400 invalid_request` with the message `Preview feature is not available for your account. Please reach out to Crustdata team.` `dev_platform_profiles` returns developer platform context (repos, orgs, activity) and adds **+1** credit. ```bash curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "dev_platform_profiles"] }' ``` Starting **from** a dev-platform URL instead? `/person/enrich` does not accept dev-platform URLs as identifiers (the legacy endpoint did). Use the [Dev Platform API](/person-docs/dev_platform/introduction) — `POST /dev_platform/enrich` takes `profile_url: "https://github.com/"` or a `crustdata_person_id` directly. `certifications` and `honors` require field-level permission on your API key. Without it, the request returns a permission error — contact Crustdata to enable them for your key. ```bash curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "certifications", "honors"] }' ``` ### More recipes Not every identifier will resolve to a person. When there is no match, the `matches` array is empty. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/no-such-profile/" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/no-such-profile/", "match_type": "professional_network_profile_url", "matches": [] } ] ``` You still get a response entry for the identifier — `matched_on` tells you which input had no match. This makes it easy to track which lookups succeeded and which need a different approach. You can enrich up to **25 identifiers** in a single request. The response returns one entry per identifier, in the same order you submitted them. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/dvdhsu/", "https://www.linkedin.com/in/abhilashchowdhary/" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu", "headline": "Founder, CEO @ Retool", "current_title": "Founder, CEO", "location": { "city": "San Francisco", "state": "California", "country": "United States", "continent": "North America", "raw": "San Francisco Bay Area" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } } } } ] }, { "matched_on": "https://www.linkedin.com/in/abhilashchowdhary/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "Abhilash Chowdhary", "headline": "Co-founder at Crustdata (YC F24) | Real-time B2B data for AI agents", "current_title": "Co-Founder & CEO", "location": { "city": "San Francisco", "state": "California", "country": "United States of America", "continent": "North America", "raw": "San Francisco, California, United States" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/abhilashchowdhary" } } } } ] } ] ``` Response trimmed for clarity. **Batch enrichment tips** - The maximum batch size is **25 identifiers** per request. - Each entry in the response corresponds to the input at the same position, so you can match results back to your input list by index. - If some identifiers fail to match, their `matches` array will be empty, but the request still succeeds for the others. The most powerful pattern combines [Person Search](/person-docs/search/introduction) with Person Enrich. Search finds people matching your criteria; Enrich gets the full profile for each match. **Step 1:** Search for decision-makers at a target company. ```bash curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.company_name", "type": "in", "value": ["Retool"] }, { "field": "experience.employment_details.current.title", "type": "(.)", "value": "VP|Director|Head of" } ] }, "limit": 5 }' ``` **Step 2:** Take the profile URLs from the search results and enrich them for full profiles. ```bash curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/dvdhsu/", "https://www.linkedin.com/in/abhilashchowdhary/" ] }' ``` This two-step pattern is the foundation for most sales, recruiting, and research workflows. Search narrows the universe; Enrich fills in the details. --- ## What to do next - **Look up request/response details** — see [Enrich reference](/person-docs/enrichment/reference) for parameters, valid `fields` values, `person_data` sections, and errors. - **Search first, then enrich** — use [Person Search](/person-docs/search/introduction) to find people by name, title, company, or location, then enrich the results. - **Fetch fresh data from the web** — use [Person Live Enrich](/person-docs/enrichment/live-enrich) when cached enrich is not enough. # Person Batch Enrich Source: https://docs.crustdata.com/person-docs/enrichment/batch-enrich Enrich people in a single asynchronous job — from the indexed database (up to 10,000) or retrieved fresh from the web at request time (up to 300). Batch enrichment takes a list of identifiers you already have and returns the enriched record for each as a single results file, instead of one [Person Enrich](/person-docs/enrichment/introduction) call per person. This page covers two endpoints: - [**Batch enrich**](#batch-enrich) — records served from the indexed database, with higher caps and faster turnaround. - [**Batch live enrich**](#batch-live-enrich) — each profile retrieved **fresh from the web at request time**. Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ## Batch enrich ``` POST https://api.crustdata.com/batch/person/enrich ``` For profiles retrieved fresh from the web at request time, see [Batch live enrich](#batch-live-enrich) below. ### Database batch enrichment `POST /batch/person/enrich` takes up to **10,000** values in `professional_network_profile_urls`. Missing and over-cap submissions are rejected. The identifier list also accepts a single comma-separated string. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile.name", "basic_profile.headline", "basic_profile.current_title"] }' ``` ```json Response { "batch_id": "22f80b2b-e5bd-480b-86cc-29f79dc6c821", "status": "pending", "entity": "person", "action": "enrich", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/22f80b2b-e5bd-480b-86cc-29f79dc6c821" } ``` When the job completes, each line in the downloaded file is one person wrapped in the enrichment envelope: ```json One record from the results file { "original_identifier": "https://www.linkedin.com/in/dvdhsu/", "internal_id": 14540, "data": { "basic_profile": { "name": "David Hsu", "headline": "Founder, CEO @ Retool", "current_title": "Founder, CEO" }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } }, "crustdata_person_id": 14540 } } ``` Record trimmed for clarity. ### The record envelope Every enrichment record in the results file has three keys: - **`original_identifier`** — the exact value you submitted, so you can join results back to your input list. - **`internal_id`** — the resolved Crustdata ID. (Live person enrichment instead echoes the submitted profile URL here.) - **`data`** — the enriched record, in the same shape as the corresponding non-batch enrich response. Identifiers that cannot be resolved are **silently dropped** — they produce no record and are not charged. Detect them by comparing `entities_requested` with `entities_fulfilled` on the job object, or by diffing the `original_identifier` values in the file against your input list: ```bash Find inputs that produced no record comm -23 <(sort inputs.txt) <(jq -r '.original_identifier' results.jsonl | sort) ``` ### Fields: requested plus defaults `fields` accepts dotted leaf paths, whole family names (`basic_profile`), or a comma-separated string of either. A family name expands to its entire subtree. The response contains the **requested fields plus the default families** (`basic_profile` and `social_handles`) — matching the non-batch [Person Enrich](/person-docs/enrichment/reference) exactly, including the selectable field list. An unsupported value returns `400` with every selectable field in `metadata.available_fields`. ### `chunk_size` Optional internal processing chunk size (identifiers per processing unit), 10–1000, default 100. Out-of-range values return `400` with `"chunk_size must be between 10 and 1000"`. --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. --- ### Errors ```json 400 — no identifier { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: professional_network_profile_urls", "metadata": [] } } ``` ```json 400 — chunk_size out of range { "error": { "type": "invalid_request", "message": "chunk_size must be between 10 and 1000", "metadata": [] } } ``` ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## Batch live enrich Live enrichment is an add-on. If your plan doesn't include it, contact your Crustdata account team to enable it. Batch live enrichment takes a list of profile URLs you already have and returns the enriched record for each as a single results file, with each profile retrieved **fresh from the web at request time** — instead of one [Person Live Enrich](/person-docs/enrichment/live-enrich) call per person. ``` POST https://api.crustdata.com/batch/person/professional_network/enrich/live ``` For records served from the indexed database — higher caps and faster — see [Batch enrich](#batch-enrich) above. ### Live batch enrichment `POST /batch/person/professional_network/enrich/live` retrieves each profile **fresh from the web at request time**. Use it when you need the latest title, headline, or employment data and the database record is not recent enough. Key differences from database enrichment: - **Profile URLs only.** `professional_network_profile_urls` is the only identifier type — anything else returns `400`. - **Cap of 300 per job** (instead of 10,000). Larger submissions are rejected with `400`. - **Slower.** Expect seconds per profile; live jobs are the most likely to end `failed` with an `error_message`. - **`internal_id` echoes the URL.** In the envelope, `internal_id` is the submitted profile URL — not the numeric Crustdata ID. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile.name", "basic_profile.current_title", "basic_profile.headline"] }' ``` ```json Response { "batch_id": "1a5786e5-0e9a-4d58-b55d-7df6f40ecdff", "status": "pending", "entity": "person", "action": "enrich_live", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/1a5786e5-0e9a-4d58-b55d-7df6f40ecdff" } ``` Note the `action` is `enrich_live` — that's how live jobs are labeled wherever you poll or list jobs. ```json One record from the results file { "original_identifier": "https://www.linkedin.com/in/dvdhsu/", "internal_id": "https://www.linkedin.com/in/dvdhsu/", "data": { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO", "headline": "Founder, CEO @ Retool" }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } } } } ``` Record trimmed for clarity. For single-profile live enrichment without the batch lifecycle, see [Person Live Enrich](/person-docs/enrichment/live-enrich). ### The record envelope Every enrichment record in the results file has three keys: - **`original_identifier`** — the exact value you submitted, so you can join results back to your input list. - **`internal_id`** — the resolved Crustdata ID. (Live person enrichment instead echoes the submitted profile URL here.) - **`data`** — the enriched record, in the same shape as the corresponding non-batch enrich response. Identifiers that cannot be resolved are **silently dropped** — they produce no record and are not charged. Detect them by comparing `entities_requested` with `entities_fulfilled` on the job object, or by diffing the `original_identifier` values in the file against your input list: ```bash Find inputs that produced no record comm -23 <(sort inputs.txt) <(jq -r '.original_identifier' results.jsonl | sort) ``` ### Fields: requested plus defaults `fields` accepts dotted leaf paths, whole family names (`basic_profile`), or a comma-separated string of either. A family name expands to its entire subtree. The response contains the **requested fields plus the default families** (`basic_profile` and `social_handles`) — matching the non-batch [Person Live Enrich](/person-docs/enrichment/live-enrich) exactly. An unsupported value returns `400` with every selectable field in `metadata.available_fields`. --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. --- ### Errors ```json 400 — wrong identifier type on live enrich { "error": { "type": "invalid_request", "message": "professional_network_profile_urls must be provided for live enrich", "metadata": [] } } ``` ```json 400 — more than 300 URLs on live enrich { "error": { "type": "invalid_request", "message": "Maximum 300 identifiers allowed for person/enrich_live. Found 301", "metadata": [] } } ``` ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## What to do next - **Need the freshest data?** — see [Batch live enrich](#batch-live-enrich) above for profiles retrieved from the web at request time. - **Higher caps, faster** — see [Batch enrich](#batch-enrich) above for records served from the indexed database. - **Find people first** — see [Batch Person Search](/person-docs/search/batch-search) to turn one query into an input list. - **Find people first (live)** — see [Batch Person Live Search](/person-docs/search/batch-search) to turn one query into an input list. - **Single-person enrichment** — see [Person Enrich](/person-docs/enrichment/introduction). - **Single-profile live enrichment** — see [Person Live Enrich](/person-docs/enrichment/live-enrich). - **Valid `fields` values** — see the [enrich reference](/person-docs/enrichment/reference). - **Enrich companies in batch** — see [Batch Company Enrich](/company-docs/enrichment/batch). - **Full schema** — see the [API reference](/openapi-specs/2025-11-01/introduction). # Dev Platform API Source: https://docs.crustdata.com/person-docs/dev_platform/introduction Enrich people with their dev platform profile data — bio, handles, org memberships, and match confidence — from a Crustdata person ID or a profile URL. The Dev Platform API returns the developer profiles linked to a person: their bio, location, website, followers, declared handles (professional network, X, personal site), organization memberships, and a confidence score on the match. Use it when you want engineering context on someone you already have in your pipeline — a recruit, a signer-up, a search result — without writing custom matching logic. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/dev_platform/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: Dev platform data is 1 credit per profile returned. See [Pricing](/general/pricing) for how this stacks with person enrich. --- ## When to use Dev Platform - You have a Crustdata person ID or a developer profile URL and want the engineering side of the profile (top repositories, star and language rollups, followers, orgs, linked handles). - You want to resolve a GitHub org URL to its profile metadata (bio, website, member count). - You want the match confidence so you can trust — or re-check — a link between a person and a GitHub account. If you have a profile URL, start with [Person Enrich](/person-docs/enrichment/introduction) and read the embedded `dev_platform_profiles` section. Come here when you want the dev platform data as the primary payload. --- ## Your first enrichment: by Crustdata person ID The simplest request takes a `crustdata_person_id` and returns every dev platform profile linked to that person. ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_person_id": 35942353 }' ``` ```json Response { "crustdata_person_id": 35942353, "dev_platform_profiles": [ { "account_type": "u", "name": "Raj Nandan Sharma", "email": null, "company_text": "@cashfree", "bio": "Micro-saas enthusiast. I create and like self hosted software.\r\nStrong believer of \"it just works\" framework for software", "website_url": "https://www.rajnandan.com", "profile_picture_url": "https://avatars.githubusercontent.com/u/16224367?v=4", "is_hireable": true, "is_site_admin": false, "confidence_score": 0.9999999999999772, "profile_url": "https://github.com/rajnandan1", "public_repo_count": 72, "followers": 99, "following": 58, "declared_handles": [ { "provider": "linkedin", "url": "https://www.linkedin.com/in/rajnandan1", "created_at": "2026-04-07T11:41:37+00:00", "last_updated": "2026-04-07T11:41:37+00:00" }, { "provider": "website", "url": "https://www.rajnandan.com/blogs", "created_at": "2026-04-07T11:41:37+00:00", "last_updated": "2026-04-07T11:41:37+00:00" }, { "provider": "x", "url": "https://x.com/_rajnandan_", "created_at": "2026-02-27T22:20:43+00:00", "last_updated": "2026-02-27T22:20:43+00:00" } ], "org_memberships": [ { "organization_id": 124094419, "organization_github_id": 25682196, "organization_login": "cashfree", "created_at": "2026-02-28T00:40:25+00:00", "last_updated": "2026-04-07T11:41:37+00:00" } ], "location": { "raw": "Bengaluru" }, "metadata": { "created_at": "2015-12-09T12:33:20+00:00", "last_scraped_source": "2026-04-06T09:33:01+00:00", "last_updated": "2026-04-07T11:41:37+00:00" } } ] } ``` ### Reading the response - **`crustdata_person_id`** — echoes back the ID you asked about (or `0` when the profile is not linked to a known Crustdata person yet). - **`dev_platform_profiles`** — an ordered array. A person with multiple linked dev platform accounts will have multiple entries. - **`account_type`** — `u` for a user account, `o` for an organization. - **`confidence_score`** — how confident the match is, from 0 to 1. Use this to filter low-confidence matches before acting on the data. - **`declared_handles`** — links the profile owner declared on their account (personal site, professional network, X). Useful for cross-platform identity resolution. - **`org_memberships`** — organizations the user account is a public member of. --- ## By developer profile URL If you only have the GitHub URL — say, from a search result or a repo contributor list — pass it as `profile_url` instead. ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "profile_url": "https://github.com/dvdhsu" }' ``` ```json Response { "crustdata_person_id": 14540, "dev_platform_profiles": [ { "profile_url": "https://github.com/dvdhsu", "account_type": "u", "name": "David Hsu", "email": null, "company_text": null, "bio": null, "website_url": "https://davidh.su", "profile_picture_url": "https://avatars.githubusercontent.com/u/1313773?v=4", "is_hireable": true, "is_site_admin": false, "confidence_score": 0.9999999946149648, "public_repo_count": 68, "followers": 19, "following": 5, "declared_handles": [ { "provider": "website", "url": "https://davidh.su", "created_at": "2026-04-06T12:35:21+00:00", "last_updated": "2026-04-06T12:35:21+00:00" } ], "org_memberships": null, "location": { "raw": "Palo Alto, CA" }, "metadata": { "created_at": "2012-01-08T23:33:42+00:00", "last_scraped_source": "2026-04-02T22:46:33+00:00", "last_updated": "2026-04-10T13:25:59.237710+00:00" } } ] } ``` Pass exactly one of `crustdata_person_id` or `profile_url`. Sending both, or neither, returns a `400` error. --- ## Before you start You need: - A **Crustdata API key** — see [Introduction](/general/introduction) for how to obtain one. - A terminal with `curl` (or any HTTP client). - The required header: `x-api-version: 2025-11-01`. For the full field list and error codes, see the [Reference](#reference) below. For real-world recipes, browse the [Examples](#examples) below. --- ## Examples Practical recipes that show how to use `POST /dev_platform/enrich` end to end. Every request and response on this page was produced by a live API call — copy them as-is and swap in your own identifiers. **What this does:** pulls every dev platform profile linked to a Crustdata person ID. Use it when you already have the person in your pipeline (from Search, Enrich, or your own CRM) and want the engineering side of their profile. ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_person_id": 35942353 }' ``` ```json Response { "crustdata_person_id": 35942353, "dev_platform_profiles": [ { "account_type": "u", "name": "Raj Nandan Sharma", "email": null, "company_text": "@cashfree", "bio": "Micro-saas enthusiast. I create and like self hosted software.\r\nStrong believer of \"it just works\" framework for software", "website_url": "https://www.rajnandan.com", "profile_picture_url": "https://avatars.githubusercontent.com/u/16224367?v=4", "is_hireable": true, "is_site_admin": false, "confidence_score": 0.9999999999999772, "profile_url": "https://github.com/rajnandan1", "public_repo_count": 72, "followers": 99, "following": 58, "declared_handles": [ { "provider": "linkedin", "url": "https://www.linkedin.com/in/rajnandan1", "created_at": "2026-04-07T11:41:37+00:00", "last_updated": "2026-04-07T11:41:37+00:00" }, { "provider": "website", "url": "https://www.rajnandan.com/blogs", "created_at": "2026-04-07T11:41:37+00:00", "last_updated": "2026-04-07T11:41:37+00:00" }, { "provider": "x", "url": "https://x.com/_rajnandan_", "created_at": "2026-02-27T22:20:43+00:00", "last_updated": "2026-02-27T22:20:43+00:00" } ], "org_memberships": [ { "organization_id": 124094419, "organization_github_id": 25682196, "organization_login": "cashfree", "created_at": "2026-02-28T00:40:25+00:00", "last_updated": "2026-04-07T11:41:37+00:00" } ], "updated_at": "2026-04-11T13:26:05.268961+00:00", "location": { "raw": "Bengaluru" }, "metadata": { "created_at": "2015-12-09T12:33:20+00:00", "last_scraped_source": "2026-04-06T09:33:01+00:00", "last_updated": "2026-04-07T11:41:37+00:00" } } ] } ``` **Takeaway:** one person can have multiple dev platform profiles. Iterate `dev_platform_profiles` and use `declared_handles` to pull LinkedIn and X links for cross-platform identity, and `org_memberships` to see which GitHub orgs they publicly belong to. **What this does:** takes a developer profile URL and returns the matched record, including the Crustdata person ID it maps to (when one exists). ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "profile_url": "https://github.com/dvdhsu" }' ``` ```json Response { "crustdata_person_id": 14540, "dev_platform_profiles": [ { "profile_url": "https://github.com/dvdhsu", "account_type": "u", "name": "David Hsu", "email": null, "company_text": null, "bio": null, "website_url": "https://davidh.su", "profile_picture_url": "https://avatars.githubusercontent.com/u/1313773?v=4", "is_hireable": true, "is_site_admin": false, "confidence_score": 0.9999999946149648, "public_repo_count": 68, "followers": 19, "following": 5, "declared_handles": [ { "provider": "website", "url": "https://davidh.su", "created_at": "2026-04-06T12:35:21+00:00", "last_updated": "2026-04-06T12:35:21+00:00" } ], "org_memberships": null, "location": { "raw": "Palo Alto, CA" }, "metadata": { "created_at": "2012-01-08T23:33:42+00:00", "last_scraped_source": "2026-04-02T22:46:33+00:00", "last_updated": "2026-04-10T13:25:59.237710+00:00" } } ] } ``` **Takeaway:** `account_type: "u"` means this is a user account. When it's `"o"` the same fields describe an organization (see Recipe 3). Check `account_type` before interpreting `followers` as a personal signal — on org accounts it's the number of accounts following the org. **What this does:** passes a GitHub organization URL and returns the org's profile metadata. Useful when you want firmographic-style context on a GitHub org before diving into its members or repos. ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "profile_url": "https://github.com/facebook" }' ``` ```json Response { "crustdata_person_id": 0, "dev_platform_profiles": [ { "profile_url": "https://github.com/facebook", "account_type": "o", "name": "Meta", "email": null, "company_text": null, "bio": "We are working to build community through open source technology. NB: members must have two-factor auth.", "website_url": "https://opensource.fb.com", "profile_picture_url": "https://avatars.githubusercontent.com/u/69631?v=4", "is_hireable": false, "is_site_admin": false, "confidence_score": 1.0, "public_repo_count": 159, "followers": 33591, "following": 0, "declared_handles": [ { "provider": "x", "url": "https://x.com/metaopensource", "created_at": "2026-02-27T22:20:43+00:00", "last_updated": "2026-02-27T22:20:43+00:00" } ], "org_memberships": null, "location": { "raw": "Menlo Park, California" }, "metadata": { "created_at": "2009-04-02T03:35:22+00:00", "last_scraped_source": "2024-12-27T21:10:02+00:00", "last_updated": "2026-04-21T07:26:38.751339+00:00" } } ] } ``` **Takeaway:** for org accounts, `bio` and `website_url` are the most useful descriptive fields, and `public_repo_count` plus `followers` give you a quick size signal. `crustdata_person_id` is `0` because organizations do not map to a person in the Crustdata graph. **What this does:** shows the error shape you get when you accidentally send both `crustdata_person_id` and `profile_url`, and how to fix it. ```bash Request curl --request POST \ --url https://api.crustdata.com/dev_platform/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_person_id": 35942353, "profile_url": "https://github.com/rajnandan1" }' ``` ```json Response { "error": { "type": "invalid_request", "message": "Exactly one of crustdata_person_id or profile_url is required", "metadata": [] } } ``` **Takeaway:** pick one identifier. The same error comes back when you send neither. If your pipeline sometimes has only one or the other, branch on whichever field is set and send only that in the request body. See the [Errors](#errors) section below for every error shape. --- ## Reference This section is the field-level reference for `POST /dev_platform/enrich`. Use it alongside the interactive [API Reference](/openapi-specs/2025-11-01/introduction) when you need to know exactly what to send and what to expect back. ``` POST https://api.crustdata.com/dev_platform/enrich ``` ### Authentication Every request requires two headers: | Header | Value | Description | | --------------- | --------------------- | ---------------------------------------- | | `authorization` | `Bearer YOUR_API_KEY` | Your Crustdata API key as a Bearer token | | `x-api-version` | `2025-11-01` | API version (required on every request) | | `content-type` | `application/json` | Request body is always JSON | A missing or invalid API key returns a `401` response with `{"message": "Invalid API key in request"}`. A missing `x-api-version` header returns a `400`. ### Request body The request body is a JSON object. **Exactly one** of `crustdata_person_id` or `profile_url` must be set. | Field | Type | Required | Description | | --------------------- | --------------- | --------------------------------------------- | ---------------------------------------------------------------- | | `crustdata_person_id` | integer (int64) | One of `crustdata_person_id` or `profile_url` | Crustdata person ID to enrich. Example: `35942353`. | | `profile_url` | string | One of `crustdata_person_id` or `profile_url` | developer profile URL (for example `https://github.com/dvdhsu`). | Sending both identifiers returns `400 invalid_request` with the message `Exactly one of crustdata_person_id or profile_url is required`. Sending neither returns the same error. ### Response body A successful request returns a JSON object with these top-level fields: | Field | Type | Description | | ----------------------- | ------------------------ | ------------------------------------------------------------------------------------------- | | `crustdata_person_id` | integer \| null | Crustdata person ID the profiles are linked to. `0` when the profile is not yet linked. | | `dev_platform_profiles` | array of profile objects | Ordered dev platform profile records for the person. `null` when no profiles are available. | #### `dev_platform_profiles[]` Each entry in the `dev_platform_profiles` array describes one dev platform account linked to the person. Every field is nullable — real-world profiles omit some values. | Field | Type | Description | | --------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- | | `account_type` | string enum | `u` for a user account, `o` for an organization account. | | `profile_url` | string | Canonical profile URL (for example `https://github.com/dvdhsu`). | | `name` | string | Display name shown on the profile. | | `email` | string | Public email listed on the profile, when available. | | `location` | object | Location info declared on the profile. See [`location`](#location) below. | | `company_text` | string | Raw free-text company string from the profile (for example `@cashfree`). | | `bio` | string | Free-text bio from the profile. | | `website_url` | string | Website URL declared on the profile. | | `profile_picture_url` | string | URL of the profile picture. | | `is_hireable` | boolean | Whether the account opted in to hireable status on GitHub. | | `is_site_admin` | boolean | Whether the account is a GitHub site administrator. | | `confidence_score` | number (0–1) | Confidence that this profile belongs to the person. | | `public_repo_count` | integer | Number of public repositories on the account. | | `followers` | integer | Number of followers. | | `following` | integer | Number of accounts this profile follows. | | `repos` | array | The account's top 25 most recently pushed owned repositories. See [`repos[]`](#repos) below. | | `total_repos` | integer | Number of owned repositories tracked for the account. | | `total_stars` | integer | Total stars across all owned repositories. | | `max_stars` | integer | Stars on the most-starred owned repository. | | `median_stars` | integer | Median stars across owned repositories. | | `all_languages` | array of string | Primary languages across all owned repositories (not just the embedded 25). | | `all_topics` | array of string | Topic tags across all owned repositories (not just the embedded 25). | | `metrics_timeseries` | array | Historical account-metric snapshots. See [`metrics_timeseries[]`](#metrics_timeseries) below. | | `declared_handles` | array | Other handles the profile owner declared on their account. See [`declared_handles[]`](#declared_handles) below. | | `org_memberships` | array | Organizations this user account is a public member of. See [`org_memberships[]`](#org_memberships) below. | | `updated_at` | string date-time | When this profile record was last refreshed in Crustdata's store. | | `metadata` | object | Record-level metadata. See [`metadata`](#metadata) below. | #### `repos[]` The account's owned repositories, limited to the **25 most recently pushed**. The portfolio rollups above (`total_repos`, `total_stars`, `max_stars`, `median_stars`, `all_languages`, `all_topics`) are computed across **all** owned repositories, so they can reflect repositories not present in this array. Every field is nullable. | Field | Type | Description | | -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `full_name` | string | `owner/repo` slug (for example `sindresorhus/got`). | | `owner` | string | Owning account login. | | `is_fork` | boolean | Whether the repository is a fork. | | `parent` | string | Parent repository slug when the repository is a fork. | | `description` | string | Repository description. | | `homepage` | string | Homepage URL declared on the repository. | | `primary_language` | string | Primary language of the repository. | | `topics` | array of string | Topic tags on the repository. | | `license_key` | string | License key (for example `mit`). | | `license_name` | string | Human-readable license name (for example `MIT License`). | | `archived` | boolean | Whether the repository is archived. | | `disabled` | boolean | Whether the repository is disabled. | | `is_template` | boolean | Whether the repository is a template. | | `has_issues` | boolean | Whether issues are enabled. | | `has_projects` | boolean | Whether projects are enabled. | | `has_wiki` | boolean | Whether the wiki is enabled. | | `stars` | integer | Star count. | | `forks_count` | integer | Fork count. | | `open_issues_count` | integer | Open issue count. | | `size` | integer | Repository size in kilobytes. | | `github_created_at` | string date-time | When the repository was created on the dev platform. | | `github_updated_at` | string date-time | When repository metadata last changed on the dev platform. | | `github_pushed_at` | string date-time | When code was last pushed to the repository. | | `metrics_timeseries` | array | Dated snapshots of `stargazers_count`, `forks_count`, `open_issues_count`, and `size` for this repository. | #### `metrics_timeseries[]` Historical account-level snapshots, one entry per capture date. | Field | Type | Description | | ------------------- | ----------- | ----------------------------------------- | | `as_of_date` | string date | Snapshot date. | | `followers_count` | integer | Followers on the snapshot date. | | `following_count` | integer | Accounts followed on the snapshot date. | | `public_repo_count` | integer | Public repositories on the snapshot date. | #### `location` | Field | Type | Description | | ----- | ------ | ------------------------------------------------------------------------- | | `raw` | string | Raw free-text location string from the profile (for example `Bengaluru`). | #### `declared_handles[]` | Field | Type | Description | | -------------- | ---------------- | ---------------------------------------------------------------------------- | | `provider` | string | Kind of declared handle — for example `website`, `x`, `linkedin`, `bluesky`. | | `url` | string | URL for the declared handle. | | `created_at` | string date-time | When the declared handle was first recorded. | | `last_updated` | string date-time | When the declared handle was last updated. | #### `org_memberships[]` | Field | Type | Description | | ------------------------ | ---------------- | -------------------------------------------------------------------- | | `organization_id` | integer | Crustdata-internal organization ID. | | `organization_github_id` | integer | GitHub-assigned numeric organization ID. | | `organization_login` | string | GitHub organization login (the `cashfree` in `github.com/cashfree`). | | `created_at` | string date-time | When the membership was first recorded. | | `last_updated` | string date-time | When the membership was last updated. | #### `metadata` | Field | Type | Description | | --------------------- | ---------------- | ------------------------------------------------------ | | `created_at` | string date-time | When the account was created on the dev platform. | | `last_scraped_source` | string date-time | When profile data was last retrieved from the web. | | `last_updated` | string date-time | When the profile record was last updated in Crustdata. | ### Errors The endpoint returns different error response shapes depending on what failed. Use the summary below to know which status to handle and which body shape to parse; field-level tables and real examples follow. | Status | When it happens | Response body | | ------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `400` | Neither or both of `crustdata_person_id` / `profile_url` supplied, or type validation failed. | [Nested error object](#400--validation-error) | | `400` | `x-api-version` header missing. | [Flat error object](#400--missing-header) | | `401` | `authorization` header missing or contains an invalid key. | [Unauthorized](#401--unauthorized) | | `422` | Request body is not valid JSON. | [Unprocessable entity](#422--unparseable-body) | #### `400` — validation error Returned when the request body has neither, both, or an invalid-type identifier. Response body has a single top-level `error` object: | Field | Type | Description | | ---------------- | ---------------- | ------------------------------------------------------------------ | | `error` | object | Structured error wrapper. | | `error.type` | string | Machine-readable error type. For this endpoint: `invalid_request`. | | `error.message` | string | Human-readable error message. | | `error.metadata` | array of objects | Additional structured context about the error. Often empty (`[]`). | ```json Missing or duplicate identifier { "error": { "type": "invalid_request", "message": "Exactly one of crustdata_person_id or profile_url is required", "metadata": [] } } ``` Fix: send exactly one of `crustdata_person_id` or `profile_url`. ```json Invalid identifier type { "error": { "type": "invalid_request", "message": "Invalid crustdata_person_id: invalid literal for int() with base 10: 'not-an-integer'", "metadata": [] } } ``` Fix: pass `crustdata_person_id` as an integer, not a string. #### `400` — missing header Returned when the `x-api-version` header is not set. This shape is different from the validation error above. | Field | Type | Description | | ------------- | ------ | -------------------------------------------------- | | `error` | string | Short error label (for example `Invalid Request`). | | `description` | string | Human-readable explanation of the problem. | ```json { "error": "Invalid Request", "description": "Missing required header: x-api-version. Please set x-api-version header appropriately." } ``` Fix: send the `x-api-version: 2025-11-01` header on every request. #### `401` — unauthorized Returned when the `authorization` header is missing or the API key is invalid. | Field | Type | Description | | --------- | ------ | --------------------------------------------------------- | | `message` | string | Human-readable explanation of the authentication failure. | ```json { "message": "Invalid API key in request" } ``` Fix: set `authorization: Bearer YOUR_API_KEY` with a valid key. #### `422` — unparseable body Returned when the request body is not valid JSON. | Field | Type | Description | | -------- | ------ | ----------------------------------------------------------------------- | | `detail` | string | Human-readable description of why the request body could not be parsed. | ```json { "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)" } ``` Fix: send a valid JSON body with `content-type: application/json`. --- ## Rate limits Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. See [Rate limits](/general/rate-limits) for all per-endpoint limits. --- ## What to do next - **Need the field-level contract?** See the [Reference](#reference) section above. - **Want to pair this with person data?** Use [Person Enrich](/person-docs/enrichment/introduction) to pull the basic profile alongside the dev platform data. - **Check the spec** — see the full [OpenAPI reference](/openapi-specs/2025-11-01/introduction) for interactive request/response exploration. # Person Enrich reference Source: https://docs.crustdata.com/person-docs/enrichment/reference Reference for Person Enrich: request parameters, response fields, fields parameter, person_data sections, and errors. Reference material for [Person Enrich](/person-docs/enrichment/introduction): request parameters, response fields, valid `fields` values, `person_data` section catalog, and errors. For walk-through examples, see [Person Enrich](/person-docs/enrichment/introduction). --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | ----------------------------------- | -------- | -------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `professional_network_profile_urls` | string[] | Yes | — | Profile URLs to enrich. Max 25. | | `fields` | string[] | No | `basic_profile`, `social_handles` | Specific field paths or section groups to return. When omitted, only `basic_profile` and `social_handles` are returned. | --- ## Response fields reference Each item in the response array contains: | Field | Type | Description | | ---------------------------- | ------ | -------------------------------------------------------- | | `matched_on` | string | The input profile URL | | `match_type` | string | `professional_network_profile_url` | | `matches` | array | Array of candidate matches (may be empty for no-match) | | `matches[].confidence_score` | number | 0 to 1. How confident the match is. `1.0` = exact match. | | `matches[].person_data` | object | Full enriched profile. See [`person_data` sections](#person_data-sections) below. | --- ## Using the `fields` parameter By default, the API returns only the `basic_profile` and `social_handles` sections. Use the `fields` parameter to request the additional sections or dot-paths you need. ```bash Request — specific fields curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/dvdhsu/" ], "fields": [ "basic_profile.summary", "dev_platform_profiles" ] }' ``` Need email addresses or phone numbers? Use [Contact Enrich](/person-docs/contact/enrich). ### Popular fields for profile and developer workflows | Field path | What it returns | | ---------------------------------------------------- | ------------------------------------------------------------------------------ | | `basic_profile.summary` | The person's summary or about text | | `dev_platform_profiles` | Developer platform profiles when available | | `social_handles.dev_platform_identifier.profile_url` | Canonical developer platform profile URL when available | | `experience.employment_details.current.name` | The person's current company name | ### `person_data` sections The `person_data` object contains the following sections. Pass any of these names to `fields` to include that section, or use dot-paths like `basic_profile.summary` to request a specific sub-field. | Section | Key fields | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `crustdata_person_id` | integer | Top-level Crustdata identifier for the enriched profile | | `updated_at` | date-time string (nullable) | Top-level timestamp of the most recent profile refresh | | `basic_profile` | `name`, `first_name`, `last_name`, `headline`, `current_title`, `normalized_title` ([JTN](/guides/job-title-normalization), beta), `professional_network_name`, `summary`, `location`, `languages`, `last_updated`, `profile_picture_permalink` | Core identity and role | | `professional_network` | `profile_picture_permalink`, `connections`, `followers`, `joined_date`, `verifications`, `open_to_cards`, `pronoun`, `location.raw`, `location.city`, `location.state`, `location.country`, `location.continent` | Profile metadata when available | | `social_handles` | `professional_network_identifier.profile_url`, `professional_network_identifier.urn_url` (permanent URN-based URL; requires field-level permission on your API key), `dev_platform_identifier.profile_url`, `twitter_identifier.slug` | Available social identifiers | | `experience` | `employment_details.current[]`, `employment_details.past[]` | Full employment history | | `education` | `schools[]` | Education background | | `certifications` | `name`, `issuing_organization`, `issue_date`, `expiration_date` | Certifications. Requires field-level permission on your API key. | | `honors` | `title`, `issuer`, `issued_date`, `description` | Honors and awards. Requires field-level permission on your API key. | | `skills` | `professional_network_skills[]` | Professional skills | | `dev_platform_profiles` | developer platform profile objects | Developer platform data when available | --- ## Fresh data and realtime enrichment `/person/enrich` serves data from the cached dataset. When you need fresh profile data retrieved in real time, use the dedicated [Person Live Enrich](/person-docs/enrichment/live-enrich) endpoint (`POST /person/professional_network/enrich/live`), which is designed for real-time retrieval from the web. If cached enrich is not enough, use [Person Live Enrich](/person-docs/enrichment/live-enrich) — it is designed for fresh profile retrieval from the web. --- ## Errors | Status | Meaning | | ------ | ---------------------------------------------------------------------- | | `400` | Invalid request — missing or wrong identifier type, or malformed body. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `500` | Internal server error. | ```json 400 — Bad request { "error": { "type": "invalid_request", "message": "Exactly one identifier type must be provided.", "metadata": [] } } ``` ```json 401 — Invalid API key { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` --- ## API reference summary | Detail | Value | | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /person/enrich` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | `professional_network_profile_urls` (max 25). Optional: `fields`. | | **Response** | Top-level array: `[{ matched_on, match_type, matches: [{ confidence_score, person_data }] }]` | | **No match** | `200` with empty `matches: []` for unmatched identifiers. | | **Errors** | `400` (bad request), `401` (bad auth), `403` (permission/credits), `500` (server error) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. # Person Live Enrich Source: https://docs.crustdata.com/person-docs/enrichment/live-enrich Fetch real-time person profile data from the web when cached data is missing or stale. **Use this when** you need the freshest possible person data from the web — for time-sensitive outreach, verifying stale profiles, or fetching data that is not yet in the cached dataset. The Person Live Enrich API retrieves a person's profile on demand from the web and returns fresh data. Use this sparingly — it is slower and more expensive than cached [Enrich](/person-docs/enrichment/introduction). If this endpoint is not enabled for your account, it returns `403 permission_error` with the message `You do not have permission to access /person/professional_network/enrich/live.` ``` POST https://api.crustdata.com/person/professional_network/enrich/live ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: 7 credits per profile. Availability: Enterprise-only. Default `rate-limit` is 10 requests per minute. Contact support to discuss higher limits if needed for your use case. --- ## Fetch a person profile in real time ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/abhilashchowdhary", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "Abhilash Chowdhary", "headline": "Co-founder at Crustdata (YC F24)", "current_title": "Co-Founder & CEO", "summary": "Love building things...", "languages": ["English", "Hindi"] }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/abhilashchowdhary" } } } } ] } ] ``` When `fields` is omitted, live enrich returns only the `basic_profile` and `social_handles` groups — a minimal default, not the full profile. To also retrieve `professional_network`, `skills`, `experience`, or `education`, request those groups explicitly with `fields` (shown below). --- ## Fetch with specific fields Pass `fields` to retrieve groups beyond the default `basic_profile` and `social_handles` — here, adding `professional_network`: ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary" ], "fields": ["basic_profile", "professional_network"] }' ``` ### Valid field groups Live enrich accepts these `fields` groups: | Group | Notes | | ---------------------- | ------------------------------------------------ | | `basic_profile` | Returned by default. | | `social_handles` | Returned by default. | | `professional_network` | | | `skills` | | | `experience` | | | `education` | | | `certifications` | Requires field-level permission on your API key. | | `honors` | Requires field-level permission on your API key. | Requesting an unsupported group returns `400` (`Invalid fields: ...`); requesting a permission-gated group without access returns `403` (`Access denied to fields: ...`). The cached-dataset `dev_platform_profiles` group is **not** available on live enrich — use the standalone [Dev Platform API](/person-docs/dev_platform/introduction) (`POST /dev_platform/enrich`), `social_handles.dev_platform_identifier`, or cached [Person Enrich](/person-docs/enrichment/introduction) for the full group. ### Request all available fields Two examples: the first requests every non-gated group; the second adds the permission-gated `certifications` and `honors`. `basic_profile` and `social_handles` return by default; add `professional_network`, `skills`, `experience`, and `education` explicitly. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": [ "basic_profile", "social_handles", "professional_network", "skills", "experience", "education" ] }' ``` ```json Response (trimmed) [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" }, "professional_network": { "connections": 701, "followers": 14391, "joined_date": "2007-12-01T00:00:00", "open_to_cards": [] }, "skills": { "professional_network_skills": [] }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "start_date": "2017-01-01T00:00:00", "crustdata_company_id": 633593 } ], "past": [] } }, "education": { "schools": [ { "school": "University of Oxford", "degree": "Bachelor of Arts (B.A.)", "field_of_study": "Philosophy and Computer Science" } ] }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } } } } ] } ] ``` Need email addresses or phone numbers? Use [Contact Enrich](/person-docs/contact/enrich). `open_to_cards` is `[]` unless the person opted in; opted-in profiles return values such as `["CAREER_INTEREST"]`. `certifications` and `honors` require field-level permission on your API key. Without it, the request returns `403` (`Access denied to fields: ...`) — contact Crustdata to enable them. Add them to the `fields` list alongside the non-gated groups: ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": [ "basic_profile", "social_handles", "professional_network", "skills", "experience", "education", "certifications", "honors" ] }' ``` Pass several URLs in `professional_network_profile_urls` to retrieve them in a single request (up to **25**). The response is one entry per input URL, in the same order you submitted them — `matched_on` tells you which input each entry belongs to. `fields` applies to every URL in the request. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/dvdhsu", "https://www.linkedin.com/in/williamhgates" ], "fields": ["basic_profile", "experience"] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/dvdhsu", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO" }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "crustdata_company_id": 633593, "start_date": "2017-01-01T00:00:00" } ], "past": [] } } } } ] }, { "matched_on": "https://www.linkedin.com/in/williamhgates", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "Bill Gates", "current_title": "Co-chair" }, "experience": { "employment_details": { "current": [ { "name": "Gates Foundation", "title": "Co-chair", "crustdata_company_id": 808180 }, { "name": "Breakthrough Energy", "title": "Founder", "crustdata_company_id": 1095080 }, { "name": "Microsoft", "title": "Co-founder", "crustdata_company_id": 2110301 } ], "past": [] } } } } ] } ] ``` Each input is processed independently, so the request still succeeds if one URL has no match — that entry simply returns an empty `matches` array (see below). Match results back to your input list by the `matched_on` value or by array position. When a profile URL cannot be retrieved from the web — for example a deleted, private, or mistyped profile — live enrich still returns `200` with an **empty `matches` array** for that input, rather than an error. You get one response entry per input either way, so you can tell which URLs resolved and which did not. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/this-profile-does-not-exist" ] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/this-profile-does-not-exist", "match_type": "professional_network_profile_url", "matches": [] } ] ``` A malformed URL (not a valid profile URL) is rejected with `400` before any retrieval — see [Errors](#errors). An empty `matches` array means the URL was well-formed but no live profile could be retrieved. --- ## Live Enrich vs Enrich | | Live Enrich | Enrich | | --------------- | ----------------------------------------------------------- | ----------------------------------- | | **Endpoint** | `/person/professional_network/enrich/live` | `/person/enrich` | | **Data source** | Real-time from the web | Crustdata cached dataset | | **Freshness** | Real-time | Cached (periodically updated) | | **Latency** | Higher (real-time retrieval) | Lower (cached response) | | **Response** | Top-level array | Top-level array | | **Best for** | Stale profile refresh, missing data, time-sensitive lookups | General enrichment, batch workflows | Use Live Enrich sparingly — it is slower and more expensive than cached Enrich. For most workflows, cached Enrich is the better starting point. ### Fields not returned by Live Enrich Live Enrich returns a deliberate subset of cached [Enrich](/person-docs/enrichment/introduction). Because it retrieves data in real time, it omits fields that depend on Crustdata's derived-data processing or freshness tracking. The fields below are available on cached Enrich but are **not returned** by Live Enrich (even when you request the parent group): | Field | On Live Enrich | | ----- | -------------- | | top-level `updated_at`, `metadata` | Not returned. | | `basic_profile.first_name`, `basic_profile.last_name` | Not returned. | | `basic_profile.location`, `basic_profile.profile_picture_permalink` | Not returned. | | `*.last_updated` (on `basic_profile`, `professional_network`, `experience`, `education`) | Not returned — these are freshness timestamps. | (`dev_platform_profiles` is also absent — it is not a valid Live Enrich field group at all; see [Valid field groups](#valid-field-groups) above.) The derived fields `experience.employment_details.*.seniority_level` and `experience.employment_details.*.function_category` are part of the same response shape but **may be `null`** on Live Enrich. Everything else matches cached Enrich: `skills`, `social_handles` (including `twitter_identifier`), `certifications`, `honors`, `education.schools[]`, and the employer sub-fields under `experience.employment_details`. --- ## Request parameters | Parameter | Type | Required | Description | | ----------------------------------- | -------- | -------- | ------------------------------------------------- | | `professional_network_profile_urls` | string[] | Yes | Profile URLs to fetch. Max 25. | | `fields` | string[] | No | Field groups to include. If omitted, only `basic_profile` and `social_handles` are returned. Other valid groups: `professional_network`, `skills`, `experience`, `education`, plus `certifications` and `honors` (require field-level permission). See [valid field groups](#valid-field-groups). | ## Errors | Status | Meaning | | ------ | ------------------------------------------------------ | | `400` | Invalid request — malformed URL or missing identifier. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `404` | Profile not found. | | `500` | Internal server error. Retry with exponential backoff. | --- ## API reference summary | Detail | Value | | ------------ | --------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /person/professional_network/enrich/live` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | `professional_network_profile_urls` (required). Optional: `fields`. | | **Response** | Array: `[{ "matched_on", "match_type", "matches": [{ "confidence_score", "person_data" }] }]` | | **Errors** | `400`, `401`, `403`, `404`, `500` | For credit pricing, see [Pricing](/general/pricing). For throughput guidance, see [Rate limits](/general/rate-limits). See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Use cached enrich for most workflows** — [Person Enrich](/person-docs/enrichment/introduction) is faster and cheaper. - **Search for people** — use [Person Search](/person-docs/search/introduction) to find people by filters. # Person Contact Enrich Source: https://docs.crustdata.com/person-docs/contact/enrich Get business emails, personal emails, and phone numbers for a person in a single synchronous request. **Use this when** you need contact data (business emails, personal emails, phone numbers) for a small number of people and want it back in the same request — for example, alongside full profile fields, or to reverse-look-up a person by their business email. **Choose this for a quick response.** Contact data comes back in the same request for up to 25 identifiers. For a **higher fill rate** on bulk lists, use [Batch Contact Enrich](/person-docs/contact/batch) — an async job (typically a 2–3 minute wait). ``` POST https://api.crustdata.com/person/contact/enrich ``` Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Availability: requires contact-enrich access (enterprise or higher-tier plans); self-serve keys receive a `403`. Limits: up to 25 identifiers per request. ## Pricing Billed **per requested contact type, per matched person** — there is no base credit, and only the types you request are fetched and billed. | Contact type requested | Credits per matched person | | ------------------------- | -------------------------- | | `contact.business_emails` | **1** | | `contact.personal_emails` | **2** | | `contact.phone_numbers` | **2** | Omitting `fields` requests all three (**5**, the maximum). `contact.websites` is not one of the billed contact types — requesting it currently uses **0** credits. Identifiers with no match are **free**. See [Pricing](/general/pricing) for all endpoints. --- ## Enrich contact data Request the `contact` field (or specific sub-fields) for one or more profile URLs. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/abhilashchowdhary" ], "fields": ["contact"] }' ``` ```json Response [ { "matched_on": "https://www.linkedin.com/in/abhilashchowdhary", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "contact": { "business_emails": [ { "email": "abhilash@crustdata.com", "status": "deliverable" } ], "personal_emails": [ { "email": "personal@example.com", "status": "deliverable" } ], "phone_numbers": ["+1-555-0100"], "websites": [] }, "crustdata_person_id": 1068035 } } ] } ] ``` Email records are `{ email, status }` objects, where `status` is one of `deliverable`, `catch_all`, `invalid`, or `unknown` (see [Email status](#email-status)). The personal email and phone values above are illustrative. Request only the sub-fields you need (for example `contact.business_emails`) to keep credit costs down. You can also combine `contact` with profile fields like `basic_profile` in the same request — see [Person Enrich](/person-docs/enrichment/introduction). ## Contact fields | Field | Returns | | ------------------------- | ---------------------------------------------------- | | `contact` | All contact records: emails, phone numbers, websites | | `contact.business_emails` | Business email records | | `contact.personal_emails` | Personal email records | | `contact.phone_numbers` | Phone numbers | | `contact.websites` | Personal or company-linked websites | ## Email status Every business and personal email comes back with a `status` that tells you how deliverable the address is. Use it to decide which emails to send to. | Status | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `deliverable` | The mailbox was confirmed to exist and accept mail at this exact address. Lowest bounce risk. | | `catch_all` | The domain accepts mail for any address, so this specific mailbox can't be confirmed. It may work, but delivery isn't guaranteed. | | `invalid` | The mailbox was confirmed not to exist or is otherwise undeliverable. Sending will bounce. | | `unknown` | Deliverability couldn't be determined (for example a timeout or an inconclusive check). Returned when no status info is available. | ## Get a specific contact type Request a single sub-field to return only that contact type — useful for keeping credit costs down. This endpoint always returns **contact only** (no profile fields like `basic_profile` or `experience`). ```bash curl --request POST \ --url https://api.crustdata.com/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/abhilashchowdhary"], "fields": ["contact.business_emails"] }' ``` Returns only the `contact.business_emails` array — no other contact fields or profile data. Billed **1 credit** per matched person. ```bash curl --request POST \ --url https://api.crustdata.com/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/abhilashchowdhary"], "fields": ["contact.personal_emails"] }' ``` Returns only the `contact.personal_emails` array. Billed **2 credits** per matched person. ```bash curl --request POST \ --url https://api.crustdata.com/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/abhilashchowdhary"], "fields": ["contact.phone_numbers"] }' ``` Returns only the `contact.phone_numbers` array. Billed **2 credits** per matched person. Pass `business_emails` instead of profile URLs — the endpoint resolves each email to its person and returns their contact data in the same request. No separate identification call is needed. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "business_emails": ["abhilash@crustdata.com"], "fields": ["contact.business_emails"] }' ``` ```json Response [ { "matched_on": "abhilash@crustdata.com", "match_type": "business_email", "matches": [ { "confidence_score": 1.0, "person_data": { "contact": { "business_emails": [ { "email": "abhilash@crustdata.co", "status": "deliverable" }, { "email": "abhilash@crustdata.com", "status": "deliverable" } ] } } } ] } ] ``` Use exactly one identifier type per request — `business_emails` and `professional_network_profile_urls` cannot be mixed in the same call. If you only need to know **who** owns an email (identity, not contact data), use [Identify (Reverse Email Lookup)](/person-docs/contact/identify). ## Request parameters | Parameter | Type | Required | Description | | ----------------------------------- | -------- | ---------------------------- | ---------------------------------------------------------- | | `professional_network_profile_urls` | string[] | One of these two identifiers | Profile URLs to enrich. Max 25. | | `business_emails` | string[] | One of these two identifiers | Business emails for reverse lookup. Max 25. | | `fields` | string[] | No | Which fields to return. Use `contact` (or its sub-fields). | ## Errors | Status | Meaning | | ------ | ------------------------------------------------------ | | `400` | Invalid request — malformed identifier or missing one. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `500` | Internal server error. Retry with exponential backoff. | --- ## API reference summary | Detail | Value | | ------------ | ---------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /person/contact/enrich` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | One of `professional_network_profile_urls` or `business_emails` (max 25). `fields`: `contact`. | | **Response** | Array: `[{ "matched_on", "match_type", "matches": [{ "confidence_score", "person_data" }] }]`. | | **Errors** | `400`, `401`, `403`, `500` | For the full field catalog and identifier behavior, see the [Person Enrich reference](/person-docs/enrichment/reference). For credit pricing, see [Pricing](/general/pricing). --- ## What to do next - **Enrich in bulk** — [Batch Contact Enrich](/person-docs/contact/batch) handles large lists asynchronously with no per-profile base fee. - **Get full profiles** — [Person Enrich](/person-docs/enrichment/introduction) returns profile, employment, and education data. - **Find people first** — use [Person Search](/person-docs/search/introduction) to build your list of profile URLs. # Batch Contact Enrich Source: https://docs.crustdata.com/person-docs/contact/batch Enrich a bulk list of profile URLs with contact data as an async job — results delivered as a file. **Use this when** you need contact data for hundreds of people at once — bulk lists, CRM backfills, or back-office pipelines. Submit a job, then poll or receive a webhook when it finishes. You pay per contact type returned for each matched person — never per candidate record. **Choose this for a higher fill rate.** Submit a bulk list and get results as a file, typically within 2–3 minutes. For a quick, synchronous response on a few people, use [Contact Enrich](/person-docs/contact/enrich) instead. Availability: requires contact-enrich access (enterprise or higher-tier plans). Limits: up to 300 profile URLs per submission. ## Pricing Billed **per contact type returned, per matched person** — the same prices as [Person Contact Enrich](/person-docs/contact/enrich), with a maximum of **5** per person. | Contact type returned | Credits per matched person | | --------------------- | -------------------------- | | Business emails | **1** | | Personal emails | **2** | | Phone numbers | **2** | Counted **per type, not per record** — a person with four business-email candidates bills once. Persons with no contact data found are not billed for any type. Result rows contain only the contact types you requested; a requested type with no data appears as an empty list. See [Pricing](/general/pricing) for all endpoints. ## How it works Batch contact enrichment is an **asynchronous, two-call flow** — you submit a job, then fetch the results once it finishes: `POST /batch/person/contact/enrich` with your list of profile URLs and the contact `fields` you want. You get a `batch_id` back immediately. `GET /batch/{batch_id}` to check the job status — or pass a `webhook_url` on submit and we call you when it finishes. Once `status` is `completed`, the status response includes a `download_url`: a gzipped JSONL file with one record per URL, valid for 5 days. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: no per-profile base fee — you are charged only for the contact values that are delivered. See [Pricing](/general/pricing) for per-field credit costs. Limits: up to 5 active batch jobs per user at a time. --- ## 1. Submit your profile URLs Send your profile URLs in `professional_network_profile_urls` along with the contact `fields` you want. The response returns a `batch_id` immediately — the job runs asynchronously. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/contact/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": [ "https://www.linkedin.com/in/cassandrebay", "https://www.linkedin.com/in/ruchavora" ], "fields": ["business_email", "personal_contact_info"] }' ``` ```json Response { "batch_id": "c97333e3-046f-4fd2-b888-a5b07793a0ac", "status": "pending", "entity": "person", "action": "contact_enrich", "identifier_count": 2, "entities_requested": 2, "status_url": "/batch/c97333e3-046f-4fd2-b888-a5b07793a0ac" } ``` To be notified when the job finishes instead of polling, include a `webhook_url` in the request body. Crustdata sends a callback to that URL when the batch completes. ## 2. Get the results Poll the `status_url` (or `GET /batch/{batch_id}`). While the job runs, `status` is `pending` or `processing`. When it is `completed`, the response includes a `download_url`. ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/c97333e3-046f-4fd2-b888-a5b07793a0ac \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response { "batch_id": "c97333e3-046f-4fd2-b888-a5b07793a0ac", "status": "completed", "entity": "person", "action": "contact_enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-05-28T10:00:00Z", "completed_at": "2026-05-28T10:06:00Z", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/...&X-Amz-Expires=432000" } ``` The `download_url` points to gzipped JSONL (`.jsonl.gz`) with one record per submitted profile URL. The link is valid for 5 days. --- ## Fields Request any combination of these contact fields. Only professional network profile URLs are supported as identifiers for this endpoint. | Field | Returns | | --------------------------------------- | -------------------------------------- | | `business_email` | Verified business email addresses | | `personal_contact_info` | Both personal emails and phone numbers | | `personal_contact_info.personal_emails` | Personal email addresses only | | `personal_contact_info.phone_numbers` | Phone numbers only | ## Request parameters | Parameter | Type | Required | Description | | ----------------------------------- | -------- | -------- | ------------------------------------------------------ | | `professional_network_profile_urls` | string[] | Yes | Profile URLs to enrich. Professional network profile URLs only. | | `fields` | string[] | Yes | Which contact fields to enrich (see the table above). | | `webhook_url` | string | No | URL to call when the job finishes, instead of polling. | ## Job lifecycle | Status | Meaning | | ------------ | ------------------------------------------- | | `pending` | Job accepted, not yet started. | | `processing` | Job is running. | | `completed` | Job finished — `download_url` is available. | | `failed` | Job failed. No contact values were billed. | ## Errors | Status | Meaning | | ------ | ---------------------------------------------------------------- | | `400` | Invalid request — missing identifier, invalid field, or no URLs. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `429` | Too many active batch jobs (max 5 per user). | | `500` | Internal server error. Retry with exponential backoff. | --- ## API reference summary | Detail | Value | | ------------ | ----------------------------------------------------------------------------------------------- | | **Submit** | `POST /batch/person/contact/enrich` | | **Poll** | `GET /batch/{batch_id}` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | `professional_network_profile_urls` (required), `fields` (required). Optional: `webhook_url`. | | **Response** | Submit returns `batch_id` + `status`; poll returns `status` and a `download_url` when complete. | | **Output** | Gzipped JSONL, one record per profile URL, download link valid 5 days. | | **Errors** | `400`, `401`, `403`, `429`, `500` | For credit pricing, see [Pricing](/general/pricing). For throughput guidance, see [Rate limits](/general/rate-limits). See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Enrich interactively** — [Contact Enrich](/person-docs/contact/enrich) returns contact data synchronously for up to 25 URLs per request. - **Find people first** — use [Person Search](/person-docs/search/introduction) to build the list of profile URLs to enrich. # Identify (Reverse Email Lookup) Source: https://docs.crustdata.com/person-docs/contact/identify Resolve a bulk list of emails (or profile URLs) to the people behind them as an async job — reverse email lookup with results delivered as a file. **Use this when** you have a list of emails and need to know *who* each one belongs to — the person, their `crustdata_person_id`, and basic profile — at bulk scale. Submit a job, then poll or receive a webhook when it finishes. You pay only for the emails that resolve to a person. **Identify vs Enrich.** Identify answers *"who is this email?"* — it returns the matched person's `crustdata_person_id` and `basic_profile` (name, headline, title). If you also need contact data, full employment history, or company IDs per role, use [Person Enrich](/person-docs/enrichment/introduction) (which also supports reverse lookup by business email). {/* TEAM NOTE — verify before publishing: 1. Max identifiers per submission: BATCH_MAX_LIMIT[(person, identify)] = 300 (api/screener/dataset/api/batch/temporal/constants.py:61). DEFAULT fallback is 1000 but identify is explicitly capped at 300. Live submit of 2 confirmed. 2. Billing: 1 credit per matched person; unmatched emails free (api_credit_utils.py lago_deduct_credits_batch_person_identify → event_value = matched count; bootstrap.py BM_API_DATASET_PERSON_IDENTIFY amount="1", unit credit). The per-phase split in PersonIdentifyUsage is INTERNAL cost analysis, not customer billing. Pricing row added to general/pricing.mdx (1 per match). 3. webhook_url is read generically in submit_batch_job (views.py:163) and applies to all actions; only POLLING was tested end-to-end for identify, not the webhook callback. Confirm the callback fires for identify before promoting it. 4. No OpenAPI entry yet (openapi-specs/2025-11-01) — add the path when speccing. 5. Endpoint shipped 2026-06-18 (PR #6112, DL-1811). Verified live on api-version 2025-11-01 with the internal token. */} ## How it works Reverse identify is an **asynchronous, two-call flow** — you submit a job, then fetch the results once it finishes: `POST /batch/person/identify` with your list of `emails` (or `professional_network_profile_urls`). You get a `batch_id` back immediately. `GET /batch/{batch_id}` to check the job status — or pass a `webhook_url` on submit and we call you when it finishes. Once `status` is `completed`, the status response includes a `download_url`: a gzipped JSONL file with one record per input, valid for 5 days. Emails already known to Crustdata resolve in seconds. Emails that require real-time retrieval take longer, which is why this endpoint is asynchronous. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. Pricing: 1 credit per email that resolves to a person — unmatched emails are free. See [Pricing](/general/pricing) for details. Limits: up to 300 identifiers per submission, and up to 5 active batch jobs per user at a time. --- ## 1. Submit your emails Send your emails in `emails` — business or personal addresses both work. The response returns a `batch_id` immediately — the job runs asynchronously. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/person/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "emails": [ "abhilash@crustdata.com", "ankit.harbhajanka@aequs.com" ] }' ``` ```json Response { "batch_id": "61f537fc-e93d-4912-8d00-0ece6c7ecf9f", "status": "pending", "entity": "person", "action": "identify", "identifier_count": 2, "entities_requested": 2, "status_url": "/batch/61f537fc-e93d-4912-8d00-0ece6c7ecf9f" } ``` You can submit `professional_network_profile_urls` instead of `emails` to resolve profile URLs to their `crustdata_person_id`. Submit exactly one identifier type per job. To be notified when the job finishes instead of polling, include a `webhook_url` in the request body. ## 2. Get the results Poll the `status_url` (or `GET /batch/{batch_id}`). While the job runs, `status` is `pending` or `processing`. When it is `completed`, the response includes a `download_url`. ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/61f537fc-e93d-4912-8d00-0ece6c7ecf9f \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response { "batch_id": "61f537fc-e93d-4912-8d00-0ece6c7ecf9f", "status": "completed", "entity": "person", "action": "identify", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-22T03:23:30Z", "completed_at": "2026-06-22T03:23:31Z", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/...&X-Amz-Expires=432000" } ``` The `download_url` points to gzipped JSONL (`.jsonl.gz`) with one record per submitted identifier. The link is valid for 5 days. ## Result format Each line is one record for an input identifier: ```json { "matched_on": "abhilash@crustdata.com", "match_type": "email", "matches": [ { "confidence_score": 1.0, "person_data": { "crustdata_person_id": 1068035, "basic_profile": { "name": "Abhilash Chowdhary", "headline": "Co-founder at Crustdata (YC F24) | Real-time B2B data for AI agents", "current_title": "Co-Founder & CEO", "summary": "Love building things...", "languages": ["English", "Hindi"], "last_updated": "2026-06-22T00:11:06Z", "location": null }, "updated_at": "2026-06-22T00:11:06Z" } } ] } ``` An email with no match returns an empty `matches` array (and is not billed): ```json { "matched_on": "nobody@unknown-domain.com", "match_type": "email", "matches": [] } ``` ### Personal emails resolve too Reverse lookup is not limited to business addresses — a personal email (for example a Gmail address) resolves the same way when it can be matched to a person. Submit it in `emails` like any other address: ```bash curl --request POST \ --url https://api.crustdata.com/batch/person/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "emails": ["jane.doe91@gmail.com"] }' ``` The result row has the same shape as above. Personal addresses that cannot be matched return the usual empty `matches` array and are not billed. ## Request parameters | Parameter | Type | Required | Description | | ----------------------------------- | -------- | ----------------- | ---------------------------------------------------------------------------- | | `emails` | string[] | One identifier of | Emails to reverse-lookup — business or personal addresses. Max 300 per submission. Result rows carry `match_type: "email"`. | | `professional_network_profile_urls` | string[] | One identifier of | Profile URLs to resolve to a `crustdata_person_id`. Max 300 per submission. | | `webhook_url` | string | No | URL to call when the job finishes, instead of polling. | Submit exactly one identifier type per job — either `emails` or `professional_network_profile_urls`, not both. ## Response fields | Field | Description | | ---------------------------------- | --------------------------------------------------------------------------------- | | `matched_on` | The input value this record resolved (the email or profile URL you submitted). | | `match_type` | `email` or `professional_network_profile_url` — mirrors the identifier key you submitted. | | `matches` | Array of matches. Empty when nothing resolved. | | `matches[].confidence_score` | Match confidence, `0`–`1`. | | `matches[].person_data` | `crustdata_person_id`, `basic_profile`, and `updated_at` for the matched person. | ## Job lifecycle | Status | Meaning | | ------------ | ------------------------------------------- | | `pending` | Job accepted, not yet started. | | `processing` | Job is running. | | `completed` | Job finished — `download_url` is available. | | `failed` | Job failed. Nothing was billed. | ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------- | | `400` | Invalid request — missing identifier, both identifier types, or too many values. | | `401` | Invalid or missing API key. | | `403` | Permission denied or insufficient credits. | | `429` | Too many active batch jobs (max 5 per user). | | `500` | Internal server error. Retry with exponential backoff. | --- ## API reference summary | Detail | Value | | ------------ | ----------------------------------------------------------------------------------------------- | | **Submit** | `POST /batch/person/identify` | | **Poll** | `GET /batch/{batch_id}` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Request** | One of `emails` or `professional_network_profile_urls` (max 300). Optional: `webhook_url`. | | **Response** | Submit returns `batch_id` + `status`; poll returns `status` and a `download_url` when complete. | | **Output** | Gzipped JSONL, one record per identifier, download link valid 5 days. | | **Billing** | 1 credit per matched person; unmatched identifiers are free. | | **Errors** | `400`, `401`, `403`, `429`, `500` | For credit pricing, see [Pricing](/general/pricing). For throughput guidance, see [Rate limits](/general/rate-limits). --- ## What to do next - **Need contact data?** Use [Contact Enrich](/person-docs/contact/enrich). - **Need full employment history?** Use [Person Enrich](/person-docs/enrichment/introduction) with a profile URL. - **Enrich contact info in bulk** — [Batch Contact Enrich](/person-docs/contact/batch) returns emails and phone numbers for a list of profile URLs. - **Find people first** — use [Person Search](/person-docs/search/introduction) to build the list to identify. # Person Entity Watcher Source: https://docs.crustdata.com/watcher-docs/person/entity Watch a list of people you supply and get notified the moment their profile changes — a new job, a title move, a fresh certification. You supply the list; the watcher delivers the diff. A **Person Entity Watcher** monitors **a list of people you supply** (by profile URL) and notifies you when *their* profiles change. You describe what to watch for; each scheduled run compares every person against their last snapshot and delivers only what changed. The list is **yours to edit any time** — [add or remove people](#edit-the-watched-list-any-time) without recreating the watch. Where [Person Discovery Watchers](/watcher-docs/person/discovery) **find new** people matching a filter, Entity Watchers **track a known set** — an ATS of candidates, key contacts, a target roster — and surface *movement* within it: - A tracked person **starts a new job** or **changes their title or headline** — a strong "in motion" signal. - A tracked person **earns a certification** or **receives an award**. - A tracked person **relocates**, **adds a degree**, or **lists a new skill**. Create a person watch with: ``` POST https://api.crustdata.com/watch/person ``` Every request requires the `x-api-version: 2025-11-01` header and a Bearer token. Replace `YOUR_API_KEY` in each example with your API key. This endpoint is **open to all API customers** — no per-endpoint grant is needed. Pricing — you pay only for notifications. The first run of every watch is a free baseline that records each person's starting values internally (for diffing only — it isn't delivered) and never fires. After that you're charged per notification — one charge for each watched person that changed on a run. A run that surfaces nothing costs nothing, and the rate does **not** depend on how often you check (`every_hours`) — only on the [data-freshness tier](#data-freshness) you pick (5 credits at the default 30-day freshness). If your balance is too low, the run is suspended rather than partially delivered. See [Pricing](/general/pricing) for the canonical rates. ## How a person entity watcher runs `POST` your `entities` (people to watch), a `track` (what to watch for), a `config` (schedule + caps), and one or more `notifications` channels. The response returns the full watch object, including its `id`. The first run records an internal **snapshot** of the fields you're tracking for each person — just enough to diff against next time. With no prior snapshot to compare, the baseline **never fires and is never charged**, and isn't itself delivered — it only establishes "before". To read a person's current profile on demand, use the Enrich APIs. On your schedule (`every_hours`), the watcher re-checks each person, diffs them against their last snapshot, and delivers a notification for every person whose `track` condition just became true. You're charged 5 credits per notification. An Entity Watcher only fires on a **transition** — the moment a value crosses from its previous state to a new one. It never fires on the baseline, and won't re-notify about a change it already reported. To read a list's current state on demand instead of watching for change, use the [Person Enrich](/person-docs/enrichment/introduction) API. ## What you can track The `track` describes the change that triggers a notification — a tree of condition **leaves**, optionally combined with `and` / `or` groups. A leaf has the shape: ```json { "field": "", "type": "", "value": "" } ``` | Operator | Applies to | Fires when | | ------------ | ---------- | ---------- | | `changed` | a **scalar** field (e.g. `basic_profile.current_title`) | the value becomes different from the last snapshot | | `added` | an **array** field (e.g. `certifications`, `honors`) | a new element appears in the array | | `>`, `<`, `=`, `!=`, `=>` (≥), `=<` (≤) | a scalar, or a predicate on an array element | the comparison flips from false to true (e.g. `professional_network.connections => 500`) | Crustdata writes "greater-than-or-equal" as **`=>`** and "less-than-or-equal" as **`=<`** — not `>=` / `<=`. Those reversed forms return `unknown operator`. `value` is required for the comparison operators and omitted for `changed` and `added`. Combine leaves with a group node — `{ "op": "and" | "or", "conditions": [ … ] }` — to watch several signals at once. An `or` group fires if **any** child fires; an `and` group fires only when **all** conditions hold and at least one just became true. ### Common person signals Examples only — a person profile has **200+ addressable fields**, and you can track any of them (any scalar with `changed`, any array with `added`). | Signal | `field` | `type` | | --- | --- | --- | | Started a new job | `experience.employment_details.current` | `added` | | Title changed | `basic_profile.current_title` | `changed` | | Headline changed | `basic_profile.headline` | `changed` | | Relocated | `basic_profile.location.raw` | `changed` | | New certification | `certifications` | `added` | | New award or honor | `honors` | `added` | | New education / degree | `education.schools` | `added` | | New skill listed | `skills.professional_network_skills` | `added` | | Connection count changed | `professional_network.connections` | `changed` | `field` paths address the same profile structure the Enrich API returns, so browse the full set of trackable fields in the [Person Enrich](/person-docs/enrichment/reference) reference. `changed` requires a **scalar** path and `added` an **array** path — mixing them returns a `400` (for example, `"changed"` on the array `experience.employment_details.current` is rejected; watch `basic_profile.current_title` instead). ### Recipes: combine signals Group leaves with `or` to fire on **any** of several signals, or `and` to require them together. These `track` blocks are all live-verified. ```json Any career move { "op": "or", "conditions": [ { "field": "experience.employment_details.current", "type": "added" }, { "field": "basic_profile.current_title", "type": "changed" } ] } ``` ```json New credential { "op": "or", "conditions": [ { "field": "certifications", "type": "added" }, { "field": "honors", "type": "added" }, { "field": "education.schools", "type": "added" } ] } ``` Drop either into the `track` field of a create request. Groups nest, so you can mix `and`/`or` — e.g. *"a title change **and** a new certification."* ## `track` and `fields` are different Two independent knobs, and the distinction matters: | Key | Controls | Answers | | -------- | -------- | ------- | | `track` | **When** the watch fires | *"What change do I want to be notified about?"* | | `fields` | **What** the notification delivers | *"What data about the person do I want in the payload?"* | Set them independently. Track one field but deliver many (watch for a title change, yet receive the full employment history and education), or track many and deliver few. `track` decides *whether* you get a notification; `fields` decides *what's inside* it. **`fields` defaults to a minimal projection.** If you omit `fields`, the delivered `record` contains only `basic_profile` and `social_handles`. The field you're *tracking* is **not** automatically added to the payload — the fired `changes` array always tells you exactly what moved, but to get the surrounding profile data in `record`, request it in `fields`. ### Requesting all fields `fields` does **not** affect pricing — you're charged per notification at your chosen [data-freshness tier](#data-freshness) (5 credits at the default), no matter how many field groups you deliver (see [Pricing](/general/pricing)). There's no cost reason to keep the payload thin; request whatever your workflow needs. There's no wildcard — to receive the complete record, list every field group your API key is entitled to. `fields` is a top-level key (a sibling of `track`), fixed at create time. The delivered `record` uses the **same schema as [Person Enrich](/person-docs/enrichment/reference)** — identical field-group names and nesting — so the example below mirrors the full Person Enrich `fields` set. ```json Person — all fields { "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] }, "track": { "field": "experience.employment_details.current", "type": "added" }, "fields": [ "basic_profile", "professional_network", "social_handles", "experience", "education", "skills", "certifications", "honors", "contact", "dev_platform_profiles" ] } ``` Some groups require a field-level entitlement on your key (e.g. `certifications`). Requesting a group you aren't entitled to returns a field-permission error, so list only the groups your key can access. The group names match the Enrich API — see the [Person Enrich](/person-docs/enrichment/reference) `fields` reference. ## Choosing the entities `entities` maps an **identifier type** to a list of values. A single watch holds up to **10,000 people**. | Accepted identifier keys | | --- | | `professional_network_profile_urls` | ```json { "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] } } ``` ## Schedule and limits The `config` block controls timing and result caps: | Field | Required | Meaning | | --- | --- | --- | | `trigger.type` | yes | Must be `"interval"`. | | `trigger.every_hours` | yes | How often the watch runs, in hours (integer ≥ 1). Because you pay per notification, a higher frequency only makes changes surface sooner — it doesn't raise the price. | | `max_results_per_run` | no | Cap on notifications per run (1–1000, default 25). | | `refresh_frequency_days` | no | How fresh to keep the tracked data, in days (1–30, default 30). Fewer days means fresher data and a higher per-notification rate — see [Data freshness](#data-freshness). | | `preferred_hour` | no | Hour of day (0–23) to prefer for the run. | | `expires_at` | no | Auto-expire date, `YYYY-MM-DD`. | ## Data freshness `config.refresh_frequency_days` sets **how fresh the tracked data is kept** — how recently each tracked field is refreshed before a run compares it against the last snapshot. It is independent of `trigger.every_hours`: `every_hours` is how often the watch *checks and notifies*; `refresh_frequency_days` is how up-to-date the data it checks against is, which is what decides how quickly a real change is detected. Fresher data is priced higher — the per-notification charge is tiered by the freshness you choose: | `refresh_frequency_days` | Credits per changed person | | --- | --- | | not set / `30` (default) | 5 | | `14` | 10 | | `7` | 20 | | `3` | 50 | | `1` | 150 | The accepted range is **1–30**. A value between two tiers is billed at the fresher (more expensive) tier. Only fields backed by a refresh asset can be kept fresh this way; if a tracked field has no backing asset, the create is rejected — drop that field or the cadence. ## Delivery channels Add one or more channels to `notifications` and every change fans out to all of them. | Channel | Shape | | --- | --- | | Webhook | `{ "type": "webhook", "url": "https://…", "headers": { … } }` | | Slack | `{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/…" }` | | Google Chat | `{ "type": "google_chat", "url": "https://chat.googleapis.com/…" }` | At least one notification channel is required today. Every delivery is also recorded, so beyond the live push you can re-read a watch's past runs from the [run-history endpoint](#pulled-from-run-history). ## Quick start Watch one person and get a webhook whenever they start a new job. This request and response are real. ```bash Request curl --request POST \ --url https://api.crustdata.com/watch/person \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] }, "track": { "field": "experience.employment_details.current", "type": "added" }, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 100 }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ```json Response { "id": 46936, "kind": "entity", "dataset": "person", "api_version": "2025-11-01", "config_version": "2025-11-01", "status": "active", "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] }, "track": { "field": "experience.employment_details.current", "type": "added" }, "fields": null, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 100, "expires_at": null, "preferred_hour": null }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ], "created_at": "2026-07-16T03:16:38.351849Z", "last_run_at": null } ``` ## Edit the watched list any time Unlike a saved search, an Entity Watcher's list is **mutable** — add or remove people as your pipeline or contact list changes, without recreating the watch. `PATCH` the watch with a new `entities` object: ```bash Request curl --request PATCH \ --url https://api.crustdata.com/watch/person/46936 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "entities": { "professional_network_profile_urls": [ "https://www.linkedin.com/in/sherryrobinson", "https://www.linkedin.com/in/williamhgates" ] } }' ``` ```json Response { "id": 46936, "status": "active", "entities": { "professional_network_profile_urls": [ "https://www.linkedin.com/in/sherryrobinson", "https://www.linkedin.com/in/williamhgates" ] } } ``` Newly added people are **baselined silently** on the next run — they establish their "before" snapshot first, so adding someone never fires a spurious notification. You can also `PATCH` `status` (`active` / `paused`), `config`, and `notifications`. A watch's `track` and `fields` are fixed once created; to change what you watch for, create a new watch. ## Manage your watches | Action | Request | | --- | --- | | List your watches | `GET /watch/person?status=active&limit=50&offset=0` | | Get one | `GET /watch/person/{id}` | | Pause / resume | `PATCH /watch/person/{id}` with `{ "status": "paused" }` | | Add/remove entities | `PATCH /watch/person/{id}` with a new `entities` | | Delete | `DELETE /watch/person/{id}` → `204` | Full request/response for each, verified live. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/watch/person?status=active&limit=50&offset=0' \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response [ { "id": 46957, "kind": "entity", "dataset": "person", "status": "active", "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] }, "track": { "type": "added", "field": "experience.employment_details.current" }, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25 }, "notifications": [{ "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }], "created_at": "2026-07-16T13:39:18.247195Z", "last_run_at": "2026-07-16T13:39:18.598964Z" } ] ``` Returns an array of your person watches. Filter with `status`, page with `limit` and `offset`. ```bash Request curl --request GET \ --url https://api.crustdata.com/watch/person/46957 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```json Response { "id": 46957, "kind": "entity", "dataset": "person", "api_version": "2025-11-01", "config_version": "2025-11-01", "status": "active", "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] }, "track": { "type": "added", "field": "experience.employment_details.current" }, "fields": null, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25, "expires_at": null, "preferred_hour": null }, "notifications": [{ "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }], "created_at": "2026-07-16T13:39:18.247195Z", "last_run_at": "2026-07-16T13:39:18.598964Z" } ``` `last_run_at` is `null` until the baseline run completes, then carries the most recent run's timestamp. ```bash Request curl --request PATCH \ --url https://api.crustdata.com/watch/person/46957 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "status": "paused" }' ``` ```json Response { "id": 46957, "status": "paused" } ``` A paused watch stops running until you resume it. Send `{ "status": "active" }` to resume. ```bash Request curl --request DELETE \ --url https://api.crustdata.com/watch/person/46957 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` Returns `204 No Content`. Deletion is terminal — the watch cannot be resumed. ## What a notification looks like Every fired person carries two things: the **`changes`** array (exactly what moved, independent of `fields`) and a **`record`** (the person's current data projected to the `fields` you requested — the same shape the Enrich API returns, so with default `fields` it's just `basic_profile` + `social_handles`). The person's identity travels **inside** the record as `crustdata_person_id`. Two ways to receive these, each with a **different envelope**: ### Pushed to your webhook When a watch fires, we `POST` this body to each channel. The example below is from a watch created with `fields: ["basic_profile", "experience"]`: ```json Webhook POST body { "metadata": { "watch_id": 46936, "kind": "entity", "dataset": "person", "api_version": "2025-11-01", "run_id": 64200, "notification_id": "ntf_64200", "delivered_at": "2026-07-16T03:20:00.000000Z", "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 100, "truncated": false } }, "results": [ { "changes": [ { "field": "experience.employment_details.current", "type": "added", "new_elements": [ { "title": "…", "name": "…", "start_date": "…" } ] } ], "record": { "basic_profile": { "name": "…", "current_title": "…", "headline": "…" }, "experience": { "employment_details": { "current": [ { "title": "…", "name": "…", "start_date": "…" } ] } }, "crustdata_person_id": 6324687 } } ] } ``` For an entity watch, `results` is a **flat list** — one object per fired person, each with its own `changes` and `record`. A `changed` scalar reports as `{ "field": …, "type": "changed", "from": …, "to": … }`; an `added` array as `{ "field": …, "type": "added", "new_elements": [ … ] }`. ### Pulled from run history To re-read a past run — or audit exactly what was delivered — the run-history endpoint returns the same content under a different envelope. ```json GET /watcher/watches/{id}/runs/{run_id}/summary { "id": 64200, "status": "SUCCESS", "new_records_count": 1, "notifications": [ { "sent_at": "2026-07-16T03:20:00Z", "http_status": 200, "payload": { "subscription_id": 46936, "event_type": "indb:entity:person", "timestamp": "2026-07-16T03:20:00Z", "notifications": [ { "uid": "person_6324687_64200", "changes": [ { "field": "experience.employment_details.current", "type": "added", "new_elements": [ { … } ] } ], "record": { "basic_profile": { … }, "experience": { … }, "crustdata_person_id": 6324687 } } ] } } ] } ``` ## Smoke-test your webhook Before a real change ever fires, push **one sample notification** through a watch to verify your receiver end-to-end — signature check, JSON parsing, routing — without waiting for a person to actually move. The test delivers the **exact envelope a real run sends** (same shape, same signature headers), flagged with `metadata.test: true`. It runs no diff, records no snapshot, **costs no credits**, and persists nothing. ``` POST https://api.crustdata.com/watch/person/{watch_id}/test ``` All three body fields are optional: | Field | Type | Description | | --- | --- | --- | | `notification_endpoint` | string | Deliver this one test to a URL you control instead of the watch's configured channels — handy with a request-inspection service. Must be a **public** `http(s)` URL; endpoints resolving to private, loopback, or link-local addresses are rejected. | | `fields` | string[] | Project the sample `record` to a specific field set for this test (defaults to the watch's own `fields`). | | `count` | integer | Deliver `N` sample people (`1`–`25`) in one envelope to exercise multi-record handling. Each gets a distinct `crustdata_person_id`. Default `1`. | This request and response are real: ```bash Request curl --request POST \ --url https://api.crustdata.com/watch/person/47356/test \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "notification_endpoint": "https://your-server.example.com/webhook", "fields": ["basic_profile", "experience"] }' ``` ```json Response { "delivered": [ { "type": "webhook", "endpoint": "https://your-server.example.com/webhook", "http_status": 200, "ok": true, "response": "…your receiver's response body, truncated to 500 chars…" } ], "envelope": { "…the exact body delivered to your webhook, shown below…" } } ``` Your receiver gets this `POST`. It is the shape of a real notification — only `metadata.test: true` and the **sample values** (`"sample_text"`, `1234`, …) mark it as a test. The `changes` array below is abbreviated to two entries; the full array mirrors your watch's `track`: ```http Request headers Content-Type: application/json x-api-version: 2025-11-01 x-crustdata-watch-id: 47356 x-crustdata-event-id: ntf_test_1785211467 x-crustdata-signature: t=1785211468,v1= ``` ```json Webhook POST body { "metadata": { "watch_id": 47356, "kind": "entity", "dataset": "person", "api_version": "2025-11-01", "run_id": 1785211467, "notification_id": "ntf_test_1785211467", "delivered_at": "2026-07-28T04:04:27.667425Z", "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 1000, "truncated": false }, "test": true }, "results": [ { "changes": [ { "field": "experience.employment_details.current", "type": "added", "new_elements": [ { "name": "sample_text", "title": "sample_text", "start_date": "sample_text", "is_default": true, "crustdata_company_id": 1234, "seniority_level": "sample_text" } ] }, { "field": "basic_profile.current_title", "type": "changed", "value": null, "from": null, "to": "sample_text" } ], "record": { "crustdata_person_id": 6324687, "basic_profile": { "name": "sample_text", "headline": "sample_text", "first_name": "sample_text", "last_name": "sample_text", "current_title": "sample_text", "summary": "sample_text", "location": { "city": "sample_text", "state": "sample_text", "country": "sample_text" } }, "experience": { "employment_details": { "current": [ { "name": "sample_text", "title": "sample_text", "start_date": "sample_text", "is_default": true } ], "past": [ { "name": "sample_text", "title": "sample_text", "start_date": "sample_text", "end_date": "sample_text" } ] } } } } ] } ``` The sample `record` carries **placeholder values** with the exact **shape** of a real notification (every field in the `fields` groups you requested is present), so your parser, signature check, and routing see production structure — just not real person data. A `changed` scalar reports `{ "type": "changed", "value": …, "from": …, "to": … }` (`value` is the threshold target for `>`/`<` operators, `null` for a plain `changed`); an `added` array reports `{ "type": "added", "new_elements": [ … ] }`. Pass `"count": 3` to receive three sample people in one envelope — distinct `crustdata_person_id`s and `summary.delivered: 3` — so you can exercise how your receiver iterates `results`. The signature is computed exactly as for a real notification — HMAC-SHA256 over `.` keyed by your API key, delivered as `x-crustdata-signature: t=,v1=`. A test that verifies here verifies real deliveries. ## Rate limits Watch-management requests are rate-limited to **10 requests per minute** per API key. This bounds bursty create/update loops; steady use is unaffected. Need to *find* new matching people instead of watching a known list? Use the Person Discovery Watcher. Browse the full set of trackable `field` paths for people. # Person Discovery Watcher Source: https://docs.crustdata.com/watcher-docs/person/discovery Turn a person search filter into a recurring feed. A Person Discovery Watcher re-runs your filters on a schedule and pushes new matching people to a webhook or Slack. Includes copy-paste curl recipes for personas, seniority, company size, tenure, and movement signals. A **Person Discovery Watcher** turns a [Person Search](/person-docs/search/introduction) filter into a continuous feed. Each run re-evaluates your filters and delivers only the **new** matches to your channel — webhook or Slack. You get a deduplicated stream of people entering your criteria, without re-running searches yourself. ``` POST https://api.crustdata.com/watch/person/search ``` Pricing: the first run is a free baseline (up to 5 matches), then 0.5 credits per new person delivered. ## Discovery vs. Entity watchers A **Discovery Watcher** finds people you don't know yet — it re-runs a *filter* and surfaces whoever newly matches. Use it to build a feed of candidates or decision-makers entering your ICP. An **Entity Watcher** tracks a *known list* of people and alerts you when their profiles change — a new job, a title bump, a location move. If you already hold the profile URLs, see [Entity Watchers](/watcher-docs/person/entity). ## How it runs `POST` your `filters`, a `config` (schedule + result cap), and one or more `notifications` channels. The response returns a watch `id`. Within seconds, the first run delivers a **free baseline sample of up to 5 matches** so you can confirm the setup and payload shape. No credits are charged. On your schedule (`every_hours`), the watcher re-runs your filters and delivers people **new or refreshed since the previous run**, up to `max_results_per_run`. You're charged per delivered person. Each run delivers matches whose profile was **added or updated since the last run** — a feed of *movement* within your filter set, not a one-time export. To pull the full current match set, use [Person Search](/person-docs/search/introduction). ## Request body | Field | Required | Description | | ----- | -------- | ----------- | | `filters` | Yes | A filter tree, identical to [Person Search](/person-docs/search/reference). `{ "op": "and"\|"or", "conditions": [ { "field", "type", "value" } ] }`. | | `config.trigger` | Yes | `{ "type": "interval", "every_hours": N }` — how often the watch runs (e.g. `1`, `6`, `24`, `168`). | | `config.max_results_per_run` | No | Max records delivered per run. Default `25`. The first (baseline) run is always capped at 5. | | `config.expires_at` | No | ISO date (`"2027-01-01"`). The watch auto-stops after this date. | | `notifications` | Yes | One or more delivery channels (see [below](#delivery-channels)). | The response returns the watch `id`: ```json { "id": 46849 } ``` All examples require the headers `authorization: Bearer YOUR_API_KEY`, `content-type: application/json`, and `x-api-version: 2025-11-01`. For the full list of `field` values and operators, see the [Person Search reference](/person-docs/search/reference). ## Filter format Watcher filters use the **same syntax and fields** as [Person Search](/person-docs/search/reference) — no new filter language to learn. Every filter is a tree: ```json { "op": "and", "conditions": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Head of Data" }, { "field": "professional_network.location.raw", "type": "(.)", "value": "United States" } ] } ``` Each leaf is a `{ field, type, value }` triple: `field` is the attribute, `type` is the operator (`(.)` for contains, `=`, `in`, `not_in`, `=>` for ≥, `=<` for ≤, and so on), and `value` is what to match. Each `op` combines its `conditions` with `and` or `or`, and op-groups nest — put an `and` group inside an `or` group's `conditions` for "A **and** B, **or** C". See the [Person Search reference](/person-docs/search/reference) for the full field and operator catalog. ## Recipes ### Persona: title + location Alert daily when new **ML engineers in Paris** appear: ```bash curl --request POST \ --url https://api.crustdata.com/watch/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "Machine Learning Engineer" }, { "field": "professional_network.location.raw", "type": "(.)", "value": "Paris" } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25 }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### Persona at a company-size band New **VPs of Finance at 51–500-employee companies**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/person/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": "experience.employment_details.current.title", "type": "in", "value": ["Chief Financial Officer", "VP Finance", "Vice President Finance"] }, { "field": "experience.employment_details.current.company_headcount_latest", "type": "=>", "value": 51 }, { "field": "experience.employment_details.current.company_headcount_latest", "type": "=<", "value": 500 } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### Seniority + skills New **CXO-level people with machine-learning skills**: ```bash curl --request POST \ --url https://api.crustdata.com/watch/person/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": "experience.employment_details.current.seniority_level", "type": "=", "value": "CXO" }, { "field": "skills.professional_network_skills", "type": "(.)", "value": "machine learning" } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ] }' ``` ### Movement signal: recently changed jobs People who **recently changed jobs** (a new decision-maker signal — best for sales/GTM): ```bash curl --request POST \ --url https://api.crustdata.com/watch/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "VP of Sales" }, { "field": "recently_changed_jobs", "type": "=", "value": true } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### Education filter, excluding companies Stanford alumni who are **not** currently at the big three: ```bash curl --request POST \ --url https://api.crustdata.com/watch/person/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": "education.schools.school", "type": "(.)", "value": "Stanford" }, { "field": "experience.employment_details.current.company_name", "type": "not_in", "value": ["Google", "Meta", "Amazon"] } ] }, "config": { "trigger": { "type": "interval", "every_hours": 168 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ## Delivery channels Every watch needs at least one channel in `notifications`. Add one or more; matches fan out to all of them. ```json Webhook { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata", "headers": { "X-Watch-Name": "ml-engineers-paris" } } ``` ```json Slack { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ``` ```json Multiple [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }, { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ] ``` A Slack channel must be a genuine Slack incoming webhook (`https://hooks.slack.com/services/…`). Any other URL will fail delivery. ## Manage a watch ```bash List runs curl --request GET \ --url 'https://api.crustdata.com/watcher/watches/46849/runs?limit=20' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```bash Run detail (delivered records) curl --request GET \ --url https://api.crustdata.com/watcher/watches/46849/runs/54811/summary \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```bash Cancel a watch curl --request DELETE \ --url https://api.crustdata.com/watcher/watches/46849 \ --header 'authorization: Bearer YOUR_API_KEY' ``` ## Pricing | | Credits | | --- | --- | | First run (baseline, up to 5 matches) | **Free** | | Each new person delivered on later runs | **0.5** | You're charged only for records actually delivered — never for a run that finds nothing new. For the full credit catalog, see [Pricing](/general/pricing). ## Related Watch a known list of people for profile changes. Run the underlying search on demand and pull the full current match set. The complete catalog of filter fields and operators. Credit costs across every Crustdata endpoint. # Search Jobs Source: https://docs.crustdata.com/job-docs/search/introduction 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](/job-docs/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. **Pulling every listing for a set of companies?** Submit an asynchronous [batch job-listings search](/api-reference/batch-apis/submit-a-batch-job-listings-search-job) instead — up to 10 companies per job, with the complete result set delivered as a single downloadable file. ## 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, source platform, employment 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 `[.]` on `content.description` to find an exact keyword or brand, and `(.)` for multi-word descriptive matching (`(.)` is typo-tolerant, so a single keyword can match lookalike words). - **`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](/job-docs/search/introduction). 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 | Detail | Value | | --------------- | ------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/job/search` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header (required) | | **Body** | **Required.** Send `{}` to match the whole dataset; every realistic query uses `filters`. | | **Body keys** | `filters`, `cursor`, `limit` (0–1000, default 20), `sorts`, `fields`, `aggregations` — all optional | | **Response** | `{ "job_listings": [ Job, ... ], "next_cursor": string?, "total_count": integer?, "aggregations"?: [ ... ] }` | | **Errors** | `400` invalid request · `401` unauthorized · `500` internal | ### Guaranteed contract vs current behavior | Topic | What it means | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Endpoint, HTTP method, auth headers | `POST /job/search`, bearer auth, `x-api-version: 2025-11-01`. | | Request body shape | Optional `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](/job-docs/search/reference#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](/job-docs/search/reference) for the detailed catalog, or [Common indexed fields](/job-docs/search/reference#common-indexed-fields) for the most-used subset. | | Pricing and rate limits | See [Pricing](/general/pricing) and [Rate limits](/general/rate-limits) for current numbers. | Worked examples like "SDR hiring in mid-market", "companies that closed Series B", and full-text keyword hunts. Sorting, cursor-based pagination, field selection, and aggregations with `count` and `group_by`. Common indexed fields, annotated `Job` example, full field catalog, id map, bucket metadata, errors. ## Request body | Parameter | Type | Required | Default | Description | | -------------- | ----------------------------- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `filters` | object | No | — | Single `SearchCondition` or nested `SearchConditionGroup`. Omit to match all indexed jobs. | | `cursor` | string | No | — | Opaque cursor from a prior response's `next_cursor`. Pass it to fetch the next page with the same filter, sort, and field set. | | `limit` | integer | No | `20` | Rows per page. Min `0`, max `1000`. Use `0` for aggregation-only queries. | | `sorts` | array of `SearchSort` | No | — | Ordering rules. Each item has `field` (dot-path) and `order` (`asc` or `desc`). Sorts are applied in array order. | | `fields` | string[] | No | all fields | Dot-paths to include in each returned job. Omit to return everything. Always specify `fields` in production for smaller payloads and faster responses. | | `aggregations` | array of `AggregationRequest` | No | — | Roll-up queries. Supports `count` and `group_by`. Use with `limit: 0` if you only want counts. | ### Response body | Field | Type | Description | | -------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `job_listings` | `Job[]` | 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. | ### 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](mailto: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`). ```bash curl 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 }' ``` ```json Response { "job_listings": [ { "company": { "basic_info": { "name": "Stripe" } }, "job_details": { "title": "Software Engineer", "url": "https://www.linkedin.com/jobs/view/4342849906" }, "location": { "raw": "United States" }, "metadata": { "date_added": "2025-12-10T15:57:16" } }, { "company": { "basic_info": { "name": "Stripe" } }, "job_details": { "title": "Software Engineer", "url": "https://www.linkedin.com/jobs/view/4302655996" }, "location": { "raw": "Dublin, County Dublin, Ireland" }, "metadata": { "date_added": "2025-09-23T13:52:55" } } ], "next_cursor": "H4sIAJKOKmoC_xXMSw4CIQwA0KsQ1rNowbbUqxgzgQKZhZE4n4Ux3t1x__I-_nW09T0veVv81XnixFYYFTg3JO2qQDUUNjGG3hjImqiVEFECYr1IN-ylBhW23P3k_DbW_bxuKJQ4EgkBwOQihiiCdD_JPvb8mG0cz7_E9P0BNbRcOogAAAA=", "total_count": 18 } ``` **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. Call [`POST /company/identify`](/company-docs/identify/introduction) 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](/job-docs/autocomplete/introduction) 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](/job-docs/search/reference#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](/job-docs/search/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. --- ## 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](/job-docs/search/reference). For sorting, pagination, field selection, and aggregations, see [Pagination & sorting](/job-docs/search/reference#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. ```bash curl 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. ```json { "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 } ] } ``` ```json { "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 } ] } ``` ```python 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. ```json { "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 } ``` ```json { "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. ```json { "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 } ] } ``` `job_details.source` identifies the platform each posting comes from — `professional_network`, or the platform of the company job board that published it (`workday`, `smartrecruiters`, `greenhouse`, `workable`, `lever`, `icims`, `ashby`, `rippling`, `yc`). Use `in` to keep only postings from specific job-board platforms, and pair it with `job_details.employment_type` to keep only the declared employment type. ```bash curl 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.source", "type": "in", "value": ["greenhouse", "lever", "ashby"] }, { "field": "job_details.employment_type", "type": "=", "value": "full_time" }, { "field": "metadata.date_added", "type": "=>", "value": "2026-01-01" } ] }, "fields": [ "crustdata_job_id", "job_details.title", "job_details.source", "job_details.employment_type", "job_details.url", "company.basic_info.name", "location.raw", "metadata.date_added" ], "sorts": [{ "field": "metadata.date_added", "order": "desc" }], "limit": 2 }' ``` ```json Response { "job_listings": [ { "company": { "basic_info": { "name": "Brotherhood Crusade" } }, "crustdata_job_id": 7490363025689380, "job_details": { "employment_type": "full_time", "source": "lever", "title": "Director, Employment Services", "url": "https://jobs.lever.co/brotherhoodcrusade/34093131-8d82-4dc2-bcf5-dcf765de5f9f" }, "location": { "raw": "Los Angeles, CA" }, "metadata": { "date_added": "2026-07-24T19:54:49" } }, { "company": { "basic_info": { "name": "Filevine" } }, "crustdata_job_id": 8481514540418341, "job_details": { "employment_type": "full_time", "source": "lever", "title": "Sales Performance Data Analyst", "url": "https://jobs.lever.co/filevine/4b142eb0-be3a-4a09-bc05-944cf1569833" }, "location": { "raw": "United States" }, "metadata": { "date_added": "2026-07-23T23:04:58" } } ], "next_cursor": "H4sIAC90ZGoC_x...", "total_count": 57403 } ``` `job_details.employment_type` is currently populated for most postings published on company job boards and is `null` for now when `source` is `professional_network`, so combine it with a `source` filter as shown above. See the [Job details catalog](/job-docs/search/reference#job-details-job_details) for both value vocabularies. Group by `job_details.source` to see how many matching listings each source platform contributes. Each bucket key is one `source` value. Sent without `filters`, it profiles the whole dataset; add `filters` to profile any slice. ```bash curl 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 '{ "limit": 0, "aggregations": [ { "type": "group_by", "field": "job_details.source", "agg": "count", "size": 10 } ] }' ``` ```json Response { "job_listings": [], "next_cursor": null, "total_count": 48713983, "aggregations": [ { "type": "group_by", "field": "job_details.source", "buckets": [ { "key": "professional_network", "count": 45699178 }, { "key": "workday", "count": 1696836 }, { "key": "smartrecruiters", "count": 581815 }, { "key": "greenhouse", "count": 250725 }, { "key": "workable", "count": 151311 }, { "key": "lever", "count": 123552 }, { "key": "icims", "count": 106868 }, { "key": "ashby", "count": 76885 }, { "key": "rippling", "count": 21350 }, { "key": "yc", "count": 5463 } ] } ] } ``` Buckets only appear for `source` values with at least one matching listing, so a filtered query can return fewer than 10 buckets. Counts drift as the dataset is refreshed. ```json { "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`. ```bash curl 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" } ] }' ``` ```json Response { "job_listings": [], "next_cursor": null, "total_count": 549, "aggregations": [{ "type": "count", "field": null, "value": 549 }] } ``` 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. ```bash curl 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 }' ``` ```json Response { "job_listings": [ { "company": { "basic_info": { "name": "Stripe" } }, "job_details": { "title": "Sales Development Representative" }, "location": { "raw": "Bengaluru, Karnataka, India" }, "metadata": { "date_added": "2026-05-20T14:24:08" } } ], "next_cursor": "H4sIACTRQWoC_x...", "total_count": 21 } ``` `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. ```bash curl 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 }' ``` ```json Response { "job_listings": [ { "company": { "basic_info": { "name": "eTeam" } }, "job_details": { "title": "Machine Learning - Machine Learning" }, "location": { "raw": "Irvine, California, United States" }, "metadata": { "date_added": "2025-11-13T14:26:33" } } ], "next_cursor": "H4sIACTRQWoC_x...", "total_count": 50879 } ``` **`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](/job-docs/autocomplete/introduction) to find the exact title, category, or company-name values a filter accepts. - **Paginate and aggregate** — see [Pagination & sorting](/job-docs/search/reference#pagination--sorting) for cursor pagination, sorting, field selection, and aggregations. - **Look up fields** — see [Reference](/job-docs/search/reference) for the full `Job` catalog, id map, bucket metadata, and errors. - **Fetch fresh listings for one company** — see [Live Search](/job-docs/search/live-search). - **Inspect the full schema** — read the [OpenAPI reference](/openapi-specs/2025-11-01/introduction). # Jobs Search reference Source: https://docs.crustdata.com/job-docs/search/reference Reference for Crustdata Jobs Search: common indexed fields, full field catalog, id map, bucket metadata, and errors. Reference material for [Search Jobs](/job-docs/search/introduction): filter grammar and operators, common indexed fields, the full `Job` catalog, id semantics, aggregation bucket metadata, null behavior, and errors. For worked examples, see [Examples](/job-docs/search/introduction#examples). For sorting, pagination, field selection, and aggregations, see [Pagination & sorting](#pagination--sorting). **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](/job-docs/search/introduction). 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. --- ## Filter grammar Every filter describes which **individual job rows** to keep. The API checks each job listing against your filter independently — it never groups or combines rows before filtering. There are two building blocks: | Building block | What it does | | --------------------------------- | ---------------------------------------------------------------------------- | | **`SearchCondition`** (leaf) | Tests one field on one job row — e.g. `title = "Software Engineer"`. | | **`SearchConditionGroup`** (node) | Combines conditions with `and` or `or`. Groups can nest inside other groups. | **Exact-match AND on the same field always returns zero results.** One listing has one title, so `(title = "Software Engineer") AND (title = "Account Executive")` can never match. This applies to `=` and `in`. **All-words operators (`(.)`) work fine in AND.** Because `(.)` checks for individual words — not a contiguous substring — a query like `(title (.) "Software Development") AND (title (.) "Software Engineer")` matches any title containing all three words "Software", "Development", and "Engineer" (e.g. "Software Development Engineer"). Need _companies_ hiring for both role X **and** role Y (two different listings)? Run two separate queries and intersect company ids client-side. See [Companies indexing both Software Engineers and Account Executives](/job-docs/search/introduction#examples). ### Single condition ```json { "filters": { "field": "job_details.category", "type": "=", "value": "Engineering" } } ``` ### AND / OR group ```json { "filters": { "op": "and", "conditions": [ { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, { "field": "job_details.category", "type": "=", "value": "Engineering" }, { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" } ] } } ``` ### Array-field filters and grouping When you filter on a string-array field like `company.basic_info.industries`, the condition is satisfied if **any** element of the array matches. For example: ```json { "field": "company.basic_info.industries", "type": "=", "value": "Technology, Information and Internet" } ``` This matches any company whose `industries` array contains that exact string. Use `(.)` to match words within any element. When you `group_by` on an array field, each array element becomes its own bucket key. A company in two industries contributes one count to **each** of the two industry buckets — so the sum of bucket counts can exceed `total_count` for array fields. --- ## Filter operators Use the table below to pick the right `type` for each condition. Every operator works on indexed fields only. | Operator | `value` shape | Meaning | | -------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `=` | scalar (string/number/boolean) | Exact match. | | `!=` | scalar | Not equal. | | `<` | scalar (numeric or ISO date) | Less than. | | `=<` | scalar (numeric or ISO date) | Less than or equal. **Not** `<=`. | | `>` | scalar (numeric or ISO date) | Greater than. | | `=>` | scalar (numeric or ISO date) | Greater than or equal. **Not** `>=`. | | `in` | array of scalars | Field value is any entry in the array. | | `not_in` | array of scalars | Field value is none of the entries in the array. | | `is_null` | `null` | **Field is null or missing.** The `value` key is required — send `"value": null`; omitting it returns `400`. | | `is_not_null` | `null` | **Field has a non-null value.** Send `"value": null` — the `value` key is required. | | `(.)` | string | **Case-insensitive all-words match, with per-word typo tolerance.** Every word in the query is matched somewhere in the field, **not necessarily next to each other or in the same order** — and each word also matches lookalike words within a small edit distance (roughly 1 edit for short words, 2 for longer ones). `"Software Engineer"` matches `"Software Engineer"`, `"Software Development Engineer"`, and `"Engineer, Software Systems"` — but `"vitally"` also matches listings that contain only `"virtually"` or `"finally"`, and `"SDR"` also matches `"USDR"`. Good for broad multi-word matching in `job_details.title` or `content.description`; for a brand, product, or other exact keyword use `[.]`. | | `[.]` | string | **Case-insensitive exact-phrase match.** The words must appear **contiguously and in order**. `"Software Engineer"` matches `"Senior Software Engineer"` but **not** `"Software Development Engineer"` (extra word in between) and **not** `"Engineer Software"` (wrong order). Use `[.]` when you need precision over recall. | | `geo_distance` | object (see below) | **Geographic radius include.** Keeps jobs located within `distance` of a center point. Only valid on `location` (or `location.raw` — both target the same geo point). See [Geographic radius filters](#geographic-radius-filters-geo_distance--geo_exclude). | | `geo_exclude` | object (see below) | **Geographic radius exclude.** Removes jobs located within `distance` of a center point. Same value format and field restrictions as `geo_distance`. | **Operator footguns.** - Use `=>` for greater-than-or-equal and `=<` for less-than-or-equal — they are **not** `>=` and `<=`. - `in` and `not_in` require **JSON arrays**, not comma-separated strings. - `is_null` / `is_not_null` require the `value` key — send `"value": null`. Omitting `value` returns `400`. - `geo_distance` / `geo_exclude` only work on `location` and `location.raw`. Using them on any other field, or sending a malformed geo value, returns `500` — see [Errors](#errors). ### Geographic radius filters (`geo_distance` / `geo_exclude`) The `value` for `geo_distance` and `geo_exclude` is an object describing a center point and a radius: | Key | Type | Required | Description | | ---------- | ----------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `location` | string | Yes, unless `lat_lng` is provided | Free-form place name (for example `"San Francisco"`). The API geocodes it into coordinates server-side. | | `lat_lng` | `[number, number]` | Yes, unless `location` is provided | Explicit `[latitude, longitude]` center point (latitude `-90` to `90`, longitude `-180` to `180`). Takes precedence over `location` when both are sent. | | `distance` | number | **Yes** | Radius around the center point. Must be positive. | | `unit` | string | No (default `"km"`) | Distance unit: `"km"`, `"mi"`, `"miles"`, `"m"`, `"meters"`, `"ft"`, or `"feet"`. | Jobs within 25 km of San Francisco: ```json { "filters": { "field": "location", "type": "geo_distance", "value": { "location": "San Francisco", "distance": 25, "unit": "km" } }, "limit": 5 } ``` US jobs **outside** a 50-mile radius of New York, combining `geo_exclude` with other conditions in an `and` group: ```json { "filters": { "op": "and", "conditions": [ { "field": "location.country", "type": "=", "value": "United States" }, { "field": "location", "type": "geo_exclude", "value": { "location": "New York", "distance": 50, "unit": "mi" } } ] }, "limit": 5 } ``` If your center point comes from your own geocoder or a map UI, pass `lat_lng` directly — for example `{ "lat_lng": [37.7749, -122.4194], "distance": 25 }` — and the server-side geocoding step is skipped. --- ## Common indexed fields These are the indexed fields most often used in `filters`, `sorts`, and `aggregations.field`. This table is a **summary of the most common paths**, not an authoritative catalog. For the deeper field catalog — including id semantics, null handling, and bucket metadata — see the full [Field reference](#field-reference) below. **Company id filter alias.** The filterable field path uses the short alias `company.basic_info.company_id`, but the response shape returns the same integer at `company.basic_info.crustdata_company_id`. They point to the same value. See [Jobs IDs: a quick map](#jobs-ids-a-quick-map). | Field | Example | | ------------------------------ | ------------------------------------------------------ | | `job_details.title` | `"Software Engineer"` | | `job_details.category` | `"Engineering"`, `"Sales"`, `"Operations"`, `"Others"` | | `job_details.workplace_type` | `"Remote"`, `"Hybrid"`, `"On-site"`, `""` | | `job_details.source` | `"professional_network"`, `"greenhouse"`, `"workday"` | | `job_details.employment_type` | `"full_time"`, `"part_time"`, `"contract"` | | `job_details.reposted_job` | `true` / `false` | | `job_details.url` | `"https://www.linkedin.com/jobs/view/4398377738"` | | Field | Example | | -------------------------------------------- | ------------------------------------------ | | `company.basic_info.company_id` | `631394` | | `company.basic_info.name` | `"Stripe"` | | `company.basic_info.primary_domain` | `"stripe.com"` | | `company.basic_info.professional_network_id` | `"2135371"` | | `company.basic_info.industries` | `["Technology, Information and Internet"]` | | Field | Example | | ------------------------------------------- | -------------- | | `company.headcount.total` | `14522` | | `company.headcount.range` | `"5001-10000"` | | `company.followers.count` | `1335688` | | `company.revenue.estimated.lower_bound_usd` | `500000000` | | Field | Example | | ------------------- | ---------------------------------- | | `location.raw` | `"Melbourne, Victoria, Australia"` | | `location.country` | `"Australia"` | | `location.state` | `"Victoria"` | | `location.district` | `"Southbank"` | | `location.city` | `"Melbourne"` | For radius queries ("within 25 km of San Francisco"), filter on `location` with the `geo_distance` / `geo_exclude` operators — see [Geographic radius filters](#geographic-radius-filters-geo_distance--geo_exclude). | Field | Example | | ----------------------- | -------------------------- | | `content.description` | Full job description text. | | `crustdata_job_id` | `41053563` | | `metadata.date_added` | `"2026-04-07T11:37:29"` | | `metadata.date_updated` | `"2026-04-08T00:00:00"` | Sending a filter on a non-indexed field returns `500` with `Unsupported columns in conditions: ['...']`. Sending an unsupported `group_by` field returns a similar `500` listing every supported aggregation field. --- ## Field reference This section covers the return shape, id semantics, aggregation bucket metadata, and the most important indexed field catalogs in one place. ### Annotated full `Job` example The code fence below uses `jsonc` because it includes inline `//` comments for annotation. Strip the comments before sending it to a strict JSON parser. ```jsonc { "crustdata_job_id": 41053563, // stable job id (use as dedupe key) "job_details": { "job_id": 41053563, // mirrors crustdata_job_id "title": "Integration Engineer (AUNZ)", "category": "Engineering", "workplace_type": "", "source": "professional_network", // posting source platform — see Job details catalog "employment_type": null, // currently populated on postings sourced from company job boards "url": "https://www.linkedin.com/jobs/view/4398377738", "reposted_job": false, "number_of_openings": 1, }, "company": { "basic_info": { "crustdata_company_id": 631394, // filter with company.basic_info.company_id "name": "Stripe", "primary_domain": "stripe.com", "website": "https://stripe.com", "professional_network_id": "2135371", "industries": ["Technology, Information and Internet"], }, "locations": { "country": "USA", "state": "California", "city": "South San Francisco", "street_address": "354 Oyster Point Blvd, South San Francisco, California, United States", }, "headcount": { "total": 7234, "range": "5001-10000", "largest_headcount_country": "USA", }, "followers": { "count": 1335688 }, "revenue": { "estimated": { "lower_bound_usd": 500000000, "upper_bound_usd": 1000000000, }, "public_markets": null, "acquisition_status": "", }, "funding": { "total_investment_usd": 9440247725.0, "valuation_usd": 50000000000.0, "last_fundraise_date": "2026-03-09T00:00:00", "last_round_type": "secondary_market", "num_funding_rounds": 23, "investors": [ "Sequoia Capital", "Andreessen Horowitz", "Founders Fund", ], }, "competitors": { "websites": ["https://plaid.com", "https://paystack.com"], }, }, "location": { "raw": "Melbourne, Victoria, Australia", // as advertised on the posting "city": "Melbourne", // city/state/country are geocoded from raw "district": null, "state": "Victoria", "country": "Australia", // normalized country name "pincode": null, }, "content": { "description": "Stripe is a financial infrastructure platform for businesses...", }, "metadata": { "date_added": "2026-04-07T11:37:29", "date_updated": "2026-04-08T00:00:00", }, } ``` **Nulls are normal.** Nested objects such as `revenue.public_markets`, `location.district`, `location.pincode`, and parts of `company.funding` can legitimately be `null` or missing. ### Jobs IDs: a quick map | ID | Lives on | Purpose | | ----------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `crustdata_job_id` | Top-level on each `Job` | Crustdata job identifier. Use it as your dedupe key in your own store. | | `job_details.job_id` | Inside `Job.job_details` | Secondary job identifier. It currently mirrors `crustdata_job_id` and is kept for backwards compatibility. | | `company.basic_info.crustdata_company_id` | Inside `Job.company.basic_info` | Crustdata company identifier returned on each row. | | `company.basic_info.company_id` | Search filter / aggregation path | Indexed alias for the same company identifier. Use this in `filters.field` and `aggregations.field`. | ### Aggregation bucket metadata When you `group_by` on `company.basic_info.company_id`, each bucket carries a `metadata` object whose keys use bucket-specific names rather than the `Job` response dot-paths: | Bucket metadata key | Equivalent `Job` value | Notes | | ------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `company_name` | `company.basic_info.name` | Plain company name. | | `company_website_domain` | `company.basic_info.primary_domain` | Primary website domain. | | `linkedin_id` | `company.basic_info.professional_network_id` | Public-profile identifier returned only inside aggregation buckets. | | `crustdata_company_id` | `company.basic_info.crustdata_company_id` | Crustdata company id. Defined in the spec as nullable; the bucket `key` already carries this value. | ### Job identifiers | Path | Type | Filter | Sort | Group | Return | Example | | -------------------- | ------- | ------ | ---- | ----- | ------ | ---------- | | `crustdata_job_id` | integer | ✅ | ✅ | — | ✅ | `41053563` | | `job_details.job_id` | integer | — | — | — | ✅ | `41053563` | ### Job details (`job_details.*`) | Path | Type | Filter | Sort | Group | Return | Example | | -------------------------------- | ------- | ------ | ---- | ----- | ------ | ------------------------------------------------- | | `job_details.title` | string | ✅ | — | ✅ | ✅ | `"Software Engineer"` | | `job_details.category` | string | ✅ | — | ✅ | ✅ | `"Engineering"` | | `job_details.workplace_type` | string | ✅ | — | ✅ | ✅ | `"Remote"`, `"Hybrid"`, `"On-site"`, `""` | | `job_details.source` | string | ✅ | — | ✅ | ✅ | `"professional_network"`, `"greenhouse"` | | `job_details.employment_type` | string | ✅ | — | ✅ | ✅ | `"full_time"` | | `job_details.reposted_job` | boolean | ✅ | — | — | ✅ | `false` | | `job_details.url` | string | ✅ | — | — | ✅ | `"https://www.linkedin.com/jobs/view/4398377738"` | | `job_details.number_of_openings` | integer | — | — | — | ✅ | `1` | **`job_details.source` — posting source platform.** Every job carries exactly one of these values: `professional_network` (listings from professional networking platforms), or the platform of the company job board that published the posting — `workday`, `smartrecruiters`, `greenhouse`, `workable`, `lever`, `icims`, `ashby`, `rippling`, or `yc`. Filter with `=` / `in`-style operators or `group_by` on it; it is **not sortable**. **`job_details.employment_type` — employment type declared by the posting.** One of `full_time`, `part_time`, `contract`, `intern`, or `temporary`. Currently populated for roughly 85% of postings published on company job boards, and `null` for now when `source` is `professional_network` — treat missing values as "not declared", not as a specific type. Filterable and groupable; **not sortable**. **Search-only fields.** `job_details.source` and `job_details.employment_type` are available on `POST /job/search` only — [Live Search](/job-docs/search/live-search) does not return them. ### Company basic info (`company.basic_info.*`) | Path | Type | Filter | Sort | Group | Return | Example | | -------------------------------------------- | -------- | ------ | ---- | ----- | ------ | ------------------------------------------ | | `company.basic_info.company_id` | integer | ✅ | — | ✅ | — | `631394` | | `company.basic_info.crustdata_company_id` | integer | — | — | — | ✅ | `631394` | | `company.basic_info.name` | string | ✅ | — | — | ✅ | `"Stripe"` | | `company.basic_info.primary_domain` | string | ✅ | — | ✅ | ✅ | `"stripe.com"` | | `company.basic_info.website` | string | — | — | — | ✅ | `"https://stripe.com"` | | `company.basic_info.professional_network_id` | string | ✅ | — | — | ✅ | `"2135371"` | | `company.basic_info.industries` | string[] | ✅ | — | ✅ | ✅ | `["Technology, Information and Internet"]` | `company.basic_info.company_id` and `company.basic_info.crustdata_company_id` refer to the same integer. Use the short alias in `filters` and `aggregations.field`. The response shape writes the value under `crustdata_company_id`. ### Company firmographics #### Headcount (`company.headcount.*`) | Path | Type | Filter | Sort | Group | Return | Example | | --------------------------------------------- | ------- | ------ | ---- | ----- | ------ | -------------- | | `company.headcount.total` | integer | ✅ | ✅ | — | ✅ | `14522` | | `company.headcount.range` | string | ✅ | — | ✅ | ✅ | `"5001-10000"` | | `company.headcount.largest_headcount_country` | string | — | — | — | ✅ | `"USA"` | #### Followers (`company.followers.*`) | Path | Type | Filter | Sort | Group | Return | Example | | ------------------------- | ------- | ------ | ---- | ----- | ------ | --------- | | `company.followers.count` | integer | ✅ | ✅ | — | ✅ | `1335688` | #### Revenue (`company.revenue.*`) | Path | Type | Filter | Sort | Group | Return | Example | | ------------------------------------------------ | -------- | ------ | ---- | ----- | ------ | ------------ | | `company.revenue.estimated.lower_bound_usd` | integer | ✅ | ✅ | — | ✅ | `500000000` | | `company.revenue.estimated.upper_bound_usd` | integer | ✅ | — | — | ✅ | `1000000000` | | `company.revenue.acquisition_status` | string | ✅ | — | — | ✅ | `""` | | `company.revenue.public_markets.stock_symbols` | string[] | ✅ | — | — | ✅ | `["STRIPE"]` | | `company.revenue.public_markets.fiscal_year_end` | string | — | — | — | ✅ | `""` | #### Funding (`company.funding.*`) | Path | Type | Filter | Sort | Group | Return | Example | | -------------------------------------- | ----------------- | ------ | ---- | ----- | ------ | ----------------------- | | `company.funding.total_investment_usd` | number | ✅ | — | — | ✅ | `9440247725.0` | | `company.funding.valuation_usd` | number | ✅ | — | — | ✅ | `50000000000.0` | | `company.funding.last_fundraise_date` | string (ISO 8601) | ✅ | ✅ | — | ✅ | `"2026-03-09T00:00:00"` | | `company.funding.last_round_type` | string | ✅ | — | ✅ | ✅ | `"secondary_market"` | | `company.funding.num_funding_rounds` | integer | ✅ | ✅ | — | ✅ | `23` | | `company.funding.investors` | string[] | ✅ | — | — | ✅ | `["Sequoia Capital"]` | #### Competitors and company locations | Path | Type | Filter | Sort | Group | Return | Example | | ---------------------------------- | -------- | ------ | ---- | ----- | ------ | ------------------------------ | | `company.competitors.websites` | string[] | ✅ | — | — | ✅ | `["https://plaid.com"]` | | `company.locations.country` | string | ✅ | — | ✅ | ✅ | `"USA"` | | `company.locations.state` | string | ✅ | — | — | ✅ | `"California"` | | `company.locations.city` | string | ✅ | — | — | ✅ | `"South San Francisco"` | | `company.locations.street_address` | string | ✅ | — | — | ✅ | `"354 Oyster Point Blvd, ..."` | ### Job location (`location.*`) The `city`, `state`, and `country` fields are derived by **geocoding** the raw location string, so they carry normalized place names rather than the raw text's wording. `city` can be an empty string `""` when the raw location resolves to an area broader than a city (for example `"San Francisco Bay Area"` geocodes to `state: "California"` with an empty `city`). | Path | Type | Filter | Sort | Group | Return | Example | | ------------------- | --------- | ------------ | ---- | ----- | ------ | ---------------------------------- | | `location` | geo point | ✅ (geo only) | — | — | — | [Geographic radius filters](#geographic-radius-filters-geo_distance--geo_exclude) | | `location.raw` | string | ✅ | — | — | ✅ | `"Melbourne, Victoria, Australia"` | | `location.city` | string | ✅ | — | — | ✅ | `"Melbourne"` | | `location.district` | string | ✅ | — | — | ✅ | `"Southbank"` | | `location.state` | string | ✅ | — | — | ✅ | `"Victoria"` | | `location.country` | string | ✅ | — | ✅ | ✅ | `"Australia"` | | `location.pincode` | string | — | — | — | ✅ | `"3006"` | **Country values are geocoded and normalized.** `location.country` carries normalized full country names (`"United States"`, `"United Kingdom"`). A small share of rows still carries a residual variant such as `"United States of America"`. When completeness matters, match both with `in`, or pre-discover the exact indexed values by running a `group_by` on `location.country`. ### Content (`content.*`) | Path | Type | Filter | Sort | Group | Return | Example | | --------------------- | ------ | ------ | ---- | ----- | ------ | ------------------------------------------- | | `content.description` | string | ✅ | — | — | ✅ | `"Stripe is a financial infrastructure..."` | To find listings by an exact technology, skill, brand, or keyword, use `[.]` on `content.description` — it guarantees the literal word or phrase is present. `(.)` is typo-tolerant, so a single keyword can match lookalike words instead (`(.) "vitally"` also matches descriptions that contain only "virtually" or "finally"). Use `(.)` for multi-word descriptive matching. ### Metadata (`metadata.*`) | Path | Type | Filter | Sort | Group | Return | Example | | ----------------------- | ----------------- | ------ | ---- | ----- | ------ | ----------------------- | | `metadata.date_added` | string (ISO 8601) | ✅ | ✅ | — | ✅ | `"2026-04-07T11:37:29"` | | `metadata.date_updated` | string (ISO 8601) | ✅ | ✅ | — | ✅ | `"2026-04-08T00:00:00"` | ### Null, blank, and sparse field behavior Most `Job` fields are nullable in the spec and can legitimately be absent or empty. - **Null or missing** — the field is not present on a given `Job`. - **Blank string `""`** — the field was present but had no indexable value (common for `job_details.workplace_type`). Treat blank as "unspecified", not as the same thing as null. - **Sparse nested objects** — `company.funding`, `company.revenue`, and `company.competitors` are often missing for smaller or private companies. - **`is_null` / `is_not_null` operators** filter on null or missing fields directly — send `"value": null` (the `value` key is required). --- ## Errors | Status | `error.type` | Meaning | | ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_request` | Request body failed validation — `limit` out of range, a missing required key, or an unrecognized parameter (for example sending `column` instead of `field`). | | `401` | `unauthorized` | The `Authorization` header is missing, malformed, or contains an invalid API key. | | `500` | `internal_error` | The query could not be executed — including when a filter or aggregation references an unsupported field, when a `geo_distance`/`geo_exclude` value is malformed or used on a non-geo field, or when a geo `location` string cannot be resolved to coordinates. Also covers transient server errors; retry after a short delay. | Every error — including `401` — uses the same nested envelope: `{ "error": { "type", "message", "metadata" } }`. Branch on `error.type` rather than string-matching `message`. ```json 400 — limit out of range { "error": { "type": "invalid_request", "message": "'limit' must be at most 1000. Got 5000.", "metadata": [ { "field": "limit", "type": "less_than_equal", "message": "'limit' must be at most 1000. Got 5000." } ] } } ``` ```json 400 — column used instead of field { "error": { "type": "invalid_request", "message": "'aggregations.0.column' is not a valid parameter.", "metadata": [ { "field": "aggregations.0.column", "type": "extra_forbidden", "message": "'aggregations.0.column' is not a valid parameter." } ] } } ``` ```json 401 — Invalid API key { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` ```json 500 — Unsupported filter field { "error": { "type": "internal_error", "message": "Unsupported columns in conditions: ['invalid_field']", "metadata": [] } } ``` ```json 500 — Malformed geo filter value { "error": { "type": "internal_error", "message": "geo_distance filter requires 'distance' field", "metadata": [] } } ``` ```json 500 — Unsupported group_by field { "error": { "type": "internal_error", "message": "Unsupported aggregation field: 'company.basic_info.name'. Supported: company.basic_info.company_id, company.basic_info.crustdata_company_id, company.basic_info.industries, company.basic_info.primary_domain, company.funding.last_round_type, company.headcount.range, company.locations.country, job_details.category, job_details.employment_type, job_details.source, job_details.title, job_details.workplace_type, location.country", "metadata": [] } } ``` ## Pagination & sorting How to paginate, sort, select fields, and aggregate results in [Search Jobs](/job-docs/search/introduction). For worked examples, see [Examples](/job-docs/search/introduction#examples). For filter grammar, operators, and the full field catalog, see [Reference](/job-docs/search/reference). Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. --- ## Sorting `sorts` is an ordered array. Each item has a `field` and `order` (`"asc"` or `"desc"`). Sorts apply in array order — the first sort is the primary key, the second breaks ties, and so on. ```json { "sorts": [ { "field": "metadata.date_added", "order": "desc" }, { "field": "company.headcount.total", "order": "desc" } ] } ``` **Sort allowlist is narrower than filter allowlist.** Sort only works on numeric, date, and a small set of scalar fields. Sorting on text fields like `job_details.title`, `job_details.category`, or `company.basic_info.name` returns `Unsupported columns in conditions`. ### Sortable fields The following indexed fields are verified sortable: - `crustdata_job_id` - `metadata.date_added` - `metadata.date_updated` - `company.headcount.total` - `company.followers.count` - `company.revenue.estimated.lower_bound_usd` - `company.funding.last_fundraise_date` - `company.funding.num_funding_rounds` - `relevance` — only valid when a text query is present in `filters` (maps to the text-match score); sorting on it without a text condition returns `400` Common sort choices: - **Newest postings first** — `{ "field": "metadata.date_added", "order": "desc" }` - **Biggest companies first** — `{ "field": "company.headcount.total", "order": "desc" }` - **Most followed companies first** — `{ "field": "company.followers.count", "order": "desc" }` - **Most funding rounds first** — `{ "field": "company.funding.num_funding_rounds", "order": "desc" }` --- ## Pagination Pagination is **cursor-based**. Each response returns a `next_cursor` (or `null` when you reach the end). To fetch the next page, resend the original request body with `cursor` set to the previous `next_cursor`. Omit `cursor` and set `limit` to your page size (max `1000`). Take `next_cursor` from the response and pass it back as `cursor` in the next request. Keep `filters`, `sorts`, and `fields` identical — if you change them, the cursor becomes meaningless. A `null` cursor means you've reached the end of the result set. ### Consistency between pages **Best-effort, not strict snapshot.** A cursor is consistent with respect to the filter, sort, and field selection you sent on the first page, so the same query will keep paging forward over a coherent result stream. However, because the underlying indexed dataset is continuously updated, new jobs indexed between page requests can cause minor drift in `total_count` and in the exact position of individual rows. Treat pagination as **best-effort**, not a strict snapshot. For bulk exports where every row matters: - **Constrain your filter to a bounded date window** (for example `metadata.date_added >= 2025-01-01` AND `< 2025-07-01`) so newly indexed jobs outside the window do not affect the walk, and - **Re-run the full walk periodically** and diff against the prior snapshot using `crustdata_job_id` as the dedupe key. ### Dataset freshness and lifecycle **What the indexed Jobs dataset represents.** The Search Jobs dataset is a rolling index of job listings discovered from the web, refreshed on an ongoing basis. Each row has: - **`metadata.date_added`** — when Job was posted. Closed or removed listings are **not** guaranteed to disappear from the index immediately. To approximate "currently hiring" queries, filter on a recent `metadata.date_added` window (for example, within the last 30 days) and pair it with the hiring company's firmographics. For alerting or repeated exports, keep your date windows bounded and dedupe rows with `crustdata_job_id`. ### Date filter semantics **Dates and timezones.** When you pass a date-only value like `"2025-01-01"`, the backend interprets it as `2025-01-01T00:00:00` in UTC. Ranges using `=>` are inclusive of the boundary and `<` is exclusive, so `"metadata.date_added" >= "2025-01-01"` **AND** `< "2025-07-01"` covers every listing indexed between Jan 1 (inclusive) and Jul 1 (exclusive) in UTC. Pass full timestamps like `"2025-01-01T08:00:00"` when you need finer precision. ### Fetch page 2 ```json { "filters": { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, "fields": ["job_details.title"], "limit": 1, "cursor": "H4sIANBG1mkC_xXMOQ4CMQxA0auMUk9hx3YScxWERs6CpkBEzFIgxN0J1W-e_se9zra9l9X21V0mx0kjMgvdq4dYPZGgclOTlLEZl2bJG_lcdVSqFa-okDlwo1qbmye39-0YryvGKBIwsQLAPDGCkAS6DXL0wx5L6efzL2MC_P4A250zQYoAAAA=" } ``` --- ## Field selection Use `fields` to return only the dot-paths you need. The top-level groups are `crustdata_job_id`, `job_details`, `company`, `location`, `content`, `metadata`. You can request: - **A whole group** — `"company"` returns every `company.*` sub-object. - **A sub-object** — `"company.basic_info"` returns only the basic info block. - **A single field** — `"company.basic_info.name"` returns just the name. ```json { "fields": [ "job_details.title", "job_details.url", "company.basic_info.name", "company.basic_info.primary_domain", "location.raw", "metadata.date_added" ] } ``` **Recommended default field set** for most dashboards: `["job_details.title", "job_details.category", "job_details.url", "company.basic_info.name", "company.basic_info.primary_domain", "location.raw", "metadata.date_added"]`. --- ## Aggregations Aggregations let you roll up results without returning individual job rows. Set `limit: 0` when you only want aggregation output. Two types are supported: - **`count`** — returns the total number of jobs matching `filters`. - **`group_by`** — buckets the results by `field` and returns per-bucket counts. ### `AggregationRequest` schema | Field | Type | Required | Description | | ------- | ------------- | ----------------------- | -------------------------------------------------------------------------------------- | | `type` | string (enum) | **Yes** | `"count"` for a simple total, `"group_by"` to bucket by `field`. | | `field` | string | Required for `group_by` | Dot-path to group by. Must be in the [Groupable fields](#groupable-fields) allowlist. | | `agg` | string (enum) | Required for `group_by` | Sub-aggregation inside each bucket. Currently only `"count"` is supported. | | `size` | integer | No (default `100`) | Maximum number of buckets to return. Min `1`, max `1000`. | Each `AggregationResponseItem` echoes `type` and `field`, then carries: - **`value`** (integer) — populated for `count` aggregations. The total match count. - **`buckets`** (array) — populated for `group_by` aggregations. Each bucket has a `key`, `count`, and a `metadata` object whose keys depend on the grouped field. See [Aggregation bucket metadata](/job-docs/search/reference#aggregation-bucket-metadata). You can include multiple aggregations in a single request; the response returns them in `aggregations[]` in the same order you sent them. ### Count all Engineering jobs ```bash curl 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": { "field": "job_details.category", "type": "=", "value": "Engineering" }, "limit": 0, "aggregations": [ { "type": "count" } ] }' ``` ```json Response { "job_listings": [], "next_cursor": null, "total_count": 4448237, "aggregations": [{ "type": "count", "field": null, "value": 4448237 }] } ``` ### Top companies indexing "Software Engineer" listings (bounded window) ```bash curl 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": "Software Engineer" }, { "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": "company.basic_info.company_id", "agg": "count", "size": 5 } ] }' ``` ```json Response { "job_listings": [], "next_cursor": null, "total_count": 25748, "aggregations": [ { "type": "group_by", "field": "company.basic_info.company_id", "buckets": [ { "key": 3674630, "count": 836, "metadata": { "company_name": "Bending Spoons", "company_website_domain": "bndspn.com", "linkedin_id": "3175130" } }, { "key": 821755, "count": 794, "metadata": { "company_name": "Jobs via Dice", "company_website_domain": "dice.com", "linkedin_id": "104085107" } }, { "key": 899641, "count": 490, "metadata": { "company_name": "Lensa", "company_website_domain": "lensa.com", "linkedin_id": "5192530" } }, { "key": 2110301, "count": 438, "metadata": { "company_name": "Microsoft", "company_website_domain": "microsoft.com", "linkedin_id": "1035" } }, { "key": 2676775, "count": 379, "metadata": { "company_name": "Goliath Partners", "company_website_domain": "goliathpartners.com", "linkedin_id": "92699700" } } ] } ] } ``` ### Groupable fields `group_by.field` is restricted to the following indexed fields: - `company.basic_info.company_id` - `company.basic_info.crustdata_company_id` - `company.basic_info.industries` - `company.basic_info.primary_domain` - `company.funding.last_round_type` - `company.headcount.range` - `company.locations.country` - `job_details.category` - `job_details.employment_type` - `job_details.source` - `job_details.title` - `job_details.workplace_type` - `location.country` Sending any other field returns `500` with `Unsupported aggregation field: '...'. Supported: ...`. --- ## What's next - [Search Jobs](/job-docs/search/introduction) — back to the main Search page. - [Examples](/job-docs/search/introduction#examples) — SDR/BDR keyword hunting, mid-market filtering, funding-triggered queries, and aggregations. - [Pagination & sorting](#pagination--sorting) — sorting, pagination, field selection, and aggregations. - [OpenAPI reference](/openapi-specs/2025-11-01/introduction) — the formal schema for every request, response, and error. # Job Autocomplete Source: https://docs.crustdata.com/job-docs/autocomplete/introduction Discover valid field values for Job Search filters using the autocomplete API. **Use this when** you need to discover exact indexed values before building a [Search Jobs](/job-docs/search/introduction) query — for filter dropdowns, input validation, dataset exploration, or guided query builders. The Job Autocomplete API returns ranked field-value suggestions so you can feed the result straight into a Search Jobs filter without guessing at valid values. This endpoint returns **field values, not job listings**. It also has **no pagination or cursor** — the `limit` parameter (max `100`) is the only way to control result size. To fetch job records, use [Search Jobs](/job-docs/search/introduction) instead. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ### At a glance | Detail | Value | | --------------- | ----------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/job/search/autocomplete` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header (required) | | **Required** | `field` (string) · `query` (string, may be empty) | | **Optional** | `limit` (integer, 1–100, default 20) | | **Response** | `{ "suggestions": [ { "value": string } ] }` | | **Errors** | `400` invalid request · `401` unauthorized · `500` internal | | **Pricing** | Free — autocomplete does not consume credits. | Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. --- ## When to use Autocomplete vs Search | You want to… | Use | | --------------------------------------------- | ------------------------------------------------ | | Discover valid filter values for a field | **Autocomplete** (this page) | | See the distinct values a field takes, ranked | **Autocomplete** with an empty `query` | | Return actual job listings matching filters | [**Search Jobs**](/job-docs/search/introduction) | | Build a type-ahead dropdown for a filter UI | **Autocomplete** with a partial `query` | --- ## Quick start: discover job title values Type-ahead lookup on a single field. Pass the user's partial input as `query` and cap the dropdown size with `limit`. ```bash curl curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "title", "query": "Software", "limit": 5 }' ``` ```python Python import os import requests response = requests.post( "https://api.crustdata.com/job/search/autocomplete", headers={ "Authorization": f"Bearer {os.environ['CRUSTDATA_API_KEY']}", "Content-Type": "application/json", "x-api-version": "2025-11-01", }, json={ "field": "title", "query": "Software", "limit": 5, }, ) response.raise_for_status() suggestions = response.json()["suggestions"] ``` ```javascript Node.js const response = await fetch( "https://api.crustdata.com/job/search/autocomplete", { method: "POST", headers: { Authorization: `Bearer ${process.env.CRUSTDATA_API_KEY}`, "Content-Type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify({ field: "title", query: "Software", limit: 5, }), }, ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { suggestions } = await response.json(); ``` ```json Response { "suggestions": [ { "value": "Software Deployment Manager Level 3 or 4" }, { "value": "Software Test Engineer Level 1/2 (AHT)" }, { "value": "Software Engineer" }, { "value": "Software Engineer, AI/ML, Google Workspace" }, { "value": "Software Engineering Manager, VP – Private Wealth Technology" } ] } ``` Each suggestion includes: - **`value`** — the exact **indexed value** stored against `field`. Use it verbatim as a Search Jobs filter value. When no values match the `query`, the response returns an empty array — not a 404: ```json No results { "suggestions": [] } ``` --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request you can adapt to your own fields and filters. For the core walkthrough (quick start, response shape), see the [Quick start](#quick-start-discover-job-title-values) above. For request parameters, autocomplete-enabled fields, and errors, see the [reference sections](#request-parameters) below. Pass an empty `query` to retrieve the top values for a field, ranked. This is ideal for a controlled vocabulary like `category` — it returns the full set of categories without an extra round-trip, perfect for seeding a filter dropdown. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "category", "query": "", "limit": 8 }' ``` ```json Response { "suggestions": [ { "value": "Others" }, { "value": "Engineering" }, { "value": "Sales" }, { "value": "Product" }, { "value": "Operations" }, { "value": "Consultancy" }, { "value": "Management and Manufacturing" }, { "value": "Project Management" } ] } ``` Free-text fields like `title` have a large value space. Pass a partial `query` to power a type-ahead input. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "title", "query": "Software", "limit": 6 }' ``` ```json Response { "suggestions": [ { "value": "Software Deployment Manager Level 3 or 4" }, { "value": "Software Test Engineer Level 1/2 (AHT)" }, { "value": "Software Engineer" }, { "value": "Software Engineer, AI/ML, Google Workspace" }, { "value": "Software Engineering Manager, VP – Private Wealth Technology" }, { "value": "Software Engineer - Guidance, Navigation, and Controls" } ] } ``` The same endpoint works for company names and locations. Use `company.name` to suggest hiring companies, or `location.country` to suggest countries. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "company.name", "query": "Strip", "limit": 5 }' ``` ```json Response { "suggestions": [ { "value": "Stripe" }, { "value": "StripMallGuy" }, { "value": "Stripes" }, { "value": "StripFood" }, { "value": "Stripes Convenience Stores" } ] } ``` ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "location.country", "query": "United", "limit": 5 }' ``` ```json Response { "suggestions": [ { "value": "United States" }, { "value": "United States of America" }, { "value": "United Kingdom" }, { "value": "United Arab Emirates" }, { "value": "United States Minor Outlying Islands" } ] } ``` Note that a single country can have several indexed spellings (for example, `United States` and `United States of America`). When you filter Search Jobs, pass all the variants you care about with the `in` operator. Autocomplete discovers the exact value Search Jobs expects, then Search finds matching listings. ### Step 1: Discover a valid category value ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"field": "category", "query": "Eng", "limit": 3}' ``` ```json Response { "suggestions": [ { "value": "Engineering" }, { "value": "Engineering and Information Technology" }, { "value": "Information Technology and Engineering" } ] } ``` **Extract:** Take `suggestions[0].value` → `"Engineering"`. Use this exact string in your Search filter. ### Step 2: Search for matching jobs ```bash Request 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": { "field": "job_details.category", "type": "=", "value": "Engineering" }, "fields": ["job_details.title", "company.basic_info.name", "location.raw"], "limit": 1 }' ``` ```json Response { "job_listings": [ { "company": { "basic_info": { "name": "Accenture" } }, "job_details": { "title": "French Application Developer" }, "location": { "raw": "Bengaluru, India" } } ], "next_cursor": "H4sIAAKbKmoC_xXMSQoCMRBA0auErHtRk0nFq4g0lXRCL8RgDwsR727cfh7_419n3d7zavvqr85X0EtOikoLopouZAGq5cwYRS1KRi2Rc6ytlSgt1YSNEwgHAgHwk_N7347xuhGpJqQwMsDkQgoMdB_g6Ic95tLP59-JiBLH7w-dPgQbiwAAAA==", "total_count": 4448237 } ``` See [Search Jobs](/job-docs/search/introduction) for the full filter grammar, and [Examples](/job-docs/search/introduction#examples) for more end-to-end recipes. --- ## Guaranteed contract vs current behavior Use this table to separate the parts you can build against with confidence from the observed behavior that may evolve. | Topic | Kind | What it means | | ----------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------- | | Endpoint, HTTP method, auth headers | **Contract** | `POST /job/search/autocomplete`, bearer auth, `x-api-version: 2025-11-01`. | | Request body shape | **Contract** | `field` and `query` required, `limit` optional. No `filters` or `count` keys on this scope. | | Response body shape | **Contract** | `{ "suggestions": [ { "value" } ] }`. Empty results return `{"suggestions": []}` with status `200`. | | `limit` bounds and default | **Contract** | Minimum `1`, maximum `100`, default `20`. | | Error status codes | **Contract** | `400` (invalid request), `401` (unauthorized), `500` (internal). | | Pricing | **Contract** | Autocomplete does not consume credits. | | Suggestion ranking | Current behavior | Suggestions are ranked by internal frequency — the ranking signal is not exposed in the response. | | Empty-query top values | Current behavior | An empty `query` returns the most common values for the field, ranked. | | Free-text duplicates | Current behavior | For high-cardinality free-text fields like `title`, an empty `query` may return repeated values. | --- ## Request parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `field` | string | Yes | Must be an **autocomplete-enabled field** — see [Autocomplete-enabled fields](#autocomplete-enabled-fields). Unsupported fields return `400`. | | `query` | string | Yes | Partial text to match against indexed values. Pass `""` to retrieve the top values for the field by frequency. | | `limit` | integer | No | Maximum number of suggestions to return. Minimum `1`, maximum `100`, default `20`. | Unlike [Person Autocomplete](/person-docs/autocomplete/introduction), Job Autocomplete does **not** accept a `filters` scope. The request body is just `field`, `query`, and `limit`. --- ## Autocomplete-enabled fields The `field` in a Job Autocomplete request must come from a **fixed allowlist**. Many entries are aliases of one another — for example `title`, `job_title`, and `job_details.title` all autocomplete job titles, so you can use whichever spelling matches the filter path you use on [Search Jobs](/job-docs/search/introduction). | Field | What it discovers | | ---------------------------------------------------- | ----------------- | | `title` · `job_title` · `job_details.title` | Job titles | | `category` · `job_category` · `job_details.category` | Job categories | | `workplace_type` · `job_details.workplace_type` | Workplace types | | Field | What it discovers | | ---------------------------------------------------------------------------------- | --------------------------- | | `company.name` · `company.basic_info.name` | Hiring company names | | `company.industries` · `company.basic_info.industries` | Company industries | | `company.basic_info.primary_domain` | Company website domains | | `company.headcount.range` | Headcount brackets | | `company.headcount.largest_country` | Largest-headcount countries | | `company.funding.last_round_type` | Funding round types | | `company.funding.investors` | Investors | | `company.revenue.acquisition_status` | Acquisition statuses | | `company.revenue.public_markets.stock_symbols` | Stock symbols | | `company.revenue.public_markets.fiscal_year_end` | Fiscal year ends | | `company.locations.city` · `company.locations.state` · `company.locations.country` | Company office locations | | Field | What it discovers | | ------------------------------------------------------- | -------------------- | | `country` · `location.country` · `locations.country` | Countries | | `country_code` | Country codes | | `continent` | Continents | | `state` · `location.state` · `locations.state` | States / regions | | `city` · `location.city` · `locations.city` | Cities | | `district` · `location.district` · `locations.district` | Districts | | `location` · `location.raw` · `locations.location` | Raw location strings | ### Verify the live list Call the endpoint with a deliberately invalid `field`. The `400` response lists every currently accepted field in its error `message`: ```bash Request curl --request POST \ --url https://api.crustdata.com/job/search/autocomplete \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "field": "not_a_field", "query": "", "limit": 1 }' ``` ```json Response (abbreviated) { "error": { "type": "invalid_request", "message": "Field 'not_a_field' is not supported on scope 'job'. Valid fields: category, city, company.basic_info.industries, company.basic_info.name, ..., job_details.title, job_title, location, ..., state, title, workplace_type", "metadata": [] } } ``` --- ## Implementation tips for UI builders - **Debounce** autocomplete calls to avoid one request per keystroke — 150–300 ms on input idle works well for typeahead UIs. - **Seed dropdowns with an empty query.** For controlled vocabularies like `category`, an empty `query` returns the full set in one call. - **De-duplicate free-text fields.** High-cardinality fields like `title` can return repeated values — collapse duplicates before rendering. - **Expand location variants.** A country may have several indexed spellings (for example `United States` and `United States of America`); pass all the variants you need to a Search Jobs filter with the `in` operator. - **Cap `limit`.** Most dropdowns need 5–15 options. A lower `limit` reduces payload size and speeds up responses. --- ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------ | | `400` | Invalid request — unsupported `field`, missing `query`, or `limit` out of range. | | `401` | Unauthorized — the `Authorization` header is missing, malformed, or contains an invalid API key. | | `500` | Internal server error — retry after a short delay. | Every error uses the nested envelope `{ "error": { "type", "message", "metadata" } }`. A `query` that matches nothing is **not** an error — the endpoint returns `{"suggestions": []}` with a `200` status. ```json 400 — Unsupported field { "error": { "type": "invalid_request", "message": "Field 'not_a_field' is not supported on scope 'job'. Valid fields: category, city, ..., job_details.title, job_title, ..., state, title, workplace_type", "metadata": [] } } ``` ```json 400 — Missing query { "error": { "type": "invalid_request", "message": "query is required", "metadata": [] } } ``` ```json 400 — limit out of range { "error": { "type": "invalid_request", "message": "limit must be between 1 and 100", "metadata": [] } } ``` ```json 401 — Invalid API key { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` --- ## API reference summary | Detail | Value | | ------------------- | ------------------------------------------------------------------------------- | | **Endpoint** | `POST /job/search/autocomplete` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Required params** | `field`, `query` | | **Optional params** | `limit` (default: 20, max: 100) | | **Response** | `{ "suggestions": [{ "value": "..." }] }` | | **Empty result** | `200` with `"suggestions": []` | | **Pricing** | Free — no credits | | **Errors** | `400` (unsupported field / bad request), `401` (bad auth), `500` (server error) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Try more patterns** — see the [Examples](#examples) above for most-common values, company and location autocomplete, and the Autocomplete → Search workflow. - **Reference** — see the [request parameters](#request-parameters), [autocomplete-enabled fields](#autocomplete-enabled-fields), implementation tips, and errors above. - **Search for jobs** — use discovered values in [Search Jobs](/job-docs/search/introduction). # Job Batch Search Source: https://docs.crustdata.com/job-docs/search/batch-search Pull every job listing for up to 10 companies in one asynchronous job and receive the complete result set as a single file. Batch job search runs one asynchronous job and delivers the **entire result set** as a single file. Where [Search Jobs](/job-docs/search/introduction) returns one cursor page per call, a batch job walks every page for you and returns **all** listings for the companies you name. Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. Pricing: billing is based on the number of records actually delivered in the results file (entities_fulfilled), not on how many you requested. Failed jobs are not charged. See [Pricing](/general/pricing) for per-endpoint rates. --- ## Batch search ``` POST https://api.crustdata.com/batch/job/search ``` Retrieve every job listing in the dataset for up to **10 companies** in a single asynchronous job. Records in the downloaded file are flat, identical to the non-batch [Search Jobs](/job-docs/search/reference) record shape. ### How batch job search differs from non-batch search - **Companies, not filters.** Provide a `crustdata_company_ids` list — a JSON array of integers (a comma-separated string is rejected). The job returns **all** listings for those companies. To search listings by arbitrary filters, use the non-batch [Search Jobs](/job-docs/search/reference) instead. - **Up to 10 companies per job.** More than 10 IDs returns `400`. Resolve IDs from a name, domain, or profile URL with the free [Company Identify](/company-docs/identify/introduction) endpoint. - **Whole result set.** The job paginates server-side and delivers every listing — there is no `max_results` or cursor control. - **Flat records.** Each line in the results file has exactly the non-batch [Search Jobs](/job-docs/search/reference) record shape. No envelope. - **Exact field projection.** When you pass `fields`, each record contains exactly those dotted paths — nothing more. Omit `fields` to get every field your account can read. The selectable fields are the same as non-batch Search Jobs. ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/job/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_ids": [6036032] }' ``` ```json Response { "batch_id": "0afed646-ce96-430a-92c0-b6b7589e937c", "status": "pending", "entity": "job", "action": "search", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/0afed646-ce96-430a-92c0-b6b7589e937c" } ``` The response is a **ticket, not the listings** — `status: "pending"` means the job is queued. Poll it with `GET /batch/{batch_id}` until `status` reaches `completed`, then download the results file, as described in [Working with batch jobs](#working-with-batch-jobs) below. (Search jobs always report `identifier_count: 1` — the one submission.) To keep each record small, project just the fields you need: ```json Request body — title and URL only { "crustdata_company_ids": [6036032], "fields": ["job_details.title", "job_details.url"] } ``` When the job completes, the downloaded file has one flat JSON record per line — exactly the fields you requested: ```json The results file (title and URL only) {"job_details": {"title": "Senior Software Engineer", "url": "https://example.com/careers/senior-software-engineer"}} {"job_details": {"title": "Product Manager, Growth", "url": "https://example.com/careers/product-manager-growth"}} ``` ### Errors ```json 400 — no company IDs { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: crustdata_company_ids", "metadata": [] } } ``` ```json 400 — comma-separated string instead of a JSON array { "error": { "type": "invalid_request", "message": "crustdata_company_ids must be a list", "metadata": [] } } ``` ```json 400 — more than 10 company IDs { "error": { "type": "invalid_request", "message": "Maximum 10 identifiers allowed for job/search. Found 11", "metadata": [] } } ``` --- ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. ### Errors shared by every batch endpoint | Status | `error.type` | When | | ------ | ------------------ | ---------------------------------------------------------------------- | | `400` | `invalid_request` | Invalid request body, or missing `x-api-version` header on submit. | | `401` | — | Missing or invalid API key (flat body, not the standard envelope). | | `403` | `permission_error` | Your account is not entitled to the batch endpoint you called. | | `404` | `not_found` | Unknown `batch_id`, or a job that belongs to another account. | | `429` | `rate_limit_error` | You already have 5 active batch jobs. | | `500` | `internal_error` | The processing workflow could not be started. Credits are refunded. | ```json 400 — missing x-api-version { "error": { "type": "invalid_request", "message": "Missing required header: x-api-version. Please set x-api-version header appropriately.", "metadata": [] } } ``` ```json 401 — invalid API key { "message": "Invalid API key in request" } ``` ```json 404 — unknown batch_id { "error": { "type": "not_found", "message": "Batch job not found", "metadata": [] } } ``` ```json 429 — too many active jobs { "error": { "type": "rate_limit_error", "message": "You already have 5 active batch jobs. Please wait for one to complete before submitting another.", "metadata": [] } } ``` --- ## What to do next - **Build queries interactively first** — iterate with [Search Jobs](/job-docs/search/introduction) page by page, then pull the full set for your target companies as a batch job. - **Look up fields** — see the [Search reference](/job-docs/search/reference). - **Resolve company IDs** — use the free [Company Identify](/company-docs/identify/introduction) endpoint. - **Search people or companies in batch** — see [Batch Person Search](/person-docs/search/batch-search) or [Batch Company Search](/company-docs/search/batch-search). - **Full schema** — see the [API reference](/openapi-specs/2025-11-01/introduction). # Live Search Source: https://docs.crustdata.com/job-docs/search/live-search Fetch the freshest job listings for a single company in real time from the web. **Use this when** you need up-to-the-minute hiring data for **one** specific company — for example, to detect brand-new requisitions the moment they appear, or to seed an alerting system that watches a handful of target accounts. For querying across many companies, filtering by title / category / date, or running aggregations, stay on [Search Jobs](/job-docs/search/introduction) against the indexed dataset. Live Search is intentionally narrow: one company per call, no filters, no pagination. ``` POST https://api.crustdata.com/job/professional_network/search/live ``` **This is a gated endpoint.** If your account does not have access, calls return `403` with `permission_error`. Contact [sales@crustdata.com](mailto:sales@crustdata.com) to enable it. Replace `YOUR_API_KEY` in each example with your actual API key. All requests require the `x-api-version: 2025-11-01` header. ## At a glance | Detail | Value | | --------------- | ------------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/job/professional_network/search/live` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header (required) | | **Required** | `crustdata_company_id` (integer) | | **Optional** | `limit` (1–100, default `100`) · `sorts` (date fields only) · `fields` (dot-paths) | | **Response** | `{ "job_listings": [ Job, ... ], "total_count": integer }` | | **Errors** | `400` invalid request · `401` unauthorized · `403` forbidden / not enabled · `500` internal | | **Gating** | Enterprise only — contact sales to enable | **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](/job-docs/search/introduction). 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. --- ## Live Search vs Search Jobs | Capability | Live Search | [Search Jobs](/job-docs/search/introduction) | | -------------------------- | ------------------------------------------------------- | -------------------------------------------- | | **Scope** | One company per call | Millions of indexed listings | | **Freshness** | Up-to-the-minute — fetched from the web on each request | Indexed dataset, refreshed periodically | | **Filters** | None — always returns the company's current listings | Full `SearchCondition` / group grammar | | **Pagination** | None | Cursor-based | | **Max results per call** | `100` | `1000` | | **Sorting / aggregations** | Sort by `metadata.date_added` or `metadata.date_updated` only; no aggregations | Full support | | **Field selection** | Yes | Yes | | **Typical use** | Detect brand-new requisitions at a known target | Market mapping, hiring trends, monitoring | | **Access** | Gated — enterprise only | Included in standard Jobs API access | --- ## Before you start You need the **`crustdata_company_id`** for the company you want to pull live listings for. If you only have a domain, name, or profile URL, resolve it first with [`POST /company/identify`](/company-docs/identify/introduction): ```bash curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["stripe.com"] }' ``` The response contains `crustdata_company_id`, which you then pass to Live Search. --- ## Your first live fetch Fetch every currently open listing at Stripe (`crustdata_company_id = 631394`) and return only the title, URL, and raw location. **Always include `crustdata_job_id` in `fields` for polling.** It is the stable dedupe key across calls — if you project responses with `fields` and leave it out, you cannot reliably tell whether a given listing is a new requisition or a carry-over from the previous poll. This example omits it for brevity, but every polling recipe should include it (see [Polling for brand-new requisitions](#polling-for-brand-new-requisitions)). ```bash curl curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 631394, "limit": 25, "fields": [ "job_details.title", "job_details.url", "location.raw" ] }' ``` ```python Python import os import requests response = requests.post( "https://api.crustdata.com/job/professional_network/search/live", headers={ "Authorization": f"Bearer {os.environ['CRUSTDATA_API_KEY']}", "Content-Type": "application/json", "x-api-version": "2025-11-01", }, json={ "crustdata_company_id": 631394, "limit": 25, "fields": ["job_details.title", "job_details.url", "location.raw"], }, ) response.raise_for_status() data = response.json() print(f"{data['total_count']} live listings") for job in data["job_listings"]: print(job["job_details"]["title"], "-", job["location"]["raw"]) ``` ```javascript Node.js const response = await fetch( "https://api.crustdata.com/job/professional_network/search/live", { method: "POST", headers: { Authorization: `Bearer ${process.env.CRUSTDATA_API_KEY}`, "Content-Type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify({ crustdata_company_id: 631394, limit: 25, fields: ["job_details.title", "job_details.url", "location.raw"], }), }, ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { job_listings, total_count } = await response.json(); ``` ```json Response (matches the requested fields) { "job_listings": [ { "job_details": { "title": "Integration Engineer (AUNZ)", "url": "https://www.linkedin.com/jobs/view/4398377738" }, "location": { "raw": "Melbourne, Victoria, Australia" } } ], "total_count": 1 } ``` Because the request asked for exactly three dot-paths (`job_details.title`, `job_details.url`, `location.raw`), the response contains only those fields. Omit `fields` from the request to get the full `Job` object including firmographics, description, and metadata — see [Field reference](#field-reference) below for the most useful returnable paths. --- ## Request body | Parameter | Type | Required | Default | Description | | ---------------------- | --------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `crustdata_company_id` | integer | **Yes** | — | Crustdata internal company identifier. Use [`POST /company/identify`](/company-docs/identify/introduction) to resolve a domain or profile URL into an id. | | `limit` | integer | No | `100` | Max listings to return. Min `1`, max `100`. The live endpoint caps at 100 per call. | | `sorts` | array of `SearchSort` | No | — | Ordering rules. Only date fields are sortable on this endpoint: `metadata.date_added` and `metadata.date_updated`. Each item is `{ field, order }` where `order` is `asc` or `desc`. | | `fields` | string[] | No | all | Dot-paths to include in each returned job (e.g. `"job_details.title"`, `"location.raw"`). | ## Response body | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `job_listings` | `Job[]` | Job listings fetched in real time for the requested company. Projected to the `fields` you requested; full `Job` object if omitted. | | `total_count` | integer | **Number of job listings returned in this response.** Equal to `job_listings.length`. This is **not** a grand total of ever-published listings for the company, and it is **not** a formal truncation indicator. If the company has more than your `limit` (or more than `100`), the excess listings are dropped from this response and `total_count` simply equals the number returned. To detect possible truncation, compare `total_count` to `limit`: if they are equal **and** equal to `100`, assume there may be more listings available. | ### Rate limits and credits Current pricing for this gated endpoint: Pricing: 2 credits per result returned. Availability: Enterprise-only and plan-gated. Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. The shape of each `Job` is identical to the one returned by [Search Jobs](/job-docs/search/introduction) — firmographics, location, description, and metadata all ship in the same structure. See the inline [Field reference](#field-reference) below for the most useful dot-paths to request with `fields`. ### Edge cases and limits The items below describe observed behavior of the live fetch, not strict contract guarantees. They may evolve as the backend changes. | Situation | What to expect | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Company has more than 100 live listings** | `limit` is capped at `100` per call — there is **no pagination**. The response returns listings in the order they currently surface on the company's source profile at fetch time, truncated to your `limit`. For full coverage of a high-volume hirer, use [Search Jobs](/job-docs/search/introduction) against the indexed dataset instead. | | **Company has zero live listings** | `job_listings` is `[]` and `total_count` is `0`. | | **Result ordering** | Ordering reflects the order in which listings surface on the source profile at fetch time. It is **not** guaranteed newest-first and may shift between calls. | | **Same job appears twice across polls** | Dedupe on `crustdata_job_id` — it is the stable identifier for a single requisition. | | **Company exists but has no linked public profile** | Returns `400` — the company record is recognized, but the backend has no public profile to fetch live listings from. See [Errors](#errors) for the exact response shape. | --- ## Field reference Live Search returns the same `Job` object as [Search Jobs](/job-docs/search/introduction). The difference is the request shape: you send `crustdata_company_id`, optional `limit`, and optional `fields`; there are no filters, sorts, or aggregations. ### Jobs IDs in live workflows | Field | Where it appears | Why it matters | | ----------------------------------------- | ---------------- | ------------------------------------------------------------------- | | `crustdata_company_id` | Request body | Crustdata company identifier required for every live fetch. | | `company.basic_info.crustdata_company_id` | Response payload | Same company identifier, returned on each `Job`. | | `crustdata_job_id` | Response payload | Stable dedupe key across repeated polls. | | `job_details.job_id` | Response payload | Secondary job identifier that currently mirrors `crustdata_job_id`. | ### Recommended field sets | Use case | `fields` array | | -------------------- | ------------------------------------------------------------------------------------------------------- | | Lightweight polling | `[` `"crustdata_job_id"`, `"job_details.title"`, `"job_details.url"`, `"location.raw"` `]` | | Alert enrichment | `[` `"crustdata_job_id"`, `"job_details.title"`, `"company.basic_info.name"`, `"location.raw"` `]` | | Freshness debugging | `[` `"crustdata_job_id"`, `"job_details.title"`, `"metadata.date_added"`, `"metadata.date_updated"` `]` | | Full company context | `[` `"job_details"`, `"company.basic_info"`, `"company.headcount"`, `"location"`, `"metadata"` `]` | Include `crustdata_job_id` in every polling workflow. Without it, you cannot reliably tell whether a listing is new or just reappeared in the latest fetch. ### Common response fields | Path | Type | Example | Notes | | ----------------------------------- | ----------------- | ------------------------------------------ | --------------------------------------------------------------- | | `crustdata_job_id` | integer | `41053563` | Stable dedupe key. | | `job_details.title` | string | `"Integration Engineer (AUNZ)"` | Human-readable title. | | `job_details.category` | string | `"Engineering"` | Category label. | | `job_details.url` | string | `"https://www.linkedin.com/jobs/view/..."` | Canonical job URL from the source profile. | | `job_details.workplace_type` | string | `"Remote"` / `"Hybrid"` / `""` | Often blank when the source profile does not expose it. | | `company.basic_info.name` | string | `"Stripe"` | Hiring company name. | | `company.basic_info.primary_domain` | string | `"stripe.com"` | Useful for alert routing and CRM joins. | | `location.raw` | string | `"Melbourne, Victoria, Australia"` | Best display-friendly location field. | | `content.description` | string | Long text | Full job description text. | | `metadata.date_added` | string (ISO 8601) | `"2026-04-07T11:37:29"` | The job-posted date: when the listing was posted on the source portal. | | `metadata.date_updated` | string (ISO 8601) | `"2026-04-08T00:00:00"` | Most recent refresh time for the indexed record representation. | ### Annotated full `Job` example ```jsonc { "job_listings": [ { "crustdata_job_id": 41053563, "job_details": { "job_id": 41053563, "title": "Integration Engineer (AUNZ)", "category": "Engineering", "workplace_type": "", "url": "https://www.linkedin.com/jobs/view/4398377738", "reposted_job": false, "number_of_openings": 1, }, "company": { "basic_info": { "crustdata_company_id": 631394, "name": "Stripe", "domains": ["stripe.com"], "website": "https://stripe.com", "professional_network_id": "2135371", "industries": ["Technology, Information and Internet"], }, "headcount": { "total": 14522, "range": "5001-10000", }, "followers": { "count": 1335688, }, }, "location": { "raw": "Melbourne, Victoria, Australia", "country": "Australia", }, "content": { "description": "Stripe is a financial infrastructure platform for businesses...", }, "metadata": { "date_added": "2026-04-07T11:37:29", "date_updated": "2026-04-08T00:00:00", }, }, ], "total_count": 1, } ``` Nulls, missing sub-objects, and blank strings are normal in live payloads. Guard all nested reads in your client, especially under `company.revenue`, `company.funding`, and `location`. --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request paired with a real trimmed response. Live Search takes **one `crustdata_company_id` per call** — there are no cross-company filters and no pagination. For querying across many companies, see [Search Jobs](/job-docs/search/introduction). The core Live Search call: pass a single `crustdata_company_id` and project a lightweight polling field set. Always include `crustdata_job_id` — it is the stable dedupe key across repeated polls. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 631394, "limit": 25, "fields": [ "crustdata_job_id", "job_details.title", "job_details.url", "location.raw" ] }' ``` ```json Response { "job_listings": [ { "crustdata_job_id": 43313025, "job_details": { "title": "Technical Account Manager, German Speaking", "url": "https://www.linkedin.com/jobs/view/4413483673" }, "location": { "raw": "Berlin, Berlin, Germany" } }, { "crustdata_job_id": 43313064, "job_details": { "title": "Manager, Sales Development", "url": "https://www.linkedin.com/jobs/view/4413481713" }, "location": { "raw": "Chicago, Illinois, United States" } }, { "crustdata_job_id": 43313874, "job_details": { "title": "Account Executive, SMB (Grower)", "url": "https://www.linkedin.com/jobs/view/4413488663" }, "location": { "raw": "Chicago, Illinois, United States" } } ], "total_count": 25 } ``` Because the request asked for four dot-paths, the response contains only those fields. Omit `fields` to get the full `Job` object (see the example below). Live results are not guaranteed newest-first by default. Pass a `sorts` rule on `metadata.date_added` to order the snapshot by posting time. Only the two date fields (`metadata.date_added` and `metadata.date_updated`) are sortable on this endpoint. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 631394, "limit": 5, "sorts": [{ "field": "metadata.date_added", "order": "desc" }], "fields": [ "crustdata_job_id", "job_details.title", "metadata.date_added", "metadata.date_updated" ] }' ``` ```json Response { "job_listings": [ { "crustdata_job_id": 48703999, "job_details": { "title": "Full Stack Engineer, Money as a Service" }, "metadata": { "date_added": "2026-06-27T02:23:44+00:00", "date_updated": "2026-06-29T00:00:00+00:00" } }, { "crustdata_job_id": 48534424, "job_details": { "title": "Account Executive, Funded Startups - Expansion" }, "metadata": { "date_added": "2026-06-26T20:24:26+00:00", "date_updated": "2026-06-29T00:00:00+00:00" } }, { "crustdata_job_id": 48736762, "job_details": { "title": "Counsel, Leasing & Operational Professional Services" }, "metadata": { "date_added": "2026-06-26T20:24:21+00:00", "date_updated": "2026-06-29T00:00:00+00:00" } } ], "total_count": 5 } ``` Omit `fields` to return the complete `Job` for each listing — company firmographics, headcount, funding, the full description, location, and metadata all ship in one payload, with no extra `/company/enrich` call. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 633593, "limit": 2 }' ``` ```json Response (one listing, trimmed) { "job_listings": [ { "crustdata_job_id": 43279160, "job_details": { "job_id": 43279160, "title": "Software Engineer, Governance", "category": "Engineering", "workplace_type": "Hybrid", "url": "https://www.linkedin.com/jobs/view/4412407234", "reposted_job": "True", "number_of_openings": 1 }, "company": { "basic_info": { "crustdata_company_id": 633593, "name": "Retool", "primary_domain": "retool.com", "website": "https://retool.com/", "professional_network_id": "11869260", "industries": [ "Software Development", "Technology, Information and Internet", "Technology, Information and Media" ] }, "headcount": { "total": 416, "range": "201-500", "largest_headcount_country": "USA" }, "followers": { "count": 41310 }, "funding": { "last_round_type": "series_c", "last_fundraise_date": "2022-07-27", "num_funding_rounds": 5, "total_investment_usd": 141000000, "valuation_usd": null }, "revenue": { "estimated": { "lower_bound_usd": 20000000, "upper_bound_usd": 50000000 } } }, "location": { "raw": "San Francisco, California, United States", "city": null, "state": null, "country": "United States of America (the)" }, "content": { "description": "About Retool\n\nNearly every company in the world runs on custom software..." }, "metadata": { "date_added": "2026-05-08T23:04:10+00:00", "date_updated": "2026-06-29T00:00:00+00:00" } } ], "total_count": 2 } ``` Live payloads can carry blank strings, `null` sub-fields, and minor type quirks (for example, `reposted_job` may arrive as the string `"True"`). Guard every nested read in your client. When each new listing feeds an alert, queue, or CRM row, project the company name and domain alongside the job so every record is self-describing — no join back to a company table required. ```bash Request curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 689494, "limit": 3, "fields": [ "crustdata_job_id", "job_details.title", "company.basic_info.name", "company.basic_info.primary_domain", "location.raw" ] }' ``` ```json Response { "job_listings": [ { "crustdata_job_id": 21349108, "job_details": { "title": "Customer Activation Manager | Enterprise" }, "company": { "basic_info": { "name": "Ramp", "primary_domain": "ramp.com" } }, "location": { "raw": "Miami, Florida, United States" } }, { "crustdata_job_id": 26037353, "job_details": { "title": "Customer Activation Manager | Enterprise" }, "company": { "basic_info": { "name": "Ramp", "primary_domain": "ramp.com" } }, "location": { "raw": "San Francisco, California, United States" } }, { "crustdata_job_id": 28331098, "job_details": { "title": "Customer Activation Manager | Enterprise" }, "company": { "basic_info": { "name": "Ramp", "primary_domain": "ramp.com" } }, "location": { "raw": "Greater Toronto Area, Canada" } } ], "total_count": 3 } ``` Don't have the `crustdata_company_id` yet? Resolve a domain, name, or profile URL with [`POST /company/identify`](/company-docs/identify/introduction) first, then pass the returned id here. ## More example queries ### Minimal polling payload Use the smallest reliable field set when you are polling on a schedule and only care about detecting new requisitions. ```json { "crustdata_company_id": 631394, "limit": 100, "fields": [ "crustdata_job_id", "job_details.title", "job_details.url", "location.raw" ] } ``` ### Full payload for manual investigation Omit `fields` when you want the complete `Job` object for each listing. ```json { "crustdata_company_id": 631394, "limit": 10 } ``` ### Sort newest-indexed listings first Pass a `sorts` rule to order the live snapshot by indexing time. Only the two date fields (`metadata.date_added` and `metadata.date_updated`) are sortable on this endpoint. ```json { "crustdata_company_id": 631394, "limit": 25, "sorts": [{ "field": "metadata.date_added", "order": "desc" }], "fields": [ "crustdata_job_id", "job_details.title", "metadata.date_added" ] } ``` ### Compare a live snapshot with indexed history Use Live Search for the fresh company snapshot, then [Search Jobs](/job-docs/search/introduction) to look back over the indexed history for the same company id. ```json { "crustdata_company_id": 631394, "limit": 100, "fields": ["crustdata_job_id", "job_details.title", "location.raw"] } ``` ```json { "filters": { "op": "and", "conditions": [ { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, { "field": "metadata.date_added", "type": "=>", "value": "2025-01-01" } ] }, "fields": ["crustdata_job_id", "job_details.title", "metadata.date_added"], "sorts": [{ "field": "metadata.date_added", "order": "desc" }], "limit": 100 } ``` ## Polling for brand-new requisitions Because Live Search returns the freshest view directly from the web, you can poll it on a schedule to detect brand-new requisitions. A simple pattern: Maintain a list of `crustdata_company_id`s for the companies you want to watch. Call Live Search for each id (e.g. every 30 minutes or every hour) and store each returned `crustdata_job_id`. Any `crustdata_job_id` not seen in the previous snapshot is a brand-new requisition. Trigger your alert, queue, or CRM update for those rows. Most companies post infrequently. Space polls out to avoid wasted calls — the live endpoint is gated and unnecessary fetches cost you quota. For **historical** hiring activity or queries spanning many companies, [Search Jobs](/job-docs/search/introduction) against the indexed dataset is both cheaper and faster. Reserve Live Search for the narrow case of "I absolutely need the freshest listings for this one company right now." --- ## Errors | Status | Envelope shape | Meaning | | ------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `{ "error": { "type", "message", "metadata" } }` | Invalid request — missing `crustdata_company_id`, company not found, company has no linked public profile to fetch live listings from, or `limit` out of range. | | `401` | `{ "message": "..." }` (flat — **not** `error` envelope) | Unauthorized — the `Authorization` header is missing, malformed, or contains an invalid API key. | | `403` | `{ "error": { "type", "message", "metadata" } }` | Forbidden — your account does not have access to Live Search. Contact [sales@crustdata.com](mailto:sales@crustdata.com) to enable it. | | `500` | `{ "error": { "type", "message", "metadata" } }` | Internal server error — retry after a short delay. | On this endpoint, `401` uses a **flat** `{ "message": ... }` shape — every other status (`400`/`403`/`500`) uses the nested `{ "error": { "type", "message", "metadata" } }` envelope. Parse based on HTTP status. ```json 400 — Missing crustdata_company_id { "error": { "type": "invalid_request", "message": "Missing required field: 'crustdata_company_id'.", "metadata": [ { "field": "crustdata_company_id", "type": "missing", "message": "Missing required field: 'crustdata_company_id'." } ] } } ``` ```json 400 — Company not found { "error": { "type": "invalid_request", "message": "Company with id=999999999 not found.", "metadata": [] } } ``` ```json 400 — Company has no linked public profile to fetch from { "error": { "type": "invalid_request", "message": "Company with id=5 has no linked public profile available for live fetch.", "metadata": [] } } ``` The live API currently returns the underlying backend wording for this error. The example above uses neutral phrasing — the key point is the status code (`400`) and `error.type` (`invalid_request`). Your code should detect this case by inspecting the status and `error.type` rather than string-matching the `message`. ```json 400 — limit out of range { "error": { "type": "invalid_request", "message": "'limit' must be at most 100. Got 500.", "metadata": [ { "field": "limit", "type": "less_than_equal", "message": "'limit' must be at most 100. Got 500." } ] } } ``` ```json 403 — Endpoint not enabled for your account { "error": { "type": "permission_error", "message": "You do not have permission to access /job/professional_network/search/live.", "metadata": [] } } ``` ```json 401 — Invalid API key { "message": "Invalid API key in request" } ``` ```json 401 — Missing API key { "message": "Missing API key in request" } ``` --- ## What's next - **Run filtered queries across the full job dataset** — use [Search Jobs](/job-docs/search/introduction). - **Resolve a domain into a company id** — use [Company Identify](/company-docs/identify/introduction). - **Inspect the formal schema** — read the [OpenAPI reference](/openapi-specs/2025-11-01/introduction). # Job Watcher Source: https://docs.crustdata.com/watcher-docs/job/watch Turn a job search filter into a recurring feed. A Job Watcher re-runs your filters on a schedule and pushes newly posted jobs to a webhook, Slack, or email. Includes copy-paste curl recipes for titles, locations, company size, and posting recency. A **Job Watcher** turns a [Job Search](/job-docs/search/introduction) filter into a continuous feed. Each run re-evaluates your filters and delivers only the **new** matching jobs to your channel, so you get a deduplicated stream of postings entering your criteria without re-running searches yourself. ``` POST https://api.crustdata.com/watch/job/search ``` Pricing: the first run is a free baseline (up to 5 matches), then 0.5 credits per new job delivered. ## How it runs `POST` your `filters`, a `config` (schedule + result cap), and one or more `notifications` channels. The response returns a watch `id`. Within seconds, the first run delivers a **free baseline sample of up to 5 matches** so you can confirm the setup and payload shape. No credits are charged. On your schedule (`every_hours`), the watcher re-runs your filters and delivers jobs **new since the previous run**, up to `max_results_per_run`. You're charged per delivered job. Bound your filter by posting date. A leaf like `{ "field": "metadata.date_added", "type": "=>", "value": "2026-07-05" }` keeps the watch focused on fresh postings instead of the full historical pool. Thirty days back is a good default. ## Request body | Field | Required | Description | | ----- | -------- | ----------- | | `filters` | Yes | A filter tree, identical to [Job Search](/job-docs/search/reference). `{ "op": "and"\|"or", "conditions": [ { "field", "type", "value" } ] }`. | | `config.trigger` | Yes | `{ "type": "interval", "every_hours": N }`, how often the watch runs (e.g. `1`, `6`, `24`, `168`). | | `config.max_results_per_run` | No | Max jobs delivered per run. Default `25`. The first (baseline) run is always capped at 5. | | `config.expires_at` | No | ISO date (`"2027-01-01"`). The watch auto-stops after this date. | | `sorts` | No | Delivery order, e.g. `[ { "field": "metadata.date_added", "order": "desc" } ]` for newest first. | | `notifications` | Yes | One or more delivery channels (see [below](#delivery-channels)). | The response returns the watch `id`: ```json { "id": 47609 } ``` All examples require the headers `authorization: Bearer YOUR_API_KEY`, `content-type: application/json`, and `x-api-version: 2025-11-01`. For the full list of `field` values and operators, see the [Job Search reference](/job-docs/search/reference). ## Filter format Watcher filters use the **same syntax and fields** as [Job Search](/job-docs/search/reference), so there is no new filter language to learn. Every filter is a tree: ```json { "op": "and", "conditions": [ { "field": "job_details.title", "type": "(.)", "value": "software engineer" }, { "field": "location.country", "type": "=", "value": "United States" } ] } ``` Each leaf is a `{ field, type, value }` triple: `field` is the attribute, `type` is the operator (`(.)` for contains, `=`, `in`, `not_in`, `=>` for ≥, `=<` for ≤, and so on), and `value` is what to match. Each `op` combines its `conditions` with `and` or `or`, and op-groups nest. ## Recipes ### Title + location + company size Alert daily on new **software engineer roles in the United States at 3,000+ person companies**, newest first: ```bash curl --request POST \ --url https://api.crustdata.com/watch/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": "software engineer" }, { "field": "location.country", "type": "=", "value": "United States" }, { "field": "company.headcount.total", "type": "=>", "value": 3000 } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25 }, "sorts": [ { "field": "metadata.date_added", "order": "desc" } ], "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ### Recent postings in a category New **engineering roles posted in the last 30 days**, at companies that raised a round: ```bash curl --request POST \ --url https://api.crustdata.com/watch/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.category", "type": "=", "value": "Engineering" }, { "field": "metadata.date_added", "type": "=>", "value": "2026-07-05" }, { "field": "company.funding.last_round_type", "type": "in", "value": ["Series A", "Series B"] } ] }, "config": { "trigger": { "type": "interval", "every_hours": 24 } }, "notifications": [ { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ] }' ``` ### Remote roles at specific companies New **remote postings** from a set of companies you track: ```bash curl --request POST \ --url https://api.crustdata.com/watch/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.primary_domain", "type": "in", "value": ["stripe.com", "figma.com"] }, { "field": "job_details.workplace_type", "type": "=", "value": "Remote" } ] }, "config": { "trigger": { "type": "interval", "every_hours": 6 } }, "notifications": [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" } ] }' ``` ## Delivery channels Every watch needs at least one channel in `notifications`. Add one or more; matches fan out to all of them. ```json Webhook { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata", "headers": { "X-Watch-Name": "swe-us-3000" } } ``` ```json Slack { "type": "slack", "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } ``` ```json Email { "type": "email", "to": ["you@company.com"] } ``` ```json Multiple [ { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }, { "type": "email", "to": ["you@company.com"] } ] ``` A Slack channel must be a genuine Slack incoming webhook (`https://hooks.slack.com/services/…`). Any other URL will fail delivery. ## Delivered record shape Each delivered job carries the posting, the company, and the location: ```json { "uid": "indb_discovery_job_58504943", "record": { "job_details": { "title": "Backend Engineer, Vehicle Software", "url": "https://www.linkedin.com/jobs/view/4448814407", "category": "Engineering", "workplace_type": "On-site", "number_of_openings": 1 }, "company": { "basic_info": { "name": "Tesla", "primary_domain": "tesla.com" } }, "location": { "raw": "Palo Alto, California, United States", "country": "United States" }, "metadata": { "date_added": "2026-08-04T05:32:47" } } } ``` ## Manage a watch ```bash Pause / resume curl --request PATCH \ --url https://api.crustdata.com/watch/job/search/47609 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "status": "paused" }' ``` ```bash Cancel a watch curl --request DELETE \ --url https://api.crustdata.com/watch/job/search/47609 \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ```bash List your job watches curl --request GET \ --url https://api.crustdata.com/watch/job/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'x-api-version: 2025-11-01' ``` ## Pricing | | Credits | | --- | --- | | First run (baseline, up to 5 matches) | **Free** | | Each new job delivered on later runs | **0.5** | You're charged only for jobs actually delivered, never for a run that finds nothing new. For the full credit catalog, see [Pricing](/general/pricing). ## Related Run the underlying search on demand and pull the full current match set. The complete catalog of filter fields and operators. The same recurring-feed model over people. Credit costs across every Crustdata endpoint. # Web Search Source: https://docs.crustdata.com/web-docs/search/introduction Search the web across multiple sources including web, news, academic papers, deep research mode, and social media. **Use this when** you want to find web pages, news articles, academic papers, author profiles, AI-generated overviews, or social media posts matching a search query. The Web Search API accepts a query and returns results from one or more source types. This page walks you through the basics: your first search, the response shape, and human mode, then folds in worked example recipes you can copy, paste, and adapt. The result shape varies by source — always specify `sources` explicitly when you need predictable parsing. For result shapes, request parameters, and error handling, see [Reference](/web-docs/search/reference). Every request goes to the same endpoint: ``` POST https://api.crustdata.com/web/search/live ``` All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` Replace `YOUR_API_KEY` in each example with your actual API key. Pricing: 1 credit per query. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. Result shapes and field presence for each source: web, news, scholar-articles, scholar-author, AI, social. Site/date filtering, multi-page, academic, deep research mode, discovery patterns, and search → fetch workflows. Request parameters, response body, error handling, and common gotchas. Open the docs assistant with a pre-filled question about Web Search. --- ## Your first search The simplest search uses a `query` with an explicit `sources` array. Always specify `sources` for predictable result parsing. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "crustdata", "sources": ["web"], "location": "US" }' ``` ```json Response { "success": true, "query": "crustdata", "timestamp": 1775195367446, "results": [ { "source": "web", "title": "Crustdata: Real-Time B2B Data Broker via API or Data Feed", "url": "https://crustdata.com/", "snippet": "Crustdata is a B2B data provider offering real-time company & people datasets. Access APIs and live signals to power sales and investment workflows.", "position": 1 }, { "source": "web", "title": "Crustdata: Real-time B2B data via simple APIs", "url": "https://www.ycombinator.com/companies/crustdata", "snippet": "Crustdata provides live company and people data via APIs and full dataset delivery.", "position": 2 } ], "metadata": { "total_results": 7, "failed_pages": [], "empty_pages": [] } } ``` Response trimmed for clarity. **Extract:** Each result in `results[]` contains `source`, `title`, `url`, `snippet`, and `position`. Use `position` for ranking and `url` for follow-up fetching. --- ## Use human mode when standard retrieval is blocked Set `human_mode: true` when you want the API to attempt a browser-like retrieval path for the search request. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "crustdata", "sources": ["web"], "human_mode": true }' ``` `human_mode: true` returns the normal search response shape with `success`, `query`, `timestamp`, `results`, and `metadata`. --- ## Examples Ready-to-copy patterns for Web Search. Each example shows a real request, the response, and what to extract. For the core walkthrough (your first search, human mode), see the sections above. For result shapes, request parameters, and error handling, see [Reference](/web-docs/search/reference). ### Filtering and pagination Use the `site` parameter to limit results to a single domain. Useful for finding company pages on professional networks, profiles on GitHub, or content on a specific website. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "ADAMSBROWN, LLC", "sources": ["web"], "site": "linkedin.com/company" }' ``` ```json Response { "success": true, "query": "site:linkedin.com/company ADAMSBROWN, LLC", "timestamp": 1775195371211, "results": [ { "source": "web", "title": "Adams Brown", "url": "https://www.linkedin.com/company/adams-brown-cpa", "snippet": "Adams Brown, LLC, a leading CPA and advisory firm, has delivered value-added accounting and advisory services to businesses and their owners since 1945.", "position": 1 } ], "metadata": { "total_results": 10, "failed_pages": [], "empty_pages": [] } } ``` **Extract:** The first result URL is typically the best match. For company profile URLs, pass the result to [Company Identify](/company-docs/identify/introduction) for a full profile. Use `start_date` and `end_date` (Unix timestamps in seconds) to limit results to a specific time range. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "distributed systems", "location": "US", "sources": ["web", "news"], "site": "example.com", "start_date": 1728259200, "end_date": 1730937600 }' ``` Convert dates to Unix timestamps: October 7, 2024 = `1728259200`. You can use any Unix timestamp converter tool. Use `page` to request multiple result pages in a single response. The `metadata` object tells you which pages succeeded. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "artificial intelligence startups", "sources": ["web"], "location": "US", "page": 3 }' ``` ```json Response (with page 2 failure) { "success": true, "query": "artificial intelligence startups", "timestamp": 1775195500000, "results": [ { "source": "web", "title": "AI Startup Landscape 2026", "url": "https://example.com/ai-startups", "snippet": "...", "position": 1 }, { "source": "web", "title": "Top AI Companies to Watch", "url": "https://example.com/top-ai", "snippet": "...", "position": 2 } ], "metadata": { "total_results": 25, "failed_pages": [2], "empty_pages": [3] } } ``` Response trimmed for clarity. Pages 1 succeeded, page 2 failed, page 3 was empty. The response aggregates results across all successful pages into a single `results[]` array. Check `metadata` to understand page-level outcomes: - **`metadata.total_results`** — total results available across all sources and pages. - **`metadata.failed_pages`** — page numbers that returned errors. Retry the request with a smaller `page` value if needed. - **`metadata.empty_pages`** — page numbers that returned no results. You have reached the end of available results — **do not retry**. ```javascript if (response.metadata.failed_pages.length > 0) { console.log("Failed pages:", response.metadata.failed_pages); } if (response.metadata.empty_pages.length > 0) { console.log( "Reached end of results at page", Math.min(...response.metadata.empty_pages), ); } ``` **Not guaranteed by the OpenAPI contract:** Each page returns approximately 10 results. If `metadata.empty_pages` contains page numbers, you have reached the end of available results. ### Company and profile discovery Search for a company by name followed by "website". The first result URL is typically the company's website. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "ADAMSBROWN, LLC website", "sources": ["web"] }' ``` ```json Response { "success": true, "query": "ADAMSBROWN, LLC website", "timestamp": 1775195388180, "results": [ { "source": "web", "title": "Adams Brown", "url": "https://www.adamsbrowncpa.com/", "snippet": "Adams Brown is a holistic professional services firm with a team of strategic allies.", "position": 1 } ], "metadata": { "total_results": 10, "failed_pages": [], "empty_pages": [] } } ``` **Extract:** `results[0].url` → `https://www.adamsbrowncpa.com/` Do **not** wrap the company name in quotes — this lets the search engine match partial name variations. If the company name is common, add city and state: `"ADAMSBROWN, LLC WICHITA KS website"`. **Bridge to Company API:** Extract the domain from the URL, then pass it to [Company Enrich](/company-docs/enrichment/introduction) for the full company profile: ```javascript const url = new URL("https://www.adamsbrowncpa.com/"); const domain = url.hostname.replace("www.", ""); // "adamsbrowncpa.com" ``` ```json Company Enrich request body { "domains": ["adamsbrowncpa.com"] } ``` Use the `site` parameter with the profile host (e.g. `linkedin.com/in`) to find a person's profile, then enrich via the Person API. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "Jeff Dean Google", "sources": ["web"], "site": "linkedin.com/in" }' ``` ```json Response { "success": true, "query": "site:linkedin.com/in Jeff Dean Google", "timestamp": 1775195400000, "results": [ { "source": "web", "title": "Jeff Dean - Google", "url": "https://www.linkedin.com/in/jeff-dean-8b212555", "snippet": "Chief Scientist at Google DeepMind.", "position": 1 } ], "metadata": { "total_results": 5, "failed_pages": [], "empty_pages": [] } } ``` **Bridge to Person API:** Pass the profile URL to [Person Enrich](/person-docs/enrichment/introduction): ```json { "professional_network_profile_urls": [ "https://www.linkedin.com/in/jeff-dean-8b212555" ] } ``` Use `site: "github.com"` to search for developer profiles on GitHub. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "Tyler Lambe", "sources": ["web"], "site": "github.com", "location": "US" }' ``` ```json Response { "success": true, "query": "site:github.com Tyler Lambe", "timestamp": 1775195572177, "results": [ { "source": "web", "title": "Tyler Lambe tylambe", "url": "https://github.com/tylambe", "snippet": "Autodidactic entrepreneur, engineer, educator. tylambe has 9 repositories available.", "position": 1 } ], "metadata": { "total_results": 10, "failed_pages": [], "empty_pages": [] } } ``` ### Research and analysis Search for academic articles with date filtering to find papers with citation data and PDF links. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "deep learning", "location": "US", "sources": ["scholar-articles"], "start_date": 1672531200, "end_date": 1704067200 }' ``` ```json Response { "success": true, "query": "deep learning", "timestamp": 1775195398144, "results": [ { "source": "scholar-articles", "title": "Understanding deep learning", "url": "https://books.google.com/books?hl=en&lr=lang_en&id=rvyxEAAAQBAJ", "snippet": "...to this field understand the principles behind deep learning.", "metadata": "SJD Prince - 2023 - books.google.com", "pdf_url": null, "position": 1, "authors": [ { "name": "SJD Prince", "profile_url": "https://scholar.google.com/citations?user=fjm67xYAAAAJ", "profile_id": "fjm67xYAAAAJ" } ], "citations": 618 } ], "metadata": { "total_results": 10, "failed_pages": [], "empty_pages": [] } } ``` **Extract:** - `citations` — citation count to gauge impact. - `pdf_url` — direct PDF download link (when available). - `authors[].profile_url` — Author profile link. - `metadata` — citation string: `"Author - Year - Publisher"`. Search for a researcher by name to get their full academic profile with h-index, citation metrics, and top publications. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "jeff dean", "location": "US", "sources": ["scholar-author"] }' ``` ```json Response { "success": true, "query": "jeff dean", "timestamp": 1775195567878, "results": [ { "source": "scholar-author", "url": "https://scholar.google.com/citations?user=NMS69lQAAAAJ", "name": "Jeff Dean", "affiliation": "Google Chief Scientist, Google Research and Google DeepMind", "citations": { "all": 401624, "since_2020": 231008 }, "h_index": { "all": 114, "since_2020": 78 }, "i10_index": { "all": 319, "since_2020": 203 }, "articles": [ { "title": "MapReduce: simplified data processing on large clusters", "year": "2008", "citations": "37255", "authors": "J Dean, S Ghemawat" } ] } ], "metadata": { "total_results": 1, "failed_pages": [], "empty_pages": [] } } ``` **Extract:** `citations.all` for total impact, `h_index.all` for research quality, `articles[]` for top publications. Use deep research mode for a synthesized answer with source references. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "uv vs pip", "location": "US", "sources": ["ai"] }' ``` ```json Response { "success": true, "query": "uv vs pip", "timestamp": 1775195563283, "results": [ { "source": "ai", "title": "AI Overview", "content": "The primary difference between uv and pip is speed and scope: uv is a modern, high-performance replacement for pip that prioritizes speed and a unified workflow.", "references": [ { "title": "uv vs pip: Managing Python Packages and Dependencies", "url": "https://realpython.com/uv-vs-pip/", "snippet": "When it comes to Python package managers..." } ], "images": [] } ], "metadata": { "total_results": 1, "failed_pages": [], "empty_pages": [] } } ``` **Extract:** `content` for the overview text, `references[].url` for source verification. Filter news results to a specific date range by providing `start_date` and `end_date` as Unix timestamps in seconds. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "artificial intelligence developments", "location": "US", "sources": ["news"], "start_date": 1728259200, "end_date": 1730937600 }' ``` `start_date` and `end_date` are Unix timestamps in **seconds**. October 7, 2024 = `1728259200`. November 7, 2024 = `1730937600`. Search for recent social media mentions of a topic or person. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "crustdata AI agents", "location": "US", "sources": ["social"] }' ``` Social media results may return an empty `results` array for some queries depending on availability. Always check `results.length` before processing. When searching multiple sources, the `results[]` array contains items with different shapes. Always branch on `result.source`. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "machine learning infrastructure", "location": "US", "sources": ["web", "news", "scholar-articles"] }' ``` **Safe parsing logic:** ```javascript const fetchableUrls = []; for (const result of response.results) { switch (result.source) { case "web": case "news": case "social": fetchableUrls.push(result.url); break; case "scholar-articles": case "scholar-articles-enriched": fetchableUrls.push(result.url); break; case "scholar-author": console.log(`Author: ${result.name} (${result.affiliation})`); break; case "ai": console.log(`AI Overview: ${result.content}`); result.references?.forEach((ref) => fetchableUrls.push(ref.url)); break; } } ``` Not every search result should go to Fetch. Academic author results are profiles, not content pages. Deep research results provide content inline and use `references[].url` for source URLs instead of a top-level `url`. ### Search-then-fetch workflows Search for competitor news, then fetch the full article content for analysis. ```bash curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "OpenAI funding 2026", "location": "US", "sources": ["news", "web"] }' ``` Extract URLs from `results[].url`. ```bash curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": [ "https://www.reuters.com/technology/openai-funding-2026/", "https://techcrunch.com/2026/01/15/openai-funding/" ] }' ``` Check `success` for each entry. Parse HTML from successful fetches. To identify which URLs failed, compare requested URLs against successful `url` values. ```javascript const successfulUrls = new Set( fetchResponse.filter(r => r.success).map(r => r.url) ); const failedUrls = requestedUrls.filter(u => !successfulUrls.has(u)); ``` Failed entries have `url: null`, so correlate failures by comparing successful URLs to your input list. See [Fetch: correlating failures](/web-docs/fetch/introduction#correlating-failures-to-input-urls). A complete Python example that searches, filters fetchable URLs by source, fetches content, and handles failures. ```python import requests API_KEY = "YOUR_API_KEY" HEADERS = { "authorization": f"Bearer {API_KEY}", "content-type": "application/json", "x-api-version": "2025-11-01", } # Step 1: Search search_resp = requests.post( "https://api.crustdata.com/web/search/live", headers=HEADERS, json={"query": "OpenAI funding 2026", "sources": ["web", "news"]}, ).json() # Step 2: Extract fetchable URLs fetchable_urls = [] for result in search_resp["results"]: if result["source"] in ("web", "news", "social", "scholar-articles", "scholar-articles-enriched"): fetchable_urls.append(result["url"]) elif result["source"] == "ai": for ref in result.get("references", []): fetchable_urls.append(ref["url"]) # Step 3: Fetch (max 10 URLs per request) fetch_resp = requests.post( "https://api.crustdata.com/web/enrich/live", headers=HEADERS, json={"urls": fetchable_urls[:10]}, ).json() # Step 4: Process results and correlate failures successful_urls = set() for item in fetch_resp: if item["success"]: successful_urls.add(item["url"]) print(f"Fetched: {item['title']} ({len(item['content'])} chars)") failed_urls = [u for u in fetchable_urls[:10] if u not in successful_urls] if failed_urls: print(f"Failed URLs: {failed_urls}") ``` When using deep research mode, the overview `content` is inline. To get the full source articles, fetch the URLs from `references[]`. ```bash curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"query": "uv vs pip", "sources": ["ai"], "location": "US"}' ``` **Extract:** `results[0].references[].url` — the source article URLs. ```bash curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"urls": ["https://realpython.com/uv-vs-pip/"]}' ``` Parse the `content` from successful entries to read the full source articles. AI results do not have a top-level `url` field. Always use `references[].url` for fetch targets. In the older API you could append `?fetch_content=true` to a web search to get page HTML inline in a `contents[]` array. **That parameter is removed in `2025-11-01`** — it is silently ignored, and the search response never returns a `contents[]` field. Instead, run web search first, then pass the result URLs to [Web Fetch](/web-docs/fetch/introduction) (`POST /web/enrich/live`). ```bash Request curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "parallel ai seed funding", "sources": ["news"] }' ``` ```json Response { "success": true, "query": "parallel ai seed funding", "timestamp": 1782698294157, "results": [ { "source": "news", "title": "Ex-Twitter CEO Agrawal's AI search startup Parallel raises $100 million", "url": "https://www.reuters.com/business/ex-twitter-ceo-agrawals-ai-search-startup-parallel-raises-100-million-2025-11-12/", "snippet": "AI startup Parallel Web Systems, founded by former Twitter CEO Parag Agrawal, has raised $100 million...", "position": 3 } ], "metadata": { "total_results": 10, "failed_pages": [], "empty_pages": [] } } ``` **Extract** the URLs you want from `results[].url`. Pass up to 10 URLs to `POST /web/enrich/live`. Each entry comes back with `success`, `url`, `timestamp`, `title`, and the full page `content`. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": [ "https://www.reuters.com/business/ex-twitter-ceo-agrawals-ai-search-startup-parallel-raises-100-million-2025-11-12/" ] }' ``` ```json Response [ { "success": true, "url": "https://www.reuters.com/business/ex-twitter-ceo-agrawals-ai-search-startup-parallel-raises-100-million-2025-11-12/", "timestamp": 1782698339, "title": "Ex-Twitter CEO Agrawal's AI search startup Parallel raises $100 million | Reuters", "content": "..." } ] ``` **What changed.** The old inline `fetch_content` flag is gone; fetching is now a separate, independently-billed call to `/web/enrich/live`. This lets you fetch only the URLs you actually need (and respect the 10-URL-per-call fetch limit) rather than fetching every search result. Search `timestamp` is in **milliseconds**; Fetch `timestamp` is in **seconds**. --- ## What to do next - **Understand result shapes** — see [Sources](/web-docs/search/reference#sources) for field presence by source. - **Look up request/response details** — see [Reference](/web-docs/search/reference) for request parameters, response body, error handling, and common gotchas. - **Fetch page content** — see [Web Fetch](/web-docs/fetch/introduction) to fetch the HTML content of URLs returned by search results. # Web Search reference Source: https://docs.crustdata.com/web-docs/search/reference Reference for Web Search: request parameters, response body, error handling, common gotchas, and API summary. Reference material for [Web Search](/web-docs/search/introduction): request parameters, response body, error handling, common gotchas, and the API summary. For walk-through examples, see [Web Search](/web-docs/search/introduction) and [Examples](/web-docs/search/introduction#examples). For result shapes and field presence by source, see [Sources](#sources). All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` Replace `YOUR_API_KEY` in each example with your actual API key. --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | ------------ | -------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | Yes | — | Search query text. Max 5,000 characters. Supports search operators like `site:` and `filetype:`. | | `location` | string | No | — | ISO 3166-1 alpha-2 country code for region-specific results (e.g., `"US"`, `"GB"`, `"JP"`). | | `sources` | string[] | No | — | Sources to query: `web`, `news`, `scholar-articles`, `scholar-articles-enriched`, `scholar-author`, `ai`, `social`. Omitting this field searches all sources. | | `site` | string | No | — | Restrict results to a domain (e.g., `"linkedin.com/company"`, `"github.com"`). Max 500 characters. | | `start_date` | integer | No | — | Unix timestamp (seconds). Only results after this date. | | `end_date` | integer | No | — | Unix timestamp (seconds). Only results before this date. Must be > `start_date`. | | `human_mode` | boolean | No | `false` | Attempt a browser-like retrieval path when standard search access is blocked by bot protection. | | `page` | integer | No | `1` | Number of result pages to aggregate into the response. Minimum: `1`. | **`site` parameter rule:** Use the `site` parameter for domain restriction (e.g., `"github.com"`). The API prepends `site:` to your query automatically. Do **not** put `site:` directly into the `query` field when using the `site` parameter — that creates a doubled `site:site:` prefix. If you need manual operator control, put `site:domain` in `query` and leave `site` empty, but never use both in the same request. --- ## Response fields reference | Field | Type | Description | | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------- | | `success` | boolean | Whether the search executed successfully. | | `query` | string | The query as interpreted by the API (includes `site:` prefix if `site` was set). | | `timestamp` | integer | Unix timestamp in milliseconds when the search was performed. | | `results` | array | Search results. Shape varies by `source` — see [Sources](#sources). | | `metadata.total_results` | integer | Total number of results available across all pages (may exceed the number in the `results` array if you requested fewer pages). | | `metadata.failed_pages` | array | Page numbers that failed to return results. | | `metadata.empty_pages` | array | Page numbers that returned no results. | **Timestamps:** Search `timestamp` is in **milliseconds**. Fetch `timestamp` is in **seconds**. Divide Search timestamps by 1000 when comparing across endpoints. --- ## Error handling Search returns `400` for invalid requests and `401` for auth failures. ```json 400 — missing query { "error": { "type": "invalid_request", "message": "query: This field is required.", "metadata": [] } } ``` ```json 400 — invalid source { "error": { "type": "invalid_request", "message": "sources: {0: [ErrorDetail(string='\"invalid_source\" is not a valid choice.', code='invalid_choice')]}", "metadata": [] } } ``` ```json 401 — bad API key { "message": "Invalid API key in request" } ``` ### Error responses Both Web API endpoints return structured errors at the **request level**. These are separate from per-item failures within a successful `200` response (see [Fetch partial failures](/web-docs/fetch/introduction#handle-partial-failures)). | Status | Meaning | Example trigger | | ------ | -------------------------------------------------------------- | ------------------------------------------- | | `400` | Invalid request — missing field, invalid value, or bad header | Omitting `query`, empty `urls`, bad `sources` value | | `401` | Missing or invalid API key | No `Authorization` header, wrong key | ```json { "error": { "type": "invalid_request", "message": "query: This field is required.", "metadata": [] } } ``` Common `400` messages: - `"query: This field is required."` — missing `query` in Search - `"query: This field may not be blank."` — empty `query` string - `"urls: This field is required."` — missing `urls` in Fetch - `"urls: This list may not be empty."` — empty `urls` array - `"sources: ... is not a valid choice."` — invalid source type - `"Insufficient credits for the requested number of pages."` — account out of credits - `"Missing required header: x-api-version."` — missing version header ```json { "message": "Invalid API key in request" } ``` Common `401` messages: - `"Missing API key in request"` — no `Authorization` header - `"Invalid API key in request"` — wrong or expired API key --- ## Common gotchas | Mistake | Fix | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Omitting `sources` and expecting uniform results | Different sources return different fields. Specify `sources` explicitly for predictable parsing. | | Using `site` with `scholar-author` or `ai` sources | `site` only applies to `web` and `news` sources. It has no effect on academic or deep research searches. | | Expecting `snippet` in deep research mode results | Deep research mode returns `content` and `references` instead of `snippet` and `position`. | | Expecting `position` in scholar-author results | Academic author results don't have `position` — they have `name`, `affiliation`, `citations`, etc. | | Using `start_date` >= `end_date` | `start_date` must be strictly less than `end_date`. | --- ## API reference summary | Detail | Value | | ------------ | ------------------------------------------------------------------------------------------------------------ | | **Endpoint** | `POST /web/search/live` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Pricing** | `1 credit per query` | | **Request** | `query` (required). Optional: `location`, `sources`, `site`, `start_date`, `end_date`, `human_mode`, `page`. | | **Response** | Object: `{ success, query, timestamp, results[], metadata }` | | **Errors** | `400` (bad request), `401` (bad auth) | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. ## Sources [Web Search](/web-docs/search/introduction) supports seven source types. Each returns a different result shape — always specify `sources` explicitly when you need predictable parsing. All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` Replace `YOUR_API_KEY` in each example with your actual API key. | Source | Best use case | Fetchable `url`? | `site` effective? | Date filters effective? | | --------------------------- | ------------------------ | ---------------- | ----------------- | ----------------------- | | `web` | General web search | Yes | Yes | Yes | | `news` | News articles | Yes | Yes | Yes | | `scholar-articles` | Academic papers | Yes | No | Yes | | `scholar-articles-enriched` | Papers + author profiles | Yes | No | Yes | | `scholar-author` | Researcher profiles | No | No | No | | `ai` | AI-generated summaries | No | No | No | | `social` | Social media mentions | Yes | No | No | --- ## Result shapes by source The `results[]` array shape depends on the `source` field of each result. Use this reference when parsing multi-source responses. Standard web and news results share the same shape. | Field | Type | Description | | ---------- | ------- | ------------------------------------------------- | | `source` | string | `"web"` or `"news"`. | | `title` | string | Page title. | | `url` | string | Page URL. | | `snippet` | string | Text excerpt. | | `position` | integer | Result position (1-based). | ```json { "source": "web", "title": "Crustdata: Real-Time B2B Data Broker via API or Data Feed", "url": "https://crustdata.com/", "snippet": "Crustdata is a B2B data provider offering real-time company & people datasets.", "position": 1 } ``` Academic article results include citation data, author information, and optional PDF links. | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------ | | `source` | string | `"scholar-articles"` or `"scholar-articles-enriched"`. | | `title` | string | Article title. | | `url` | string | Link to the article. | | `snippet` | string | Abstract excerpt. | | `metadata` | string | Citation string: `"Author - Year - Publisher"`. | | `pdf_url` | string? | Direct PDF link, if available. | | `position` | integer | Result position (1-based). | | `authors` | array | `[{ name, profile_url, profile_id }]`. | | `citations` | integer | Total citation count. | ```json { "source": "scholar-articles", "title": "Understanding deep learning", "url": "https://books.google.com/books?hl=en&lr=lang_en&id=rvyxEAAAQBAJ", "snippet": "...to this field understand the principles behind deep learning.", "metadata": "SJD Prince - 2023 - books.google.com", "pdf_url": null, "position": 1, "authors": [ { "name": "SJD Prince", "profile_url": "https://scholar.google.com/citations?user=fjm67xYAAAAJ&hl=en&oi=sra", "profile_id": "fjm67xYAAAAJ" } ], "citations": 618 } ``` Use `scholar-articles-enriched` instead of `scholar-articles` to get richer author profile data. The result shape is the same, with more author fields populated. Author profile results have a completely different shape — no `snippet`, `position`, or `title`. Instead, you get a full researcher profile. | Field | Type | Description | | ------------- | ------- | --------------------------------------------------- | | `source` | string | `"scholar-author"`. | | `url` | string | Academic profile URL. | | `name` | string | Author full name. | | `affiliation` | string | Institutional affiliation. | | `website` | string? | Personal or institutional website. | | `interests` | array | `[{ title, link }]` — research interests. | | `thumbnail` | string? | Profile photo URL. | | `citations` | object | `{ all, since_2020 }` — total and recent counts. | | `h_index` | object | `{ all, since_2020 }`. | | `i10_index` | object | `{ all, since_2020 }`. | | `articles` | array | Top publications: `[{ title, url, year, citations, authors, publication }]`. | ```json { "source": "scholar-author", "url": "https://scholar.google.com/citations?user=NMS69lQAAAAJ&hl=en&oi=ao", "name": "Jeff Dean", "affiliation": "Google Chief Scientist, Google Research and Google DeepMind", "website": "http://research.google.com/people/jeff", "interests": [ { "title": "Distributed systems", "link": "https://scholar.google.com/..." } ], "citations": { "all": 401624, "since_2020": 231008 }, "h_index": { "all": 114, "since_2020": 78 }, "i10_index": { "all": 319, "since_2020": 203 }, "articles": [ { "title": "MapReduce: simplified data processing on large clusters", "url": "https://scholar.google.com/...", "year": "2008", "citations": "37255", "authors": "J Dean, S Ghemawat", "publication": "Communications of the ACM 51 (1), 107-113, 2008" } ] } ``` Deep research mode returns a single AI-generated overview with source references. No `snippet`, `position`, or standard search fields. | Field | Type | Description | | ------------ | ------- | -------------------------------------------------------- | | `source` | string | `"ai"`. | | `title` | string | Always `"AI Overview"`. | | `content` | string | AI-generated overview text. | | `references` | array | Source articles: `[{ title, url, snippet }]`. | | `images` | array | Embedded images: `[{ url, alt, width, height }]`. | ```json { "source": "ai", "title": "AI Overview", "content": "The primary difference between uv and pip is speed and scope...", "references": [ { "title": "uv vs pip: Managing Python Packages and Dependencies", "url": "https://realpython.com/uv-vs-pip/", "snippet": "When it comes to Python package managers..." } ], "images": [] } ``` Social media results use the same shape as web/news results. | Field | Type | Description | | ---------- | ------- | ------------------------------------------------- | | `source` | string | `"social"`. | | `title` | string | Post or page title. | | `url` | string | Post URL. | | `snippet` | string | Post excerpt. | | `position` | integer | Result position (1-based). | Social search results may return empty for some queries depending on availability. Always check `results.length` before processing. ### Result ordering and ranking When querying a single source, `position` reflects the source's natural ranking order. When querying multiple sources, results from different sources are interleaved and `position` may reflect a per-source rank rather than a global rank. `metadata.total_results` is the total count across all requested sources and pages. ### Parsing multi-source responses When you query multiple sources at once (or omit `sources`), the `results[]` array can contain items with different shapes. Always check the `source` field of each result to determine which fields are available: ```javascript for (const result of response.results) { switch (result.source) { case "web": case "news": case "social": // Standard: title, url, snippet, position console.log(result.title, result.url); break; case "scholar-articles": case "scholar-articles-enriched": // Academic: standard fields + authors, citations, pdf_url, metadata console.log(result.title, result.citations, result.authors); break; case "scholar-author": // Author profile: name, affiliation, h_index, articles[] console.log(result.name, result.affiliation, result.h_index); break; case "ai": // AI overview: content, references[] console.log(result.content, result.references); break; } } ``` --- ## Field presence by source Use this reference to determine which fields are present for each source type. **Naming note:** The API uses `metadata` in two different contexts. The **response-level** `metadata` is an object with `total_results`, `failed_pages`, and `empty_pages`. The **per-result** `metadata` field (scholar-articles only) is a citation string like `"Author - Year - Publisher"`. Always use the full path (`response.metadata` vs `result.metadata`) to avoid confusion. **Standard fields** — present in `web`, `news`, `social`, and `scholar-articles` / `scholar-articles-enriched`: | Field | Sources with this field | Notes | | ---------- | -------------------------------------------------------------- | -------------------------------- | | `source` | All sources | Always present | | `title` | `web`, `news`, `social`, `scholar-articles*`, `ai` | AI: always `"AI Overview"` | | `url` | `web`, `news`, `social`, `scholar-articles*`, `scholar-author` | Academic author: profile link | | `snippet` | `web`, `news`, `social`, `scholar-articles*` | Absent in `ai`, `scholar-author` | | `position` | `web`, `news`, `social`, `scholar-articles*` | Absent in `ai`, `scholar-author` | **Academic article fields** — `scholar-articles` and `scholar-articles-enriched` only: | Field | Type | Notes | | ----------- | ------- | --------------------------------------------------- | | `metadata` | string | Citation string: `"Author - Year - Publisher"` | | `pdf_url` | string? | Direct PDF download link — handle outside Web Fetch | | `authors` | array | `[{ name, profile_url, profile_id }]` | | `citations` | integer | Total citation count | **Academic author fields** — `scholar-author` only: | Field | Type | Notes | | ------------- | ------- | ------------------------------------------------------------ | | `name` | string | Author full name | | `affiliation` | string | Institutional affiliation | | `website` | string? | Personal or institutional website | | `interests` | array | `[{ title, link }]` | | `thumbnail` | string? | Profile photo URL | | `citations` | object | `{ all, since_2020 }` — different type than scholar-articles | | `h_index` | object | `{ all, since_2020 }` | | `i10_index` | object | `{ all, since_2020 }` | | `articles` | array | `[{ title, url, year, citations, authors, publication }]` | **Deep research mode fields** — `ai` only: | Field | Type | Notes | | ------------ | ------ | ---------------------------------------------- | | `content` | string | AI-generated overview text | | `references` | array | `[{ title, url, snippet }]` — fetch these URLs | | `images` | array | `[{ url, alt, width, height }]` | --- # Web Fetch Source: https://docs.crustdata.com/web-docs/fetch/introduction Fetch the HTML content of public webpages by URL for content extraction, data collection, and monitoring. **Use this when** you have specific URLs and need to retrieve their full HTML content — for content extraction, data collection, SEO analysis, or change tracking. The Web Fetch API accepts a list of URLs and returns the page title and full HTML content for each. You can fetch up to 10 URLs in a single request. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/web/enrich/live ``` The endpoint path is `/web/enrich/live` (not `/web/fetch/live`) because it follows the Crustdata convention where "enrich" means adding data to a known identifier — in this case, enriching a URL with its page content. All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` Replace `YOUR_API_KEY` in each example with your actual API key. Pricing: 1 credit per page. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. ## Request body | Parameter | Type | Required | Default | Description | | ------------ | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------- | | `urls` | string[] | Yes | — | URLs to fetch. Min: 1, max: 10. Must include `http://` or `https://`. | | `human_mode` | boolean | No | `false` | Attempt a browser-like fetch path when a site is protected by Cloudflare or similar bot controls. | ## Response body The response is an **array** (not an object) — one entry per URL in your request. | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------------- | | `success` | boolean | Whether this URL was fetched successfully. | | `url` | string? | The URL that was fetched. `null` if the fetch failed. | | `timestamp` | integer? | Unix timestamp (**seconds**) when fetched. `null` on failure. | | `title` | string? | The `` tag content. `null` on failure. | | `content` | string? | Full HTML content of the page. `null` on failure. | <Note> **Timestamps:** Fetch timestamps are in **seconds**. Search timestamps are in **milliseconds**. Account for this when comparing timestamps across endpoints. </Note> --- ## Fetch a single URL The simplest request fetches one URL and returns its HTML content. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": ["https://example.com"] }' ``` ```json Response [ { "success": true, "url": "https://example.com", "timestamp": 1775193366, "title": "Example Domain", "content": "<html lang=\"en\"><head><title>Example Domain...

Example Domain

This domain is for use in documentation examples.

" } ] ``` The `content` field is trimmed here. It contains the full HTML of the fetched page. **Extract:** Parse `content` using an HTML parser (BeautifulSoup for Python, Cheerio for Node.js) to extract specific elements like text, links, or metadata. --- ## Examples Ready-to-copy patterns for Web Fetch. Each example shows a real request, the response, and what to extract. Pass up to 10 URLs to fetch their content in parallel. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": [ "https://example.com", "https://example.org", "https://www.crustdata.com" ] }' ``` ```json Response [ { "success": true, "url": "https://example.org", "timestamp": 1775193386, "title": "Example Domain", "content": "Example Domain..." }, { "success": true, "url": "https://example.com", "timestamp": 1775193386, "title": "Example Domain", "content": "Example Domain..." }, { "success": true, "url": "https://www.crustdata.com", "timestamp": 1775193387, "title": "Crustdata: Real-Time B2B Data Broker", "content": "..." } ] ``` The response array order may differ from the request order. Match successful results by their `url` field, not by array index. When some URLs succeed and others fail, the request still returns `200`. Failed URLs have `success: false` with all other fields as `null`. ```bash Request curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": [ "https://example.com", "https://this-domain-does-not-exist-xyz.com" ] }' ``` ```json Response [ { "success": true, "url": "https://example.com", "timestamp": 1775193366, "title": "Example Domain", "content": "Example Domain..." }, { "success": false, "url": null, "timestamp": null, "title": null, "content": null } ] ``` ### Correlating failures to input URLs Failed entries have `url: null`, so you cannot directly identify which input URL failed. To correlate failures: 1. Track the URLs you sent. 2. Collect the `url` values from all successful entries. 3. Any input URL not in the successful set is the one that failed. ```javascript const requestedUrls = [ "https://example.com", "https://this-domain-does-not-exist-xyz.com", ]; const successfulUrls = new Set( fetchResponse.filter((r) => r.success).map((r) => r.url), ); const failedUrls = requestedUrls.filter((url) => !successfulUrls.has(url)); // failedUrls = ["https://this-domain-does-not-exist-xyz.com"] ``` Always check the `success` field for each entry in the response array. Build your parsing logic to handle both successful and failed entries gracefully. Some websites use Cloudflare to block automated requests. Set `human_mode: true` to attempt a browser-like fetch path for these pages. ```bash curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": ["https://example.com"], "human_mode": true }' ``` Cloudflare bypass is not guaranteed. Some sites have additional protections that may still block the request. The `content` field returns raw HTML. Here are common next steps: | Task | Approach | | --------------------- | -------------------------------------------------------------- | | Extract text | Parse HTML and strip tags (BeautifulSoup, Cheerio, etc.) | | Extract links | Find all `` tags and their `href` attributes | | Extract metadata | Parse `` tags for SEO data (description, og:title, etc.) | | Detect changes | Fetch periodically and diff the `content` or `title` fields | | Resolve relative URLs | Combine relative paths with the base `url` from the response | --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | ------------ | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------- | | `urls` | string[] | Yes | — | URLs to fetch. Min: 1, max: 10. Must include `http://` or `https://`. | | `human_mode` | boolean | No | `false` | Attempt a browser-like fetch path when a site is protected by Cloudflare or similar bot controls. | ## Response fields reference The response is a top-level array. Each item corresponds to one URL in your request. | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------------- | | `success` | boolean? | Whether this URL was fetched successfully. May be `null` when the result entry contains only error information. | | `url` | string? | The URL that was fetched. `null` if the fetch failed. | | `timestamp` | integer? | Unix timestamp (**seconds**) when fetched. `null` on failure. | | `title` | string? | The `` tag content. `null` on failure. | | `content` | string? | Full HTML content of the page. `null` on failure. | <Note> **Timestamps:** Fetch timestamps are in **seconds**. Search timestamps are in **milliseconds**. Account for this when comparing timestamps across endpoints. </Note> <Note> **Two kinds of failure, two places to check:** - **Request-level errors** (`400`, `401`) — the entire request failed. You get an error object, not an array. Caused by missing fields, empty arrays, or bad auth. - **Per-URL failures** within a `200` — individual entries with `success: false` and `null` fields. Caused by unreachable URLs, timeouts, or bot protection. Always check the HTTP status first, then check `success` for each entry in the array. </Note> --- ## Error handling Fetch returns request-level errors for invalid input or auth failures. These are separate from per-URL `success: false` entries within a `200` response. <CodeGroup> ```json 400 — missing urls { "error": { "type": "invalid_request", "message": "urls: This field is required.", "metadata": [] } } ``` ```json 400 — empty urls array { "error": { "type": "invalid_request", "message": "urls: This list may not be empty.", "metadata": [] } } ``` ```json 401 — bad API key { "message": "Invalid API key in request" } ``` </CodeGroup> ### Error responses Both Web API endpoints return structured errors at the **request level**. These are separate from per-item failures within a successful `200` response (see [Fetch partial failures](/web-docs/fetch/introduction#handle-partial-failures)). | Status | Meaning | Example trigger | | ------ | -------------------------------------------------------------- | ------------------------------------------- | | `400` | Invalid request — missing field, invalid value, or bad header | Omitting `query`, empty `urls`, bad `sources` value | | `401` | Missing or invalid API key | No `Authorization` header, wrong key | <Accordion title="400 error format"> ```json { "error": { "type": "invalid_request", "message": "query: This field is required.", "metadata": [] } } ``` Common `400` messages: - `"query: This field is required."` — missing `query` in Search - `"query: This field may not be blank."` — empty `query` string - `"urls: This field is required."` — missing `urls` in Fetch - `"urls: This list may not be empty."` — empty `urls` array - `"sources: ... is not a valid choice."` — invalid source type - `"Insufficient credits for the requested number of pages."` — account out of credits - `"Missing required header: x-api-version."` — missing version header </Accordion> <Accordion title="401 error format"> ```json { "message": "Invalid API key in request" } ``` Common `401` messages: - `"Missing API key in request"` — no `Authorization` header - `"Invalid API key in request"` — wrong or expired API key </Accordion> --- ## Common gotchas | Mistake | Fix | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Omitting `http://` or `https://` in URLs | All URLs must include the protocol prefix. | | Sending more than 10 URLs | The API accepts a maximum of 10 URLs per request. Batch larger lists. | | Assuming response order matches request | Match results by the `url` field, not by array index. | | Treating a `200` as all-success | A `200` can contain failed entries. Check `success` for each item. | | Sending an empty `urls` array | Returns `400`: `"urls: This list may not be empty."`. | | Expecting JavaScript-rendered content | The API fetches server-side HTML. JavaScript-heavy SPAs may return minimal HTML. | | Comparing Search and Fetch timestamps | Search uses milliseconds, Fetch uses seconds. Divide Search by 1000 to compare. | --- ## API reference summary | Detail | Value | | ------------ | -------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST /web/enrich/live` | | **Auth** | Bearer token + `x-api-version: 2025-11-01` | | **Pricing** | `1 credit per page` | | **Request** | `urls` (1–10 URLs, required). Optional: `human_mode`. | | **Response** | Top-level array: `[{ success, url, timestamp, title, content }]` | | **Errors** | `400` (bad request), `401` (bad auth), `500` (server error). Per-URL failures appear in the array. | See the [full API reference](/openapi-specs/2025-11-01/introduction) for the complete OpenAPI schema. --- ## What to do next - **Search then fetch** — see [Web Search examples](/web-docs/search/introduction#examples) for search-then-fetch workflow patterns. - **Find URLs to fetch** — use [Web Search](/web-docs/search/introduction) to find URLs for downstream fetching. # Search Social Posts Source: https://docs.crustdata.com/social-post-docs/search/introduction Search public posts from professional networks by keyword and filter on author, mentioned company, industry, member, content type, and recency. **Use this when** you want to find recent posts that match a topic, are authored by people in a specific industry or title, are written by or mention a specific person or company, or contain a specific content type (videos, photos, jobs, documents, live videos, collaborative articles). The Search Social Posts API accepts an optional keyword plus filters and returns matching posts in real time, retrieved from the web. Either a keyword or at least one filter is needed to scope results. Each result includes the post text, engagement metrics (reactions, comments, shares), author details, and extracted hyperlinks. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/social_post/professional_network/search/live ``` All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> <Callout icon="lock" color="#f59e0b"> <strong>Pricing:</strong> <code>1 credit per post returned</code> by default · <code>3 credits/post</code> with{" "} <code>exact_keyword_match=true</code>. Reactor and comment enrichment is not available on this endpoint — use{" "} <a href="/social-post-docs/enrichment/introduction">Enrich Social Posts</a>{" "} with each post's <code>share_url</code>. </Callout> <Note> Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. </Note> <CardGroup cols={1}> <Card title="Filters" icon="user" href="/social-post-docs/search/reference#filters"> All seven filter dimensions — `PROFESSIONAL_NETWORK_MEMBER`, `MENTIONING_PROFESSIONAL_NETWORK_MEMBER`, `COMPANY`, `MENTIONING_COMPANY`, `AUTHOR_INDUSTRY`, `AUTHOR_COMPANY`, `AUTHOR_TITLE` — plus `date_posted`, `content_type`, `sort_by`, and `exact_keyword_match`. </Card> <Card title="Examples" icon="list-filter" href="#examples" > Boolean keywords, member-restricted searches, content-type filtering, and exact-phrase matching. </Card> <Card title="Reference" icon="book" href="/social-post-docs/search/reference" > Request body, response shape, every response field, and error handling. </Card> </CardGroup> --- ## Your first search The simplest search uses a `keyword` and a `date_posted` recency window. Pass `limit` to control how many posts come back. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "AI agents", "date_posted": "past-week", "limit": 1 }' ``` ```json Response [ { "actor": { "actor_type": "company", "name": "TekkPak Technologies", "headline": "2,234 followers", "professional_network_url": "https://www.linkedin.com/company/tekkpak/posts", "professional_network_urn": "urn:li:fsd_company:34229072", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" }, "backend_urn": "urn:li:activity:7477412345997144066", "share_url": "https://www.linkedin.com/posts/tekkpak_aivoiceagents-activity-7477412345997144066-wcok", "text": "The era of Press 1 for Sales, press 2 for support is over...", "date_posted": "2026-06-29", "post_type": "original", "has_video": false, "engagement": { "total_reactions": 0, "total_comments": 0, "num_shares": 0, "reactions_by_type": {} } } ] ``` </CodeGroup> <Note>Response trimmed for clarity.</Note> **Extract:** The response is a top-level **array** (not an object) — the same post shape the enrich endpoint returns. Each post carries a nested `actor` object (`actor_type` is `person` or `company`), an `engagement` object (`total_reactions`, `total_comments`, `num_shares`, `reactions_by_type`), the `post_type`, and the original `share_url`. --- ## Pagination uses `page` OR `limit` — never both Provide **exactly one** of these two — they are mutually exclusive, and sending neither returns a `400`: - **`page`** (`1`–`20`): steps through results a fixed **5 posts per page** (up to 100 posts total). Use it to page through a result set. - **`limit`** (`1`–`100`): returns that many posts in a single response, from the top with no page cursor. Use it to pull **more than 5 posts in one request**. <CodeGroup> ```bash Page-based curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "fundraising", "page": 1 }' ``` ```bash Limit-based curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "fundraising", "limit": 50 }' ``` </CodeGroup> <Warning> Sending both `page` and `limit` returns a `400`:{" "} <code>'limit' and 'page' cannot be provided together</code>. When using{" "} <code>exact_keyword_match: true</code>, you must use <code>limit</code> (the API scans the first <em>n</em> posts and returns those that match exactly). </Warning> --- ## Examples Practical patterns for searching social posts. Each example is a full working request; every one was verified by a live API call. For the operator list, field catalog, and validation rules, see [Search reference](/social-post-docs/search/reference). All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> <AccordionGroup> <Accordion title="1. Boolean keywords with OR / AND"> Combine up to six keywords with `OR` and `AND` operators directly in the `keyword` string. Useful for fundraising or hiring signal queries. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "fundraise OR raised", "date_posted": "past-week", "limit": 1, "filters": [ {"field": "AUTHOR_INDUSTRY", "value": ["Software Development"]} ] }' ``` </CodeGroup> <Tip> Keep keyword lists tight — six terms is the practical ceiling. Beyond that, split the work into multiple requests and merge by <code>backend_urn</code>. </Tip> </Accordion> <Accordion title="2. Series A signals — combine industry + title + investor mention"> Surface posts where founders are talking about Series A rounds and tagging a specific investor. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "Series A", "date_posted": "past-month", "limit": 1, "filters": [ {"field": "AUTHOR_INDUSTRY", "value": ["Software Development"]}, {"field": "AUTHOR_TITLE", "value": ["Co-Founder"]}, {"field": "MENTIONING_COMPANY", "value": ["https://www.linkedin.com/company/sequoia"]} ] }' ``` </CodeGroup> The three filters AND together — every returned post must satisfy all of them. Within `value[]`, multiple entries OR together. </Accordion> <Accordion title="3. Posts by specific people in the last 24 hours"> When you want to monitor a small set of named accounts, pass their profile URLs to `PROFESSIONAL_NETWORK_MEMBER` — `keyword` can be omitted entirely. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "date_posted": "past-24h", "limit": 5, "filters": [ {"field": "PROFESSIONAL_NETWORK_MEMBER", "value": [ "https://www.linkedin.com/in/satyanadella", "https://www.linkedin.com/in/dvdhsu" ]} ] }' ``` </CodeGroup> <Note> <code>keyword</code> is optional. The high-frequency placeholder{" "} <code>"a"</code> shown in older examples is no longer required — omit the field entirely when filtering by member or company. </Note> </Accordion> <Accordion title="4. Filter to videos and photos"> Find launch announcements, product demos, or any visual content. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "product launch", "date_posted": "past-week", "limit": 1, "content_type": ["videos", "photos"], "filters": [ {"field": "AUTHOR_INDUSTRY", "value": ["Software Development"]} ] }' ``` </CodeGroup> `content_type` accepts an array of values to OR together. Other valid values: `documents`, `jobs`, `liveVideos`, `collaborativeArticles`. </Accordion> <Accordion title="5. Exact-phrase match for "starting a new position""> The job-change signal — find posts that contain an exact phrase. Use `limit` (not `page`); the API scans the first `n` posts and returns those that match the phrase exactly. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "Starting a new position", "limit": 5, "exact_keyword_match": true, "date_posted": "past-week" }' ``` </CodeGroup> <Warning> With <code>exact_keyword_match: true</code>, use <code>limit</code>, not{" "} <code>page</code> — the API scans the first <em>n</em> posts (where{" "} <em>n</em> = <code>limit</code>) and returns only those that contain the exact phrase. These requests are billed at <code>3 credits per post</code>. </Warning> </Accordion> <Accordion title="6. Posts mentioning a competitor's company page"> Track the conversation around a specific company without restricting to authors at that company. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "date_posted": "past-week", "limit": 5, "filters": [ {"field": "MENTIONING_COMPANY", "value": ["https://www.linkedin.com/company/openai/"]} ] }' ``` </CodeGroup> Pair this with `MENTIONING_PROFESSIONAL_NETWORK_MEMBER` to see who is talking about a specific person. </Accordion> <Accordion title="7. Search → enrich workflow"> Search returns post-level engagement totals in `engagement` (`total_reactions`, `total_comments`, `num_shares`) but does **not** include the individual `reactors` or `comments` per post. To pull those, take each `share_url` from the search response and feed it into the [Enrich Social Posts](/social-post-docs/enrichment/introduction) endpoint. <Steps> <Step title="Search"> Run a search with the filters you care about. Capture each post's{" "} <code>share_url</code> and <code>backend_urn</code>. </Step> <Step title="Enrich each post"> For each <code>share_url</code>, call the enrich endpoint with{" "} <code>social_post_url</code> and{" "} <code>fields: "reactors,comments"</code>. </Step> <Step title="Merge"> Combine the search-level context with the engagement list using{" "} <code>backend_urn</code> as the join key. </Step> </Steps> See [Enrichment Examples](/social-post-docs/enrichment/introduction#examples) for the enrich-side calls. </Accordion> </AccordionGroup> --- ## What to do next - **Narrow with filters** — see [Filters](/social-post-docs/search/reference#filters) for `AUTHOR_INDUSTRY`, `PROFESSIONAL_NETWORK_MEMBER`, `MENTIONING_COMPANY`, and the rest. - **Look up field details** — see [Reference](/social-post-docs/search/reference) for the full response shape and error handling. - **Pull reactors and comments** — see [Enrich Social Posts](/social-post-docs/enrichment/introduction) when you already have a profile URL or company identifier. # Search reference Source: https://docs.crustdata.com/social-post-docs/search/reference Reference for Search Social Posts: request parameters, response body, every field, error handling, and common gotchas. Reference material for [Search Social Posts](/social-post-docs/search/introduction): request parameters, response shape, every response field, and error handling. For walk-through examples, see [Introduction](/social-post-docs/search/introduction) and [Examples](/social-post-docs/search/introduction#examples). For all filter dimensions, see [Filters](#filters). All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> --- ## Request parameter reference | Parameter | Type | Required | Default | Description | | --------------------- | -------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `keyword` | string | No | — | Keyword or phrase to search for. Supports Boolean `OR` / `AND` with up to six terms (e.g., `"AI OR Documentation OR ai-safety"`). Optional — omit it to filter by `filters[]` alone. | | `page` | integer | One of† | — | Page-through mode. Page number `1`–`20`; each page returns a fixed **5 posts** (up to 100 posts total). Cannot be combined with `limit`. | | `limit` | integer | One of† | — | Single-batch mode. Number of posts returned in one response, `1`–`100`, from the top with no page cursor. Cannot be combined with `page`. Required when `exact_keyword_match` is `true`. | | `date_posted` | string | No | — | Recency window. One of `past-24h`, `past-week`, `past-month`, `past-quarter`, `past-year`. | | `content_type` | string[] | No | — | Filter by media. Any of `photos`, `jobs`, `collaborativeArticles`, `videos`, `documents`, `liveVideos`. Multiple values OR together. | | `sort_by` | string | No | `relevance` | Result order. `relevance` (top match first) or `date_posted` (deprecated; prefer `relevance`). | | `exact_keyword_match` | boolean | No | `false` | When `true`, the API scans the first `n` posts and returns only those that contain the exact keyword phrase. Use `limit`, not `page`. Billed at `3 credits/post`. | | `filters` | object[] | No | — | Additional filter conditions combined with AND. Each entry has `field` and `value[]`. See [Filters](#filters). | <Note> **† Provide exactly one of `page` or `limit`.** Sending neither returns `400` (`Either 'page' or 'limit' must be provided`); sending both returns `400` (`'limit' and 'page' cannot be provided together`). Use `page` to step through results 5 at a time (up to page `20` = 100 posts); use `limit` to pull up to 100 posts in a single request. </Note> <Note> The legacy `filter_type` key inside a filter is **rejected**. Use `field`. </Note> --- ## Response shape The response is a top-level **array** of post objects (not an object with a `posts` wrapper). Each entry is a `SocialPost` with a nested `actor` and `engagement` object — the same shape the enrich endpoint returns. ```json [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool", "professional_network_url": "https://www.linkedin.com/in/dvdhsu", "professional_network_urn": "urn:li:fsd_profile:ACoAAAEnBBoB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" }, "backend_urn": "urn:li:activity:7473081804934049792", "share_urn": "urn:li:ugcPost:7473081638059683840", "share_url": "https://www.linkedin.com/posts/dvdhsu_...", "text": "...", "date_posted": "2026-06-24", "post_type": "original", "has_video": true, "hyperlinks": { "...": "..." }, "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37, "reactions_by_type": { "LIKE": 172, "PRAISE": 37 } } } ] ``` --- ## Response field reference ### Core post fields | Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------ | | `actor` | object? | The post author (person or company). See [`actor`](#actor--reposter) below. | | `reposter` | object? | The reposting actor. Present only on reposts. Same shape as `actor`. | | `backend_urn` | string? | Internal URN of the post (e.g., `urn:li:activity:...`). | | `share_urn` | string? | Internal share URN of the post. | | `share_url` | string? | Public URL of the post. | | `text` | string? | Full post body text. | | `date_posted` | string? | Date the post was published (`YYYY-MM-DD`). | | `post_type` | string? | `original`, `repost_without_thoughts`, or `repost_quote`. | | `has_video` | boolean? | `true` when the post contains a video. | | `hyperlinks` | object? | URLs extracted from the body (see below). | | `engagement` | object? | Reaction, comment, and share counts (see below). | ### `actor` / `reposter` | Field | Type | Description | | -------------------------- | ------- | ----------------------------------------------- | | `actor_type` | string? | `person` or `company`. | | `name` | string? | Display name. | | `headline` | string? | Profile headline (person) or follower summary (company). | | `professional_network_url` | string? | Profile or company page URL. | | `professional_network_urn` | string? | Internal URN identifier. | | `profile_picture_url` | string? | Profile or company logo image URL. | ### `engagement` | Field | Type | Description | | ------------------- | -------- | ------------------------------------ | | `total_reactions` | integer? | Total reactions across all types. | | `total_comments` | integer? | Total comment count. | | `num_shares` | integer? | Number of times the post was shared. | | `reactions_by_type` | object? | Reaction counts by type (see below). | ### `reactions_by_type` Only reaction types present on the post are returned. | Field | Type | Description | | --------------- | -------- | ----------------------------------- | | `LIKE` | integer? | Count of `LIKE` reactions. | | `PRAISE` | integer? | Count of `PRAISE` reactions. | | `EMPATHY` | integer? | Count of `EMPATHY` reactions. | | `INTEREST` | integer? | Count of `INTEREST` reactions. | | `APPRECIATION` | integer? | Count of `APPRECIATION` reactions. | | `ENTERTAINMENT` | integer? | Count of `ENTERTAINMENT` reactions. | ### `hyperlinks` | Field | Type | Description | | ----------------------------------- | --------- | ----------------------------------------------------- | | `company_professional_network_urls` | string[]? | Company profile URLs mentioned in the post. | | `person_professional_network_urls` | string[]? | Person profile URLs mentioned in the post. | | `other_urls` | string[]? | External URLs that aren't company or person profiles. | | `media_urls` | string[]? | Media attachment URLs (images, videos, documents). | <Note> Field names suffixed with <code>?</code> may be <code>null</code>. Search returns the same post object as enrich — a nested <code>actor</code> and{" "} <code>engagement</code> — but does <strong>not</strong> return per-post{" "} <code>reactors</code> or <code>comments</code>. To pull those, use{" "} [Enrich Social Posts](/social-post-docs/enrichment/introduction). </Note> --- ## Reactor and commenter enrichment The search endpoint returns post-level engagement totals in `engagement` (`total_reactions`, `total_comments`, `num_shares`) but does not include the individual reactors or comments. To pull those, take each `share_url` from the search response and call [Enrich Social Posts](/social-post-docs/enrichment/introduction) with{" "} `social_post_url` and `fields: "reactors,comments"`. --- ## Error handling | Status | Type | Common causes | | ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | `invalid_request` | Both `page` and `limit` provided. Unsupported `field` value in a filter. Unsupported `date_posted` value. `filter_type` used instead of `field`. | | `401` | `unauthorized` | Invalid or missing API key. | | `500` | `internal_error` | Unexpected server error. Retry with backoff. | ### Examples <CodeGroup> ```json 400 — page and limit together { "error": { "type": "invalid_request", "message": "'limit' and 'page' cannot be provided together", "metadata": [] } } ``` ```json 400 — invalid value for field { "error": { "type": "invalid_request", "message": "filters: Invalid filter_type 'BAD_FIELD'. Allowed types are: MEMBER, MENTIONING_MEMBER, COMPANY, MENTIONING_COMPANY, AUTHOR_INDUSTRY, AUTHOR_COMPANY, AUTHOR_TITLE", "metadata": [] } } ``` ```json 400 — invalid date_posted { "error": { "type": "invalid_request", "message": "date_posted: Invalid date_posted value. Must be one of: past-24h, past-week, past-month, past-quarter, past-year", "metadata": [] } } ``` ```json 401 — auth failure { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` </CodeGroup> <Note> Although the request key is <code>field</code>, the API's error message still uses the legacy term <code>filter_type</code> when the value is unrecognized. The list of allowed types in the message is authoritative. </Note> --- ## Common gotchas - **Provide exactly one of `page` or `limit` — one is required.** They are mutually exclusive; sending neither returns a `400`. Use `page` (`1`–`20`) to step through results **5 posts at a time** (100 posts max); use `limit` (`1`–`100`) to pull a larger batch in a single request (no page cursor). - **`exact_keyword_match: true` requires `limit`.** Pagination via `page` is not supported in exact-match mode. - **Search response is a top-level array.** There is no `posts:` wrapper around the array (unlike the enrich endpoint). - **Author identity lives in `actor`.** `actor.actor_type` is `person` or `company`; reposts also carry a `reposter` actor. - **`keyword` is optional.** You can omit it entirely and rely on `filters[]` alone (e.g., to fetch all recent posts by a `PROFESSIONAL_NETWORK_MEMBER`). --- ## API summary | Detail | Value | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/social_post/professional_network/search/live` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **Version** | `x-api-version: 2025-11-01` header (required) | | **Pricing** | `1 credit/post` default · `3 credits/post` with `exact_keyword_match`. Reactors and comments are not available on search — use [Enrich Social Posts](/social-post-docs/enrichment/introduction). | | **Rate limit** | 10 requests/minute (default; contact gtm@crustdata.co for higher) | ## Filters [Search Social Posts](/social-post-docs/search/introduction) accepts seven filter dimensions in `filters[]` plus four top-level qualifiers (`date_posted`, `content_type`, `sort_by`, `exact_keyword_match`). Filters are combined with AND logic — a post must satisfy every filter to match. All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> --- ## Filter shape Every entry in `filters[]` has the same two keys. | Key | Type | Description | | ------- | -------- | ----------------------------------------------------------------- | | `field` | string | Filter dimension. One of the seven values listed below. | | `value` | string[] | One or more values combined with OR within the same filter entry. | ```json { "filters": [ { "field": "AUTHOR_INDUSTRY", "value": ["Software Development"] } ] } ``` <Warning> The legacy <code>filter_type</code> key is rejected with{" "} <code>'filter_type' is not supported. Use 'field' instead.</code> Always use{" "} <code>field</code>. </Warning> --- ## The seven filter fields | `field` | Filters posts by | Value format | | ---------------------------------------- | ------------------------------------------------------------------- | -------------------- | | `PROFESSIONAL_NETWORK_MEMBER` | Author profile URL (post is **written by** this person) | Profile URL | | `MENTIONING_PROFESSIONAL_NETWORK_MEMBER` | Profile URL **mentioned** in the post text | Profile URL | | `COMPANY` | Author company URL (post is **written by** this company) | Company profile URL | | `MENTIONING_COMPANY` | Company URL **mentioned** in the post | Company profile URL | | `AUTHOR_INDUSTRY` | Industry of the post's author (works for person and company actors) | Industry name string | | `AUTHOR_COMPANY` | Author's current company (for person actors) | Company profile URL | | `AUTHOR_TITLE` | Author's current title (for person actors) | Title string | ### `PROFESSIONAL_NETWORK_MEMBER` — posts authored by a specific person <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "AI", "limit": 1, "filters": [ {"field": "PROFESSIONAL_NETWORK_MEMBER", "value": ["https://www.linkedin.com/in/dvdhsu"]} ] }' ``` ```json Response (trimmed) [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool", "professional_network_url": "https://www.linkedin.com/in/dvdhsu" }, "date_posted": "2026-06-24", "post_type": "original", "text": "93% of technical execs we surveyed are alarmed...", "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37 } } ] ``` </CodeGroup> ### `MENTIONING_PROFESSIONAL_NETWORK_MEMBER` — posts that mention a specific profile <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "limit": 1, "filters": [ {"field": "MENTIONING_PROFESSIONAL_NETWORK_MEMBER", "value": ["https://www.linkedin.com/in/dvdhsu"]} ] }' ``` </CodeGroup> <Tip> <code>keyword</code> is optional — you can omit it entirely and rely on filters alone, as shown above. </Tip> ### `COMPANY` — posts authored by a specific company <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "limit": 1, "filters": [ {"field": "COMPANY", "value": ["https://www.linkedin.com/company/openai/"]} ] }' ``` ```json Response (trimmed) [ { "actor": { "actor_type": "company", "name": "OpenAI", "professional_network_url": "https://www.linkedin.com/company/openai/posts", "professional_network_urn": "urn:li:fsd_company:11130470" }, "date_posted": "2026-06-24", "post_type": "original", "engagement": { "total_reactions": 890, "total_comments": 137, "num_shares": 82 } } ] ``` </CodeGroup> ### `MENTIONING_COMPANY` — posts that mention a specific company <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "limit": 1, "filters": [ {"field": "MENTIONING_COMPANY", "value": ["https://www.linkedin.com/company/openai/"]} ] }' ``` </CodeGroup> ### `AUTHOR_INDUSTRY` — posts by authors in specific industries <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "AI", "limit": 1, "filters": [ {"field": "AUTHOR_INDUSTRY", "value": ["Software Development"]} ] }' ``` </CodeGroup> <Tip> Pass multiple industries to OR them together within the filter:{" "} <code> "value": ["Software Development", "Technology, Information and Internet"] </code> . </Tip> ### `AUTHOR_COMPANY` — posts by authors at a specific company <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "limit": 1, "filters": [ {"field": "AUTHOR_COMPANY", "value": ["https://www.linkedin.com/company/openai/"]} ] }' ``` </CodeGroup> ### `AUTHOR_TITLE` — posts by authors with a specific title <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "limit": 1, "filters": [ {"field": "AUTHOR_TITLE", "value": ["Co-Founder"]} ] }' ``` </CodeGroup> --- ## Top-level qualifiers These four parameters live alongside `keyword` in the request body, not inside `filters[]`. ### `date_posted` — recency window | Value | Window | | -------------- | -------------- | | `past-24h` | Last 24 hours | | `past-week` | Last 7 days | | `past-month` | Last 30 days | | `past-quarter` | Last 3 months | | `past-year` | Last 12 months | ```json { "keyword": "AI agents", "date_posted": "past-week" } ``` Sending an unsupported value returns a `400`: `Invalid date_posted value. Must be one of: past-24h, past-week, past-month, past-quarter, past-year`. ### `content_type` — filter by media type | Value | Description | | ----------------------- | ---------------------- | | `photos` | Posts with photos | | `videos` | Posts with videos | | `documents` | Posts with documents | | `jobs` | Job postings | | `liveVideos` | Live video posts | | `collaborativeArticles` | Collaborative articles | ```json { "keyword": "product launch", "content_type": ["videos", "photos"] } ``` `content_type` is an **array** — pass multiple values to OR them. ### `sort_by` — result ordering | Value | Order | | ------------- | -------------------------------------------------- | | `relevance` | Top match first (default) | | `date_posted` | Most recent first (deprecated; prefer `relevance`) | ```json { "keyword": "AI", "sort_by": "relevance" } ``` ### `exact_keyword_match` — exact phrase only When `true`, the API scans the first _n_ posts (where _n_ = `limit`) and returns only those that contain the exact keyword phrase. Use `limit`, not `page`. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "product launch", "exact_keyword_match": true, "limit": 3, "date_posted": "past-week" }' ``` </CodeGroup> <Note> <code>exact_keyword_match: true</code> requires <code>limit</code> (not{" "} <code>page</code>) — the API scans the first <em>n</em> posts and returns only those that contain the exact phrase. These requests are billed at{" "} <code>3 credits per post returned</code>. </Note> --- ## Combining filters Filters in `filters[]` AND together. To OR within a dimension, list multiple values inside one filter entry. ```json { "keyword": "Series A", "date_posted": "past-week", "filters": [ { "field": "AUTHOR_INDUSTRY", "value": [ "Software Development", "Technology, Information and Internet" ] }, { "field": "AUTHOR_TITLE", "value": ["Co-Founder", "Founder"] }, { "field": "MENTIONING_COMPANY", "value": ["https://www.linkedin.com/company/sequoia"] } ] } ``` The query above matches posts that: - contain `"Series A"`, - were posted in the last 7 days, - AND were authored by someone in Software Development OR Tech/Internet, - AND whose title is Co-Founder OR Founder, - AND mention Sequoia's company page. # Enrich Social Posts Source: https://docs.crustdata.com/social-post-docs/enrichment/introduction Fetch recent posts for a person profile URL, a single post URL, or a company identifier — with engagement metrics, hyperlinks, and optional reactor and commenter detail. **Use this when** you already know _who_ you want posts for: a specific person, a single post URL, or a company. The endpoint pulls recent posts for that entity in real time, retrieved from the web, with engagement metrics and optional reactor and commenter enrichment. The Enrich Social Posts API accepts exactly one identifier per request and returns recent posts for that entity. You can also pass `fields` to opt into reactor or commenter enrichment per post. Every request goes to the same endpoint: ``` POST https://api.crustdata.com/social_post/professional_network/enrich/live ``` All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> <Callout icon="lock" color="#f59e0b"> <strong>Pricing:</strong> <code>1 credit per post returned</code> by default · <code>5 credits/post</code> with <code>fields=reactors</code> or{" "} <code>fields=comments</code> · <code>10 credits/post</code> with both. </Callout> <Note> Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. **Latency** is typically 30–60 seconds when pulling reactors or comments for a full page. </Note> <CardGroup cols={1}> <Card title="Identifiers" icon="key" href="#examples" > The four identifier shapes — person URL, single post URL, company name/domain/id/URL — each with a verified example. </Card> <Card title="Examples" icon="list-filter" href="#examples" > Pull reactors, pull comments, paginate, filter to originals only, fetch a single post by URL. </Card> <Card title="Reference" icon="book" href="/social-post-docs/enrichment/reference" > Request body, response shape, every response field, and error handling. </Card> </CardGroup> --- ## Your first enrichment The simplest call passes `professional_network_profile_url` and a `limit`. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 1 }' ``` ```json Response { "posts": [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool", "professional_network_url": "https://www.linkedin.com/in/dvdhsu", "professional_network_urn": "urn:li:fsd_profile:ACoAAAEnBBoBUwCVyafB-XsEBTeHaFN8ScMKh6U", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example/profile-displayphoto-shrink_100_100.jpg" }, "backend_urn": "urn:li:activity:7473081804934049792", "share_urn": "urn:li:ugcPost:7473081638059683840", "share_url": "https://www.linkedin.com/posts/dvdhsu_93-of-technical-execs-activity-7473081804934049792-zACV", "text": "93% of technical execs we surveyed are alarmed about vibe-coded apps in their orgs...", "date_posted": "2026-06-17", "post_type": "original", "has_video": true, "hyperlinks": { "company_professional_network_urls": [], "person_professional_network_urls": [], "other_urls": [], "media_urls": [] }, "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37, "reactions_by_type": { "LIKE": 172, "PRAISE": 37, "EMPATHY": 11, "INTEREST": 2, "APPRECIATION": 1 } } } ] } ``` </CodeGroup> **Extract:** The response wraps posts in a `posts[]` array (different from the search endpoint, which returns a top-level array). Each post carries a nested `actor` object (author identity), an `engagement` object (reaction and comment counts, plus `reactions_by_type`), the `post_type`, and the public `share_url`. Reposts also include a `reposter` actor. --- ## Identifier rules You must pass **exactly one** identifier: | Identifier | Type | Returns | | ---------------------------------- | ------- | ------------------------------------------------------------------------ | | `professional_network_profile_url` | string | Recent posts authored by the person. | | `social_post_url` | string | The single post at this URL (pagination is ignored). | | `company_name` | string | Recent posts authored by the company. | | `company_domain` | string | Recent posts authored by the company (resolved by domain). | | `crustdata_company_id` | integer | Recent posts authored by the company (resolved by Crustdata company ID). | | `company_professional_network_url` | string | Recent posts authored by the company (resolved by company page URL). | <Warning> Sending more than one identifier returns a <code>400</code>:{" "} <code>Only one identifier type can be provided at a time.</code> Sending none returns a <code>400</code> listing the required keys. </Warning> --- ## Pagination uses `page` OR `limit` — never both Pass `page` (1–20, returns 5 posts/page) or `limit` (1–100 in one response). When using `social_post_url`, pagination is not applicable — the response always contains exactly one post. ```json { "professional_network_profile_url": "...", "page": 1 } ``` ```json { "professional_network_profile_url": "...", "limit": 25 } ``` <Warning> Sending both <code>page</code> and <code>limit</code> returns a 400 with{" "} <code>Only one of 'page' or 'limit' can be provided, not both.</code> </Warning> --- ## Examples Worked recipes you can copy, paste, and adapt. Each example is a full working request. For the core walkthrough (your first enrichment, identifier rules, pagination), see the sections above. For the request body, response shape, every response field, and error handling, see [Reference](/social-post-docs/enrichment/reference). <AccordionGroup> <Accordion title="Pull recent posts with reactors"> Pass `fields: "reactors"` to include the list of profiles who reacted. Use `max_reactors` to set the upper bound per post. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 1, "fields": "reactors", "max_reactors": 100 }' ``` ```json Response (trimmed) { "posts": [ { "reactors": [ { "reaction_type": "LIKE", "reactor": { "actor_type": "person", "name": "Jenni Goodman", "headline": "Principal Recruiter at Plenty Search", "professional_network_url": "https://www.linkedin.com/in/ACoAAAFSHMMB...", "professional_network_urn": "urn:li:fsd_profile:ACoAAAFSHMMB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" } } ] } ] } ``` </CodeGroup> <Note> Each entry pairs a <code>reaction_type</code> with the reactor's{" "} <code>actor</code> — <code>actor_type</code>, <code>name</code>,{" "} <code>headline</code>, <code>professional_network_url</code>,{" "} <code>professional_network_urn</code>, and{" "} <code>profile_picture_url</code>. </Note> <Tip> Need full profile data (employer, education, skills, emails) on the reactors? Take each <code>professional_network_url</code> and batch them through the [Person Enrich](/person-docs/enrichment/introduction) API. </Tip> </Accordion> <Accordion title="Pull recent posts with comments"> Pass `fields: "comments"` to include the comment thread. Each comment contains the body text, timestamp, and the commenter's profile. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 1, "fields": "comments", "max_comments": 50 }' ``` ```json Response (trimmed) { "posts": [ { "comments": [ { "comment_urn": "urn:li:fsd_comment:(7478111341950447616,urn:li:ugcPost:7473081638059683840)", "comment_text": "I've been writing software for a long time...", "permalink": "https://www.linkedin.com/feed/update/urn:li:ugcPost:7473081638059683840?commentUrn=...", "created_at": "2026-07-01T15:44:40.511000", "comment_reactions": {}, "commenter": { "actor_type": "person", "name": "AXEO Systems", "headline": "Fractional CTO. Software Development", "professional_network_url": "https://www.linkedin.com/in/axeo", "professional_network_urn": "urn:li:fsd_profile:ACoAADr82dUB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" } } ] } ] } ``` </CodeGroup> <Note> Each comment carries the <code>comment_text</code>, a{" "} <code>created_at</code> timestamp, <code>comment_reactions</code>, and the commenter's <code>actor</code> (<code>actor_type</code>, <code>name</code>,{" "} <code>headline</code>, <code>professional_network_url</code>,{" "} <code>professional_network_urn</code>, <code>profile_picture_url</code>). </Note> </Accordion> <Accordion title="Pull both reactors and comments for a single post"> The most thorough engagement query — point to one post URL and pull everyone who liked, reacted, or commented on it. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "social_post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7445158857850654722", "fields": "reactors,comments", "max_reactors": 100, "max_comments": 50 }' ``` </CodeGroup> This costs `10 credits` for the one post returned. <Warning> Latency for reactor/comment-rich responses can reach 30–60 seconds for a full page of 5 posts. Time out your client to at least 90 seconds. </Warning> </Accordion> <Accordion title="Pull reactors or comments for a company's posts"> Reactor and commenter enrichment is not limited to person posts. Pass `fields` alongside any company identifier (`company_domain`, `company_name`, `crustdata_company_id`, or `company_professional_network_url`) to pull everyone who engaged with a company's posts — the response shape matches the person case. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "company_domain": "crustdata.com", "limit": 1, "fields": "reactors", "max_reactors": 100 }' ``` ```json Response { "posts": [ { "reactors": [ { "reaction_type": "LIKE", "reactor": { "actor_type": "person", "name": "Ipsita priyadarshini Dash", "headline": "President & CEO | Executive Hiring | Angel Investor", "professional_network_url": "https://www.linkedin.com/in/ACoAAEPJmpIB...", "professional_network_urn": "urn:li:fsd_profile:ACoAAEPJmpIB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" } } ] } ] } ``` </CodeGroup> <Note> Pricing matches the person case: `5 credits/post` for `fields: "reactors"` or `fields: "comments"`, `10 credits/post` for both. To pull comments too, set `fields: "reactors,comments"` and add `max_comments`. </Note> </Accordion> <Accordion title="Filter to originals only (skip reposts)"> When you only care about content the author wrote themselves, use `post_types: ["original"]`. Default behavior returns both originals and reposts. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 3, "post_types": ["original"] }' ``` </CodeGroup> `post_types` accepts an array (`["original"]`) or a comma-separated string (`"original,repost"`). </Accordion> <Accordion title="Paginate through 20 pages of recent posts"> Use `page` (1-indexed, 5 posts per page) to traverse the latest 100 posts of a person or company. Pages are ordered most recent first. <CodeGroup> ```bash Page 1 curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "page": 1 }' ``` ```bash Page 2 curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "page": 2 }' ``` </CodeGroup> <Note> Up to 20 pages of latest posts (100 posts total) are supported. Need deeper history? Email [info@crustdata.com](mailto:info@crustdata.com). </Note> </Accordion> <Accordion title="Fetch one specific post by URL"> When you have a specific post URL — from a search result, a Slack share, a bookmark — pass it as `social_post_url`. The response contains exactly that post; pagination params are ignored. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "social_post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7445158857850654722" }' ``` </CodeGroup> </Accordion> <Accordion title="Search → enrich workflow"> The [search endpoint](/social-post-docs/search/introduction) returns post-level engagement totals only. To pull reactors and comments per post, take each `share_url` from the search response and feed it into the enrich endpoint. <Steps> <Step title="Search for posts of interest"> Call <code>POST /social_post/professional_network/search/live</code>{" "} with your keyword and filters. Capture each post's{" "} <code>share_url</code> and <code>backend_urn</code>. </Step> <Step title="Enrich each post for engagement"> For every <code>share_url</code>, call this endpoint with{" "} <code>social_post_url</code> and{" "} <code>fields: "reactors,comments"</code>. </Step> <Step title="Join"> Combine search-level context (author, mentioning company) with enrich-level engagement (reactor and commenter profiles) using{" "} <code>backend_urn</code> as the join key. </Step> </Steps> </Accordion> <Accordion title="professional_network_profile_url — recent posts by a person"> Pass a profile URL when you want the latest posts authored by a specific person. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 1 }' ``` ```json Response (trimmed) { "posts": [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool" }, "date_posted": "2026-06-17", "post_type": "original", "text": "93% of technical execs we surveyed are alarmed about vibe-coded apps...", "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37 }, "share_url": "https://www.linkedin.com/posts/dvdhsu_93-of-technical-execs-activity-7473081804934049792-zACV" } ] } ``` </CodeGroup> </Accordion> <Accordion title="social_post_url — fetch a single specific post"> Pass a post URL to fetch exactly one post. Pagination params are ignored when this identifier is used. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "social_post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7445158857850654722" }' ``` ```json Response (trimmed) { "posts": [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool" }, "date_posted": "2026-04-02", "post_type": "original", "text": "95% of enterprises have blocked Claude and ChatGPT...", "engagement": { "total_reactions": 90, "total_comments": 5, "num_shares": 4 } } ] } ``` </CodeGroup> <Tip> Pair this with <code>fields: "reactors,comments"</code> to grab everyone who engaged with a single post — useful for analyzing a specific announcement or thread. See{" "} [Examples](/social-post-docs/enrichment/introduction#examples). </Tip> </Accordion> <Accordion title="company_name — recent posts by a company"> When you only have a company name, this is the easiest path. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "company_name": "Crustdata", "limit": 1 }' ``` ```json Response (trimmed) { "posts": [ { "actor": { "actor_type": "person", "name": "Jonathan Jenkins", "headline": "ML Engineer | LLMs, Machine Learning, Python" }, "reposter": { "actor_type": "company", "name": "Crustdata (YC F24)", "professional_network_urn": "urn:li:fsd_company:73854007" }, "date_posted": "2026-06-03", "post_type": "repost_without_thoughts" } ] } ``` </CodeGroup> <Note> Company name resolution prefers exact matches but tolerates minor variations (e.g., parenthesized suffixes). For ambiguous names, prefer{" "} <code>company_domain</code> or <code>crustdata_company_id</code>. </Note> </Accordion> <Accordion title="company_domain — recent posts by a company (resolved by domain)"> The most reliable company identifier when you have the company's primary web domain. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "company_domain": "crustdata.com", "limit": 1 }' ``` </CodeGroup> </Accordion> <Accordion title="crustdata_company_id — recent posts by a company (Crustdata ID)"> Pass the integer Crustdata company ID when you already know it from a previous `/company/enrich` or `/company/identify` call. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 6036032, "limit": 1 }' ``` ```json Response (trimmed) { "posts": [ { "actor": { "actor_type": "person", "name": "Jonathan Jenkins", "headline": "ML Engineer | LLMs, Machine Learning, Python" }, "reposter": { "actor_type": "company", "name": "Crustdata (YC F24)", "professional_network_urn": "urn:li:fsd_company:73854007" }, "date_posted": "2026-06-03", "post_type": "repost_without_thoughts" } ] } ``` </CodeGroup> <Tip> Need to look up a company ID? Use the [Company Identify](/company-docs/identify/introduction) endpoint with{" "} <code>domains: ["crustdata.com"]</code> to get the{" "} <code>crustdata_company_id</code>. </Tip> </Accordion> <Accordion title="company_professional_network_url — recent posts by company page URL"> Pass the company's professional network page URL. <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "company_professional_network_url": "https://www.linkedin.com/company/crustdata", "limit": 1 }' ``` </CodeGroup> </Accordion> <Accordion title="Pagination: page vs limit"> Both pagination params are optional, but **they cannot be combined.** Use `page` for continuous traversal in 5-post pages, or `limit` to cap a single response at up to 100 posts. | Parameter | Type | Range | Behavior | | --------- | ------- | ----- | --------------------------------------------------------------------------- | | `page` | integer | 1–20 | Returns 5 posts per page. Up to 20 pages of latest posts (100 posts total). | | `limit` | integer | 1–100 | Returns up to N posts in one response. | ```json { "professional_network_profile_url": "...", "page": 2 } ``` ```json { "professional_network_profile_url": "...", "limit": 50 } ``` <Warning> When using <code>social_post_url</code>, both parameters are ignored — the response always returns exactly the one requested post. </Warning> </Accordion> <Accordion title="Filter to original or repost content"> Use `post_types` to narrow returned posts. Accepts a comma-separated string or an array. <CodeGroup> ```bash Array form curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 2, "post_types": ["original"] }' ``` ```bash String form curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 2, "post_types": "original,repost" }' ``` </CodeGroup> | Value | Meaning | | ---------- | ---------------------------------- | | `original` | Posts the author wrote themselves. | | `repost` | Reposts of someone else's post. | Default behavior (when `post_types` is omitted) is to return both types. </Accordion> </AccordionGroup> --- ## What to do next - **Pick your identifier shape** — see the [Examples](#examples) above. - **Pull reactors or comments** — see [Pull recent posts with reactors](/social-post-docs/enrichment/introduction#examples). - **Look up response field details** — see [Reference](/social-post-docs/enrichment/reference). - **Find posts by topic** — see [Search Social Posts](/social-post-docs/search/introduction) when you don't know the entity yet. # Social Post Batch Enrich Source: https://docs.crustdata.com/social-post-docs/enrichment/batch Retrieve recent posts for up to 10 profile, post, or company identifiers in a single asynchronous job — posts retrieved from the web at request time, delivered as one results file. Batch enrich takes a list of identifiers and returns recent posts for each as a single downloadable results file, instead of one [Social Post Enrich](/social-post-docs/enrichment/introduction) call per identifier. Posts are retrieved from the web at request time, so jobs are slower than database-backed batches — submit, then poll or receive a webhook. ``` POST https://api.crustdata.com/batch/social_post/professional_network/enrich/live ``` <Note> Replace `YOUR_API_KEY` in each example with your actual API key. Submitting a batch job requires the `x-api-version: 2025-11-01` header; polling and listing jobs do not. An account may have at most **5 active** (`pending` or `processing`) batch jobs at a time — a sixth submission returns `429`. </Note> <Callout icon="coins" color="#5345e4"> <strong>Pricing:</strong> <code>1 credit per post delivered</code> ·{" "} <code>+5 per 100 reactors</code> and <code>+5 per 100 comments</code>{" "} retrieved per post. Same rates as the non-batch [Social Post Enrich](/general/pricing#social-post-endpoints). Failed jobs are not charged. </Callout> <Note> Available on enterprise plans. If your account is not enabled for this endpoint, requests return `403` — contact [sales@crustdata.com](mailto:sales@crustdata.com) to enable it. </Note> ## Identifiers Provide **exactly one** identifier type per job — providing none, or more than one type, returns `400`. Up to **10 identifiers** per job; identifier lists also accept a single comma-separated string. | Identifier | What each value is | | ------------------------------------ | ----------------------------------------- | | `professional_network_profile_urls` | Person profile URL — recent posts by that person | | `social_post_urls` | Individual post URL — resolves that single post | | `company_professional_network_urls` | Company profile URL — recent posts by that company | | `crustdata_company_ids` | Crustdata company ID | | `company_names` | Company name | | `company_domains` | Company website domain | ## Post options Each option applies to every identifier in the job: | Parameter | Type | Default | Notes | | -------------- | ------- | ------- | ----- | | `limit` | integer | — | Posts retrieved per identifier, `1`–`100`. **Required** for every identifier type except `social_post_urls`, which always resolves a single post. The cap drops to **25** when engagement is requested. | | `max_reactors` | integer | `0` | Reactors retrieved per post, `0`–`3000`. Values above `0` lower the `limit` cap to 25. | | `max_comments` | integer | `0` | Comments retrieved per post, `0`–`3000`. Values above `0` lower the `limit` cap to 25. | | `post_types` | array | both | Which post types to include: `original`, `repost`, or both when omitted. | | `webhook_url` | string | — | Optional URL that receives a POST notification when the job finishes, so you do not have to poll. | ## Submit a job <CodeGroup> ```bash Request curl --request POST \ --url https://api.crustdata.com/batch/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "limit": 5 }' ``` ```json Response { "batch_id": "8295939d-1741-4c93-a845-0ceff668023d", "status": "pending", "entity": "social_post", "action": "enrich_live", "identifier_count": 1, "entities_requested": 1, "status_url": "/batch/8295939d-1741-4c93-a845-0ceff668023d" } ``` </CodeGroup> ## Retrieve the results Poll `GET /batch/{batch_id}` until `status` reaches `completed` (or `failed`), then download the gzipped JSONL results file from `download_url`: <CodeGroup> ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/de61dcb5-d6ee-4c03-aa0f-fa6ea5c57da8 \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "de61dcb5-d6ee-4c03-aa0f-fa6ea5c57da8", "status": "completed", "entity": "social_post", "action": "enrich_live", "identifier_count": 1, "result_count": 1, "entities_requested": 1, "entities_fulfilled": 1, "created_at": "2026-07-17T01:49:04.545498+00:00", "completed_at": "2026-07-17T01:49:19.259358+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/de61dcb5-d6ee-4c03-aa0f-fa6ea5c57da8/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/de61dcb5-d6ee-4c03-aa0f-fa6ea5c57da8/output/part-000.jsonl.gz?..." ] } ``` ```bash Download and decompress curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz ``` </CodeGroup> ## Results file Each line is **one record per identifier**: an `{original_identifier, data}` envelope where `original_identifier` echoes the exact value you submitted and `data` is an array of post objects — the same post shape as the non-batch [Social Post Enrich response](/social-post-docs/enrichment/reference). ```json One record from the results file (trimmed) { "original_identifier": "https://www.linkedin.com/in/dvdhsu/", "data": [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool", "professional_network_url": "https://www.linkedin.com/in/dvdhsu" }, "share_url": "https://www.linkedin.com/posts/dvdhsu_93-of-technical-execs-activity-7473081804934049792-zACV", "text": "93% of technical execs we surveyed are alarmed about vibe-coded apps in their orgs...", "date_posted": "2026-06-17", "post_type": "original", "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37 } } ] } ``` <Note> Identifiers that return no posts are omitted from the results file — the job still completes, and `entities_requested` vs `entities_fulfilled` on the status response is your no-match signal. `entities_fulfilled` counts identifiers that returned at least one post; billing counts the posts themselves. </Note> ## Working with batch jobs Every batch job follows the same three-step lifecycle: **submit** the job, **poll** until it completes, **download** the results file. ### Poll a job: `GET /batch/{batch_id}` The submit response returns immediately with a `batch_id` and a `status` of `pending`. Poll the job until `status` reaches `completed` (or `failed`, in which case `error_message` carries a human-readable reason). <CodeGroup> ```bash Request curl --request GET \ --url https://api.crustdata.com/batch/53ab686b-c054-496b-8baf-baff5ecc85cf \ --header 'authorization: Bearer YOUR_API_KEY' ``` ```json Response (completed job) { "batch_id": "53ab686b-c054-496b-8baf-baff5ecc85cf", "status": "completed", "entity": "company", "action": "enrich", "identifier_count": 2, "result_count": 2, "entities_requested": 2, "entities_fulfilled": 2, "created_at": "2026-06-12T12:23:03.945900+00:00", "completed_at": "2026-06-12T12:23:15.993293+00:00", "download_url": "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/results.jsonl.gz?...", "download_urls": [ "https://crustdata-batch-api-data.s3.amazonaws.com/53ab686b-c054-496b-8baf-baff5ecc85cf/output/part-000.jsonl.gz?..." ] } ``` </CodeGroup> A simple shell loop covers most scripting needs: ```bash Poll until done while true; do STATUS=$(curl -s "https://api.crustdata.com/batch/$BATCH_ID" \ -H "authorization: Bearer YOUR_API_KEY" | jq -r '.status') [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 5 done ``` ### Download the results Completed jobs include a `download_url` (one merged file) and `download_urls` (the same data split into parts, for streaming large result sets). Files are gzipped JSONL — one JSON record per line: ```bash Download, decompress, inspect curl -s "$DOWNLOAD_URL" -o results.jsonl.gz gunzip results.jsonl.gz jq -c '.' results.jsonl | head -n 2 ``` <Warning> Download links are pre-signed (the real URLs carry long authentication query parameters) and expire **5 days** after the job completes. A completed job with `result_count: 0` still returns a `download_url`, but the file was never written — fetching it returns `404`. Check `result_count` before downloading. </Warning> ### List your jobs: `GET /batch` Lists your jobs, most recent first, with optional status filtering (`?status=` `pending`, `processing`, `completed`, or `failed`), a `limit` of 1–100 (default 20), and cursor pagination — pass the response's `next_cursor` as `cursor` for the next page. Job summaries omit download links; poll a specific job to get them. ```bash Request curl --request GET \ --url 'https://api.crustdata.com/batch/?limit=2&status=completed' \ --header 'authorization: Bearer YOUR_API_KEY' ``` ### Skip polling with webhooks Pass a `webhook_url` in any submit body and Crustdata sends a `POST` notification to that URL when the job finishes, including the download link. Treat the notification as a signal to fetch `GET /batch/{batch_id}` for the authoritative job state. ## Errors | Status | When | Message | | ------ | -------------------------------------------------------- | ----------------------------------------------------------------------- | | `400` | No identifier, or more than one identifier type | `Exactly one identifier must be provided: professional_network_profile_urls, social_post_urls, company_professional_network_urls, crustdata_company_ids, company_names, company_domains` | | `400` | `limit` omitted for a profile or company identifier | `'limit' is required (an integer between 1 and 100).` | | `400` | More than 10 identifiers | `Maximum 10 identifiers allowed for social_post/enrich_live. Found 11` | | `400` | Unsupported `post_types` value | `Invalid post_types: ['bogus']. Valid values: ['original', 'repost']` | | `403` | Account not enabled for this endpoint | `You do not have permission to access /batch/social_post/professional_network/enrich/live.` | | `429` | Already 5 active (`pending` or `processing`) batch jobs | — | # Enrichment reference Source: https://docs.crustdata.com/social-post-docs/enrichment/reference Reference for Enrich Social Posts: request parameters, response body, every response field including reactors and comments, error handling, and common gotchas. Reference material for [Enrich Social Posts](/social-post-docs/enrichment/introduction): request parameters, response shape, every response field, and error handling. For walk-through examples, see [Introduction](/social-post-docs/enrichment/introduction) and [Examples](/social-post-docs/enrichment/introduction#examples). For each identifier shape with a verified example, see [Examples](/social-post-docs/enrichment/introduction#examples). All requests use **Bearer token authentication** and require the API version header: ```bash --header 'authorization: Bearer YOUR_API_KEY' --header 'x-api-version: 2025-11-01' ``` <Note>Replace `YOUR_API_KEY` in each example with your actual API key.</Note> --- ## Request parameter reference Pass exactly **one** identifier per request. | Parameter | Type | Required | Default | Description | | ---------------------------------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------- | | `professional_network_profile_url` | string | One of | — | Person profile URL (e.g., `https://www.linkedin.com/in/dvdhsu/`). | | `social_post_url` | string | One of | — | Direct URL of a single post. Pagination is ignored when this is provided. | | `company_name` | string | One of | — | Company name. Provide only one company identifier per request. | | `company_domain` | string | One of | — | Company web domain (e.g., `crustdata.com`). | | `crustdata_company_id` | integer | One of | — | Crustdata company ID (look up via [Company Identify](/company-docs/identify/introduction)). | | `company_professional_network_url` | string | One of | — | Company page URL (e.g., `https://www.linkedin.com/company/crustdata`). | Plus pagination and field selection: | Parameter | Type | Required | Default | Description | | -------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `page` | integer | No\* | — | Page number (1–20). Returns 5 posts per page. **Mutually exclusive with `limit`.** Ignored when `social_post_url` is used. | | `limit` | integer | No\* | — | Number of posts (1–100) in one response. **Mutually exclusive with `page`.** Ignored when `social_post_url` is used. | | `fields` | string | No | — | Comma-separated list of optional fields. Values: `reactors`, `comments`. Triggers reactor/commenter enrichment. | | `post_types` | string \| string[] | No | both | Filter by `original` or `repost`. Accepts an array or a comma-separated string. | | `max_reactors` | integer | No | `0` | Max reactors to fetch per post. Range 0–5000. Requires `reactors` in `fields`. ≤ 100 returns full profile data; > 100 returns basic only. | | `max_comments` | integer | No | `0` | Max comments to fetch per post. Range 0–5000. Requires `comments` in `fields`. ≤ 100 returns full commenter data; > 100 returns basic only. | <Note> *At least one of <code>page</code> or <code>limit</code> is needed when not using <code>social_post_url</code>. </Note> --- ## Response shape The response wraps posts in a `posts[]` array. Each entry is a `SocialPost` with a nested `actor` and `engagement` object — the same shape the search endpoint returns. ```json { "posts": [ { "actor": { "actor_type": "person", "name": "David Hsu", "headline": "Founder, CEO @ Retool", "professional_network_url": "https://www.linkedin.com/in/dvdhsu", "professional_network_urn": "urn:li:fsd_profile:ACoAAAEnBBoB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" }, "backend_urn": "urn:li:activity:7473081804934049792", "share_urn": "urn:li:ugcPost:7473081638059683840", "share_url": "https://www.linkedin.com/posts/dvdhsu_...", "text": "...", "date_posted": "2026-06-17", "post_type": "original", "has_video": true, "hyperlinks": { "...": "..." }, "engagement": { "total_reactions": 223, "total_comments": 12, "num_shares": 37, "reactions_by_type": { "LIKE": 172, "PRAISE": 37 } }, "reactors": [{ "reaction_type": "LIKE", "reactor": { "...": "..." } }], "comments": [{ "comment_urn": "...", "commenter": { "...": "..." } }] } ] } ``` --- ## Core post fields | Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------ | | `actor` | object? | The post author (person or company). See [`actor`](#actor--reposter) below. | | `reposter` | object? | The reposting actor. Present only on reposts. Same shape as `actor`. | | `backend_urn` | string? | Internal URN of the post. | | `share_urn` | string? | Internal share URN of the post. | | `share_url` | string? | Public URL of the post. | | `text` | string? | Full post body text. | | `date_posted` | string? | Date the post was published (`YYYY-MM-DD`). | | `post_type` | string? | `original`, `repost_without_thoughts`, or `repost_quote`. | | `has_video` | boolean? | `true` when the post contains a video. | | `hyperlinks` | object? | URLs extracted from the body (see below). | | `engagement` | object? | Reaction, comment, and share counts (see below). | | `reactors` | object[]? | Reactions on the post. Present only when `fields=reactors` and `max_reactors > 0`. | | `comments` | object[]? | Comments on the post. Present only when `fields=comments` and `max_comments > 0`. | ### `actor` / `reposter` | Field | Type | Description | | -------------------------- | ------- | ----------------------------------------------- | | `actor_type` | string? | `person` or `company`. | | `name` | string? | Display name. | | `headline` | string? | Profile headline (person) or follower summary (company). | | `professional_network_url` | string? | Profile or company page URL. | | `professional_network_urn` | string? | Internal URN identifier. | | `profile_picture_url` | string? | Profile or company logo image URL. | ### `engagement` | Field | Type | Description | | ------------------- | -------- | ---------------------------------------- | | `total_reactions` | integer? | Total reactions across all types. | | `total_comments` | integer? | Total comment count. | | `num_shares` | integer? | Number of times the post was shared. | | `reactions_by_type` | object? | Reaction counts by type (see below). | ### `reactions_by_type` Only reaction types present on the post are returned. | Field | Type | Description | | --------------- | -------- | ----------------------------------- | | `LIKE` | integer? | Count of `LIKE` reactions. | | `PRAISE` | integer? | Count of `PRAISE` reactions. | | `EMPATHY` | integer? | Count of `EMPATHY` reactions. | | `INTEREST` | integer? | Count of `INTEREST` reactions. | | `APPRECIATION` | integer? | Count of `APPRECIATION` reactions. | | `ENTERTAINMENT` | integer? | Count of `ENTERTAINMENT` reactions. | ### `hyperlinks` | Field | Type | Description | | ----------------------------------- | --------- | ----------------------------------------------------- | | `company_professional_network_urls` | string[]? | Company profile URLs mentioned in the post. | | `person_professional_network_urls` | string[]? | Person profile URLs mentioned in the post. | | `other_urls` | string[]? | External URLs that aren't company or person profiles. | | `media_urls` | string[]? | Media attachment URLs (images, videos, documents). | --- ## `reactors[]` — when `fields=reactors` Set `fields=reactors` and `max_reactors > 0` to include reactions. Each entry pairs the reaction type with the reactor's `actor`. | Field | Type | Description | | --------------- | ------- | ------------------------------------------------------------------------------------ | | `reaction_type` | string? | Type of reaction (`LIKE`, `PRAISE`, `EMPATHY`, `INTEREST`, `APPRECIATION`, `ENTERTAINMENT`). | | `reactor` | object? | The profile that reacted. Same [`actor`](#actor--reposter) shape. | ```json { "reaction_type": "LIKE", "reactor": { "actor_type": "person", "name": "Jenni Goodman", "headline": "Principal Recruiter at Plenty Search", "professional_network_url": "https://www.linkedin.com/in/ACoAAAFSHMMB...", "professional_network_urn": "urn:li:fsd_profile:ACoAAAFSHMMB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" } } ``` --- ## `comments[]` — when `fields=comments` Set `fields=comments` and `max_comments > 0` to include comments. Each comment carries the body text, a timestamp, and the commenter's `actor`. | Field | Type | Description | | ------------------- | ------- | ----------------------------------------------------------------------- | | `comment_urn` | string? | Internal URN of the comment. | | `comment_text` | string? | Comment body text. | | `permalink` | string? | Direct link to the comment. | | `created_at` | string? | Comment creation timestamp (ISO-8601). | | `comment_reactions` | object? | Reaction counts on the comment, keyed by type. Empty when no reactions. | | `commenter` | object? | The comment author. Same [`actor`](#actor--reposter) shape. | ```json { "comment_urn": "urn:li:fsd_comment:(7478111341950447616,urn:li:ugcPost:7473081638059683840)", "comment_text": "I've been writing software for a long time...", "permalink": "https://www.linkedin.com/feed/update/urn:li:ugcPost:7473081638059683840?commentUrn=...", "created_at": "2026-07-01T15:44:40.511000", "comment_reactions": {}, "commenter": { "actor_type": "person", "name": "AXEO Systems", "headline": "Fractional CTO. Software Development", "professional_network_url": "https://www.linkedin.com/in/axeo", "professional_network_urn": "urn:li:fsd_profile:ACoAADr82dUB...", "profile_picture_url": "https://media.licdn.com/dms/image/v2/example.jpg" } } ``` --- ## Error handling | Status | Type | Common causes | | ------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_request` | No identifier provided. Multiple identifiers provided. Both `page` and `limit`. Profile URL cannot be resolved. Wrong types. | | `401` | `unauthorized` | Invalid or missing API key. | | `500` | `internal_error` | Unexpected server error. Retry with backoff. | ### Examples <CodeGroup> ```json 400 — no identifier { "error": { "type": "invalid_request", "message": "One of 'professional_network_profile_url', 'social_post_url', or a company identifier (company_name, company_domain, crustdata_company_id, company_professional_network_url) is required.", "metadata": [] } } ``` ```json 400 — multiple identifiers { "error": { "type": "invalid_request", "message": "Only one identifier type can be provided at a time.", "metadata": [] } } ``` ```json 400 — page + limit { "error": { "type": "invalid_request", "message": "Only one of 'page' or 'limit' can be provided, not both.", "metadata": [] } } ``` ```json 400 — unresolvable profile URL { "error": { "type": "invalid_request", "message": "Failed to resolve profile URN from the provided URL.", "metadata": [] } } ``` ```json 401 — auth failure { "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } } ``` </CodeGroup> --- ## Common gotchas - **Pass exactly one identifier.** Multiple identifiers → 400. None → 400. - **`page` and `limit` are mutually exclusive.** Choose one per request. - **`social_post_url` ignores pagination.** Always returns exactly the one post. - **Reactor/comment counts use total, not max.** `total_reactions` is the post's actual reaction count; the `reactors[]` array is capped by `max_reactors`. - **Response wraps posts in `posts[]`.** This is different from the search endpoint, which returns a top-level array. - **Latency.** Pulling reactors and comments for a full page can take 30–60 seconds. Set generous client timeouts. --- ## API summary | Detail | Value | | -------------- | -------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/social_post/professional_network/enrich/live` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **Version** | `x-api-version: 2025-11-01` header (required) | | **Pricing** | `1 credit/post` default · `5 credits/post` with `fields=reactors` or `fields=comments` · `10 credits/post` with both | | **Rate limit** | 10 requests/minute (default; contact gtm@crustdata.co for higher) | # Overview Source: https://docs.crustdata.com/migrations/overview Quick reference and index of migration guides for moving from the legacy /screener and /data_lab APIs to the new versioned endpoints. This section maps every legacy endpoint to its replacement and points you at the migration guide that walks the change in detail. Each guide follows the same structure — request keys, identifier behavior, field mapping, type changes, removed features, added features, response shape, error responses, and a migration checklist — so once you have ported one endpoint, porting the next is mechanical. <Note> If you are starting fresh and not porting an existing integration, skip this section and go straight to the [Documentation](/general/introduction) or the API reference for the product you need. </Note> --- ## Quick reference Every legacy endpoint and the guide that walks the migration: | Domain | Legacy endpoint | Current endpoint | Migration guide | | ----------- | --------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | | Company | `POST /screener/companydb/search` | `POST /company/search` | [Company search](/migrations/company/companydb-search) | | Company | `POST /screener/company/search` | `POST /company/professional_network/search/live` | [Live company search](/migrations/company/company-search-live) | | Company | `GET /screener/company` | `POST /company/enrich` | [Company enrich](/migrations/company/company-enrich) | | Person | `POST /screener/persondb/search` | `POST /person/search` | [Person search](/migrations/person/persondb-search) | | Person | `GET /screener/person/enrich` | `POST /person/enrich` | [Person enrich](/migrations/person/person-enrich) | | Person | `POST /screener/person/search` | `POST /person/professional_network/search/live` | [Live person search](/migrations/person/person-search-live) | | Person | `GET /screener/person/enrich?enrich_realtime=true` | `POST /person/professional_network/enrich/live` | [Live person enrich](/migrations/person/person-enrich-live) | | Job | `POST /data_lab/job_listings/Table/` | `POST /job/search` | [Job search](/migrations/job/job-search) | | Job | `POST /data_lab/job_listings/Table?sync_from_source=true` | `POST /job/professional_network/search/live` | [Live job search](/migrations/job/job-search-live) | | Web | `POST /screener/web-search` | `POST /web/search/live` | [Web search](/migrations/web/web-search) | | Web | `POST /screener/web-fetch` | `POST /web/enrich/live` | [Web fetch](/migrations/web/web-fetch) | | Social Post | `GET /screener/linkedin_posts` | `POST /social_post/professional_network/enrich/live` | [Social posts](/migrations/social-post/social-post-enrich-live) | | Social Post | `POST /screener/linkedin_posts/keyword_search` | `POST /social_post/professional_network/search/live` | [Search social posts](/migrations/social-post/social-post-search-live) | --- ## What changes for every endpoint Three changes apply to every migration. Apply them once across your client code, then follow the per-endpoint guide for the rest. ### 1. Authorization scheme The legacy endpoints accepted `Authorization: Token <key>`. Every current endpoint uses `Authorization: Bearer <key>`. ```diff - Authorization: Token YOUR_API_KEY + Authorization: Bearer YOUR_API_KEY ``` ### 2. API version header Every current endpoint requires `x-api-version: 2025-11-01`. Calls without it are rejected with `400`. ```http x-api-version: 2025-11-01 ``` ### 3. Error envelope Legacy `4xx` and `5xx` responses returned a free-form `{ "error": "..." }` string. Every current endpoint returns a structured envelope (including `401`, where `type` is `unauthorized`): ```json { "error": { "type": "invalid_request", "message": "Human-readable description.", "metadata": [] } } ``` Update your error handlers once — the new envelope is identical across every endpoint in this section. --- ## Common shape changes These show up in most migrations but not all. Use the per-endpoint guide for the exact details. | Change | Where it applies | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET` query parameters → `POST` JSON body | All `GET /screener/*` and `GET /screener/.../enrich` endpoints became `POST`. Comma-separated query values became JSON arrays. | | `filter_type` / `column` → `field` | Every search/live endpoint renamed the per-condition identifier key. Filter operator values (`in`, `not_in`, `=`, `(.)`, etc.) are unchanged. | | Match-result envelope | Enrich endpoints now wrap results in `{ matched_on, match_type, matches: [{ confidence_score, person_data \| company_data }] }`. | | Cursor pagination | Some search endpoints replaced numeric `offset` with opaque `cursor`. Pass `next_cursor` from the response as `cursor` on the next request. | | Nested response sections | Flat top-level keys (`linkedin_*`, `glassdoor.*`, `g2.*`, `crunchbase_*`, etc.) moved under neutral sections (`basic_info`, `employee_reviews`, …). | | `404` no-match → `200` empty matches | Many endpoints replaced `404` for no-results with `200` and an empty `matches: []` or `[]` array. Handle both during the transition. | --- ## Migration order If you are porting a multi-endpoint integration, this order minimizes rework: <Steps> <Step title="Update the HTTP plumbing once"> Switch the `Authorization` scheme to `Bearer`, add the `x-api-version: 2025-11-01` header, and update your error handler for the new `error.type` / `error.message` envelope. These three changes are identical for every endpoint, so make them in your shared client first. </Step> <Step title="Port enrich endpoints"> Enrich calls are usually on the critical path (a single user lookup, often blocking). Start with [Company enrich](/migrations/company/company-enrich) and [Person enrich](/migrations/person/person-enrich) and update response parsers to handle the new `matches[].company_data` / `matches[].person_data` envelope. </Step> <Step title="Port search endpoints"> Search calls usually drive batch jobs and dashboards. Rename `column`/`filter_type` to `field`, migrate field names per the mapping tables, and adopt cursor pagination where applicable. See [Company search](/migrations/company/companydb-search) and [Person search](/migrations/person/persondb-search). </Step> <Step title="Port live (real-time) endpoints"> Live endpoints replace the legacy `enrich_realtime=true` / `sync_from_source=true` flags. They are now distinct URLs — see the [Live company search](/migrations/company/company-search-live), [Live person search](/migrations/person/person-search-live), [Live person enrich](/migrations/person/person-enrich-live), [Live job search](/migrations/job/job-search-live), and [Search social posts](/migrations/social-post/social-post-search-live) guides. </Step> <Step title="Port utility endpoints"> Finally, port the cross-cutting utilities — [Web search](/migrations/web/web-search), [Web fetch](/migrations/web/web-fetch), and [Social posts](/migrations/social-post/social-post-enrich-live). These typically don't share parsing code with the rest of your integration, so they can move at any point. </Step> </Steps> --- ## By domain <CardGroup cols={2}> <Card title="Company" icon="building" href="/migrations/company/companydb-search" > Three migrations: indexed search, live search, and enrich. </Card> <Card title="Person" icon="user" href="/migrations/person/persondb-search"> Four migrations: indexed search, indexed enrich, live search, and live enrich. </Card> <Card title="Job" icon="briefcase" href="/migrations/job/job-search"> Two migrations: indexed search and live search. The legacy generic dataset-query body is replaced by purpose-built endpoints with cursor pagination and structured aggregations. </Card> <Card title="Web" icon="globe" href="/migrations/web/web-search"> Two migrations: web search and web fetch. The legacy fetch_content=true mode is removed. </Card> <Card title="Social Post" icon="message-square" href="/migrations/social-post/social-post-enrich-live" > Two migrations: per-entity enrich and keyword search. </Card> </CardGroup> --- ## Conventions used across guides Every per-endpoint migration page follows the same template: | Section | What you'll find | | ---------------------- | ----------------------------------------------------------------------------------------- | | Header table | Path, method, auth, version, base URL — legacy vs current at a glance. | | 1. Request keys | Mapping of every legacy query parameter / body key to its current equivalent. | | 2. Identifier behavior | How many identifiers per call, supported types, mutual-exclusion rules. | | 3. Field-name mapping | Legacy field paths → current dot-paths, grouped by response section. | | 4. Type changes | Fields whose JSON type changed (e.g. `year_founded` string → integer). | | 5. Removed features | Legacy parameters or response sections that have no direct replacement, with workarounds. | | 6. Added features | New capabilities exposed on the current endpoint. | | 7. Response shape | Side-by-side legacy/current envelope examples. | | 8. Error responses | New error envelope and the cases that trigger each status code. | | 9. End-to-end example | A `curl` invocation against each endpoint with equivalent inputs. | | Migration checklist | A copy-pasteable bullet list of every change required to port the endpoint. | If a section is missing on a particular guide, that aspect of the API is unchanged for that endpoint. --- ## Need help? If a field you depend on isn't listed in the mapping table or the checklist doesn't cover your use case, email [support@crustdata.co](mailto:support@crustdata.co) with the legacy request you were making and the field you can't find — we will point you at the right replacement. # Company search Source: https://docs.crustdata.com/migrations/company/companydb-search Reference for migrating from the legacy /screener/companydb/search endpoint to the new /company/search endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/companydb/search</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /company/search</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy company-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | --------------------------------- | -------------------------------------- | | Path | `POST /screener/companydb/search` | `POST /company/search` | | Auth | `Authorization: Bearer <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **The `x-api-version` header is required on every request to `/company/search`.** Calls without it are rejected. The legacy endpoint did not use this header. </Warning> --- ## 1. Request body — top-level keys | Key | Legacy | Current | Notes | | -------------- | -------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `filters` | object | object | Grammar changed — see [Filter grammar](#2-filter-grammar) below. | | `sorts` | array of `{column, order}` | array of `{field, order}` | `column` renamed to `field`. | | `limit` | integer 1–1000, default 20 | integer 1–1000, default 20 | Unchanged. | | `count` | integer alias for `limit` | (removed) | No longer accepted — sending `count` returns `400` (`'count' is not a valid parameter`). Use `limit`. | | `cursor` | string nullable | string nullable | Unchanged. | | `fields` | (not supported) | string[] | New — request a subset of response sections via dot-paths. | | `preview` | boolean | boolean | Unchanged. | | `search` | object (semantic / hybrid) | (removed) | Semantic/hybrid search has no replacement on this endpoint — see [Removed features](#5-removed-features). | | `return_query` | boolean (debug) | (removed) | No longer accepted — sending `return_query` returns `400` (`'return_query' is not a valid parameter`). | --- ## 2. Filter grammar The condition key changed from `column` to `field`. Operator names and the `and`/`or` group shape are unchanged. <CodeGroup> ```json Legacy condition { "column": "linkedin_industries", "type": "in", "value": ["Software Development"] } ``` ```json Current condition { "field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" } ``` </CodeGroup> <Note> **Two changes per condition:** 1. Rename `column` → `field`. 2. Map the legacy field name to the new dataset path (see [Field-name mapping](#3-field-name-mapping)). </Note> ### Operators | Operator | Legacy | Current | Notes | | ------------------------ | ------ | ------- | ------------------------------------------------------ | | `=` | ✅ | ✅ | | | `!=` | ✅ | ✅ | | | `<`, `>` | ✅ | ✅ | | | `=<`, `=>` | ✅ | ✅ | `>=` and `<=` are **not** supported — use `=>` / `=<`. | | `in`, `not_in` | ✅ | ✅ | Value must be an array. | | `(.)` | ✅ | ✅ | Fuzzy / all-words match. | | `[.]` | ✅ | ✅ | Exact phrase match. | | `is_null`, `is_not_null` | ✅ | ✅ | Send `"value": null` — the `value` key is required. | ### `and` / `or` groups The group shape is unchanged — `op: "and"` or `op: "or"` plus a `conditions` array. Only the leaf-condition key (`column` → `field`) and the leaf field names need to change. --- ## 3. Field-name mapping The legacy endpoint exposed flat names like `company_name`, `hq_country`, and `employee_metrics.latest_count`. The current endpoint groups those fields under `basic_info`, `headcount`, `funding`, `taxonomy`, `revenue`, and `locations`. <Note> Whether each current field is **filterable** and **sortable** is marked per field in the [Company Search reference](/company-docs/search/reference#searchable-fields) — see the **Filterable** and **Sortable** columns. Sorting on a non-sortable field returns `400`. </Note> ### Identity and profile | Legacy field | Current field | | ------------------------------ | ------------------------------------------------------------------------------- | | `company_id` | `basic_info.company_id` (also exposed at top level as `crustdata_company_id`) | | `company_name` | `basic_info.name` | | `company_website_domain` | `basic_info.primary_domain` | | `company_website` | `basic_info.website` | | `domains` | `basic_info.all_domains` | | `linkedin_profile_url` | `basic_info.professional_network_url` | | `linkedin_profile_name` | `basic_info.profile_name` | | `linkedin_id` | `basic_info.professional_network_id` | | `linkedin_company_description` | `basic_info.description` | | `company_type` | `basic_info.company_type` | | `year_founded` | `basic_info.year_founded` (now an integer; see [Type changes](#4-type-changes)) | | `employee_count_range` | `basic_info.employee_count_range` | | `markets` | `basic_info.markets` | ### Taxonomy | Legacy field | Current field | | ----------------------- | ------------------------------------------------------- | | `linkedin_industries` | `taxonomy.professional_network_industry` | | `linkedin_industry` | `taxonomy.professional_network_industry` (single label) | | `linkedin_categories` | `taxonomy.professional_network_specialities` | | `crunchbase_categories` | `taxonomy.categories` | ### Headcount and roles | Legacy field | Current field | | ------------------------------- | ---------------------------------------------------------------------------- | | `employee_metrics.latest_count` | `headcount.total` | | `largest_headcount_country` | `headcount.largest_headcount_country` | | `role_distribution.*` | `roles.distribution.*` | | `role_distribution_percent.*` | (removed — request `roles.distribution` and compute percentages client-side) | | `role_growth_6m.*` | `roles.growth_6m.*` | | `role_growth_yoy.*` | `roles.growth_yoy.*` | | `employee_metrics.growth_*` | `headcount.growth_percent.{1m,3m,6m,12m}` / `headcount.growth_absolute.{1m,3m,6m,12m}` | ### Followers | Legacy field | Current field | | ------------------------------------- | ------------------------------------- | | `follower_metrics.latest_count` | `followers.count` | | `follower_metrics.growth_1m_percent` | `followers.mom_percent` | | `follower_metrics.growth_3m_percent` | `followers.qoq_percent` | | `follower_metrics.growth_6m_percent` | `followers.six_months_growth_percent` | | `follower_metrics.growth_12m_percent` | `followers.yoy_percent` | ### Revenue and public-market data | Legacy field | Current field | | ------------------------------------ | -------------------------------------------------------------------------------------------- | | `estimated_revenue_lower_bound_usd` | `revenue.estimated.lower_bound_usd` | | `estimated_revenue_higher_bound_usd` | `revenue.estimated.upper_bound_usd` | | `acquisition_status` | `revenue.acquisition_status` | | `stock_symbols` | `revenue.public_markets.stock_symbols` | | `fiscal_year_end` | `revenue.public_markets.fiscal_year_end` | | `ipo_date` | `revenue.public_markets.ipo_date` (returned in search; also filterable and sortable) | ### Funding and investors | Legacy field | Current field | | ----------------------------------- | ---------------------------------------------------------------------- | | `crunchbase_total_investment_usd` | `funding.total_investment_usd` | | `last_funding_round_investment_usd` | `funding.last_round_amount_usd` | | `last_funding_round_type` | `funding.last_round_type` | | `last_funding_date` | `funding.last_fundraise_date` | | `crunchbase_investors` | `funding.investors` | | `crunchbase_valuation_usd` | (removed — no current equivalent in search or enrich) | ### Locations | Legacy field | Current field | | ---------------------------- | -------------------------------- | | `hq_country` | `locations.country` | | `hq_location` | `locations.headquarters` | | `hq_street_address_and_city` | `locations.street_address` | | `office_addresses` | `locations.all_office_addresses` | ### Competitors | Legacy field | Current field | | --------------------- | ------------------------- | | `competitor_ids` | `competitors.company_ids` | | `competitor_websites` | `competitors.websites` | ### Social profiles | Legacy field | Current field | | ------------------------ | --------------------------------- | | `twitter_handle` | `social_profiles.twitter_url` | | `twitter_profile_url` | `social_profiles.twitter_url` | | `crunchbase_profile_url` | `social_profiles.crunchbase.url` | | `crunchbase_uuid` | `social_profiles.crunchbase.uuid` | <Note> **Filterable but not returned.** `roles.*` (role distribution and growth) and `competitors.*` can be used in `filters` but are not included in the search response. Fetch full values via [Company Enrich](/company-docs/enrichment/introduction). </Note> For the full searchable-field catalog, see [Company Search reference](/company-docs/search/reference#searchable-fields). --- ## 4. Type changes | Field | Legacy type | Current type | Notes | | --------------------------------------------------- | ----------- | ------------------------------ | ------------------------------------------------------------------ | | `year_founded` | string | integer | Past responses returned `"2017"`; current responses return `2017`. | | `last_funding_date` / `funding.last_fundraise_date` | string | string (ISO 8601 `YYYY-MM-DD`) | Format clarified. | <Warning> **`year_founded` is now an integer.** Update any parsing logic that expected a quoted string (`"2017"`) — values are now bare numbers (`2017`). </Warning> --- ## 5. Removed features ### Semantic search (`search` parameter) The legacy endpoint accepted a top-level `search` object for natural-language queries: ```json { "search": { "query": "fintech companies hiring engineers", "mode": "hybrid" } } ``` The current endpoint does not accept `search`. Sending it returns `400`. Use structured `filters` instead, or contact [support@crustdata.co](mailto:support@crustdata.co) if you need semantic retrieval for an account. ### `single_query` debug flag Removed. The flag had no externally documented behavior. ### `crunchbase_valuation_usd` / `crunchbase_valuation_date` filters Removed — company valuation has no current equivalent in either Company Search or [Company Enrich](/company-docs/enrichment/introduction). Other funding signals (last round amount, total investment, investors, milestones) remain available via enrich. ### Flat per-section growth fields The flat `employee_metrics.growth_*` and `follower_metrics.growth_*` names were renamed, not removed. Filter headcount growth under `headcount.growth_percent.{1m, 3m, 6m, 12m}` (or `headcount.growth_absolute.*`) and follower growth under `followers.{mom_percent, qoq_percent, six_months_growth_percent, yoy_percent}`. ### `role_distribution_percent.*` Removed. Request `roles.distribution` (raw counts) and compute percentages in your client. --- ## 6. Added features | Feature | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `fields` parameter | Request only the dot-paths you need (e.g. `["basic_info.name", "headcount.total"]`). | | Nested response sections | Records return structured `basic_info`, `headcount`, `funding`, `taxonomy`, `revenue`, `locations`, `followers`, `social_profiles`, `hiring`, and `software_reviews` sections. | | Stable error envelope | All `4xx`/`5xx` errors return `{ "error": { "type", "message", "metadata" } }` (legacy returned `{ "error": "...", "details": {...} }`). | | `updated_at` and `indexed_at` | New top-level timestamps marking the most recent profile refresh and indexing. | | Live `professional_network` profile | `social_profiles.professional_network` returns the company's professional-network profile URL alongside legacy social links. | | Discoverable filter values | [Company Autocomplete](/company-docs/autocomplete/introduction) returns valid indexed values for any supported field. | | Software review metrics | `software_reviews` returns `average_rating`, `review_count`, and review-count growth percentages when available. | | Company logo | `basic_info.logo_permalink` returns a stable Crustdata-hosted company logo URL. | --- ## 7. Response shape The response envelope keeps the same top-level keys, but each company object moved from a flat structure to a nested one. ### Envelope | Key | Legacy | Current | | ------------- | --------------------------------- | ------------------ | | `companies` | array of companies | array of companies | | `next_cursor` | string \| null | string \| null | | `total_count` | integer \| null | integer \| null | | `query` | present when `return_query: true` | (not returned) | ### Company object Legacy records returned a flat object with keys like `company_name`, `linkedin_profile_url`, `hq_country`, `linkedin_followers`, and per-section nested objects (`employee_metrics`, `follower_metrics`, `funding_and_investment`, etc.). Current records group those fields into named sections: <CodeGroup> ```json Legacy record (truncated) { "company_id": 633593, "company_name": "Retool", "company_website_domain": "retool.com", "year_founded": "2017", "hq_country": "USA", "employee_metrics": { "latest_count": 443 }, "follower_metrics": { "latest_count": 95000 }, "estimated_revenue_lower_bound_usd": 50000000 } ``` ```json Current record (truncated) { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "year_founded": 2017 }, "locations": { "country": "USA" }, "headcount": { "total": 443 }, "followers": { "count": 95000 }, "revenue": { "estimated": { "lower_bound_usd": 50000000 } } } ``` </CodeGroup> ### Response-key map | Legacy key | Current key | | ---------------------------------------- | ------------------------------------------------------------ | | `company_id` | `crustdata_company_id` (top-level) / `basic_info.crustdata_company_id` | | `company_name` | `basic_info.name` | | `company_website_domain` / `domains` | `basic_info.primary_domain` / `basic_info.all_domains` | | `linkedin_profile_url` | `basic_info.professional_network_url` | | `linkedin_industries` | `taxonomy.professional_network_industry` | | `year_founded` | `basic_info.year_founded` | | `hq_country` / `hq_location` | `locations.country` / `locations.headquarters` | | `employee_metrics.latest_count` | `headcount.total` | | `follower_metrics.latest_count` | `followers.count` | | `estimated_revenue_lower_bound_usd` | `revenue.estimated.lower_bound_usd` | | `estimated_revenue_higher_bound_usd` | `revenue.estimated.upper_bound_usd` | | `crunchbase_total_investment_usd` | `funding.total_investment_usd` | | `last_funding_round_type` | `funding.last_round_type` | | `last_funding_date` | `funding.last_fundraise_date` | | `competitor_ids` / `competitor_websites` | (not returned in search; filterable only — fetch via [Company Enrich](/company-docs/enrichment/introduction)) | <Tip> Use the `fields` request parameter to request just the sections you need (for example, `fields: ["basic_info.name", "headcount.total"]`). This keeps responses small and avoids parsing sections you do not use. See [Response fields](/company-docs/search/reference#response-fields). </Tip> --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Unsupported column 'industry'", "details": { "supported_columns": ["company_name", "linkedin_industries", "..."] } } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Unsupported columns in conditions: ['industry']", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 9. End-to-end example The same query — _software-development companies headquartered in the USA, sorted by headcount_ — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url https://api.crustdata.com/screener/companydb/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "filters": { "op": "and", "conditions": [ { "column": "linkedin_industries", "type": "in", "value": ["Software Development"] }, { "column": "hq_country", "type": "=", "value": "USA" } ] }, "sorts": [{"column": "employee_metrics.latest_count", "order": "desc"}], "limit": 2 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/company/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": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development" }, { "field": "locations.country", "type": "=", "value": "USA" } ] }, "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 2, "fields": [ "crustdata_company_id", "basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country" ] }' ``` </CodeGroup> --- ## Migration checklist - [ ] Replace base path `/screener/companydb/search` → `/company/search`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename `column` → `field` in every filter condition. - [ ] Rename `column` → `field` in every sort directive. - [ ] Remove `count` — it is no longer accepted (returns `400`); use `limit` instead. - [ ] Map legacy field names to the current dataset paths (see [Field-name mapping](#3-field-name-mapping)). - [ ] Update integrations that depend on `year_founded` being a string — it now returns an integer. - [ ] Drop the `search` parameter (semantic mode is no longer accepted). - [ ] Drop the `single_query` debug flag. - [ ] Replace `employee_metrics.growth_*` filters with `headcount.growth_percent.*` / `headcount.growth_absolute.*`. - [ ] Replace `follower_metrics.growth_*_percent` filters with `followers.{mom_percent, qoq_percent, six_months_growth_percent, yoy_percent}`. - [ ] Compute role-distribution percentages client-side from `roles.distribution`. - [ ] Update response parsing for the nested record shape (see [Response-key map](#response-key-map)). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. --- ## See also - [Company Search](/company-docs/search/introduction) — entry point for the new endpoint. - [Company Search reference](/company-docs/search/reference) — full operator, field, and response catalog. - [Company Autocomplete](/company-docs/autocomplete/introduction) — discover valid filter values. - [Company Enrich](/company-docs/enrichment/introduction) — fetch a full company profile by domain, name, or ID. - [Person search migration](/migrations/person/persondb-search) — equivalent guide for the person dataset. # Live company search Source: https://docs.crustdata.com/migrations/company/company-search-live Reference for migrating from the legacy /screener/company/search endpoint to the new /company/professional_network/search/live endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/company/search</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /company/professional_network/search/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy live-company-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------- | ------------------------------------------------ | | Path | `POST /screener/company/search` | `POST /company/professional_network/search/live` | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Two header changes are required.** The authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing either header are rejected. </Warning> --- ## 1. Request body — top-level keys | Key | Legacy | Current | Notes | | ---------------- | ---------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | Saved search URL | `linkedin_sales_navigator_search_url` (string) | `professional_network_search_url` (string) | Key renamed. Value semantics unchanged — pass the full saved search URL. | | `filters` | array of filter objects | array of filter objects | Filter object grammar changed — see [Filter grammar](#2-filter-grammar). | | `page` | integer (default `1`) | integer (required when using `filters`) | Now **required** when sending `filters`. Sending filters without `page` returns `400`. Optional when using the saved-search URL. | | `limit` | (not supported) | integer 1–25 (default `25`) | New — caps the number of profiles returned per call. | | `keyword` | string | (removed) | Use the `KEYWORD` filter field instead. | <Note> **One identifier per request.** Provide either `professional_network_search_url` **or** `filters + page`. Both can be sent together only if the saved-search URL contains everything you need — when you pass `filters`, `page` becomes required. </Note> --- ## 2. Filter grammar The filter object key changed from `filter_type` to `field`. Operator names and the overall array shape are unchanged. <CodeGroup> ```json Legacy filter { "filter_type": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200", "201-500"] } ``` ```json Current filter { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200", "201-500"] } ``` </CodeGroup> <Note> **One change per filter:** rename `filter_type` → `field`. Filter-field enum values (`INDUSTRY`, `COMPANY_HEADCOUNT`, …) are unchanged. </Note> ### Supported filter fields | Field | Description | Operators | | ----------------------------- | ---------------------------------------------------------------- | -------------- | | `INDUSTRY` | Company industry | `in`, `not in` | | `COMPANY_HEADCOUNT` | Employee-count range (bucket strings like `51-200`) | `in` | | `REGION` | Geographic region | `in`, `not in` | | `ANNUAL_REVENUE` | Revenue range. Requires `sub_filter: "USD"`. | `between` | | `NUM_OF_FOLLOWERS` | Follower-count range | `in` | | `FORTUNE` | Fortune list membership | `in` | | `TECHNOLOGIES_USED` | Technologies the company uses | `in` | | `DEPARTMENT_HEADCOUNT` | Department size. Requires `sub_filter` (department name). | `between` | | `DEPARTMENT_HEADCOUNT_GROWTH` | Department growth rate. Requires `sub_filter` (department name). | `between` | | `COMPANY_HEADCOUNT_GROWTH` | Headcount growth rate | `between` | | `KEYWORD` | Free-text keyword (replaces the legacy `keyword` body parameter) | `in` | | `ACCOUNT_ACTIVITIES` | Recent company activities (funding, hiring) | `in` | | `JOB_OPPORTUNITIES` | Hiring activity | `in` | ### Operators | Operator | Legacy | Current | Notes | | --------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------- | | `in` | ✅ | ✅ | Value is a string or array of strings. | | `not in` | ✅ | ✅ | Value is a string or array of strings. Supported only on `REGION` and `INDUSTRY`. | | `between` | ✅ | ✅ | Value is `{"min": <number>, "max": <number>}`. Required for the range fields `ANNUAL_REVENUE`, `DEPARTMENT_HEADCOUNT`, `DEPARTMENT_HEADCOUNT_GROWTH`, and `COMPANY_HEADCOUNT_GROWTH`. | --- ## 3. Field-name mapping This endpoint returns a live snapshot rather than the indexed dataset. The response moved from a flat record (with `company_name`, `linkedin_id`, `industry`, `location`, `revenue`, etc. at the top level) to a structured record grouped under `basic_info`, `headcount`, `taxonomy`, `locations`, `revenue`, and `people`. ### Identity and profile | Legacy field | Current field | | ------------------------------ | ------------------------------------------------------------------------------------------ | | `company_name` | `basic_info.name` | | `company_id` | (resolve via [Company Identify](/company-docs/identify/introduction); not returned inline) | | `linkedin_profile_url` | `basic_info.professional_network_url` | | `linkedin_id` | `basic_info.professional_network_id` | | `company_website_domain` | `basic_info.primary_domain` | | `company_website` | `basic_info.website` | | `linkedin_company_description` | `basic_info.description` | | `company_type` | `basic_info.company_type` | | `year_founded` | `basic_info.year_founded` (now an integer; see [Type changes](#4-type-changes)) | | `employee_count_range` | `basic_info.employee_count_range` | | `linkedin_logo_url` | `basic_info.logo_permalink` | | `industry` | `basic_info.industries[]` (array) | ### Taxonomy | Legacy field | Current field | | ------------------- | ---------------------------------------- | | `linkedin_industry` | `taxonomy.professional_network_industry` | ### Headcount | Legacy field | Current field | | ------------------------------ | ----------------- | | `total_employees` | `headcount.total` | | `headcount.linkedin_headcount` | `headcount.total` | ### Locations | Legacy field | Current field | | -------------- | ------------------------ | | `hq_country` | `locations.country` | | `hq_city` | `locations.city` | | `hq_state` | `locations.state` | | `headquarters` | `locations.headquarters` | ### Revenue | Legacy field | Current field | | ---------------------------------------- | ----------------------------------- | | `revenue.estimated_revenue_lower_bound` | `revenue.estimated.lower_bound_usd` | | `revenue.estimated_revenue_higher_bound` | `revenue.estimated.upper_bound_usd` | ### People | Legacy field | Current field | | ----------------------- | ------------------------------ | | `total_decision_makers` | `people.decision_makers_count` | --- ## 4. Type changes | Field | Legacy type | Current type | Notes | | --------------------- | ----------- | ------------ | ------------------------------------------------------------------- | | `year_founded` | string | integer | Past responses returned `"2017"`; current responses return `2017`. | | `total_display_count` | string | string | Unchanged — values like `"32K+"` are approximate, not exact counts. | <Warning> **`year_founded` is now an integer.** Update any parsing logic that expected a quoted string (`"2017"`) — values are now bare numbers (`2017`). </Warning> --- ## 5. Removed features ### Top-level `keyword` body parameter The legacy endpoint accepted a top-level `keyword` string. The current endpoint does not. Send keyword text as a filter with `field: "KEYWORD"`: <CodeGroup> ```json Legacy keyword { "keyword": "fintech", "filters": [ { "filter_type": "REGION", "type": "in", "value": ["United States"] } ], "page": 1 } ``` ```json Current keyword { "filters": [ { "field": "KEYWORD", "type": "in", "value": "fintech" }, { "field": "REGION", "type": "in", "value": ["United States"] } ], "page": 1 } ``` </CodeGroup> ### Implicit `page: 1` default with filters The legacy endpoint defaulted `page` to `1`. The current endpoint **requires `page` when `filters` are present**. Sending filters without a page returns `400`. --- ## 6. Added features | Feature | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `limit` parameter | Cap the page size between 1 and 25. Defaults to 25 (the full page) when omitted. | | Structured profile sections | Records return structured `basic_info`, `headcount`, `taxonomy`, `locations`, `revenue`, and `people` sections. | | `logo_permalink` | Company logo image URL surfaced on each result. | | `description` | Public company description returned directly on each result. | | Stable error envelope | All `4xx`/`5xx` errors return `{ "error": { "type", "message", "metadata" } }` (legacy returned `{ "error": "..." }`). | --- ## 7. Response shape The response envelope is unchanged. Each company record is restructured. ### Envelope | Key | Legacy | Current | | --------------------- | ------------------------ | ------------------------ | | `companies` | array of company records | array of company records | | `total_display_count` | string (e.g., `"32K+"`) | string (e.g., `"32K+"`) | ### Company record <CodeGroup> ```json Legacy record (truncated) { "company_name": "Mintlify", "linkedin_profile_url": "https://www.linkedin.com/company/mintlify", "company_website_domain": "mintlify.com", "year_founded": "2022", "industry": "Software Development", "hq_country": "United States", "hq_city": "San Francisco", "total_employees": 35, "revenue": { "estimated_revenue_lower_bound": 1000000, "estimated_revenue_higher_bound": 5000000 }, "total_decision_makers": 6 } ``` ```json Current record (truncated) { "basic_info": { "name": "Mintlify", "professional_network_url": "https://www.linkedin.com/company/mintlify", "primary_domain": "mintlify.com", "year_founded": 2022, "industries": ["Software Development"] }, "taxonomy": { "professional_network_industry": "Software Development" }, "headcount": { "total": 35 }, "locations": { "country": "United States", "city": "San Francisco" }, "revenue": { "estimated": { "lower_bound_usd": 1000000, "upper_bound_usd": 5000000 } }, "people": { "decision_makers_count": "6" } } ``` </CodeGroup> ### Response-key map | Legacy key | Current key | | ---------------------------------------- | -------------------------------------------------------------------- | | `company_name` | `basic_info.name` | | `linkedin_profile_url` | `basic_info.professional_network_url` | | `company_website_domain` | `basic_info.primary_domain` | | `year_founded` | `basic_info.year_founded` (integer) | | `industry` / `industries` | `basic_info.industries[]` / `taxonomy.professional_network_industry` | | `total_employees` | `headcount.total` | | `hq_country` / `hq_city` / `hq_state` | `locations.country` / `locations.city` / `locations.state` | | `headquarters` | `locations.headquarters` | | `revenue.estimated_revenue_lower_bound` | `revenue.estimated.lower_bound_usd` | | `revenue.estimated_revenue_higher_bound` | `revenue.estimated.upper_bound_usd` | | `total_decision_makers` | `people.decision_makers_count` | --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Failed to parse search query" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "'filter_type' is not supported. Use 'field' instead.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 9. End-to-end example The same query — _mid-size tech companies in the Technology industry_ — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url https://api.crustdata.com/screener/company/search \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "filters": [ { "filter_type": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"] }, { "filter_type": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"] } ], "page": 1 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/company/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["51-200"] }, { "field": "INDUSTRY", "type": "in", "value": ["Technology, Information and Internet"] } ], "page": 1 }' ``` </CodeGroup> --- ## Migration checklist - [ ] Replace base path `/screener/company/search` → `/company/professional_network/search/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename `linkedin_sales_navigator_search_url` → `professional_network_search_url`. - [ ] Rename `filter_type` → `field` in every filter object. - [ ] Move top-level `keyword` into the filters array as `{ field: "KEYWORD", type: "in", value: "<keyword>" }`. - [ ] Always send `page` when sending `filters` (it is no longer defaulted). - [ ] Optionally add `limit` (1–25) to cap the page size. - [ ] Update parsers for the structured response shape (see [Response-key map](#response-key-map)). - [ ] Update integrations that depend on `year_founded` being a string — it now returns an integer. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. --- ## See also - [Company Realtime Search](/company-docs/search/live-search) — entry point for the new endpoint. - [Company Search (indexed)](/company-docs/search/introduction) — when you need structured queries over the indexed dataset instead of a live snapshot. - [Company Identify](/company-docs/identify/introduction) — resolve a domain or name to a `crustdata_company_id` for downstream enrich calls. - [Company-search migration](/migrations/company/companydb-search) — equivalent guide for the indexed company-search endpoint. # Company enrich Source: https://docs.crustdata.com/migrations/company/company-enrich Reference for migrating from the legacy /screener/company endpoint to the new /company/enrich endpoint. <Columns cols={1}> <Column> <Danger>GET /screener/company</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /company/enrich</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy company-enrich endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ---------------------------- | -------------------------------------- | | Path | `GET /screener/company` | `POST /company/enrich` | | Method | `GET` with query parameters | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Three header / shape changes are required.** The HTTP method moved from `GET` to `POST`, the authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. </Warning> --- ## 1. Request — top-level keys Identifiers moved from comma-separated query parameters to JSON arrays in the request body. Submit exactly one identifier type per request (up to 25 values). | Legacy query parameter | Current body key | Notes | | --------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `?company_domain=stripe.com,google.com` | `{ "domains": ["stripe.com", "google.com"] }` | Comma-separated string → JSON array. | | `?company_name=Stripe,Google` | `{ "names": ["Stripe", "Google"] }` | Comma-separated string → JSON array. | | `?company_id=12345,67890` | `{ "crustdata_company_ids": [12345, 67890] }` | Strings of digits → JSON array of integers. | | `?company_linkedin_url=https://...` | `{ "professional_network_profile_urls": ["https://..."] }` | Key renamed. | | `?fields=name,headcount` | `{ "fields": ["basic_info.name", "headcount"] }` | Comma-separated string → JSON array. **Defaults to `basic_info` only** — request other sections explicitly. Section names changed — see [Field-name mapping](#3-field-name-mapping). | | `?exact_match=true` | `{ "exact_match": true }` | Moved into the body. Default is now `null` (auto-detect) rather than `false`. | | `?enrich_realtime=true` | (removed) | No direct replacement on this endpoint — see [Removed features](#5-removed-features). | <Note> **Submit exactly one identifier type per request.** Mixing `domains`, `names`, `crustdata_company_ids`, and `professional_network_profile_urls` in the same call returns `400`. Maximum 25 values per identifier array. </Note> <Warning> **Unlike the legacy endpoint, `fields` defaults to `basic_info` only.** The legacy `GET /screener/company` returned the full profile by default; `POST /company/enrich` returns only `basic_info` (plus `crustdata_company_id`) when `fields` is omitted — every other section (`funding`, `revenue`, `headcount`, `locations`, `people`, `hiring`, `web_traffic`, `seo`, `competitors`, `employee_reviews`, `software_reviews`, `news`, `social_profiles`, `taxonomy`, `followers`, …) comes back as a **`null` placeholder, not missing data**. Request the sections you need explicitly, e.g. `"fields": ["basic_info", "funding", "revenue", "headcount"]`. </Warning> --- ## 2. Identifier behavior | Topic | Legacy | Current | | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------- | | Identifier shape | Comma-separated string in a query parameter | JSON array in the body | | Max values per request | 25 | 25 | | Mutual exclusion | Pass one identifier parameter | Pass one identifier array | | Mixed-identifier error | `400` with `"Please provide only one type of input"` | `400` with `{ error: { type: "invalid_request", message: "..." } }` | | Over-limit error | `400` with `provided_count` / `max_allowed` fields | `400` with the new error envelope | --- ## 3. Field-name mapping The legacy response was a flat object with `linkedin_*`, `glassdoor`, `g2`, `crunchbase_*`, `producthunt`, and `gartner` sections at the top level. The current response groups fields under `basic_info`, `headcount`, `funding`, `taxonomy`, `revenue`, `locations`, `competitors`, `followers`, `social_profiles`, `hiring`, `web_traffic`, `seo`, `employee_reviews`, `software_reviews`, `public_launches`, and `market_intel`. ### Identity and profile | Legacy field | Current field | | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `company_id` | `crustdata_company_id` (top-level) / `basic_info.crustdata_company_id` | | `company_name` | `basic_info.name` | | `company_website_domain` | `basic_info.primary_domain` | | `company_website` | `basic_info.website` | | `domains` | `basic_info.all_domains` | | `linkedin_profile_url` | `basic_info.professional_network_url` | | `linkedin_id` | `basic_info.professional_network_id` | | `linkedin_profile_name` | `basic_info.profile_name` | | `linkedin_company_description` | `basic_info.description` | | `linkedin_logo_url` / `linkedin_logo_permalink` | `basic_info.logo_permalink` | | `company_type` | `basic_info.company_type` | | `year_founded` | `basic_info.year_founded` (now an integer; see [Type changes](#4-type-changes)) | | `employee_count_range` | `basic_info.employee_count_range` | | `markets` | `basic_info.markets` | | `last_updated` | `metadata.updated_at` | ### Taxonomy | Legacy field | Current field | | ---------------------------------------------------------- | -------------------------------------------- | | `linkedin_industry` | `taxonomy.professional_network_industry` | | `taxonomy.linkedin_industry` | `taxonomy.professional_network_industry` | | `linkedin_industries` / `taxonomy.linkedin_industries` | `taxonomy.professional_network_industries` | | `taxonomy.linkedin_specialities` | `taxonomy.professional_network_specialities` | | `crunchbase_categories` / `taxonomy.crunchbase_categories` | `taxonomy.categories` | | `taxonomy.primary_naics_detail` | `taxonomy.primary_naics_detail` | | `taxonomy.sic_detail_list` | `taxonomy.sic_detail_list` | ### Headcount | Legacy field | Current field | | --------------------------------------------------- | --------------------------------------------------------- | | `headcount.linkedin_headcount` | `headcount.total` | | `headcount.linkedin_headcount_total_growth_percent` | `headcount.growth_percent` | | `headcount.linkedin_headcount_by_role_absolute` | `headcount.by_role_absolute` | | `headcount.linkedin_headcount_by_role_percent` | `headcount.by_role_percent` | | `headcount.linkedin_headcount_by_region_absolute` | `headcount.by_region_absolute` | | `headcount.linkedin_headcount_timeseries` | (available via the broader timeseries response on enrich) | | `largest_headcount_country` | `headcount.largest_headcount_country` | ### Followers | Legacy field | Current field | | ----------------------------------------------------------------- | ------------------------------------- | | `linkedin_followers.linkedin_followers` | `followers.count` | | `linkedin_followers.linkedin_followers_mom_percent` | `followers.mom_percent` | | `linkedin_followers.linkedin_followers_qoq_percent` | `followers.qoq_percent` | | `linkedin_followers.linkedin_followers_six_months_growth_percent` | `followers.six_months_growth_percent` | | `linkedin_followers.linkedin_followers_yoy_percent` | `followers.yoy_percent` | ### Funding and investors | Legacy field | Current field | | ---------------------------------------------------------- | -------------------------------------------------------------------------- | | `funding_and_investment.crunchbase_total_investment_usd` | `funding.total_investment_usd` | | `funding_and_investment.last_funding_round_investment_usd` | `funding.last_round_amount_usd` | | `funding_and_investment.last_funding_round_type` | `funding.last_round_type` | | `funding_and_investment.crunchbase_investors` | `funding.investors` | | `funding_and_investment.crunchbase_investors_info_list` | `funding.investors_detailed` | | `funding_and_investment.funding_milestones_timeseries` | `funding.milestones[]` (renamed; same one-row-per-round shape) | | `funding_and_investment.acquisitions` | `funding.acquisitions[]` (see [Acquisitions schema](#acquisitions-schema)) | | `funding_and_investment.acquired_by` | `funding.acquired_by[]` | | `crunchbase_valuation_usd` / `crunchbase_valuation_date` | (removed — no current equivalent; see [Removed features](#5-removed-features)) | #### Acquisitions schema The acquisitions object keeps the same fields but the field names are consistently prefixed by `acquirer_*` / `acquiree_*` in the new endpoint. | Legacy field | Current field | | ----------------------- | ----------------------- | | `acquirer_company_id` | `acquirer_company_id` | | `acquirer_company_name` | `acquirer_company_name` | | `acquiree_company_id` | `acquiree_company_id` | | `acquiree_company_name` | `acquiree_company_name` | | `announced_on_date` | `announced_on_date` | | `transaction_text` | `transaction_text` | ### Revenue and public-market data | Legacy field | Current field | | ------------------------------------ | ---------------------------------------- | | `estimated_revenue_lower_bound_usd` | `revenue.estimated.lower_bound_usd` | | `estimated_revenue_higher_bound_usd` | `revenue.estimated.upper_bound_usd` | | `estimated_revenue_timeseries` | `revenue.estimated.timeseries` | | `acquisition_status` | `revenue.acquisition_status` | | `stock_symbols` | `revenue.public_markets.stock_symbols` | | `fiscal_year_end` | `revenue.public_markets.fiscal_year_end` | | `ipo_date` | `revenue.public_markets.ipo_date` | ### Locations | Legacy field | Current field | | ---------------------- | ---------------------------------------------------------- | | `hq_country` | `locations.country` | | `hq_state` | `locations.state` | | `headquarters` | `locations.headquarters` | | `hq_street_address` | `locations.street_address` | | `all_office_addresses` | `locations.all_office_addresses` | | `hq_city` | (removed from response; available via the location parser) | ### Competitors | Legacy field | Current field | | ----------------------------------------------------- | ------------------------- | | `competitors.competitor_website_domains` | `competitors.all_domains` | | `competitors.paid_seo_competitors_website_domains` | `competitors.paid_seo` | | `competitors.organic_seo_competitors_website_domains` | `competitors.organic_seo` | ### Job openings → hiring The legacy `job_openings` section is now `hiring`. | Legacy field | Current field | | ------------------------------------------------ | -------------------------------- | | `job_openings.recent_job_openings_title` | `hiring.recent_titles_csv` | | `job_openings.job_openings_count` | `hiring.openings_count` | | `job_openings.job_openings_count_growth_percent` | `hiring.openings_growth_percent` | | `job_openings.open_jobs_timeseries` | `hiring.open_jobs_timeseries` | | `job_openings.recent_job_openings[]` | `hiring.recent_openings[]` | ### Web traffic and SEO Field paths are unchanged in shape — they remain under `web_traffic` and `seo` — but the response is keyed by domain under `web_traffic` (so you can distinguish traffic across multiple owned domains). ### Social profiles The legacy spec exposed brand-specific keys (`company_twitter_url`, `crunchbase_profile_url`). These are consolidated under `social_profiles`: | Legacy field | Current field | | ----------------------------------------------- | -------------------------------------- | | `company_twitter_url` | `social_profiles.twitter_url` | | `crunchbase_profile_url` | `social_profiles.crunchbase.url` | | `crunchbase_profile_uuid` | `social_profiles.crunchbase.uuid` | | (new — see [Added features](#6-added-features)) | `social_profiles.professional_network` | ### Employee reviews Brand-specific keys (`glassdoor.glassdoor_*`) are replaced by neutral keys under `employee_reviews`: | Legacy field | Current field | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `glassdoor.glassdoor_overall_rating` | `employee_reviews.overall_rating` | | `glassdoor.glassdoor_review_count` | `employee_reviews.review_count` | | `glassdoor.glassdoor_culture_rating` | `employee_reviews.culture_and_values_rating` | | `glassdoor.glassdoor_work_life_balance_rating` | `employee_reviews.work_life_balance_rating` | | `glassdoor.glassdoor_compensation_rating` | `employee_reviews.compensation_and_benefits_rating` | | `glassdoor.glassdoor_career_opportunities_rating` | `employee_reviews.career_opportunities_rating` | | `glassdoor.glassdoor_senior_management_rating` | `employee_reviews.senior_management_rating` | | `glassdoor.glassdoor_diversity_rating` | `employee_reviews.diversity_and_inclusion_rating` | | `glassdoor.glassdoor_recommend_to_friend_pct` | `employee_reviews.recommend_to_friend_rating` | | `glassdoor.glassdoor_business_outlook_pct` | `employee_reviews.business_outlook_rating` | | `glassdoor.glassdoor_ceo_approval_pct` | (now surfaced via [Employee Review Enrich](/company-docs/employee_review/introduction) — `company_ceo.ceo_rating`) | ### Software reviews Brand-specific `g2.*` keys are replaced by neutral keys under `software_reviews`: | Legacy field | Current field | | ---------------------- | --------------------------------- | | `g2.g2_review_count` | `software_reviews.review_count` | | `g2.g2_average_rating` | `software_reviews.average_rating` | ### Product launches Brand-specific `producthunt.*` keys are replaced by neutral keys under `public_launches`: | Legacy field | Current field | | --------------------------- | ------------------------------- | | `producthunt.rating` | `public_launches.rating` | | `producthunt.num_upvotes` | `public_launches.num_upvotes` | | `producthunt.num_reviews` | `public_launches.num_reviews` | | `producthunt.num_followers` | `public_launches.num_followers` | | `producthunt.categories` | `public_launches.categories` | | `producthunt.makers[]` | `public_launches.makers[]` | | `producthunt.reviews[]` | `public_launches.reviews[]` | | `producthunt.launches[]` | `public_launches.launches[]` | | `producthunt.last_updated` | `public_launches.last_updated` | ### Market intelligence Brand-specific `gartner.*` keys are replaced by neutral keys under `market_intel`: | Legacy field | Current field | | ------------------------------------ | ----------------------------------------- | | `gartner.year_founded` | `market_intel.year_founded` (integer) | | `gartner.head_office_city` | `market_intel.head_office_city` | | `gartner.head_office_country` | `market_intel.head_office_country` | | `gartner.num_employees_min` / `_max` | `market_intel.num_employees_min` / `_max` | | `gartner.products[]` | `market_intel.products[]` | | `gartner.reviews[]` | `market_intel.reviews[]` | ### People | Legacy field | Current field | | --------------------- | ---------------------------------- | | `decision_makers[]` | `people.decision_makers[]` | | `founders.profiles[]` | `people.founders[]` | | `cxos[]` | `people.cxos[]` | | `ceo_location` | (available inside `people.cxos[]`) | ### News | Legacy field | Current field | | ---------------------------------------- | ------------------------------- | | `news_articles[].article_url` | `news[].article_url` | | `news_articles[].article_title` | `news[].article_title` | | `news_articles[].article_publish_date` | `news[].article_publish_date` | | `news_articles[].article_publisher_name` | `news[].article_publisher_name` | For the full enrich-response field catalog, see [Company Enrich reference](/company-docs/enrichment/reference#company_data-sections). --- ## 4. Type changes | Field | Legacy type | Current type | Notes | | --------------------- | ----------- | -------------------- | --------------------------------------------------------------------------------------------------- | | `year_founded` | string | integer | Past responses returned `"2017"`; current responses return `2017`. | | `exact_match` default | `false` | `null` (auto-detect) | The new endpoint treats `null` as "let the backend decide". Pass `true` for strict domain matching. | <Warning> **`year_founded` is now an integer.** Update any parsing logic that expected a quoted string (`"2017"`) — values are now bare numbers (`2017`). </Warning> --- ## 5. Removed features ### `enrich_realtime` query parameter The legacy endpoint used `enrich_realtime=true` to fall back to live retrieval when a company was not in the indexed dataset. This flag has been removed and has **no direct replacement** on the current endpoint — `/company/enrich` serves the indexed dataset only. If a company is not present, the request returns an entry with empty `matches: []` rather than attempting a live fetch. Contact [support@crustdata.co](mailto:support@crustdata.co) if you need on-demand live company enrichment for an account. ### Company valuation fields The legacy `crunchbase_valuation_usd` and `crunchbase_valuation_date` fields are removed and have **no current equivalent** on `/company/enrich`. Other funding signals — `funding.last_round_amount_usd`, `funding.total_investment_usd`, `funding.last_fundraise_date`, `funding.milestones[]`, and `funding.investors[]` — remain available. ### Brand-named response sections Top-level `glassdoor`, `g2`, `producthunt`, `gartner` sections are removed. Their values are returned under the neutralized `employee_reviews`, `software_reviews`, `public_launches`, and `market_intel` sections — see the mapping tables above. ### `status: "enriching" | "not_found"` field — and background enrichment The legacy response included a `status` field with values `"enriching"` and `"not_found"`, and an accompanying `companies_to_be_enriched` array. When a company was unknown, the legacy endpoint **queued it for background enrichment** (typically completed within 24 hours) and you could poll until data appeared. The current endpoint is a **synchronous dataset lookup only**: no-match is returned as an entry with an empty `matches` array (see [Response shape](#7-response-shape)), and the request does **not** queue the company for enrichment. There is no poll-again state. If a company you need is missing from the dataset, contact [support](https://crustdata.com/demo) to request coverage. ### Comma-separated query parameter style The endpoint moved from `GET` with query parameters to `POST` with a JSON body. All identifiers must be sent as JSON arrays. --- ## 6. Added features | Feature | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Match-result envelope | The response wraps each result in `{ matched_on, match_type, matches: [{ confidence_score, company_data }] }`, making batch matching unambiguous. | | `confidence_score` | Numeric score on each match candidate. Higher is better; `1.0` is common for direct identifier lookups. | | `metadata.updated_at`, `metadata.indexed_at` | Timestamps under `metadata` marking the most recent profile refresh and indexing. | | `social_profiles.professional_network` | Professional-network profile URL surfaced alongside legacy social links. | | `public_launches`, `market_intel` | Neutralized replacements for the legacy `producthunt` / `gartner` blocks. | | Stable error envelope | All `4xx`/`5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | --- ## 7. Response shape The envelope shape changed. The legacy endpoint returned a top-level array of company records. The current endpoint returns a top-level array of match-result envelopes, each containing one or more `company_data` candidates. ### Envelope <CodeGroup> ```json Legacy [ { "company_id": 633593, "company_name": "Retool", "company_website_domain": "retool.com", "year_founded": "2017", "linkedin_industry": "Software Development", "headcount": { "linkedin_headcount": 443 } } ] ``` ```json Current [ { "matched_on": "retool.com", "match_type": "domain", "matches": [ { "confidence_score": 1.0, "company_data": { "crustdata_company_id": 633593, "basic_info": { "name": "Retool", "primary_domain": "retool.com", "year_founded": 2017 }, "taxonomy": { "professional_network_industry": "Software Development" }, "headcount": { "total": 443 } } } ] } ] ``` </CodeGroup> ### Match-result fields | Field | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------- | | `matched_on` | The input value you submitted (a domain, name, ID, or profile URL). | | `match_type` | One of `domain`, `name`, `crustdata_company_id`, `professional_network_profile_url`. | | `matches` | Array of candidate matches. Empty when nothing matched. | | `matches[].confidence_score` | Number. Higher is better. `1.0` is common for direct identifier lookups. | | `matches[].company_data` | Full enriched company profile — see [Field-name mapping](#3-field-name-mapping) for section paths. | ### No-match behavior Each entry corresponds to one input identifier. When nothing matches, the entry's `matches` array is empty: ```json [ { "matched_on": "thisdomaindoesnotexist12345xyz.com", "match_type": "domain", "matches": [] } ] ``` <Note> The legacy endpoint returned `404` when no companies matched. The current endpoint returns `200` with empty `matches: []` for unmatched identifiers. The OpenAPI contract still defines `404` for completeness — handle both. </Note> --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Please provide only one type of input: company_name, company_domain, company_linkedin_url, or company_id" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: crustdata_company_ids, names, domains, or professional_network_profile_urls", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 9. End-to-end example Enriching two companies by domain — written against both endpoints. <CodeGroup> ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/company?company_domain=retool.com,serverobotics.com&fields=company_name,headcount.linkedin_headcount,funding_and_investment.crunchbase_total_investment_usd' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["retool.com", "serverobotics.com"], "fields": ["basic_info", "headcount", "funding"] }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `GET /screener/company` (query params) to `POST /company/enrich` (JSON body). - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename identifier keys: `company_domain` → `domains`, `company_name` → `names`, `company_id` → `crustdata_company_ids`, `company_linkedin_url` → `professional_network_profile_urls`. - [ ] Convert comma-separated parameter values to JSON arrays. - [ ] Drop `enrich_realtime=true` — no direct replacement; the new endpoint serves cached data only. - [ ] Pass `fields` explicitly — unlike the legacy endpoint, the new default is `basic_info` only, and omitted sections come back as `null` placeholders. - [ ] Unwrap each result from the new `{ matched_on, match_type, matches: [{ confidence_score, company_data }] }` envelope. - [ ] Update parsers for the nested `company_data` shape and the renamed sections (`linkedin_*` → `professional_network_*` / sectioned paths). - [ ] Replace `glassdoor.*` parsing with `employee_reviews.*`. - [ ] Replace `g2.*` parsing with `software_reviews.*`. - [ ] Replace `producthunt.*` parsing with `public_launches.*`. - [ ] Replace `gartner.*` parsing with `market_intel.*`. - [ ] Map `funding_and_investment.*` paths to `funding.*` (rename only; field semantics preserved). - [ ] Map `job_openings.*` paths to `hiring.*`. - [ ] Handle no-match as `matches: []` (legacy returned `404` or a record with `status: "not_found"`). - [ ] Update integrations that depend on `year_founded` being a string — it now returns an integer. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. --- ## See also - [Company Enrich](/company-docs/enrichment/introduction) — entry point for the new endpoint. - [Company Enrich reference](/company-docs/enrichment/reference) — full request, response, and field catalog. - [Company Identify](/company-docs/identify/introduction) — resolve a domain or name to a `crustdata_company_id` first. - [Company-search migration](/migrations/company/companydb-search) — equivalent guide for the indexed company-search endpoint. # Person search Source: https://docs.crustdata.com/migrations/person/persondb-search Reference for migrating from the legacy /screener/persondb/search endpoint to the new /person/search endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/persondb/search</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /person/search</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy person-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | -------------------------------- | -------------------------------------- | | Path | `POST /screener/persondb/search` | `POST /person/search` | | Auth | `Authorization: Bearer <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **The `x-api-version` header is required on every request to `/person/search`.** Calls without it are rejected. The legacy endpoint did not use this header. </Warning> --- ## 1. Request body — top-level keys | Key | Legacy | Current | Notes | | ----------------- | ------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `filters` | object | object | Grammar changed — see [Filter grammar](#2-filter-grammar) below. | | `sorts` | array of `{column, order}` | array of `{field, order}` | `column` renamed to `field`. | | `limit` | integer 1–1000, default 20 | integer 1–1000, default 20 | Unchanged. | | `count` | integer alias for `limit` | integer alias for `limit` | Alias for `limit`. Sending both `count` and `limit` returns `400`. | | `cursor` | string nullable | string nullable | Unchanged. | | `fields` | (not supported) | string[] | New — request a subset of response sections via dot-paths. | | `post_processing` | `{ exclude_profiles, exclude_names }` | `{ exclude_profiles, exclude_names }` | Same shape. `exclude_profiles` still accepts the same profile-URL list. | | `preview` | boolean | boolean | Available on enabled accounts only; otherwise returns `400`. | | `search` | object (semantic / hybrid) | (removed) | Semantic/hybrid search has no replacement on this endpoint — see [Removed features](#5-removed-features). | | `single_query` | boolean (debug) | (removed) | Removed. | | `return_query` | boolean (debug) | boolean (accepted, response does not include `query`) | Behavioral change. | --- ## 2. Filter grammar The condition key changed from `column` to `field`. Operator names and the `and`/`or` group shape are unchanged. <CodeGroup> ```json Legacy condition { "column": "current_employers.title", "type": "=", "value": "CEO" } ``` ```json Current condition { "field": "experience.employment_details.current.title", "type": "=", "value": "CEO" } ``` </CodeGroup> <Note> **Two changes per condition:** 1. Rename `column` → `field`. 2. Map the legacy field name to the new dataset path (see [Field-name mapping](#3-field-name-mapping)). </Note> ### Operators | Operator | Legacy | Current | Notes | | ------------------------ | --------------------- | ------------------------------------------------ | ---------------------------------------------------------------- | | `=` | ✅ | ✅ | | | `!=` | ✅ | ✅ | | | `<`, `>` | ✅ | ✅ | | | `=<`, `=>` | ✅ | ✅ | `>=` and `<=` are **not** supported — use `=>` / `=<`. | | `in`, `not_in` | ✅ | ✅ | Value must be an array. | | `(.)` | ✅ | ✅ | Fuzzy / all-words match (supports `\|` for OR). | | `(!)` | — | ✅ | Fuzzy negation — excludes profiles whose value contains the substring (case-insensitive). Multi-word values match as a literal phrase. | | `[.]` | ✅ | ✅ | Exact phrase match. | | `geo_distance` | ✅ (only on `region`) | ✅ (only on `professional_network.location.raw`) | Field renamed; payload `{ location, distance, unit }` unchanged. `lat_lng: [lat, lng]` may replace `location` to skip geocoding. | | `geo_exclude` | — | ✅ (only on `professional_network.location.raw`) | Inverse of `geo_distance` — excludes profiles inside the radius. Same `{ location \| lat_lng, distance, unit }` payload. | | `is_null`, `is_not_null` | ✅ | ✅ | Send `"value": null` — the `value` key is required. | ### `and` / `or` groups The group shape is unchanged — `op: "and"` or `op: "or"` plus a `conditions` array. Only the leaf-condition key (`column` → `field`) and the leaf field names need to change. --- ## 3. Field-name mapping The legacy endpoint exposed flat names like `name`, `region`, and `current_employers.title`. The current endpoint uses structured dataset paths under `basic_profile`, `experience.employment_details`, `education`, etc. <Note> Whether each current field is **filterable** and **sortable** is marked per field in the [Person Search reference](/person-docs/search/reference#searchable-fields) — see the **Filterable** and **Sortable** columns. Sorting on a non-sortable field returns `400`. </Note> ### Identity and location | Legacy field | Current field | | -------------------- | --------------------------------------------------------------------------------------------------- | | `name` | `basic_profile.name` | | `first_name` | `basic_profile.first_name` | | `last_name` | `basic_profile.last_name` | | `headline` | `basic_profile.headline` | | `summary` | `basic_profile.summary` | | `languages` | `basic_profile.languages` | | `region` | `professional_network.location.raw` (also accepted as `basic_profile.location.full_location` alias) | | `location_city` | `basic_profile.location.city` | | `location_state` | `basic_profile.location.state` | | `location_country` | `basic_profile.location.country` | | `location_continent` | `basic_profile.location.continent` | The legacy endpoint also accepted the nested form `location_details.city`, `location_details.state`, `location_details.country`, and `location_details.continent`. Both spellings resolved to the same data, and both map to `basic_profile.location.*` here. ### Skills and connections | Legacy field | Current field | | -------------------- | ---------------------------------------- | | `skills` | `skills.professional_network_skills` | | `num_of_connections` | `professional_network.connections` | | `open_to_cards` | `professional_network.open_to_cards` | | `twitter_handle` | `social_handles.twitter_handle` | | `num_of_followers` | `professional_network.followers` | `professional_network.connections`, `.followers`, and `.open_to_cards` are filter fields here, not response fields. The section returns only `last_updated`. See the [response-key map](#response-key-map). ### Employment — current role The legacy `current_employers.*` array maps to `experience.employment_details.current.*`. Same keys, new prefix. | Legacy field | Current field | | ------------------------------------------------ | ----------------------------------------------------------------------------- | | `current_employers.name` / `.company_name` | `experience.employment_details.current.name` | | `current_employers.company_id` | `experience.employment_details.current.company_id` to filter, returned as `experience.employment_details.current.crustdata_company_id` | | `current_employers.title` | `experience.employment_details.current.title` | | `current_employers.seniority_level` | `experience.employment_details.current.seniority_level` | | `current_employers.function_category` | `experience.employment_details.current.function_category` | | `current_employers.start_date` | `experience.employment_details.current.start_date` | | `current_employers.company_website_domain` | `experience.employment_details.current.company_website_domain` | | `current_employers.company_headcount_range` | `experience.employment_details.current.company_headcount_range` | | `current_employers.company_headcount_latest` | `experience.employment_details.current.company_headcount_latest` | | `current_employers.company_industries` | `experience.employment_details.current.company_industries` | | `current_employers.company_linkedin_industry` | `experience.employment_details.current.company_professional_network_industry` | | `current_employers.company_type` | `experience.employment_details.current.company_type` | | `current_employers.company_hq_location` | `experience.employment_details.current.company_hq_location` | | `current_employers.company_headquarters_country` | `experience.employment_details.current.company_headquarters_country` | | `current_employers.business_email_verified` | `experience.employment_details.current.business_email_verified` | | `current_employers.years_at_company_raw` | `experience.employment_details.current.years_at_company_raw` | ### Employment — past roles The legacy `past_employers.*` array maps to `experience.employment_details.past.*` with the same suffix conventions as the current role. The legacy `all_employers.*` (current and past combined) maps to `experience.employment_details.*` without the `current`/`past` segment. ### Education, certifications, honors | Legacy field | Current field | | ------------------------------------- | ------------------------------------- | | `education_background.institute_name` | `education.schools.school` | | `education_background.degree_name` | `education.schools.degree` | | `education_background.field_of_study` | `education.schools.field_of_study` | | `education_background.institute_linkedin_id` | `education.schools.professional_network_id` | | `education_background.institute_logo_url` | `education.schools.institute_logo_permalink` | | `certifications.name` | `certifications.name` | | `certifications.issued_date` | `certifications.issue_date` | | `certifications.expiration_date` | `certifications.expiration_date` | | `certifications.issuer_organization` | `certifications.issuing_organization` | | `honors.title` | `honors.title` | ### Contact and identity flags | Legacy field | Current field | | ------------------------- | ------------------------------------------------------------------------------------------------------ | | `emails` | (not filterable; retrieve with [Contact Enrich](/person-docs/contact/enrich)) | | `business_emails` | (not filterable on search; use `experience.employment_details.business_email_verified` boolean filter) | | `person_id` | `crustdata_person_id` | | `recently_changed_jobs` | `recently_changed_jobs` | | `years_of_experience_raw` | `years_of_experience_raw` | For the full searchable-field catalog, see [Person Search reference](/person-docs/search/reference#searchable-fields). --- ## 4. Response shape The response envelope keeps the same top-level keys, but each profile object moved from a flat structure to a nested one. ### Envelope | Key | Legacy | Current | | ------------- | --------------------------------- | ----------------- | | `profiles` | array of profiles | array of profiles | | `next_cursor` | string \| null | string \| null | | `total_count` | integer \| null | integer \| null | | `total_count_relation` | (not present) | string \| null — qualifies `total_count` (for example, exact vs. estimate); often `null` | | `query` | present when `return_query: true` | (not returned) | ### Profile object Legacy profiles returned a flat object with keys like `name`, `linkedin_profile_url`, `current_employers`, `education_background`. Current profiles group those fields into named sections: <CodeGroup> ```json Legacy profile (truncated) { "person_id": 14540, "name": "David Hsu", "headline": "Founder, CEO @ Retool", "region": "San Francisco Bay Area", "linkedin_profile_url": "https://www.linkedin.com/in/dvdhsu", "current_employers": [{ "name": "Retool", "title": "Founder, CEO" }], "education_background": [{ "institute_name": "Oxford" }] } ``` ```json Current profile (truncated) { "crustdata_person_id": 14540, "basic_profile": { "name": "David Hsu", "headline": "Founder, CEO @ Retool", "current_title": "Founder, CEO", "normalized_title": { "matched_title": "Co-Founder, CEO, CTO", "department": "Executive Leadership", "sub_department": "Founder & Entrepreneurship Leadership", "similarity": 0.6012, "confident": true }, "location": { "raw": "San Francisco Bay Area", "city": "San Francisco", "state": "California", "country": "United States", "continent": "North America" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO", "company_profile_picture_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/company/72f60d0ccad488216922fb784abc89890b49eeed8ab1eca1a0a12c72a68a0620.jpg" } ] } }, "education": { "schools": [ { "school": "University of Oxford", "degree": "Bachelor of Arts (B.A.)", "description": "", "location": { "raw": "Oxford, England", "city": "Oxford", "state": "England", "country": "United Kingdom", "continent": null }, "institute_logo_permalink": "https://crustdata-media.s3.us-east-2.amazonaws.com/company/3d2093a16f7cf7b459a0d30d4e795d0be5a41a3396f21dee899e43e8a8b6de8d.jpg" } ] } } ``` </CodeGroup> ### Response-key map | Legacy key | Current key | | ------------------------------------------------------ | ------------------------------------------------------------ | | `person_id` | `crustdata_person_id` | | `name` | `basic_profile.name` | | `headline` | `basic_profile.headline` | | `summary` | (not returned in search; filterable only — fetch via [Person Enrich](/person-docs/enrichment/introduction)) | | `region` | `basic_profile.location.raw` | | `location_city` / `_state` / `_country` / `_continent` | `basic_profile.location.{city,state,country,continent}` | | `flagship_profile_url` | `social_handles.professional_network_identifier.profile_url` | | `linkedin_profile_url` | `social_handles.professional_network_identifier.urn_url` (requires field-level permission on your key) | | `profile_picture_permalink` | `basic_profile.profile_picture_permalink` | | `twitter_handle` | `social_handles.twitter_identifier.slug` (filter on `social_handles.twitter_handle`) | | `num_of_followers` | `professional_network.followers` (filter and sort only; not returned) | | `open_to_cards` | `professional_network.open_to_cards` (filter only; not returned) | | `skills` | (not returned in search; filterable only — fetch via [Person Enrich](/person-docs/enrichment/introduction)) | | `num_of_connections` | `professional_network.connections` (filter and sort only; not returned) | | `current_employers[]` | `experience.employment_details.current[]` | | `past_employers[]` | `experience.employment_details.past[]` | | `education_background[]` | `education.schools[]` | | `certifications[]` | (not returned in search; filterable only — fetch via [Person Enrich](/person-docs/enrichment/introduction)) | | `honors[]` | (not returned in search; filterable only — fetch via [Person Enrich](/person-docs/enrichment/introduction)) | <Note> **Some fields are filterable but not returned.** `basic_profile.summary`, `certifications.*`, `honors.title`, and `skills.professional_network_skills` can be used in `filters` but are not included in the search response. Retrieve their values with [Person Enrich](/person-docs/enrichment/introduction). The whole `professional_network` section behaves this way. `professional_network.followers`, `.connections`, and `.open_to_cards` all filter, and the first two also sort, but the section returns only `last_updated`. Naming any of the three in `fields` returns `400 Invalid fields`. Two fields are filtered under one name and returned under another. Filter on `social_handles.twitter_handle` and read the value back at `social_handles.twitter_identifier.slug`. Filter on `experience.employment_details.current.company_id` and read it back at `experience.employment_details.current.crustdata_company_id`. </Note> <Tip> Use the `fields` request parameter to request just the sections you need (for example, `fields: ["basic_profile.name", "experience.employment_details.current.title"]`). This keeps responses small and avoids parsing sections you do not use. See [Response fields](/person-docs/search/reference#response-fields). </Tip> --- ## 5. Removed features ### Semantic search (`search` parameter) The legacy endpoint accepted a top-level `search` object that ran a natural-language query against an embedding model: ```json { "search": { "query": "engineers who like climbing", "mode": "hybrid", "min_similarity": 0.2 } } ``` The current endpoint does not accept `search`. Sending it returns `400`. Use structured filters in `filters` instead, or contact [support@crustdata.co](mailto:support@crustdata.co) if you need semantic retrieval for an account. ### `single_query` debug flag Removed. The flag had no externally documented behavior. --- ## 6. Added features | Feature | Description | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fields` parameter | Request only the dot-paths you need (e.g. `["basic_profile.name", "experience.employment_details.current.title"]`). | | Nested response sections | Profiles return structured `basic_profile`, `experience`, `education`, `contact`, `social_handles`, `professional_network` sections. | | Stable error envelope | All `4xx`/`5xx` errors return `{ "error": { "type", "message", "metadata" } }` (legacy returned `{ "error": "...", "details": {...} }`). | | `metadata.updated_at` filter | New sortable, filterable timestamp marking when the profile record was last refreshed. | | Contact availability flag | `contact.has_business_email` reports whether a verified business email is on file, in the search response. | | Discoverable filter values | [Person Autocomplete](/person-docs/autocomplete/introduction) returns valid indexed values for any supported field. | | Normalized title | `basic_profile.normalized_title` returns `matched_title`, `department`, `sub_department`, `similarity`, and `confident`. Filter on `basic_profile.normalized_title.matched_title` / `.department` / `.sub_department`. | | Education location | `education.schools[]` now include `location` (`raw`, `city`, `state`, `country`, `continent`) and `description`. Filter on `education.schools.location.*`. The legacy `institute_logo_url` is now the Crustdata-hosted `education.schools.institute_logo_permalink`, which is a rename rather than a new field. | | Company logo permalink | `experience.employment_details[].company_profile_picture_permalink` returns a stable Crustdata-hosted company logo URL for rendering in your own interface. | | Exclusion operators | `(!)` (fuzzy negation — exclude profiles whose value contains a substring) and `geo_exclude` (exclude profiles inside a geographic radius) let you filter profiles out, not just in. | --- ## 7. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Unsupported column 'job_title'", "details": { "supported_columns": ["name", "headline", "..."] } } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Unsupported filter field: 'job_title'. Supported fields: basic_profile.name, basic_profile.headline, ...", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 8. End-to-end example The same query — _Co-Founders in San Francisco_ — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url https://api.crustdata.com/screener/persondb/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "filters": { "op": "and", "conditions": [ { "column": "current_employers.title", "type": "(.)", "value": "Co-Founder" }, { "column": "region", "type": "(.)", "value": "San Francisco" } ] }, "limit": 2 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/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": "experience.employment_details.current.title", "type": "(.)", "value": "Co-Founder" }, { "field": "basic_profile.location.full_location", "type": "(.)", "value": "San Francisco" } ] }, "limit": 2 }' ``` </CodeGroup> --- ## Migration checklist - [ ] Replace base path `/screener/persondb/search` → `/person/search`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename `column` → `field` in every filter condition. - [ ] Rename `column` → `field` in every sort directive. - [ ] Map legacy field names to the current dataset paths (see [Field-name mapping](#3-field-name-mapping)). - [ ] Drop the `search` parameter (semantic mode is no longer accepted). - [ ] Drop the `single_query` debug flag. - [ ] Update response parsing for the nested profile shape (see [Response-key map](#response-key-map)). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] Send only one of `count` or `limit` — sending both returns `400`. - [ ] If you read `total_count`, also handle the new `total_count_relation` field. - [ ] Re-fetch fields that are filterable but not returned in search (`summary`, `certifications.*`, `honors.title`, `social_handles.twitter_handle`, `skills`) via Person Enrich. --- ## See also - [Person Search](/person-docs/search/introduction) — entry point for the new endpoint. - [Person Search reference](/person-docs/search/reference) — full operator, field, and response catalog. - [Person Autocomplete](/person-docs/autocomplete/introduction) — discover valid filter values. - [Person Enrich](/person-docs/enrichment/introduction) — fetch a full profile by URL. # Person enrich Source: https://docs.crustdata.com/migrations/person/person-enrich Reference for migrating from the legacy /screener/person/enrich endpoint (cached path) to the new /person/enrich endpoint. <Columns cols={1}> <Column> <Danger>GET /screener/person/enrich</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /person/enrich</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy person-enrich endpoint (called without `enrich_realtime=true`, i.e. the cached/indexed path) and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ----------------------------- | -------------------------------------- | | Path | `GET /screener/person/enrich` | `POST /person/enrich` | | Method | `GET` with query parameters | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Three header / shape changes are required.** The HTTP method moved from `GET` to `POST`, the authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. </Warning> <Note> This guide covers the **indexed (cached) path** of the legacy endpoint — requests sent without `enrich_realtime=true`. The current `/person/enrich` endpoint serves the indexed dataset only; on-demand live retrieval is not part of this endpoint. See [Removed features](#5-removed-features) for what happened to `enrich_realtime`. </Note> <Info> The legacy endpoint also returned contact data. Migrate that part of your integration to [Contact Enrich](/person-docs/contact/enrich). The current `/person/enrich` documentation covers profile data only. </Info> --- ## 1. Request — top-level keys Identifiers moved from comma-separated query parameters to JSON arrays in the request body. Submit exactly one identifier type per request (up to 25 values). | Legacy query parameter | Current body key | Notes | | ----------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `?linkedin_profile_url=https://...,https://...` | `{ "professional_network_profile_urls": ["https://...", "..."] }` | Key renamed. Comma-separated string → JSON array. | | `?business_email=jane@example.com` | `POST /person/contact/enrich` | Move email-based contact workflows to [Contact Enrich](/person-docs/contact/enrich). | | `?github_profile_url=https://github.com/...` | (removed from this endpoint) | Dev-platform identifiers moved to a dedicated endpoint — see [Removed features](#5-removed-features). | | `?personal_email=jane@gmail.com` | `POST /batch/person/identify` with `{ "business_emails": ["jane@gmail.com"] }` | Moved to the batch identify endpoint, whose reverse-lookup waterfall resolves personal addresses too — see [Removed features](#5-removed-features). | | `?fields=name,headline,summary` | `{ "fields": ["basic_profile.name", "basic_profile.headline"] }` | Comma-separated string → JSON array. Selects which sections are returned (see note below). Section names changed — see [Field-name mapping](#3-field-name-mapping). | | `?min_similarity_score=0.8` | See [Contact Enrich](/person-docs/contact/enrich) | This parameter belongs with email-based workflows. | | `?preview=true` | `{ "preview": true }` | Moved into the body. Same semantics — returns basic-profile fields only and charges 0 credits. | | `?enrich_realtime=true` | (removed) | No direct replacement on this endpoint — see [Removed features](#5-removed-features). | | `?force_fetch=true` | (removed) | Tied to `enrich_realtime`; removed alongside it. | | `?open_to_cards=true` | (removed) | Tied to `enrich_realtime`; removed alongside it. | | `?colleagues=true` | (removed) | Colleague expansion is no longer part of person enrich. | | `?enrichment_method=salesnavigator\|voyager` | (removed) | Internal routing flag — no replacement. | <Note> Submit up to 25 values in `professional_network_profile_urls`. </Note> <Warning> **Request the sections you need via `fields`.** When `fields` is omitted, each match returns only `basic_profile` and `social_handles`. List `professional_network`, `experience`, `education`, `skills`, `certifications`, `honors`, or `dev_platform_profiles` explicitly to include them. </Warning> --- ## 2. Identifier behavior | Topic | Legacy | Current | | ---------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- | | Identifier shape | Comma-separated string in a query parameter | JSON array in the body | | Identifier types | Professional-network URL, business email, dev-platform URL | Professional-network URL | | Max values per request | 25 | 25 | | Mutual exclusion | Pass one identifier parameter | Pass one identifier array | | Business email | Single value (`business_email=jane@…`) | Move to [Contact Enrich](/person-docs/contact/enrich) | | Mixed-identifier error | `400` with a free-form `error` string | `400` with `{ error: { type: "invalid_request", message: "..." } }` | | Over-limit error | `400` with a free-form `error` string | `400` with the new error envelope | --- ## 3. Field-name mapping The legacy response was a flat object with top-level fields like `linkedin_profile_url`, `name`, `headline`, `current_employers`, and `github_profiles`. The current response groups fields under `basic_profile`, `professional_network`, `social_handles`, `experience`, `education`, `skills`, `certifications`, `honors`, and `dev_platform_profiles`. ### Identity and profile | Legacy field | Current field | | -------------------------------------- | ------------------------------------------------------------------------------------------------- | | `person_id` | `crustdata_person_id` (top-level) | | `name` | `basic_profile.name` | | `title` | `basic_profile.current_title` | | `headline` | `basic_profile.headline` | | `summary` | `basic_profile.summary` | | `location` (string) | `basic_profile.location.raw` (plus parsed `city`, `state`, `country`, `continent`) | | `languages[]` | `basic_profile.languages[]` | | `profile_picture_url` | `professional_network.profile_picture_url` | | `profile_picture_permalink` | `basic_profile.profile_picture_permalink` / `professional_network.profile_picture_permalink` | | `last_updated` | `basic_profile.last_updated` (plus top-level `updated_at`) | | `enriched_realtime` | (removed — this endpoint serves indexed data only) | | `query_linkedin_profile_urn_or_slug[]` | (removed; the input URL is echoed back as `matched_on` — see [Response shape](#7-response-shape)) | ### Professional-network attributes | Legacy field | Current field | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `linkedin_profile_url` | `social_handles.professional_network_identifier.profile_url` | | `linkedin_flagship_url` | `social_handles.professional_network_identifier.profile_url` (canonical URL only — flagship variant is no longer surfaced separately) | | `num_of_connections` | `professional_network.connections` | | `num_of_followers` | `professional_network.followers` | | `linkedin_joined_date` | `professional_network.joined_date` | | `linkedin_verifications[]` | `professional_network.verifications[]` | | `linkedin_open_to_cards[]` | `professional_network.open_to_cards[]` (only populated for opted-in profiles) | ### Social handles | Legacy field | Current field | | ----------------------- | ------------------------------------------------------------------------------------------------------------- | | `linkedin_flagship_url` | `social_handles.professional_network_identifier.profile_url` | | `linkedin_profile_url` | `social_handles.professional_network_identifier.urn_url` (permanent URN-based URL; field-gated by default — ask support to enable it for your account) | | `twitter_handle` | `social_handles.twitter_identifier.slug` | | `github_profiles[].url` | `social_handles.dev_platform_identifier.profile_url` (primary handle); full list in `dev_platform_profiles[]` | ### Skills | Legacy field | Current field | | ------------ | -------------------------------------- | | `skills[]` | `skills.professional_network_skills[]` | ### Experience and employers The legacy response carried four parallel arrays (`current_employers`, `past_employers`, `all_employers`, `all_employers_company_id`, `all_titles`). The current response consolidates these under `experience.employment_details`, with one entry per employment record. | Legacy field | Current field | | --------------------------------- | ----------------------------------------------------------------------------------- | | `current_employers[]` | `experience.employment_details.current[]` | | `past_employers[]` | `experience.employment_details.past[]` | | `all_employers[]` (company names) | (derive from `experience.employment_details.{current,past}[].name`) | | `all_employers_company_id[]` | (derive from `experience.employment_details.{current,past}[].crustdata_company_id`) | | `all_titles[]` | (derive from `experience.employment_details.{current,past}[].title`) | ### Education | Legacy field | Current field | | ------------------------ | ----------------------------------------------- | | `education_background[]` | `education.schools[]` | | `all_schools[]` | (derive from `education.schools[].school`) | | `all_degrees[]` | (derive from `education.schools[].degree`) | ### Contact data The legacy endpoint returned email addresses inline. Contact data now has a dedicated endpoint. Move these fields to [Contact Enrich](/person-docs/contact/enrich), which documents the current request, response, and pricing. ### Dev-platform profiles | Legacy field | Current field | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `github_profiles[]` | `dev_platform_profiles[]` — neutral name; same one-entry-per-profile shape. To enrich a person by a dev-platform URL only, use the dedicated dev-platform endpoint. | For the full enrich-response field catalog, see [Person Enrich reference](/person-docs/enrichment/reference). --- ## 4. Type changes | Field | Legacy type | Current type | Notes | | ------------------------------ | ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email[]` | array of strings | Moved | Use [Contact Enrich](/person-docs/contact/enrich) and update your parser to its response shape. | | `location` | string | object | The raw string is preserved at `basic_profile.location.raw`; parsed `city`, `state`, `country`, `continent` are also returned. | | `business_email` request | single string | Moved | Move email-based contact workflows to [Contact Enrich](/person-docs/contact/enrich). | | `linkedin_profile_url` request | comma-separated string | array of strings | Renamed to `professional_network_profile_urls` and now a JSON array. | --- ## 5. Removed features ### `enrich_realtime`, `force_fetch`, `open_to_cards` The legacy endpoint used `enrich_realtime=true` to fall back to real-time retrieval from the web when a person was not in the indexed dataset, with `force_fetch=true` bypassing the cache and `open_to_cards=true` pulling additional opt-in fields. These flags have been removed from `/person/enrich` — the current endpoint serves the indexed dataset only. If a person is not present, the request returns an entry with empty `matches: []` rather than attempting a real-time fetch. If you previously called the legacy endpoint with `enrich_realtime=true`, use the dedicated live-enrich endpoint instead — see [Person Live Enrich](/person-docs/enrichment/live-enrich). It performs real-time retrieval and supports the same identifier shape as `/person/enrich`. ### `github_profile_url` identifier The legacy endpoint accepted a comma-separated list of dev-platform profile URLs as an identifier. This input mode has been removed from `/person/enrich`. Dev-platform profiles are still returned in the **response** under `dev_platform_profiles[]` when present, but a separate endpoint is used to enrich a person starting from a dev-platform URL — see [Person Dev-Platform reference](/person-docs/dev_platform/introduction). ### `personal_email` identifier The legacy endpoint accepted `personal_email` and resolved it to a person via reverse lookup. On the current API this moved to batch: - **Use [`POST /batch/person/identify`](/person-docs/contact/identify)** — its email resolution runs a reverse-lookup waterfall that resolves personal addresses (for example Gmail) as well as business ones. Submit the address in `business_emails`; the field name reflects the primary use case, but the lookup is not limited to business addresses. - The synchronous `/person/contact/enrich` endpoint resolves emails against business addresses only — a personal email returns `matches: []` there even when the address is present in a profile's contact data. ### `colleagues` The legacy `colleagues=true` flag expanded the response with a list of the target person's colleagues. This feature has been removed; build a colleague list explicitly by calling [Person Search](/person-docs/search/introduction) with the appropriate company filter. ### `enrichment_method` The legacy `enrichment_method` parameter (`salesnavigator` / `voyager`) routed requests to specific upstream collection paths. This parameter has been removed — routing is handled internally. ### `enriched_realtime` response flag The legacy `enriched_realtime` boolean indicated whether a record was served from cache or fetched live. Because `/person/enrich` serves the indexed dataset only, this flag has been removed from responses. ### Comma-separated query parameter style The endpoint moved from `GET` with query parameters to `POST` with a JSON body. All identifiers must be sent as JSON arrays. --- ## 6. Added features | Feature | Description | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Match-result envelope | The response wraps each result in `{ matched_on, match_type, matches: [{ confidence_score, person_data }] }`, making batch enrichment unambiguous. | | `confidence_score` | Numeric score on each match candidate. Higher is better; `1.0` is common for direct identifier lookups. | | Batch profile-URL lookups | `professional_network_profile_urls` is an array (up to 25). | | Parsed `location` object | Returns `city`, `state`, `country`, `continent`, and `raw` rather than just the unparsed string. | | Stable error envelope | All `4xx` / `5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | --- ## 7. Response shape The envelope shape changed. The legacy endpoint returned a top-level array of person records. The current endpoint returns a top-level array of match-result envelopes, each containing one or more `person_data` candidates. ### Envelope <CodeGroup> ```json Legacy [ { "person_id": 14540, "name": "David Hsu", "title": "Founder, CEO", "linkedin_profile_url": "https://www.linkedin.com/in/dvdhsu/", "location": "San Francisco, California, United States", "num_of_connections": 4321, "skills": ["Python", "React"], "email": ["david@example.com"], "current_employers": [ { "employer_name": "Retool", "title": "Founder, CEO" } ], "enriched_realtime": false } ] ``` ```json Current [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "crustdata_person_id": 14540, "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO", "location": { "city": "San Francisco", "state": "California", "country": "United States", "raw": "San Francisco, California, United States" } }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu/" } }, "professional_network": { "connections": 4321 }, "skills": { "professional_network_skills": ["Python", "React"] }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO" } ] } } } } ] } ] ``` </CodeGroup> ### Match-result fields | Field | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------- | | `matched_on` | The profile URL you submitted. | | `match_type` | `professional_network_profile_url`. | | `matches` | Array of candidate matches. Empty when nothing matched. | | `matches[].confidence_score` | Number. Higher is better. `1.0` is common for direct identifier lookups. | | `matches[].person_data` | Full enriched person profile — see [Field-name mapping](#3-field-name-mapping) for section paths. | ### No-match behavior Each entry corresponds to one input identifier. When nothing matches, the entry's `matches` array is empty: ```json [ { "matched_on": "https://www.linkedin.com/in/no-such-profile/", "match_type": "professional_network_profile_url", "matches": [] } ] ``` <Note> The legacy endpoint returned `404` when no profiles matched. The current endpoint returns `200` with empty `matches: []` for unmatched identifiers. The OpenAPI contract still defines `404` for completeness — handle both. </Note> ### Legacy `error_code` values: not-found vs still-enriching The legacy `404` body carries an `error_code` that distinguishes "no data yet, enrichment queued" from terminal failures — use it while you are still on the legacy endpoint: | `error_code` | Meaning | What to do | | ------------ | ------------------------------------------------------------------------ | -------------------------------------------- | | `PE03` | Not in the dataset yet — *"Data will be enriched shortly."* Enrichment is queued. | Poll again later. | | `PE01` | Profile is unavailable (private, deleted, or requires authentication). | Terminal — do not retry. | | `PE02` | Internal error while processing the profile. | Retry with backoff. | | `PE04` | Profile data could not be parsed. | Retry later; contact support if it persists. | The legacy body also includes `last_tried_linkedin_enrichment_date` and `did_last_linkedin_enrichment_succeed` — `null` values mean no enrichment attempt has completed yet (typically alongside `PE03`). <Note> The current endpoint has no queued-enrichment state: `POST /person/enrich` serves the dataset synchronously and represents no-match as `matches: []`. For on-demand retrieval of a missing profile, use [Person Live Enrich](/person-docs/enrichment/live-enrich). </Note> --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Please provide exactly one of linkedin_profile_url, business_email, or github_profile_url", "error_code": "INVALID_INPUT" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Exactly one identifier type must be provided.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 9. End-to-end example Enriching one person by professional-network URL — written against both endpoints. <CodeGroup> ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/person/enrich?linkedin_profile_url=https://www.linkedin.com/in/dvdhsu/&fields=name,headline,current_employers' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "experience"] }' ``` </CodeGroup> If your legacy integration used business emails or returned contact data, migrate that flow to [Contact Enrich](/person-docs/contact/enrich). --- ## Migration checklist - [ ] Switch from `GET /screener/person/enrich` (query params) to `POST /person/enrich` (JSON body). - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename `linkedin_profile_url` to `professional_network_profile_urls`. - [ ] Convert comma-separated profile URLs to a JSON array. - [ ] Move business-email and contact-data workflows to [Contact Enrich](/person-docs/contact/enrich). - [ ] Drop `enrich_realtime=true`, `force_fetch=true`, and `open_to_cards=true` — the new endpoint serves indexed data only. For real-time retrieval, use [Person Live Enrich](/person-docs/enrichment/live-enrich) instead. - [ ] Drop `colleagues=true` — build colleague lists with [Person Search](/person-docs/search/introduction) instead. - [ ] Drop `enrichment_method` — routing is handled internally. - [ ] Drop `github_profile_url` as an identifier — see [Person Dev-Platform reference](/person-docs/dev_platform/introduction) for dev-platform-rooted enrichment. - [ ] Convert `fields` from a comma-separated string to a JSON array, and migrate field names to the new section-prefixed paths (see [Field-name mapping](#3-field-name-mapping)). Only `basic_profile` and `social_handles` return by default — request other sections explicitly. - [ ] Unwrap each result from the new `{ matched_on, match_type, matches: [{ confidence_score, person_data }] }` envelope. - [ ] Update parsers for the nested `person_data` shape: `name` → `basic_profile.name`, `title` → `basic_profile.current_title`, `headline` → `basic_profile.headline`, `summary` → `basic_profile.summary`. - [ ] Update `location` parsing — it is now an object with `raw`, `city`, `state`, `country`, `continent` rather than a single string. - [ ] Map `current_employers[]` / `past_employers[]` to `experience.employment_details.current[]` / `experience.employment_details.past[]`. Derive `all_employers`, `all_titles`, `all_employers_company_id` from these arrays if you still need them. - [ ] Map `education_background[]` to `education.schools[]`. - [ ] Map `linkedin_profile_url` (response) to `social_handles.professional_network_identifier.profile_url`, `twitter_handle` to `social_handles.twitter_identifier.slug`. - [ ] Map `num_of_connections` to `professional_network.connections`, `skills[]` to `skills.professional_network_skills[]`. - [ ] Map `github_profiles[]` to `dev_platform_profiles[]`. - [ ] Map `person_id` to `crustdata_person_id`. - [ ] Handle no-match as `matches: []` (legacy returned `404`). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] Drop any parsing of `enriched_realtime` from the response — the field no longer exists. --- ## See also - [Person Enrich](/person-docs/enrichment/introduction) — entry point for the new endpoint. - [Person Enrich reference](/person-docs/enrichment/reference) — full request, response, and field catalog. - [Contact Enrich](/person-docs/contact/enrich) — migrate legacy email and phone fields here. - [Person Live Enrich](/person-docs/enrichment/live-enrich) — real-time retrieval (replaces `enrich_realtime=true`). - [Person Search](/person-docs/search/introduction) — for queries that return multiple profiles (replaces `colleagues=true`). - [Person-search migration](/migrations/person/persondb-search) — equivalent guide for the indexed person-search endpoint. # Live person search Source: https://docs.crustdata.com/migrations/person/person-search-live Reference for migrating from the legacy /screener/person/search endpoint to the new /person/professional_network/search/live endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/person/search</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /person/professional_network/search/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy live-person-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------ | ----------------------------------------------- | | Path | `POST /screener/person/search` | `POST /person/professional_network/search/live` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Two header changes are required.** The authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing either header are rejected. </Warning> --- ## 1. Request body — top-level keys | Key | Legacy | Current | Notes | | ----------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Saved search URL | `linkedin_sales_navigator_search_url` (string) | `professional_network_search_url` (string) | Key renamed. Value semantics unchanged — pass the full saved search URL. | | `filters` | array of filter objects | array of filter objects | Filter object grammar changed — see [Filter grammar](#2-filter-grammar). | | `page` | integer (optional) | integer (optional) | Use `page` for a paged synchronous search (max `limit` 25 per page). With `filters` you must send at least one of `page`, `limit`, or `preview`. | | `limit` | integer | integer | Per-page cap of `25` on synchronous calls with `page`. A `limit` above `2000` with no `page` runs asynchronously (as does `background_job: true`), up to `10000`. | | `background_job` | (not exposed directly — implied when `page` was omitted) | boolean | An explicit flag to force an asynchronous job; the response returns a `job_id` to poll. Async also starts implicitly when `limit` exceeds `2000` with no `page`. | | `job_id` | UUID (string) | UUID (string) | Same shape. Submit the `job_id` from a prior asynchronous response to retrieve results. | | `post_processing` | `{ exclude_profiles, exclude_names, strict_title_and_company_match }` | `{ exclude_profiles, exclude_names, strict_title_and_company_match }` | Same shape. `exclude_profiles` still accepts professional-network profile URLs. | | `preview` | boolean | boolean | Same semantics — lightweight basic-profile fields only. Cannot be combined with `page`, `limit`, `background_job`, or `post_processing`. | | `keyword` | string | (removed) | Use the `KEYWORD_TITLE`, `KEYWORD_COMPANY`, `KEYWORD_FIRST_NAME`, `KEYWORD_LAST_NAME`, or `KEYWORD_SCHOOL` filter fields instead. | <Note> **Three ways to call this endpoint:** 1. Pass `professional_network_search_url` to replay a saved search URL. 2. Pass `filters` (+ `page` for synchronous, or `background_job: true` for asynchronous). 3. Pass `job_id` to fetch results from a previously submitted background job. </Note> --- ## 2. Filter grammar The filter object key changed from `filter_type` to `field`. Operator names and the overall array shape are unchanged. <CodeGroup> ```json Legacy filter { "filter_type": "CURRENT_TITLE", "type": "in", "value": ["CEO", "Founder"] } ``` ```json Current filter { "field": "CURRENT_TITLE", "type": "in", "value": ["CEO", "Founder"] } ``` </CodeGroup> <Note> **One change per filter:** rename `filter_type` → `field`. Filter-field enum values (`CURRENT_TITLE`, `INDUSTRY`, `COMPANY_HEADCOUNT`, ...) and operator values (`in`, `not in`) are unchanged. </Note> ### Supported filter fields The set of supported live-search filter fields is unchanged from the legacy endpoint: | Field | Description | Operators | | --------------------------- | ----------------------------------------------- | -------------- | | `CURRENT_COMPANY` | Match the person's current employer. | `in`, `not in` | | `PAST_COMPANY` | Match any prior employer. | `in`, `not in` | | `CURRENT_TITLE` | Match the person's current title. | `in`, `not in` | | `PAST_TITLE` | Match any prior title. | `in`, `not in` | | `SCHOOL` | Match an educational institution attended. | `in`, `not in` | | `COMPANY_HEADQUARTERS` | Match the current employer's HQ location. | `in`, `not in` | | `COMPANY_HEADCOUNT` | Match the current employer's headcount bracket. | `in`, `not in` | | `COMPANY_TYPE` | Match the current employer's organization type. | `in`, `not in` | | `FUNCTION` | Match the person's professional function. | `in`, `not in` | | `INDUSTRY` | Match the current employer's industry. | `in`, `not in` | | `NUM_OF_FOLLOWERS` | Match the person's follower-count bracket. | `in`, `not in` | | `REGION` | Match the person's region or geography. | `in`, `not in` | | `SENIORITY_LEVEL` | Match the person's seniority level. | `in`, `not in` | | `YEARS_AT_CURRENT_COMPANY` | Match the tenure-at-employer bracket. | `in`, `not in` | | `YEARS_IN_CURRENT_POSITION` | Match the tenure-in-role bracket. | `in`, `not in` | | `YEARS_OF_EXPERIENCE` | Match the total-experience bracket. | `in`, `not in` | | `FOLLOWER_OF` | Match followers of a given profile. | `in`, `not in` | | `CONNECTION_OF` | Match connections of a given profile. | `in`, `not in` | | `KEYWORD_COMPANY` | Keyword match against employer names. | `in`, `not in` | | `KEYWORD_FIRST_NAME` | Keyword match against first names. | `in`, `not in` | | `KEYWORD_LAST_NAME` | Keyword match against last names. | `in`, `not in` | | `KEYWORD_SCHOOL` | Keyword match against schools. | `in`, `not in` | | `KEYWORD_TITLE` | Keyword match against titles. | `in`, `not in` | | `NETWORK_DEGREE` | Match by network-degree relative to the caller. | `in`, `not in` | | `PROFILE_LANGUAGE` | Match the profile language. | `in`, `not in` | | `SERVICE_CATEGORY` | Match service-category tags. | `in`, `not in` | | `CONTACT_INTEREST` | Match contact-interest categories. | `in`, `not in` | For value formats and the autocomplete-discovery flow, see [Live Person Search](/person-docs/search/live-search). --- ## 3. Identifier / search modes The endpoint accepts the same three modes as before. The difference is that `background_job` is now exposed as an explicit boolean; async also still starts implicitly for large filter requests (`limit` above `2000` with no `page`). | Mode | Legacy | Current | | -------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | | Replay a saved search URL | `linkedin_sales_navigator_search_url` (+ optional `limit`) | `professional_network_search_url` (+ optional `limit`) | | Synchronous search with filters | `filters` + `page` | `filters` + `page` | | Asynchronous search with filters | `filters` (`page` omitted; up to `limit: 2000`) | `filters` + `background_job: true`, or `limit` above `2000` with no `page` (up to `limit: 10000`) | | Poll a prior async job | `job_id` | `job_id` | <Warning> **`background_job` is now explicit — but implicit async still works.** Set `background_job: true` to enqueue an async job directly. A filter-based request also still goes async automatically when `limit` exceeds `2000` with no `page`. With `filters`, send at least one of `page`, `limit`, or `preview`; omitting all three returns `400`. </Warning> --- ## 4. Removed features ### `keyword` top-level parameter The legacy `keyword` string at the top level has been removed. Use one of the `KEYWORD_*` filter fields instead — `KEYWORD_TITLE`, `KEYWORD_COMPANY`, `KEYWORD_FIRST_NAME`, `KEYWORD_LAST_NAME`, or `KEYWORD_SCHOOL`. ### `linkedin_sales_navigator_search_url` The saved-search URL field was renamed to `professional_network_search_url`. The URL value itself (a public saved-search URL) is unchanged. --- ## 5. Added features | Feature | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Explicit `background_job` | Submit an async job by setting `background_job: true`. Up to `limit: 10000` per job. | | Stable error envelope | All `4xx`/`5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | | Structured profile shape | Each result is a nested `basic_profile`, `professional_network`, `social_handles`, `contact`, `skills`, `experience`, `education` object — matching the indexed-search and enrich endpoints. | --- ## 6. Response shape The envelope shape is the same — `{ profiles, total_display_count }` — but the per-profile structure changed. The legacy response returned profiles as flat objects with `linkedin_*`, `current_employers`, and similar top-level fields. The current response groups profile fields under nested sections (`basic_profile`, `professional_network`, `social_handles`, `contact`, `skills`, `experience`, `education`), matching the indexed [Person Search](/person-docs/search/introduction) and [Person Enrich](/person-docs/enrichment/introduction) endpoints. ### Synchronous response <CodeGroup> ```json Legacy { "profiles": [ { "linkedin_profile_url": "https://www.linkedin.com/in/janesmith", "name": "Jane Smith", "headline": "CEO at TechStartup", "title": "CEO", "current_employers": [ { "employer_name": "TechStartup", "title": "CEO" } ] } ], "total_display_count": "2,500" } ``` ```json Current { "profiles": [ { "basic_profile": { "name": "Jane Smith", "headline": "CEO at TechStartup", "current_title": "CEO" }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/janesmith" } }, "experience": { "employment_details": { "current": [{ "title": "CEO", "name": "TechStartup" }] } } } ], "total_display_count": "2,500" } ``` </CodeGroup> ### Asynchronous (background job) response When `background_job: true` is set (or you submit a saved-search URL with `limit` for the async path), the response carries a `job_id` and a status message: ```json { "job_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Your request is being processed. Please use the job_id to retrieve the results." } ``` Poll for results by sending the same endpoint with `{ "job_id": "..." }`. Once the job is complete, the endpoint returns the standard `{ profiles, total_display_count }` envelope. ### Profile-object fields | Section | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | | `basic_profile` | `name`, `headline`, `summary`, `current_title`, `languages`. | | `professional_network` | `connections`, `profile_picture_url`, `pronoun`, `location.raw`. | | `social_handles` | `professional_network_identifier.profile_url`, `professional_network_identifier.urn`, `twitter_identifier.slug`. | | `contact` | `business_emails[]`, `websites[]`. | | `skills` | `professional_network_skills[]`. | | `experience` | `employment_details.current[]` and `employment_details.past[]`, each with `title`, `name`, dates, etc. | | `education` | `schools[]` with `school`, `degree`, dates. | | `updated_at` | Timestamp marking when the profile was last fetched. | For the full field catalog, see [Live Person Search](/person-docs/search/live-search). --- ## 7. Field-name mapping The flat legacy profile fields have been reorganized into nested sections: | Legacy field | Current field | | ------------------------ | ------------------------------------------------------------ | | `name` | `basic_profile.name` | | `title` | `basic_profile.current_title` | | `headline` | `basic_profile.headline` | | `summary` | `basic_profile.summary` | | `languages[]` | `basic_profile.languages[]` | | `location` | `professional_network.location.raw` | | `num_of_connections` | `professional_network.connections` | | `profile_picture_url` | `professional_network.profile_picture_url` | | `linkedin_profile_url` | `social_handles.professional_network_identifier.profile_url` | | `linkedin_urn` | `social_handles.professional_network_identifier.urn` | | `twitter_handle` | `social_handles.twitter_identifier.slug` | | `skills[]` | `skills.professional_network_skills[]` | | `current_employers[]` | `experience.employment_details.current[]` | | `past_employers[]` | `experience.employment_details.past[]` | | `education_background[]` | `education.schools[]` | | `email[]` | `contact.business_emails[].email` | --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `402`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Invalid filter type 'CURRENT_TITLE' value 'XYZ'. Correct values are [...]" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "non_field_errors: No mapping found for COMPANY_HEADCOUNT: BAD. Correct values are ['Self-employed', '1-10', '11-50', '51-200', '201-500', '501-1,000', '1,001-5,000', '5,001-10,000', '10,001+']", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | -------------------------------------------------------------------------------------------------- | | `400` | `field` value is not in the supported enum. | | `400` | `value` does not match the supported enum for that filter (e.g., bad `COMPANY_HEADCOUNT` bracket). | | `400` | Passing `page` together with `professional_network_search_url`. | | `400` | Sending `filters` without any of `page`, `limit`, or `preview`. | | `400` | `limit` exceeds `25` on a synchronous (with-`page`) call. | | `401` | Missing or invalid `Authorization` header. | | `402` | Insufficient credits. | --- ## 9. End-to-end example Synchronous search — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/person/search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "filters": [ { "filter_type": "CURRENT_TITLE", "type": "in", "value": ["CEO"] }, { "filter_type": "COMPANY_HEADCOUNT", "type": "in", "value": ["11-50"] } ], "page": 1 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "CURRENT_TITLE", "type": "in", "value": ["CEO"] }, { "field": "COMPANY_HEADCOUNT", "type": "in", "value": ["11-50"] } ], "page": 1 }' ``` </CodeGroup> Asynchronous (background) search: <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/person/search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "filters": [ { "filter_type": "CURRENT_TITLE", "type": "in", "value": ["CEO"] } ], "limit": 2000 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "CURRENT_TITLE", "type": "in", "value": ["CEO"] } ], "limit": 10000, "background_job": true }' ``` </CodeGroup> Replay a saved search URL: <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/person/search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "linkedin_sales_navigator_search_url": "https://www.linkedin.com/sales/search/people?query=(keywords:CEO)" }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_search_url": "https://www.linkedin.com/sales/search/people?query=(keywords:CEO)" }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `POST /screener/person/search` to `POST /person/professional_network/search/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename `linkedin_sales_navigator_search_url` → `professional_network_search_url`. - [ ] Rename every filter's `filter_type` key to `field`. - [ ] Drop the `keyword` top-level parameter and rewrite it as a `KEYWORD_TITLE` / `KEYWORD_COMPANY` / `KEYWORD_FIRST_NAME` / `KEYWORD_LAST_NAME` / `KEYWORD_SCHOOL` filter. - [ ] Implicit async still works (a filter request with `limit` above `2000` and no `page`); set `background_job: true` to force async explicitly, or add `page` for a paged synchronous search. - [ ] Update parsers for the new nested profile shape (`basic_profile`, `professional_network`, `social_handles`, `contact`, `skills`, `experience`, `education`). - [ ] Update flat-field reads using the [Field-name mapping](#7-field-name-mapping) table. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. --- ## See also - [Live Person Search](/person-docs/search/live-search) — entry point for the new endpoint. - [Person Search](/person-docs/search/introduction) — indexed-dataset person search. - [Person Search examples](/person-docs/search/introduction) — example cookbook for indexed person-search. - [Person-search migration](/migrations/person/persondb-search) — equivalent guide for the indexed person-search endpoint. - [Live company-search migration](/migrations/company/company-search-live) — equivalent guide for the live company-search endpoint. # Live person enrich Source: https://docs.crustdata.com/migrations/person/person-enrich-live Reference for migrating from the legacy /screener/person/enrich endpoint called with enrich_realtime=true to the new /person/professional_network/enrich/live endpoint. <Columns cols={1}> <Column> <Danger>GET /screener/person/enrich?enrich_realtime=true</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /person/professional_network/enrich/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy real-time person-enrich path (the `/screener/person/enrich` endpoint called with `enrich_realtime=true`) and the current live person-enrich endpoint. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------------------------------------------ | ----------------------------------------------- | | Path | `GET /screener/person/enrich` with `enrich_realtime=true` in query | `POST /person/professional_network/enrich/live` | | Method | `GET` with query parameters | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Three header / shape changes are required.** The HTTP method moved from `GET` to `POST`, the authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. </Warning> <Note> This guide covers only the **real-time retrieval path** of the legacy endpoint — requests sent with `enrich_realtime=true`. For the indexed (cached) path (`enrich_realtime` omitted or `false`), see [Person-enrich migration](/migrations/person/person-enrich) instead. The two paths are now strictly separated into different endpoints. </Note> <Warning> **Live person enrich is gated.** The new endpoint returns `403` unless your account is provisioned for live enrich. Contact support to enable it. </Warning> --- ## 1. Request — top-level keys The legacy real-time path piggybacked on the cached-enrich query parameters and added a few live-only flags. The current endpoint narrows the input to a single identifier type and an optional field selector — no live-mode flags, no business-email input. | Legacy query parameter | Current body key | Notes | | ----------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `?linkedin_profile_url=https://...,https://...` | `{ "professional_network_profile_urls": ["https://...", "..."] }` | Key renamed. Comma-separated string → JSON array. Required identifier. | | `?enrich_realtime=true` | (removed) | Real-time retrieval is now selected by hitting the live endpoint path. The flag is not part of the body. | | `?fields=basic_profile,experience` | `{ "fields": ["basic_profile", "experience"] }` | Comma-separated string → JSON array. Section names changed — see [Field-name mapping](#3-field-name-mapping). | | `?business_email=jane@example.com` | (removed from this endpoint) | Move email-based contact workflows to [Contact Enrich](/person-docs/contact/enrich). | | `?github_profile_url=https://github.com/...` | (removed from this endpoint) | Dev-platform identifiers are not accepted here — see [Person Dev-Platform reference](/person-docs/dev_platform/introduction). | | `?force_fetch=true` | (removed) | The live endpoint always fetches live; there is no cache to bypass. The flag is unnecessary. | | `?open_to_cards=true` | (removed) | Open-to cards are surfaced under `professional_network.open_to_cards[]` in the response when present. | | `?colleagues=true` | (removed) | Colleague expansion is not part of this endpoint. Build a colleague list with [Person Search](/person-docs/search/introduction) instead. | | `?enrichment_method=salesnavigator\|voyager` | (removed) | Internal routing flag — no replacement. | | `?preview=true` | (removed) | Preview mode is not exposed on the live endpoint. | | `?min_similarity_score=0.8` | (removed) | Similarity scoring applied to business-email matches; not relevant for live profile-URL fetches. | <Note> **One identifier type only.** Submit profile URLs in the `professional_network_profile_urls` array (up to 25). The live endpoint does not accept business-email or dev-platform-URL inputs. Use [Contact Enrich](/person-docs/contact/enrich) for contact data and the [Dev Platform API](/person-docs/dev_platform/introduction) for dev-platform URLs. </Note> <Warning> **Request the sections you need via `fields`.** When `fields` is omitted, each match returns only `basic_profile` and `social_handles`. List `professional_network`, `experience`, `education`, or `skills` explicitly to include them. </Warning> --- ## 2. Identifier behavior | Topic | Legacy | Current | | ---------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- | | Identifier shape | Comma-separated string in a query parameter | JSON array in the body | | Identifier types | Professional-network URL, business email, dev-platform URL | Professional-network URL only | | Max values per request | 25 | 25 | | Mutual exclusion | Pass one identifier parameter | Pass one identifier array (only profile URLs are accepted) | | Mixed-identifier error | `400` with a free-form `error` string | `400` with `{ error: { type: "invalid_request", message: "..." } }` | --- ## 3. Field-name mapping The legacy response was a flat object with top-level fields like `linkedin_profile_url`, `name`, `headline`, `current_employers`, and `github_profiles`. The current response groups fields under `basic_profile`, `professional_network`, `social_handles`, `experience`, `education`, and `skills`. ### Identity and profile | Legacy field | Current field | | -------------------------------------- | ------------------------------------------------------------------------------------------------- | | `person_id` | (not returned by this live endpoint — match results by the profile URL in `matched_on`) | | `name` | `basic_profile.name` | | `title` | `basic_profile.current_title` | | `headline` | `basic_profile.headline` | | `summary` | `basic_profile.summary` | | `location` (string) | `professional_network.location.raw` (plus parsed `city`, `state`, `country`, `continent`) | | `languages[]` | `basic_profile.languages[]` | | `profile_picture_url` | `professional_network.profile_picture_url` | | `profile_picture_permalink` | `professional_network.profile_picture_permalink` | | `enriched_realtime` | (removed — this endpoint always returns live data) | | `query_linkedin_profile_urn_or_slug[]` | (removed; the input URL is echoed back as `matched_on` — see [Response shape](#6-response-shape)) | ### Professional-network attributes | Legacy field | Current field | | -------------------------- | --------------------------------------------------------------------------------- | | `linkedin_profile_url` | `social_handles.professional_network_identifier.profile_url` | | `linkedin_flagship_url` | `social_handles.professional_network_identifier.profile_url` (canonical URL only) | | `num_of_connections` | `professional_network.connections` | | `num_of_followers` | `professional_network.followers` | | `linkedin_joined_date` | `professional_network.joined_date` | | `linkedin_verifications[]` | `professional_network.verifications[]` | | `linkedin_open_to_cards[]` | `professional_network.open_to_cards[]` | ### Social handles | Legacy field | Current field | | ----------------------- | ---------------------------------------------------- | | `twitter_handle` | `social_handles.twitter_identifier.slug` | | `github_profiles[].url` | `social_handles.dev_platform_identifier.profile_url` | ### Skills | Legacy field | Current field | | ------------ | --------------------------------------------------------------------------------------- | | `skills[]` | `skills.professional_network_skills[]` | ### Experience and employers | Legacy field | Current field | | --------------------------------- | ----------------------------------------------------------------------------------- | | `current_employers[]` | `experience.employment_details.current[]` | | `past_employers[]` | `experience.employment_details.past[]` | | `all_employers[]` (company names) | (derive from `experience.employment_details.{current,past}[].name`) | | `all_employers_company_id[]` | (derive from `experience.employment_details.{current,past}[].crustdata_company_id`) | | `all_titles[]` | (derive from `experience.employment_details.{current,past}[].title`) | ### Education | Legacy field | Current field | | ------------------------ | ----------------------------------------------- | | `education_background[]` | `education.schools[]` | | `all_schools[]` | (derive from `education.schools[].school_name`) | | `all_degrees[]` | (derive from `education.schools[].degree`) | ### Contact data The legacy endpoint returned email addresses inline. Move contact-data handling to [Contact Enrich](/person-docs/contact/enrich). For the full live-enrich-response field catalog, see [Person Live Enrich](/person-docs/enrichment/live-enrich). --- ## 4. Type changes | Field | Legacy type | Current type | Notes | | ------------------------------ | ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email[]` | array of strings | Moved | Use [Contact Enrich](/person-docs/contact/enrich) and update your parser to its response shape. | | `location` | string | object | The raw string is preserved at `professional_network.location.raw`; parsed `city`, `state`, `country`, `continent` are also returned. | | `linkedin_profile_url` request | comma-separated string | array of strings | Renamed to `professional_network_profile_urls` and now a JSON array. | --- ## 5. Removed features ### `enrich_realtime`, `force_fetch`, `open_to_cards`, `colleagues`, `enrichment_method`, `preview`, `min_similarity_score` All of these legacy query flags are removed from the live endpoint: - `enrich_realtime` is replaced by the endpoint path itself. - `force_fetch` is unnecessary — the live endpoint always fetches live. - `open_to_cards` data is surfaced in the response under `professional_network.open_to_cards[]` when available; there is no request-side flag. - `colleagues` is no longer expanded inline; build a colleague list with [Person Search](/person-docs/search/introduction) instead. - `enrichment_method` is handled internally and not exposed. - `preview` and `min_similarity_score` are specific to the cached path and are not part of the live endpoint. ### `business_email` and `github_profile_url` identifiers The live endpoint accepts only professional-network profile URLs. For contact data, use [Contact Enrich](/person-docs/contact/enrich). To enrich starting from a dev-platform URL, use [Person Dev-Platform reference](/person-docs/dev_platform/introduction). ### `enriched_realtime` response flag The legacy `enriched_realtime` boolean indicated whether a record was served from cache or fetched live. Because `/person/professional_network/enrich/live` always returns live data, this flag has been removed from responses. ### Comma-separated query parameter style The endpoint moved from `GET` with query parameters to `POST` with a JSON body. All identifiers must be sent as JSON arrays. --- ## 6. Response shape The envelope shape changed. The legacy endpoint returned a top-level array of person records. The current endpoint returns a top-level array of match-result envelopes, each containing one or more `person_data` candidates. ### Envelope <CodeGroup> ```json Legacy [ { "person_id": 14540, "name": "David Hsu", "title": "Founder, CEO", "linkedin_profile_url": "https://www.linkedin.com/in/dvdhsu/", "location": "San Francisco, California, United States", "num_of_connections": 4321, "skills": ["Python", "React"], "current_employers": [ { "employer_name": "Retool", "title": "Founder, CEO" } ], "enriched_realtime": true } ] ``` ```json Current [ { "matched_on": "https://www.linkedin.com/in/dvdhsu/", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "name": "David Hsu", "current_title": "Founder, CEO", "headline": "Founder, CEO @ Retool" }, "social_handles": { "professional_network_identifier": { "profile_url": "https://www.linkedin.com/in/dvdhsu" } }, "professional_network": { "connections": 4321, "location": { "city": "San Francisco", "state": "California", "country": "United States", "raw": "San Francisco, California, United States" } }, "skills": { "professional_network_skills": ["Python", "React"] }, "experience": { "employment_details": { "current": [ { "name": "Retool", "title": "Founder, CEO" } ] } } } } ] } ] ``` </CodeGroup> ### Match-result fields | Field | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------- | | `matched_on` | The input profile URL you submitted. | | `match_type` | `professional_network_profile_url` for the live endpoint. | | `matches` | Array of candidate matches. Empty when no live data could be retrieved. | | `matches[].confidence_score` | Number. Higher is better. `1.0` is common for direct identifier lookups. | | `matches[].person_data` | Full real-time person profile — see [Field-name mapping](#3-field-name-mapping) for section paths. | ### No-match behavior Each entry corresponds to one input identifier. When live data cannot be retrieved, the entry's `matches` array is empty: ```json [ { "matched_on": "https://www.linkedin.com/in/no-such-person", "match_type": "professional_network_profile_url", "matches": [] } ] ``` <Note> The legacy endpoint returned `404` when no profiles matched. The current endpoint also defines a `404` for unrecoverable lookups but typically returns `200` with empty `matches: []` for missing profiles — handle both during the transition. </Note> --- ## 7. Added features | Feature | Description | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Match-result envelope | The response wraps each result in `{ matched_on, match_type, matches: [{ confidence_score, person_data }] }`, making batch live enrichment unambiguous. | | `confidence_score` | Numeric score on each match candidate. Higher is better; `1.0` is common for direct identifier lookups. | | Parsed `location` object | Returns `city`, `state`, `country`, `continent`, and `raw` rather than just the unparsed string. | | Stable error envelope | All `4xx` / `5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | --- ## 8. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `404`, `500`). The current endpoint adds explicit `403` gating — see the warning at the top of this page. <CodeGroup> ```json Legacy 400 { "error": "Please provide exactly one of linkedin_profile_url, business_email, or github_profile_url", "error_code": "INVALID_INPUT" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Exactly one identifier must be provided: professional_network_profile_urls", "metadata": [] } } ``` ```json Current 403 { "error": { "type": "permission_error", "message": "You do not have permission to access /person/professional_network/enrich/live.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. --- ## 9. End-to-end example Live-enriching one person by professional-network URL — written against both endpoints. <CodeGroup> ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/person/enrich?linkedin_profile_url=https://www.linkedin.com/in/dvdhsu/&enrich_realtime=true&fields=name,headline,current_employers' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/person/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_urls": ["https://www.linkedin.com/in/dvdhsu/"], "fields": ["basic_profile", "experience"] }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `GET /screener/person/enrich?enrich_realtime=true` (query params) to `POST /person/professional_network/enrich/live` (JSON body). - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Confirm your account is provisioned for live enrich — the endpoint returns `403` otherwise. Contact support if you need access. - [ ] Rename the identifier key: `linkedin_profile_url` → `professional_network_profile_urls`. - [ ] Convert comma-separated identifier values to a JSON array. - [ ] Drop `enrich_realtime=true` — the endpoint path itself selects the live behavior. - [ ] Drop `force_fetch`, `open_to_cards`, `colleagues`, `enrichment_method`, `preview`, and `min_similarity_score` — none are supported on the live endpoint. - [ ] Drop `business_email` and `github_profile_url` identifier inputs — only profile URLs are accepted. Move contact-data workflows to [Contact Enrich](/person-docs/contact/enrich). - [ ] Convert `fields` from a comma-separated string to a JSON array, and migrate field names to the new section-prefixed paths (see [Field-name mapping](#3-field-name-mapping)). Only `basic_profile` and `social_handles` return by default — request other sections explicitly. - [ ] Unwrap each result from the new `{ matched_on, match_type, matches: [{ confidence_score, person_data }] }` envelope. - [ ] Update parsers for the nested `person_data` shape: `name` → `basic_profile.name`, `title` → `basic_profile.current_title`, `headline` → `basic_profile.headline`, `summary` → `basic_profile.summary`. - [ ] Update `location` parsing — it is now an object with `raw`, `city`, `state`, `country`, `continent` rather than a single string. - [ ] Map `current_employers[]` / `past_employers[]` to `experience.employment_details.current[]` / `experience.employment_details.past[]`. Derive `all_employers`, `all_titles`, `all_employers_company_id` from these arrays if you still need them. - [ ] Map `education_background[]` to `education.schools[]`. - [ ] Map `linkedin_profile_url` (response) to `social_handles.professional_network_identifier.profile_url`, `twitter_handle` to `social_handles.twitter_identifier.slug`. - [ ] Map `num_of_connections` to `professional_network.connections`, `skills[]` to `skills.professional_network_skills[]`. - [ ] Drop `person_id` — this live endpoint does not return `crustdata_person_id`; match results by the profile URL in `matched_on`. - [ ] Handle no-match as `matches: []` (legacy returned `404`). - [ ] Update error handlers for the new `error.type` / `error.message` envelope and the new `403 permission_error` case. - [ ] Drop any parsing of `enriched_realtime` from the response — the field no longer exists. --- ## See also - [Person Live Enrich](/person-docs/enrichment/live-enrich) — entry point for the new endpoint. - [Person Enrich reference](/person-docs/enrichment/reference) — full request, response, and field catalog for the cached path; field names match. - [Person Search](/person-docs/search/introduction) — for queries that return multiple profiles (replaces `colleagues=true`). - [Person-enrich migration](/migrations/person/person-enrich) — equivalent guide for the cached (indexed) path of person enrich. # Job search Source: https://docs.crustdata.com/migrations/job/job-search Reference for migrating from the legacy /data_lab/job_listings/Table/ dataset-query endpoint to the new /job/search endpoint. <Warning> **`POST /data_lab/job_listings/Table/` is deprecated.** The legacy requests on this page are here for side-by-side comparison while you port an integration, not for new work. Point new calls at [`POST /job/search`](/job-docs/search/introduction). </Warning> <Columns cols={1}> <Column> <Danger>POST /data_lab/job_listings/Table/</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /job/search</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy job-listings dataset-query endpoint and the current job-search endpoint. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------------ | -------------------------------------- | | Path | `POST /data_lab/job_listings/Table/` | `POST /job/search` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Three header / shape changes are required.** The path and body grammar moved to a job-specific endpoint, the authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. </Warning> <Note> The legacy endpoint was a generic dataset-query API parameterized by a `dataset_id` (`job_listings`) and a `view_type` (`Table`). The new endpoint is a purpose-built job-search API — there is no `dataset_id` or `view_type` in the URL, and the request body no longer carries a `dataset` object. For real-time retrieval of currently-open roles for a single company, use [Live Job Search](/job-docs/search/live-search) instead of the legacy `sync_from_source=true` flag. </Note> --- ## 1. Request body — top-level keys The request body moved from a generic-dataset shape (with `dataset`, `tickers`, `functions`, `groups`, `sync_from_source`, `background_task`, `offset`) to a job-specific shape with cursor pagination and structured aggregations. | Legacy key | Current key | Notes | | ------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | `dataset` | (removed) | The endpoint path identifies the dataset. Drop `{ "name": "job_listings", "id": "joblisting" }` from the body. | | `filters` | `filters` | Grammar changed — see [Filter grammar](#2-filter-grammar) below. The key is unchanged, but `column` → `field`. | | `sorts` | `sorts` | `column` renamed to `field` (`{ field, order }`). Field names changed — see [Field-name mapping](#3-field-name-mapping). | | `count` | `limit` | Removed `count` as an alias. Use `limit` exclusively. | | `limit` | `limit` | Unchanged key. Default `20`, max `1000` (up from `100`). | | `offset` | `cursor` | Pagination moved from numeric offset to opaque cursor — see [Pagination](#7-pagination). | | `groups` | `aggregations[].type=group_by` | Group-by is now expressed inside `aggregations` — see [Aggregations](#5-aggregations). | | `aggregations` | `aggregations` | Same key. Shape changed — see [Aggregations](#5-aggregations). | | `functions` | (removed) | Custom expression functions are not exposed on the new endpoint. | | `tickers` | (removed) | Ticker-based scoping is not used; use a `company.basic_info.company_id` or `company.basic_info.primary_domain` filter instead. | | `sync_from_source` | (removed — see callout) | Real-time retrieval moved to a dedicated endpoint — see [Removed features](#6-removed-features). | | `background_task` | (removed) | Asynchronous bulk fetches are not part of the new endpoint. | | (new) | `fields` | Request a subset of response sections via dot-paths — see [Field selection](#4-field-selection). | --- ## 2. Filter grammar The condition key changed from `column` to `field`. The `and`/`or` group shape is unchanged, and operators are unchanged. The list of filterable fields was narrowed to indexed fields only. <CodeGroup> ```json Legacy condition { "column": "company_id", "type": "in", "value": [631394, 631811] } ``` ```json Current condition { "field": "company.basic_info.company_id", "type": "in", "value": [631394, 631811] } ``` </CodeGroup> <Note> **Two changes per condition:** 1. Rename `column` → `field`. 2. Map the legacy column name to the new dot-path (see [Field-name mapping](#3-field-name-mapping)). </Note> ### Operators Operators are unchanged. Both endpoints accept: | Operator | Meaning | | -------------------- | ----------------------------------------- | | `=`, `!=` | Exact match / negation. | | `<`, `=<`, `>`, `=>` | Numeric or date comparison. | | `in`, `not_in` | Set membership. `value` must be an array. | | `(.)` | Case-insensitive all-words match with per-word typo tolerance (not a substring match). | | `[.]` | Case-insensitive exact word/phrase match. | ### Group shape The `and` / `or` group shape is unchanged: ```json { "op": "and", "conditions": [ { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, { "field": "job_details.category", "type": "=", "value": "Engineering" } ] } ``` Nested groups are supported (mix `and` and `or` at any depth). ### Filterable fields The new endpoint restricts filters to **indexed fields only**. The filterable set is: | Group | Fields | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Job details | `job_details.title`, `job_details.category`, `job_details.workplace_type`, `job_details.reposted_job`, `job_details.url` | | Company basic info | `company.basic_info.company_id`, `company.basic_info.name`, `company.basic_info.primary_domain`, `company.basic_info.professional_network_id`, `company.basic_info.industries` | | Company firmographics | `company.headcount.total`, `company.headcount.range`, `company.followers.count`, `company.revenue.estimated.lower_bound_usd` | | Location | `location.raw`, `location.country`, `location.state`, `location.district`, `location.city` | | Content | `content.description` | | Identifiers | `crustdata_job_id` | | Metadata | `metadata.date_added`, `metadata.date_updated` | <Warning> Filtering on any other column returns `400` with `"Unsupported columns in conditions: [...]"`. Legacy integrations that relied on filtering by raw firmographic columns (e.g., `linkedin_industries` directly, or `last_funding_round_type`) should move those filters to the indexed names above or compose them on [Company Search](/company-docs/search/introduction) first. </Warning> --- ## 3. Field-name mapping The legacy endpoint returned a `fields[] + rows[][]` table where each column was identified by a flat `api_name` like `title`, `company_name`, `linkedin_industries`, or `date_added`. The current endpoint returns structured job objects with nested sections (`job_details`, `company`, `location`, `content`, `metadata`) — filter and sort references must use the new dot-paths. <Note> Each field's **Filter** and **Sort** support is marked per field in the [Job Search reference](/job-docs/search/reference#field-reference). Sorting on a non-sortable field returns `400`. </Note> ### Job details | Legacy column | Current field | | -------------------- | -------------------------------- | | `title` | `job_details.title` | | `category` | `job_details.category` | | `url` | `job_details.url` | | `workplace_type` | `job_details.workplace_type` | | `reposted_job` | `job_details.reposted_job` | | `number_of_openings` | `job_details.number_of_openings` | | `dataset_row_id` | `crustdata_job_id` (top-level) | ### Job location The legacy response carried a single flat `city`, `location_text`, `country`, `state`, and `district` columns (and several `*_geocode` foreign-key columns). The current response groups these under `location` with both parsed values and the raw posting string. | Legacy column | Current field | | --------------- | ------------------- | | `location_text` | `location.raw` | | `city` | `location.city` | | `state` | `location.state` | | `country` | `location.country` | | `district` | `location.district` | | `pincode` | `location.pincode` | ### Content | Legacy column | Current field | | ------------- | --------------------- | | `description` | `content.description` | ### Metadata | Legacy column | Current field | | -------------- | ----------------------- | | `date_added` | `metadata.date_added` | | `date_updated` | `metadata.date_updated` | ### Company firmographics The legacy response interleaved flat company columns (`company_id`, `company_name`, `company_website_domain`, `linkedin_industries`, `linkedin_headcount`, `linkedin_followers`, `crunchbase_*`, `gartner_*`, `glassdoor_*`, ...) into the same row as the job columns. The current response nests these under a single `company` object that reuses the section names from [Company Enrich](/company-docs/enrichment/reference). Each section carries the fields relevant to job data, so take the exact paths from the table below. This endpoint returns the industry list at `company.basic_info.industries`, and Company Enrich returns it at `taxonomy.professional_network_industries`. | Legacy column | Current field | | --------------------------------------------------- | ------------------------------------------------ | | `company_id` | `company.basic_info.crustdata_company_id` | | `company_name` | `company.basic_info.name` | | `company_website_domain` | `company.basic_info.primary_domain` | | `company_website` | `company.basic_info.website` | | `linkedin_id` | `company.basic_info.professional_network_id` | | `linkedin_industries` | `company.basic_info.industries[]` | | `linkedin_headcount` | `company.headcount.total` | | `linkedin_headcount_range` / `employee_count_range` | `company.headcount.range` | | `largest_headcount_country` | `company.headcount.largest_headcount_country` | | `linkedin_followers` | `company.followers.count` | | `hq_country` | `company.locations.country` | | `hq_state` | `company.locations.state` | | `hq_city` | `company.locations.city` | | `hq_street_address` | `company.locations.street_address` | | `estimated_revenue_lower_bound_usd` | `company.revenue.estimated.lower_bound_usd` | | `estimated_revenue_higher_bound_usd` | `company.revenue.estimated.upper_bound_usd` | | `stock_symbols` | `company.revenue.public_markets.stock_symbols` | | `fiscal_year_end` | `company.revenue.public_markets.fiscal_year_end` | | `acquisition_status` | `company.revenue.acquisition_status` | | `crunchbase_total_investment_usd` | `company.funding.total_investment_usd` | | `crunchbase_valuation_usd` | `company.funding.valuation_usd` | | `last_funding_round_date` | `company.funding.last_fundraise_date` | | `last_funding_round_type` | `company.funding.last_round_type` | | `num_funding_rounds` | `company.funding.num_funding_rounds` | | `crunchbase_investors` | `company.funding.investors[]` | | `competitor_website_domains` | `company.competitors.websites[]` | For the full company-data field catalog, see [Company Enrich reference](/company-docs/enrichment/reference). --- ## 4. Field selection The legacy endpoint returned every column in the dataset by default. The new endpoint accepts an optional `fields` array — request only the sections you need to keep payloads small. ```json { "fields": [ "job_details.title", "job_details.url", "company.basic_info.name", "location.raw", "metadata.date_added" ] } ``` Valid top-level groups: `crustdata_job_id`, `job_details`, `company`, `location`, `content`, `metadata`. Use dot-notation for nested fields (`company.headcount`, `company.basic_info.name`). When `fields` is omitted, all available sections are returned. --- ## 5. Aggregations Group-by has moved out of the top-level `groups` key and now lives inside `aggregations`. Each aggregation declares its `type` (`count` or `group_by`) explicitly. ### Count <CodeGroup> ```json Legacy { "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "column": "category", "type": "=", "value": "Engineering" }, "aggregations": [{ "column": "id", "type": "count" }], "limit": 0 } ``` ```json Current { "filters": { "field": "job_details.category", "type": "=", "value": "Engineering" }, "aggregations": [{ "type": "count" }], "limit": 0 } ``` </CodeGroup> ### Group-by <CodeGroup> ```json Legacy { "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "column": "title", "type": "=", "value": "Software Engineer" }, "groups": [{ "column": "company_id" }], "aggregations": [{ "column": "id", "type": "count" }], "limit": 0 } ``` ```json Current { "filters": { "field": "job_details.title", "type": "=", "value": "Software Engineer" }, "aggregations": [ { "type": "group_by", "field": "company.basic_info.company_id", "agg": "count", "size": 5 } ], "limit": 0 } ``` </CodeGroup> <Note> Set `limit: 0` on the request when you only want aggregation output and no job rows. The response's `job_listings` array will be empty and `aggregations[]` will carry the buckets. </Note> ### Supported group-by fields The set of fields you can group by is restricted on the new endpoint: `company.basic_info.company_id`, `company.basic_info.crustdata_company_id`, `company.basic_info.industries`, `company.basic_info.primary_domain`, `company.funding.last_round_type`, `company.headcount.range`, `company.locations.country`, `job_details.category`, `job_details.title`, `job_details.workplace_type`, `location.country`. Grouping by any other field returns `400` with `"Unsupported aggregation field: '...'"`. ### Aggregation response shape Aggregation results are returned in a typed `aggregations[]` array on the response: ```json { "job_listings": [], "next_cursor": null, "total_count": 30495, "aggregations": [ { "type": "group_by", "field": "company.basic_info.company_id", "buckets": [ { "key": 821755, "count": 1037, "metadata": { "company_name": "Jobs via Dice", "company_website_domain": "dice.com" } } ] } ] } ``` For `count` aggregations the result includes a `value` field instead of `buckets`. --- ## 6. Removed features ### `sync_from_source` (real-time retrieval) The legacy endpoint accepted `sync_from_source: true` to bypass the indexed dataset and pull live job listings from the web for a single company. This flag has been removed from `/job/search` — the current endpoint serves the indexed dataset only. For real-time job retrieval, call the dedicated live endpoint instead — see [Live Job Search](/job-docs/search/live-search). It accepts a single `crustdata_company_id` and returns up to 100 currently-open job listings from the web. ### `background_task` The legacy `background_task: true` flag enqueued an asynchronous bulk fetch of job listings for up to 10 companies, returning a task handle the caller could poll. This mode has been removed; build pagination around cursor-based requests on `/job/search` instead. ### `dataset` and `view_type` The legacy URL embedded a `view_type` (`Table`) and the body required a `dataset` object (`{ "name": "job_listings", "id": "joblisting" }`). Both have been removed — `/job/search` is purpose-built for job listings. ### `tickers` The legacy `tickers` array narrowed results to a set of stock or private tickers (e.g., `"PRIVATE:STRIPE"`). This input is not part of `/job/search`; filter by `company.basic_info.company_id` or `company.basic_info.primary_domain` instead. ### `functions` The legacy `functions` array allowed custom expression operations on columns. This input is not part of `/job/search`. ### `groups` (top-level) Group-by moved into `aggregations[].type = group_by` — see [Aggregations](#5-aggregations). ### `offset` Numeric `offset` pagination is replaced by an opaque cursor — see [Pagination](#7-pagination). ### `count` `count` as an alias for `limit` is removed. Use `limit` exclusively. ### `compressed` view (`/data_lab/job_listings/Table/compressed`) The legacy server-side compressed-response variant has been removed. If you need a compact payload, request only the fields you need with the new `fields` array. --- ## 7. Pagination Pagination moved from numeric offset (`offset + limit`) to opaque cursors. Each response carries a `next_cursor` value — pass it back as `cursor` on the next request to get the following page. <CodeGroup> ```json Legacy first page { "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "column": "company_id", "type": "=", "value": 631394 }, "limit": 20, "offset": 0 } ``` ```json Legacy second page { "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "column": "company_id", "type": "=", "value": 631394 }, "limit": 20, "offset": 20 } ``` ```json Current first page { "filters": { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, "limit": 20 } ``` ```json Current second page { "filters": { "field": "company.basic_info.company_id", "type": "=", "value": 631394 }, "limit": 20, "cursor": "H4sIAJJG1mkC_xX..." } ``` </CodeGroup> <Note> Cursors encode the full query state (filter, sort, field selection). If you change any of those between page requests, drop the cursor and paginate from the beginning. `next_cursor` is `null` on the final page. </Note> --- ## 8. Response shape The envelope shape changed. The legacy endpoint returned a `{ fields, rows }` table where each row was a positional array of column values. The current endpoint returns a paginated `{ job_listings, next_cursor, total_count, aggregations }` envelope where each entry is a structured `Job` object. ### Envelope <CodeGroup> ```json Legacy { "fields": [ { "api_name": "title", "type": "string" }, { "api_name": "company_name", "type": "string" }, { "api_name": "country", "type": "string" }, { "api_name": "date_added", "type": "date" } ], "rows": [ ["Integration Engineer", "Stripe", "Australia", "2026-04-07T11:37:29"] ], "total": 1676 } ``` ```json Current { "job_listings": [ { "crustdata_job_id": 41053563, "job_details": { "title": "Integration Engineer (AUNZ)", "url": "https://www.linkedin.com/jobs/view/4398377738" }, "company": { "basic_info": { "name": "Stripe", "primary_domain": "stripe.com" } }, "location": { "country": "Australia" }, "metadata": { "date_added": "2026-04-07T11:37:29" } } ], "next_cursor": "H4sIAJJG1mkC_xX...", "total_count": 1676 } ``` </CodeGroup> ### Top-level response fields | Field | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `job_listings` | Array of `Job` objects matching the filter for the current page. Empty when `limit` is `0` or when only aggregations were requested. | | `next_cursor` | Opaque cursor for the next page. `null` on the last page. | | `total_count` | Total number of matching listings across all pages. | | `aggregations` | Aggregation results — present only when the request included an `aggregations` array. | ### `Job` object sections | Section | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `crustdata_job_id` | Top-level integer identifier for the listing. | | `job_details` | Job title, category, URL, workplace type, repost flag, number of openings. | | `company` | Hiring company's firmographics: `basic_info`, `locations`, `headcount`, `followers`, `revenue`, `funding`, `competitors`. | | `location` | Job's advertised location: `raw`, `city`, `state`, `country`, `district`, `pincode`. | | `content` | Full job description text under `content.description`. | | `metadata` | Job timing: `date_added` (job-posted date), `date_updated` (most recent refresh). | For the full field catalog, see [Job Search reference](/job-docs/search/reference). --- ## 9. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). <CodeGroup> ```json Legacy 400 { "error": "Invalid filter structure: column 'foo' not found" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Unsupported columns in conditions: ['foo']", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------- | | `400` | `Unsupported columns in conditions: [...]` — a filter references a non-indexed field. | | `400` | `Unsupported aggregation field: '...'` — `group_by` references a field outside the allowed set. | | `400` | `limit` exceeds `1000` or is negative. | | `401` | Missing or invalid `Authorization` header. | --- ## 10. End-to-end example Fetching the first page of Engineering jobs at a single company, sorted by most recently added — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/data_lab/job_listings/Table/' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "op": "and", "conditions": [ { "column": "company_id", "type": "=", "value": 631394 }, { "column": "category", "type": "=", "value": "Engineering" } ] }, "sorts": [ { "column": "date_added", "order": "desc" } ], "limit": 20, "offset": 0 }' ``` ```bash Current 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.category", "type": "=", "value": "Engineering" } ] }, "sorts": [ { "field": "metadata.date_added", "order": "desc" } ], "fields": [ "job_details.title", "job_details.url", "location.country", "metadata.date_added" ], "limit": 20 }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `POST /data_lab/job_listings/Table/` to `POST /job/search`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Drop the `dataset` object from the body — the path identifies the dataset. - [ ] Drop `tickers`, `functions`, `background_task` from the body. - [ ] Drop `sync_from_source: true` — for real-time retrieval, call [Live Job Search](/job-docs/search/live-search) instead. - [ ] Rename `column` → `field` in every filter and sort condition. - [ ] Map legacy column names to the new dot-paths (see [Field-name mapping](#3-field-name-mapping)). - [ ] Replace `offset`-based pagination with `cursor` from `next_cursor`. - [ ] Drop `count` as an alias for `limit` — use `limit` exclusively. Raise the default cap if needed: new max is `1000`. - [ ] Move `groups` into `aggregations[]` with `type: group_by`, keyed by `field` (not `column`). Add the required `agg: count` and optional `size`. - [ ] Wrap legacy `aggregations: [{ column, type: count }]` as `aggregations: [{ type: count }]` — drop the `column` for `count`. - [ ] Set `limit: 0` when you only want aggregation output (and no job rows). - [ ] Update parsers for the new structured envelope `{ job_listings, next_cursor, total_count, aggregations }` — the legacy `{ fields, rows }` table shape is gone. - [ ] Update row parsers: each `Job` is a nested object with `crustdata_job_id`, `job_details`, `company`, `location`, `content`, `metadata` rather than a positional array. - [ ] Replace flat `company_*`, `linkedin_*`, `crunchbase_*` column reads with the new nested `company.*` paths (see [Field-name mapping](#3-field-name-mapping)). - [ ] Replace flat `city` / `state` / `country` / `location_text` reads with `location.{city,state,country,raw,district,pincode}`. - [ ] Replace `date_added` / `date_updated` reads with `metadata.date_added` / `metadata.date_updated`. - [ ] Add a `fields` array if you only need a subset of sections. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] If you relied on `/data_lab/job_listings/Table/compressed`, request a slimmer payload via `fields` instead. --- ## See also - [Job Search](/job-docs/search/introduction) — entry point for the new endpoint. - [Job Search reference](/job-docs/search/reference) — full request, response, and field catalog. - [Live Job Search](/job-docs/search/live-search) — real-time retrieval (replaces `sync_from_source=true`). - [Company Search](/company-docs/search/introduction) — for building company-level filters before querying jobs. - [Company-search migration](/migrations/company/companydb-search) — equivalent guide for the indexed company-search endpoint. # Live job search Source: https://docs.crustdata.com/migrations/job/job-search-live Reference for migrating from the legacy real-time job-listings retrieval (sync_from_source=true) to the new /job/professional_network/search/live endpoint. <Warning> **`POST /data_lab/job_listings/Table` with `sync_from_source: true` is deprecated.** The legacy requests on this page are here for side-by-side comparison while you port an integration, not for new work. Point new calls at [`POST /job/professional_network/search/live`](/job-docs/search/live-search). </Warning> <Columns cols={1}> <Column> <Danger>POST /data_lab/job_listings/Table?sync_from_source=true</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /job/professional_network/search/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy real-time job-listings retrieval path (the dataset-query endpoint called with `sync_from_source: true`) and the current live job-search endpoint. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------------------------------------------------------ | -------------------------------------------- | | Path | `POST /data_lab/job_listings/Table/` with `sync_from_source: true` in the body | `POST /job/professional_network/search/live` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Three header / shape changes are required.** The path moved to a dedicated live job-search endpoint, the authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. </Warning> <Note> This guide covers only the **real-time retrieval path** of the legacy endpoint — requests sent with `sync_from_source: true` (single company, live fetch from the web). For the indexed-dataset path (`sync_from_source` omitted or `false`), see [Job-search migration](/migrations/job/job-search) instead. </Note> --- ## 1. Request body — top-level keys The legacy endpoint reused a generic dataset-query body to express live retrieval. The current endpoint takes a small purpose-built body — no `filters` array, no `dataset` object, just the company identifier and optional pagination/field selection. | Legacy key | Current key | Notes | | --------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `sync_from_source: true` | (removed) | Real-time retrieval is now selected by hitting the live endpoint path. The flag is not part of the body. | | `dataset` | (removed) | The endpoint path identifies the dataset. Drop `{ "name": "job_listings", "id": "joblisting" }` from the body. | | `filters` (with `company_id` condition) | `crustdata_company_id` (integer) | The company is now identified by a top-level integer field, not by a filter clause. Exactly one company per request. | | `count` | `limit` | Removed `count` as an alias. Use `limit` exclusively. | | `limit` | `limit` | Default `100`, max `100` (unchanged ceiling). | | `offset` | (removed) | Real-time retrieval does not paginate — the endpoint returns one batch of currently-open listings per call. | | `sorts` | `sorts` | `column` renamed to `field` (`{ field, order }`). Only date fields are sortable on the live endpoint — see [Sort](#3-sort). | | `groups` | (removed) | Group-by is not supported on the live endpoint. For aggregations, use [Job Search](/job-docs/search/introduction) over the indexed dataset. | | `aggregations` | (removed) | Aggregations are not supported on the live endpoint. Use the indexed `/job/search` endpoint for aggregations. | | `functions` | (removed) | Custom expression functions are not exposed. | | `tickers` | (removed) | Ticker-based scoping is not used. | | `background_task` | (removed) | Asynchronous bulk fetches are not part of the live endpoint. | | (new) | `fields` | Request a subset of response sections via dot-paths — see [Field selection](#4-field-selection). | ### Identifier The company is now identified by a top-level integer field: <CodeGroup> ```json Legacy (sync_from_source path) { "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "op": "and", "conditions": [{ "column": "company_id", "type": "=", "value": 631394 }] }, "sync_from_source": true, "limit": 100 } ``` ```json Current { "crustdata_company_id": 631394, "limit": 100 } ``` </CodeGroup> <Note> **Exactly one company per request.** The legacy endpoint accepted only a single value in the `company_id` filter (passing more than one returned `400`). The current endpoint enforces the same rule by making `crustdata_company_id` a scalar integer instead of an array. If you have a domain, name, or profile URL instead of a Crustdata company ID, resolve it with [Company Identify](/company-docs/identify/introduction) first. </Note> --- ## 2. Identifier behavior | Topic | Legacy | Current | | ------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | How the company is given | `filters.conditions[].column = "company_id"` (single value) | Top-level integer field `crustdata_company_id` | | Max companies per call | 1 | 1 | | Other identifier types | Not supported | Not supported — resolve via [Company Identify](/company-docs/identify/introduction) first | | Missing identifier | `400` with `ERROR_MESSAGES["company_id_required"]` | `400` with `{ error: { type: "invalid_request", message: "..." } }` | | Multiple companies | `400` with `ERROR_MESSAGES["multiple_companies_sync"]` | `400` from schema validation (scalar field cannot accept an array) | --- ## 3. Sort The legacy endpoint applied the dataset's default sort (`date_updated` descending) and accepted custom `sorts` on any column. The current endpoint accepts `sorts` only on date fields, and the default is most recently added first. | Topic | Legacy | Current | | --------------- | -------------------------------- | ---------------------------------------------------------- | | Sort grammar | `sorts: [{ column, order }]` | `sorts: [{ field, order }]` | | Allowed columns | Any column in the legacy dataset | `metadata.date_added`, `metadata.date_updated` | | Default order | `date_updated` descending | `metadata.date_added` descending | | Field rename | `date_added`, `date_updated` | `metadata.date_added`, `metadata.date_updated` (dot-paths) | ```json { "crustdata_company_id": 631394, "limit": 100, "sorts": [{ "field": "metadata.date_added", "order": "desc" }] } ``` --- ## 4. Field selection The legacy endpoint returned every column in the dataset by default. The current endpoint accepts an optional `fields` array — request only the sections you need to keep payloads small. ```json { "crustdata_company_id": 631394, "limit": 25, "fields": ["job_details.title", "job_details.url", "location.raw"] } ``` Valid top-level groups: `crustdata_job_id`, `job_details`, `company`, `location`, `content`, `metadata`. Use dot-notation for nested fields (`company.basic_info.name`, `metadata.date_added`). When `fields` is omitted, all available sections are returned. --- ## 5. Pagination and limits | Topic | Legacy | Current | | --------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------- | | Result cap per call | `100` (enforced by the live-fetch path) | `100` (unchanged) | | Default `limit` | None — clients had to supply `limit` or `count` explicitly | `100` | | Multi-page pagination | `offset` accepted on the dataset-query body but ignored for the live-fetch result | Not supported — the endpoint returns one batch per call | | `count` alias | Accepted as an alias for `limit` | Removed — use `limit` exclusively | | Empty-result behavior | `404` with `ERROR_MESSAGES["no_active_jobs"]` | `200` with `{ "job_listings": [], "total_count": 0 }` | <Note> The legacy endpoint returned `404` when no live listings were found. The current endpoint returns `200` with an empty `job_listings` array — handle both during the transition. </Note> --- ## 6. Field-name mapping The legacy response was a flat `{ fields, rows }` table identical to the indexed dataset-query shape. The current response wraps each listing in a structured `Job` object with nested `job_details`, `company`, `location`, `content`, and `metadata` sections. ### Job details | Legacy column | Current field | | -------------------- | -------------------------------- | | `title` | `job_details.title` | | `category` | `job_details.category` | | `url` | `job_details.url` | | `workplace_type` | `job_details.workplace_type` | | `reposted_job` | `job_details.reposted_job` | | `number_of_openings` | `job_details.number_of_openings` | | `dataset_row_id` | `crustdata_job_id` (top-level) | ### Job location | Legacy column | Current field | | --------------- | ------------------- | | `location_text` | `location.raw` | | `city` | `location.city` | | `state` | `location.state` | | `country` | `location.country` | | `district` | `location.district` | | `pincode` | `location.pincode` | ### Content | Legacy column | Current field | | ------------- | --------------------- | | `description` | `content.description` | ### Metadata | Legacy column | Current field | | -------------- | ----------------------- | | `date_added` | `metadata.date_added` | | `date_updated` | `metadata.date_updated` | ### Company firmographics The legacy response interleaved flat company columns into the same row as the job columns. The current response nests these under `company`, reusing the section names from [Company Enrich](/company-docs/enrichment/reference). Each section carries the fields relevant to job data, so take the exact paths from the table below. This endpoint returns the industry list at `company.basic_info.industries`, and Company Enrich returns it at `taxonomy.professional_network_industries`. | Legacy column | Current field | | ------------------------------------- | ------------------------------------------------ | | `company_id` | `company.basic_info.crustdata_company_id` | | `company_name` | `company.basic_info.name` | | `company_website_domain` | `company.basic_info.primary_domain` | | `linkedin_id` | `company.basic_info.professional_network_id` | | `linkedin_industries` | `company.basic_info.industries[]` | | `linkedin_headcount` | `company.headcount.total` | | `linkedin_followers` | `company.followers.count` | | `hq_country` / `hq_state` / `hq_city` | (not returned by this live endpoint) | For the full job-data field catalog, see [Live Job Search reference](/job-docs/search/live-search). --- ## 7. Response shape The envelope shape changed. The legacy endpoint returned a `{ fields, rows }` table where each row was a positional array of column values. The current endpoint returns `{ job_listings, total_count }` where each entry is a structured `Job` object. ### Envelope <CodeGroup> ```json Legacy (sync_from_source path) { "fields": [ { "api_name": "title", "type": "string" }, { "api_name": "company_name", "type": "string" }, { "api_name": "country", "type": "string" }, { "api_name": "date_added", "type": "date" } ], "rows": [ ["Integration Engineer", "Stripe", "Australia", "2026-04-07T11:37:29"] ] } ``` ```json Current { "job_listings": [ { "crustdata_job_id": 41053563, "job_details": { "title": "Integration Engineer (AUNZ)", "url": "https://www.linkedin.com/jobs/view/4398377738" }, "company": { "basic_info": { "name": "Stripe", "primary_domain": "stripe.com" } }, "location": { "country": "Australia" }, "metadata": { "date_added": "2026-04-07T11:37:29" } } ], "total_count": 1 } ``` </CodeGroup> ### Top-level response fields | Field | Description | | -------------- | --------------------------------------------------------------------------------------------------------- | | `job_listings` | Array of `Job` objects fetched in real time for the requested company. Empty when no listings were found. | | `total_count` | Number of listings returned in this response. | The live endpoint does not return `next_cursor` or `aggregations` — both are exclusive to the indexed [Job Search](/job-docs/search/introduction) endpoint. ### `Job` object sections | Section | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `crustdata_job_id` | Top-level integer identifier for the listing. | | `job_details` | Job title, category, URL, workplace type, repost flag, number of openings. | | `company` | Hiring company's firmographics: `basic_info`, `headcount`, `followers`, `revenue`, `funding`. | | `location` | Job's advertised location: `raw`, `city`, `state`, `country`, `district`, `pincode`. | | `content` | Full job description text under `content.description`. | | `metadata` | Job timing: `date_added` (job-posted date), `date_updated` (most recent refresh). | --- ## 8. Removed features ### Filter grammar The legacy live-fetch path reused the dataset-query body, so callers had to send `filters: { op: "and", conditions: [{ column: "company_id", ... }] }` just to identify the company. The current endpoint replaces this with a single top-level `crustdata_company_id` integer. Filtering by job attributes (title, category, date range) is not supported on the live endpoint — fetch the listings, then filter client-side, or call [Job Search](/job-docs/search/introduction) on the indexed dataset. ### `sync_from_source` flag The flag itself is removed; real-time retrieval is now selected by hitting the live endpoint path. The indexed-dataset path is served by [Job Search](/job-docs/search/introduction). ### `background_task` The legacy `background_task: true` flag enqueued an asynchronous bulk fetch for up to 10 companies, returning a task handle the caller could poll. This mode has been removed; the live endpoint operates synchronously and is scoped to a single company per call. ### `dataset`, `view_type`, `tickers`, `functions`, `aggregations`, `groups`, `offset`, `count` All removed — see [Request body](#1-request-body--top-level-keys) for the per-key migration mapping. ### Server-side compressed variant The legacy `/data_lab/job_listings/Table/compressed` variant has been removed. Request a slimmer payload via the new `fields` array instead. --- ## 9. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). The legacy `404 no_active_jobs` case is now `200` with an empty `job_listings` array — see [Pagination and limits](#5-pagination-and-limits). <CodeGroup> ```json Legacy 400 { "error": "company_id filter is required for job_listings dataset" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "crustdata_company_id is required.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | ---------------------------------------------------------------------------- | | `400` | `crustdata_company_id` missing, not an integer, or `limit` outside `1..100`. | | `401` | Missing or invalid `Authorization` header. | | `500` | Internal error during live retrieval. | --- ## 10. End-to-end example Fetching live job listings for a single company — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/data_lab/job_listings/Table/' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "dataset": { "name": "job_listings", "id": "joblisting" }, "filters": { "op": "and", "conditions": [ { "column": "company_id", "type": "=", "value": 631394 } ] }, "sync_from_source": true, "limit": 100 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/job/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "crustdata_company_id": 631394, "limit": 100, "fields": [ "job_details.title", "job_details.url", "location.raw", "metadata.date_added" ] }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `POST /data_lab/job_listings/Table/` (with `sync_from_source: true`) to `POST /job/professional_network/search/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Drop the `dataset` object from the body — the path identifies the dataset. - [ ] Drop `sync_from_source`, `tickers`, `functions`, `aggregations`, `groups`, `background_task`, `offset` from the body. - [ ] Replace the `company_id` filter clause with a top-level integer `crustdata_company_id`. If you have a domain or profile URL, resolve it via [Company Identify](/company-docs/identify/introduction) first. - [ ] Drop `count` as an alias for `limit` — use `limit` exclusively. - [ ] If you used `sorts`, rename the sort key `column` → `field` and migrate names to dot-paths (`date_added` → `metadata.date_added`). Only date fields are sortable on the live endpoint. - [ ] Update parsers for the new structured envelope `{ job_listings, total_count }` — the legacy `{ fields, rows }` table shape is gone. - [ ] Update row parsers: each `Job` is a nested object with `crustdata_job_id`, `job_details`, `company`, `location`, `content`, `metadata` rather than a positional array. - [ ] Replace flat `company_*`, `linkedin_*` column reads with the new nested `company.*` paths (see [Field-name mapping](#6-field-name-mapping)). - [ ] Replace flat `city` / `state` / `country` / `location_text` reads with `location.{city,state,country,raw,district,pincode}`. - [ ] Replace `date_added` / `date_updated` reads with `metadata.date_added` / `metadata.date_updated`. - [ ] Add a `fields` array if you only need a subset of sections. - [ ] Handle no-result as `200` with an empty `job_listings` array (legacy returned `404 no_active_jobs`). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] If you relied on `/data_lab/job_listings/Table/compressed`, request a slimmer payload via `fields` instead. - [ ] If you depended on the `background_task: true` bulk-fetch mode for multiple companies, loop over `crustdata_company_id` values client-side or contact [support@crustdata.co](mailto:support@crustdata.co) to discuss higher-throughput options. --- ## See also - [Live Job Search](/job-docs/search/live-search) — entry point for the new endpoint. - [Job Search](/job-docs/search/introduction) — indexed-dataset job search (used when you want filters or aggregations). - [Job-search migration](/migrations/job/job-search) — equivalent guide for the indexed-dataset path of the legacy endpoint. - [Company Identify](/company-docs/identify/introduction) — resolve a domain, name, or profile URL to a `crustdata_company_id`. - [Live company-search migration](/migrations/company/company-search-live) — equivalent guide for the live company-search endpoint. # Web search Source: https://docs.crustdata.com/migrations/web/web-search Reference for migrating from the legacy /screener/web-search endpoint to the new /web/search/live endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/web-search</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /web/search/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy web-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ---------------------------- | -------------------------------------- | | Path | `POST /screener/web-search` | `POST /web/search/live` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Two header changes are required.** The authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing either header are rejected. </Warning> <Note> The companion content-fetch endpoint has also moved — the legacy `?fetch_content=true` query parameter and the standalone `POST /screener/web-fetch` route are replaced by [Web Fetch](/web-docs/fetch/introduction) at `POST /web/enrich/live`. Both endpoints are now strictly separated. </Note> --- ## 1. Request body — top-level keys The request shape is mostly preserved. Three parameters were renamed (camelCase to snake_case), one was renamed semantically (`solveCloudflare` → `human_mode`), and the `?fetch_content=true` query parameter was removed in favor of calling the dedicated fetch endpoint. | Legacy key | Current key | Notes | | ------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | `query` | Required. Same semantics. Max length raised from 1000 to 5000 characters. | | `geolocation` | `location` | Renamed. Same ISO 3166-1 alpha-2 country codes (`US`, `GB`, `JP`, …). | | `sources` | `sources` | Unchanged enum: `news`, `web`, `scholar-articles`, `scholar-articles-enriched`, `scholar-author`, `ai`, `social`. | | `site` | `site` | Unchanged. | | `startDate` | `start_date` | Renamed to snake_case. Same Unix-seconds timestamp. | | `endDate` | `end_date` | Renamed to snake_case. Same Unix-seconds timestamp. | | `solveCloudflare` | `human_mode` | Renamed. Same boolean semantics — attempt a browser-like retrieval path when standard access is blocked. Now neutrally named. | | `numPages` | `page` | Renamed. Now represents the page number to fetch (1-indexed), not "number of pages to fetch in one call". Call repeatedly to paginate. | | `?fetch_content=true` (query string) | (removed) | Augmented fetch is no longer offered on this endpoint. Call [Web Fetch](/web-docs/fetch/introduction) at `POST /web/enrich/live` separately. | <Note> **`numPages` → `page` is a semantic change, not just a rename.** The legacy `numPages` meant "fetch `N` pages of results in a single call". The current `page` means "return page number `N`". To fetch multiple pages, call the endpoint once per page. </Note> --- ## 2. Removed features ### `?fetch_content=true` query parameter The legacy endpoint accepted a `?fetch_content=true` query parameter, which augmented the response with fetched page contents under a `contents` array. This parameter has been removed. To retrieve page content for search-result URLs, call [Web Fetch](/web-docs/fetch/introduction) at `POST /web/enrich/live` with the URLs returned by `/web/search/live`. ### Companion `POST /screener/web-fetch` The legacy standalone fetch route has moved to `POST /web/enrich/live`. Same input shape (`{ urls: [...] }`), same per-call cap of 10 URLs. ### `numPages` multi-page fetch in one call The legacy `numPages` parameter retrieved up to `N` result pages in a single response. The current endpoint serves one page per call via the `page` parameter. Pre-check your credit budget before paginating aggressively. ### Free-form `error` strings Legacy `400` and `500` responses returned a single `error` string. The current endpoint returns a structured error envelope — see [Error responses](#5-error-responses). --- ## 3. Field-name mapping The response envelope is the same overall, but two fields under `metadata` were renamed and `failed_pages` / `empty_pages` were added for paginated diagnostics. ### Request fields | Legacy field | Current field | | ----------------- | ------------- | | `geolocation` | `location` | | `startDate` | `start_date` | | `endDate` | `end_date` | | `solveCloudflare` | `human_mode` | | `numPages` | `page` | ### Response fields | Legacy field | Current field | Notes | | ----------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- | | `success` | `success` | Unchanged. | | `query` | `query` | Unchanged. | | `timestamp` | `timestamp` | Unchanged. Unix milliseconds. | | `results[]` | `results[]` | Same array. Each result now carries a `source` field. | | `metadata.totalResults` | `metadata.total_results` | Renamed to snake_case. | | (new) | `metadata.failed_pages[]` | Pages that failed to return results. | | (new) | `metadata.empty_pages[]` | Pages that returned no results. | | `contents[]` (when `?fetch_content=true`) | (removed) — call [Web Fetch](/web-docs/fetch/introduction) separately | Drop this field; fetch separately. | ### Per-result fields The legacy response returned the same per-result shape for all sources. The current response carries a top-level `source` field on each entry and adds source-specific fields. Common fields (`title`, `url`, `snippet`, `position`) are unchanged. | Legacy field | Current field | Notes | | ------------ | ------------- | --------------------------------------------------------------------------------- | | `title` | `title` | Unchanged. | | `url` | `url` | Unchanged. | | `snippet` | `snippet` | Unchanged. Present for `web`, `news`, `scholar-articles` results. | | `position` | `position` | Unchanged. 1-indexed. | | (new) | `source` | The source type that returned this result (`web`, `news`, `scholar-articles`, …). | Source-specific additions on the current endpoint: | Source | Additional response fields | | ------------------ | --------------------------------------------------------------------------------------------------- | | `scholar-articles` | `authors[]`, `citations` (integer), `pdf_url`, `metadata` (citation string). | | `scholar-author` | `name`, `affiliation`, `website`, `interests[]`, `articles[]`, `citations`, `h_index`, `i10_index`. | | `ai` | `content` (generated overview), `references[]`, `images[]` (no `snippet` for this source). | --- ## 4. Sources Source values are unchanged: `news`, `web`, `scholar-articles`, `scholar-articles-enriched`, `scholar-author`, `ai`, `social`. For the per-source response shape and example-style usage, see [Web Search sources](/web-docs/search/reference#sources). --- ## 5. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `402`, `500`). <CodeGroup> ```json Legacy 400 { "error": "query: This field is required." } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "query: This field is required.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | -------------------------------------------------------- | | `400` | `query` missing, empty, or whitespace-only. | | `400` | `sources[]` contains a value outside the supported enum. | | `400` | `start_date` is greater than or equal to `end_date`. | | `400` | Insufficient credits for the requested number of pages. | | `401` | Missing or invalid `Authorization` header. | | `500` | Internal error while performing the search. | --- ## 6. End-to-end example A multi-source search with site restriction and date filtering — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/web-search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "query": "distributed systems", "geolocation": "US", "sources": ["web", "news"], "site": "example.com", "startDate": 1728259200, "endDate": 1730937600, "numPages": 1 }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "distributed systems", "location": "US", "sources": ["web", "news"], "site": "example.com", "start_date": 1728259200, "end_date": 1730937600, "page": 1 }' ``` </CodeGroup> Fetching page contents — split into two calls now: <CodeGroup> ```bash Legacy (single call) curl --request POST \ --url 'https://api.crustdata.com/screener/web-search?fetch_content=true' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "query": "crustdata", "geolocation": "US" }' ``` ```bash Current (search, then fetch) # 1. Search curl --request POST \ --url https://api.crustdata.com/web/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "query": "crustdata", "location": "US" }' # 2. Fetch contents using URLs from step 1 curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": ["https://crustdata.com/", "https://www.crustdata.com/about"] }' ``` </CodeGroup> --- ## Migration checklist - [ ] Switch from `POST /screener/web-search` to `POST /web/search/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename request keys: `geolocation` → `location`, `startDate` → `start_date`, `endDate` → `end_date`, `solveCloudflare` → `human_mode`, `numPages` → `page`. - [ ] Update pagination logic — `page` returns one page, not `N` pages. Loop over `page` values to paginate. - [ ] Drop the `?fetch_content=true` query parameter. Call [Web Fetch](/web-docs/fetch/introduction) at `POST /web/enrich/live` with the result URLs instead. - [ ] If you used the legacy `POST /screener/web-fetch` route, switch to `POST /web/enrich/live` (same body shape, same 10-URL-per-call cap). - [ ] Rename `metadata.totalResults` reads to `metadata.total_results`. - [ ] Read the new `source` field on each result if you display source labels client-side. - [ ] Adopt per-source response fields when you query `scholar-articles`, `scholar-author`, or `ai` (see [Field-name mapping](#3-field-name-mapping)). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] If you used the legacy auto-augmented `contents[]` array on the response, replace it with a follow-up call to [Web Fetch](/web-docs/fetch/introduction). --- ## See also - [Web Search](/web-docs/search/introduction) — entry point for the new endpoint. - [Web Search reference](/web-docs/search/reference) — full request, response, and source-specific field catalog. - [Web Search sources](/web-docs/search/reference#sources) — per-source response details. - [Web Search examples](/web-docs/search/introduction#examples) — common query patterns. - [Web Fetch](/web-docs/fetch/introduction) — page-content retrieval (replaces `?fetch_content=true` and `/screener/web-fetch`). # Web fetch Source: https://docs.crustdata.com/migrations/web/web-fetch Reference for migrating from the legacy /screener/web-fetch endpoint to the new /web/enrich/live endpoint. <Columns cols={1}> <Column> <Danger>POST /screener/web-fetch</Danger> </Column> <Column classList="text-center mx-auto cd-justify-center"> <div class="flex justify-center"> <Icon icon="arrow-down" size={32} /> </div> </Column> <Column> <Check>POST /web/enrich/live</Check> </Column> </Columns> This page lists every behavioral and contract change between the legacy web-fetch endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ---------------------------- | -------------------------------------- | | Path | `POST /screener/web-fetch` | `POST /web/enrich/live` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token <key>` | `Authorization: Bearer <key>` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | <Warning> **Two header changes are required.** The authorization scheme moved from `Token <key>` to `Bearer <key>`, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing either header are rejected. </Warning> <Note> The companion search endpoint has also moved — `POST /screener/web-search` is replaced by [Web Search](/web-docs/search/introduction) at `POST /web/search/live`. The two endpoints are now strictly separated; the legacy `?fetch_content=true` mode on `/screener/web-search` is removed, and `/web/enrich/live` is the only way to retrieve page content. </Note> --- ## 1. Request body — top-level keys The request shape is preserved almost exactly. One field was renamed semantically (`solveCloudflare` → `human_mode`); everything else carries across as-is. | Legacy key | Current key | Notes | | ----------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | | `urls` | `urls` | Required. Array of fully-qualified URLs (each must include `http://` or `https://`). Maximum 10 URLs per request (unchanged). | | `solveCloudflare` | `human_mode` | Renamed. Same boolean semantics — attempt a browser-like retrieval path when standard fetch is blocked. Now neutrally named. | <Note> **One value rename to apply across your codebase:** `solveCloudflare` → `human_mode`. Same `true`/`false` semantics. No other request keys need to change. </Note> --- ## 2. Removed features ### Free-form `error` strings Legacy `400` and `500` responses returned a single `error` string. The current endpoint returns a structured error envelope — see [Error responses](#5-error-responses). ### `solveCloudflare` key name Renamed to `human_mode`. The legacy key is ignored (it has no effect) — send `human_mode` instead. --- ## 3. Field-name mapping The request and response shapes are nearly identical — one request key was renamed and one response field was renamed. ### Request fields | Legacy field | Current field | | ----------------- | ------------- | | `urls` | `urls` | | `solveCloudflare` | `human_mode` | ### Response per-result fields The response remains a top-level array, one entry per URL submitted. Each entry's shape changed in one place: `pageTitle` → `title`. | Legacy field | Current field | Notes | | ------------ | ------------- | ----------------------------------------------------------------------------------------------------- | | `success` | `success` | Unchanged. Boolean. | | `url` | `url` | Unchanged. The URL that was fetched. `null` when the fetch fails. | | `timestamp` | `timestamp` | Unchanged. Unix timestamp in **seconds** (not milliseconds). `null` when the fetch fails. | | `pageTitle` | `title` | Renamed. Same value — the page title parsed from the HTML `<title>` tag. `null` when the fetch fails. | | `content` | `content` | Unchanged. Full HTML content. `null` when the fetch fails. | <Note> **Partial failures stay in the response.** If you submit 5 URLs and 2 fail, you still receive 5 entries — the failed entries have `success: false` and `url`, `timestamp`, `title`, `content` all `null`. Don't reject the whole response on a partial failure. </Note> --- ## 4. Behavior and limits | Topic | Legacy | Current | | --------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | URLs per call | 1–10 | 1–10 (unchanged) | | Max URL length | 2000 characters | 2000 characters (unchanged) | | Response envelope | Top-level array of fetch results | Top-level array of fetch results (unchanged shape) | | Partial-failure handling | Returns one entry per URL with null fields on failure | Returns one entry per URL with null fields on failure (unchanged) | | Cloudflare bypass parameter | `solveCloudflare` | `human_mode` | | Empty `urls` array | `400` with `"This list may not be empty"` | `400` with `{ error: { type: "invalid_request", message: "urls: This list may not be empty.", metadata: [] } }` | | Missing `urls` field | `400` with `"This field is required"` | `400` with `{ error: { type: "invalid_request", message: "urls: This field is required.", metadata: [] } }` | --- ## 5. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `402`, `500`). <CodeGroup> ```json Legacy 400 { "error": "urls: This field is required." } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "urls: This field is required.", "metadata": [] } } ``` </CodeGroup> `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | ------------------------------------------ | | `400` | `urls` missing or empty array. | | `400` | More than 10 URLs submitted. | | `400` | A URL longer than 2000 characters. | | `401` | Missing or invalid `Authorization` header. | | `402` | Insufficient credits. | | `500` | Internal error while fetching content. | <Note> **Per-URL failures are not `4xx`/`5xx` responses.** When the endpoint succeeds at the request level but individual URLs cannot be retrieved, you get `200` with the failing entries marked `success: false`. Only request-level problems (missing `urls`, invalid auth, etc.) return non-`2xx`. </Note> --- ## 6. End-to-end example Fetching content for a handful of URLs — written against both endpoints. <CodeGroup> ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/web-fetch' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "urls": [ "https://example.com", "https://www.crustdata.com" ], "solveCloudflare": false }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/web/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "urls": [ "https://example.com", "https://www.crustdata.com" ], "human_mode": false }' ``` </CodeGroup> Response shape (both endpoints return a top-level array): <CodeGroup> ```json Legacy response [ { "success": true, "url": "https://www.example.com", "timestamp": 1765188176, "pageTitle": "Example Domain", "content": "<html lang=\"en\"><head><title>Example Domain..." } ] ``` ```json Current response [ { "success": true, "url": "https://example.com", "timestamp": 1775193366, "title": "Example Domain", "content": "Example Domain..." } ] ``` --- ## Migration checklist - [ ] Switch from `POST /screener/web-fetch` to `POST /web/enrich/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename the request key `solveCloudflare` → `human_mode`. - [ ] Rename response reads `pageTitle` → `title`. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] If you previously composed `/screener/web-search?fetch_content=true` into a single call, split it into a [Web Search](/web-docs/search/introduction) call followed by a `/web/enrich/live` call with the result URLs. - [ ] Continue handling partial failures: a successful request can still contain entries with `success: false` and `null` fields. --- ## See also - [Web Fetch](/web-docs/fetch/introduction) — entry point for the new endpoint. - [Web Fetch reference](/web-docs/fetch/introduction) — full request and response field catalog. - [Web Fetch examples](/web-docs/fetch/introduction#examples) — common content-extraction patterns. - [Web Search](/web-docs/search/introduction) — companion search endpoint at `/web/search/live`. - [Web-search migration](/migrations/web/web-search) — equivalent guide for the web-search endpoint (the legacy `?fetch_content=true` mode lives here). # Social posts Source: https://docs.crustdata.com/migrations/social-post/social-post-enrich-live Reference for migrating from the legacy /screener/linkedin_posts endpoint to the new /social_post/professional_network/enrich/live endpoint. GET /screener/linkedin_posts
POST /social_post/professional_network/enrich/live
This page lists every behavioral and contract change between the legacy social-posts endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ------------------------------ | ---------------------------------------------------- | | Path | `GET /screener/linkedin_posts` | `POST /social_post/professional_network/enrich/live` | | Method | `GET` with query parameters | `POST` with a JSON body | | Auth | `Authorization: Token ` | `Authorization: Bearer ` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | **Three header / shape changes are required.** The HTTP method moved from `GET` to `POST`, the authorization scheme moved from `Token ` to `Bearer `, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing any of these are rejected. The companion keyword-search endpoint also moved: `POST /screener/linkedin_posts/keyword_search` is replaced by [Search Social Posts](/social-post-docs/search/introduction) at `POST /social_post/professional_network/search/live`. This guide covers the per-entity enrich path only — for keyword discovery, see that endpoint's reference page. --- ## 1. Request — top-level keys Identifiers moved from a wide set of comma-separated query parameters into a single JSON body. The current endpoint accepts exactly one identifier per call and unifies the four legacy company identifiers into a single set of company keys. | Legacy query parameter | Current body key | Notes | | --------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | `?person_linkedin_url=https://...` | `professional_network_profile_url` | Renamed. Single string (not an array). | | `?linkedin_post_url=https://...` | `social_post_url` | Renamed. Single post URL. When provided, exactly one post is returned and pagination is ignored. | | `?company_name=Stripe,Google` | `company_name` | Now a single string per call (not comma-separated). Submit one company per request. | | `?company_domain=stripe.com,google.com` | `company_domain` | Now a single string per call. | | `?company_id=12345,67890` | `crustdata_company_id` | Renamed and now a single integer per call. | | `?company_linkedin_url=https://...` | `company_professional_network_url` | Renamed. Single URL per call. | | `?page=1` | `page` | Same semantics. Page number (1–20). Each page returns up to 5 posts. | | `?limit=20` | `limit` | Same semantics. 1–100 per call. Still mutually exclusive with `page`. | | `?fields=reactors,comments` | `fields` (string) | Same semantics. Comma-separated string. Used to opt into `reactors` / `comments` payloads. | | `?post_types=original,repost` | `post_types` (string or array) | Now accepts either a comma-separated string or a JSON array. Allowed values: `original`, `repost`. | | `?max_reactors=100` | `max_reactors` | Unchanged semantics. Range `0..5000`. Requires `reactors` in `fields`. | | `?max_comments=100` | `max_comments` | Unchanged semantics. Range `0..5000`. Requires `comments` in `fields`. | **One identifier per request.** Submit exactly one of `professional_network_profile_url`, `social_post_url`, `company_name`, `company_domain`, `crustdata_company_id`, or `company_professional_network_url`. Mixing identifier types returns `400`. Legacy callers that passed multiple `company_*` values in a comma-separated list must now loop one identifier at a time. **`page` and `limit` remain mutually exclusive.** Exactly one of the two must be provided (unless you use `social_post_url`, which always returns one post and ignores pagination). --- ## 2. Identifier behavior | Topic | Legacy | Current | | --------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- | | Identifier shape | Comma-separated string in a query parameter | JSON scalar in the body | | Identifier types | Person URL, post URL, four company keys | Person URL, post URL, four company keys (unified naming) | | Max identifiers per request | Multiple values in a comma-separated list (e.g., 2+ company IDs) | One identifier per request | | Mutual exclusion | Pass one identifier parameter | Pass one identifier field | | Mixed-identifier error | `400` with a free-form `error` string | `400` with `{ error: { type: "invalid_request", message: "..." } }` | | Unresolvable input | `400` or `404` with a free-form `error` string | `400` with the structured error envelope | --- ## 3. Removed features ### Comma-separated company lists The legacy endpoint accepted multiple company identifiers in a single comma-separated value (e.g., `?company_name=Stripe,Google`). The current endpoint accepts exactly one identifier per request — loop client-side if you need to fetch posts for several companies. ### Legacy `error` strings `400` and `500` responses returned a single `error` string. The current endpoint returns a structured error envelope — see [Error responses](#6-error-responses). ### Query-string ergonomics `POST` with a JSON body replaces `GET` with query parameters. URL length limits no longer constrain identifier or `fields` values. --- ## 4. Field-name mapping The legacy and current responses both return a top-level `posts[]` array. The per-post shape is preserved for the default fields, with two renames inside `hyperlinks` and a couple of new optional fields. ### Request fields | Legacy field | Current field | | ---------------------- | ---------------------------------- | | `person_linkedin_url` | `professional_network_profile_url` | | `linkedin_post_url` | `social_post_url` | | `company_id` | `crustdata_company_id` | | `company_linkedin_url` | `company_professional_network_url` | | `company_name` | `company_name` | | `company_domain` | `company_domain` | ### Per-post response fields | Legacy field | Current field | | ----------------------------------- | ---------------------------------------------- | | `backend_urn` | `backend_urn` | | `share_urn` | `share_urn` | | `share_url` | `share_url` | | `text` | `text` | | `actor_name` | `actor_name` | | `date_posted` | `date_posted` | | `total_reactions` | `total_reactions` | | `total_comments` | `total_comments` | | `num_shares` | `num_shares` | | `is_repost_without_thoughts` | `is_repost_without_thoughts` | | `reactions_by_type.{LIKE,PRAISE,…}` | `reactions_by_type.{LIKE,PRAISE,…}` | | `hyperlinks.company_linkedin_urls` | `hyperlinks.company_professional_network_urls` | | `hyperlinks.person_linkedin_urls` | `hyperlinks.person_professional_network_urls` | | `hyperlinks.other_urls` | `hyperlinks.other_urls` | | (new) | `hyperlinks.media_urls[]` | | (new) | `has_video` (boolean) | | (new) | `actor_type` (`person` / `company`) | | (new) | `person_details` / `company_details` — author firmographics returned inline | | `reactors[]` | `reactors[]` — opt-in via `fields=reactors` | | `comments[]` | `comments[]` — opt-in via `fields=comments` | For the full enrich-response field catalog, see [Social Post Enrich reference](/social-post-docs/enrichment/reference). --- ## 5. Response shape The response envelope is unchanged at the top level (`{ "posts": [...] }`), but two `hyperlinks` keys were renamed and three optional fields were added. ```json Legacy { "posts": [ { "backend_urn": "urn:li:activity:123456789", "share_urn": "urn:li:share:987654321", "share_url": "https://www.linkedin.com/feed/update/urn:li:activity:123456789/", "text": "Post content", "actor_name": "Company Name or Person Name", "date_posted": "2024-08-17", "total_reactions": 10, "total_comments": 2, "reactions_by_type": { "LIKE": 5, "EMPATHY": 3, "PRAISE": 2 }, "hyperlinks": { "company_linkedin_urls": [ "https://www.linkedin.com/company/company-name/" ], "person_linkedin_urls": [ "https://www.linkedin.com/in/person-name/" ], "other_urls": [] }, "num_shares": 1, "is_repost_without_thoughts": false } ] } ``` ```json Current { "posts": [ { "backend_urn": "urn:li:activity:7454580577267216384", "share_urn": "urn:li:share:7454580576751316992", "share_url": "https://www.linkedin.com/feed/update/urn:li:share:7454580576751316992/", "text": "Looking forward to this conversation!", "actor_name": "David Hsu", "date_posted": "2026-04-28", "total_reactions": 33, "total_comments": 1, "reactions_by_type": { "LIKE": 30, "PRAISE": 2, "APPRECIATION": 1 }, "hyperlinks": { "company_professional_network_urls": [], "person_professional_network_urls": [], "other_urls": [], "media_urls": [] }, "num_shares": 1, "is_repost_without_thoughts": false, "has_video": false } ] } ``` ### `social_post_url` shortcut When `social_post_url` is provided, the response is the same `{ posts: [...] }` envelope but always contains a single post. Pagination parameters are ignored. --- ## 6. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `403`, `500`). ```json Legacy 400 { "error": "person_linkedin_url and company_linkedin_url cannot both be provided" } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "Only one identifier type can be provided at a time.", "metadata": [] } } ``` `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | -------------------------------------------------------------------------------------------- | | `400` | No identifier provided. | | `400` | More than one identifier provided. | | `400` | Both `page` and `limit` provided, or neither (unless `social_post_url` is used). | | `400` | `limit` outside the `1..100` range. | | `400` | `page` outside the `1..20` range. | | `400` | `max_reactors` set without `reactors` in `fields`, or `max_comments` set without `comments`. | | `400` | Profile URL cannot be resolved. | | `401` | Missing or invalid `Authorization` header. | --- ## 7. Added features | Feature | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `hyperlinks.media_urls[]` | Image and media attachments referenced by the post. | | `has_video` | Boolean — true when the post includes embedded video. | | `person_details` / `company_details` | Author firmographics (name, profile URL, title or industry) returned inline alongside each post. | | `post_types` as JSON array | The new endpoint accepts `post_types` as either a comma-separated string or a JSON array — pick whichever fits your client. | | Stable error envelope | All `4xx` / `5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | --- ## 8. End-to-end example Recent posts for a person profile URL — written against both endpoints. ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/linkedin_posts?person_linkedin_url=https://www.linkedin.com/in/dvdhsu/&limit=2' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "professional_network_profile_url": "https://www.linkedin.com/in/dvdhsu/", "limit": 2 }' ``` Single-post lookup: ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/linkedin_posts?linkedin_post_url=https://www.linkedin.com/feed/update/urn:li:activity:7445158857850654722' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "social_post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7445158857850654722" }' ``` Company posts with reactor enrichment: ```bash Legacy curl --request GET \ --url 'https://api.crustdata.com/screener/linkedin_posts?company_domain=crustdata.com&limit=1&fields=reactors&max_reactors=50' \ --header 'authorization: Token YOUR_API_KEY' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/enrich/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "company_domain": "crustdata.com", "limit": 1, "fields": "reactors", "max_reactors": 50 }' ``` --- ## Migration checklist - [ ] Switch from `GET /screener/linkedin_posts` (query params) to `POST /social_post/professional_network/enrich/live` (JSON body). - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename identifier keys: `person_linkedin_url` → `professional_network_profile_url`, `linkedin_post_url` → `social_post_url`, `company_id` → `crustdata_company_id`, `company_linkedin_url` → `company_professional_network_url`. `company_name` and `company_domain` are unchanged. - [ ] Submit one identifier per request — drop any code that passes comma-separated lists of company values. Loop client-side over multiple companies. - [ ] Convert each company identifier to a scalar (single string / integer) in the body, not a comma-separated string in the URL. - [ ] Keep `page` / `limit` mutually exclusive — exactly one of the two is required unless you use `social_post_url`. - [ ] Update response parsers: rename `hyperlinks.company_linkedin_urls` → `hyperlinks.company_professional_network_urls`, `hyperlinks.person_linkedin_urls` → `hyperlinks.person_professional_network_urls`. - [ ] Adopt the new optional fields if useful: `hyperlinks.media_urls[]`, `has_video`. - [ ] Continue using `fields=reactors,comments` to opt into reactor and commenter enrichment, with `max_reactors` / `max_comments` to control payload size. - [ ] Update error handlers for the new `error.type` / `error.message` envelope. - [ ] If you previously called `/screener/linkedin_posts/keyword_search` for keyword discovery, switch to [Search Social Posts](/social-post-docs/search/introduction) at `POST /social_post/professional_network/search/live`. --- ## See also - [Social Post Enrich](/social-post-docs/enrichment/introduction) — entry point for the new endpoint. - [Social Post Enrich reference](/social-post-docs/enrichment/reference) — full request, response, and field catalog. - [Social Post Examples](/social-post-docs/enrichment/introduction#examples) — which identifier to use and when. - [Search Social Posts](/social-post-docs/search/introduction) — keyword-and-filter search across social posts (replaces `/screener/linkedin_posts/keyword_search`). - [Live person-enrich migration](/migrations/person/person-enrich-live) — equivalent guide for the live person-enrich endpoint. # Search social posts Source: https://docs.crustdata.com/migrations/social-post/social-post-search-live Reference for migrating from the legacy /screener/linkedin_posts/keyword_search endpoint to the new /social_post/professional_network/search/live endpoint. POST /screener/linkedin_posts/keyword_search
POST /social_post/professional_network/search/live
This page lists every behavioral and contract change between the legacy social-post keyword-search endpoint and the current one. Use it as a side-by-side reference when porting an integration; each section gives the old shape, the new shape, and the smallest edit that closes the gap. | Topic | Legacy | Current | | -------- | ---------------------------------------------- | ---------------------------------------------------- | | Path | `POST /screener/linkedin_posts/keyword_search` | `POST /social_post/professional_network/search/live` | | Method | `POST` with a JSON body | `POST` with a JSON body | | Auth | `Authorization: Token ` | `Authorization: Bearer ` | | Version | (not required) | `x-api-version: 2025-11-01` (required) | | Base URL | `https://api.crustdata.com` | `https://api.crustdata.com` | **Two header changes are required.** The authorization scheme moved from `Token ` to `Bearer `, and the `x-api-version: 2025-11-01` header is now required on every call. Calls missing either header are rejected. The companion per-entity enrich endpoint also moved: `GET /screener/linkedin_posts` is replaced by [Social Post Enrich](/social-post-docs/enrichment/introduction) at `POST /social_post/professional_network/enrich/live`. This guide covers only the keyword-and-filter search path. --- ## 1. Request body — top-level keys The request body is mostly preserved. The filter object's identifier key changed from `filter_type` to `field`, and the legacy `result_type` field was dropped (the endpoint always returns post content). | Legacy key | Current key | Notes | | ----------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `keyword` | `keyword` | Optional. When omitted, results are scoped by `filters[]` alone. Supports Boolean `OR`/`AND` with up to 6 terms. | | `page` | `page` | Mutually exclusive with `limit`. 1-based page number. | | `limit` | `limit` | Mutually exclusive with `page`. Range `1..100`. Required when `exact_keyword_match` is `true`. | | `date_posted` | `date_posted` | Same enum: `past-24h`, `past-week`, `past-month`, `past-quarter`, `past-year`. | | `content_type` | `content_type` | Same enum: `photos`, `jobs`, `collaborativeArticles`, `videos`, `documents`, `liveVideos`. Array of strings. | | `sort_by` | `sort_by` | Same enum: `relevance`, `date_posted`. | | `exact_keyword_match` | `exact_keyword_match` | Same semantics. Requires `limit` (not `page`). | | `fields` | `fields` (string) | Same semantics. Comma-separated string to opt into `reactors` / `comments` payloads. | | `max_reactors` | `max_reactors` | Unchanged. Range `0..5000`. Requires `reactors` in `fields`. | | `max_comments` | `max_comments` | Unchanged. Range `0..5000`. Requires `comments` in `fields`. | | `filters[].filter_type` | `filters[].field` | Renamed. Filter-field enum values are unchanged (see [Filter grammar](#2-filter-grammar)). | | `filters[].value` | `filters[].value` | Same array of strings. | | `filters[].type` | (removed) | The legacy serializer hard-coded `type: "in"`. The current endpoint drops the field entirely — every filter is an `IN` membership check. | | `result_type` | (removed) | The endpoint always returns post content. | --- ## 2. Filter grammar The filter object key changed from `filter_type` to `field`. The `and`/`or` group shape is unchanged in spirit — each filter is a membership condition combined with the others by AND. ```json Legacy filter { "filter_type": "AUTHOR_INDUSTRY", "type": "in", "value": ["Software Development"] } ``` ```json Current filter { "field": "AUTHOR_INDUSTRY", "value": ["Software Development"] } ``` **Two changes per filter:** 1. Rename `filter_type` → `field`. 2. Drop the `type` field (it was always `in`; the new endpoint infers membership semantics from the array `value`). ### Supported filter fields The set of supported filter fields is unchanged from the legacy endpoint: | Field | Description | | -------------------- | ------------------------------------------------------- | | `MEMBER` | Posts authored by a specific person profile URL or URN. | | `MENTIONING_MEMBER` | Posts that mention a specific person. | | `COMPANY` | Posts authored by a specific company. | | `MENTIONING_COMPANY` | Posts that mention a specific company. | | `AUTHOR_INDUSTRY` | Posts whose author is in a given industry. | | `AUTHOR_COMPANY` | Posts whose author works at a given company name. | | `AUTHOR_TITLE` | Posts whose author has a given title. | ### Invalid filter combinations The same invalid combinations remain rejected by both endpoints (`400`): - `AUTHOR_COMPANY` + `COMPANY` - `MEMBER` + `COMPANY` - `AUTHOR_TITLE` + `COMPANY` - `AUTHOR_INDUSTRY` + `COMPANY` --- ## 3. Removed features ### `filters[].type` field The legacy filter object required a `type: "in"` field (the only allowed value). The current endpoint drops the field — every filter is implicitly an `IN` membership check on the value array. ### `result_type` parameter The legacy `result_type` parameter (default `CONTENT`) is removed — the endpoint always returns post content. ### Free-form `error` strings Legacy `400` and `404` responses returned a single `error` string. The current endpoint returns a structured error envelope — see [Error responses](#6-error-responses). ### `404 no matching posts found` for empty results The legacy endpoint returned `404 "No matching posts found"` when the result set was empty. The current endpoint returns `200` with an empty array — see [Pagination and limits](#5-pagination-and-limits). --- ## 4. Field-name mapping The legacy and current responses both return a top-level array (no wrapper object). The per-post shape is preserved for the default fields, with two renames inside `hyperlinks` and a couple of new optional fields. ### Request fields | Legacy field | Current field | | ----------------------- | --------------------- | | `keyword` | `keyword` | | `page` | `page` | | `limit` | `limit` | | `date_posted` | `date_posted` | | `content_type` | `content_type` | | `sort_by` | `sort_by` | | `exact_keyword_match` | `exact_keyword_match` | | `fields` | `fields` | | `max_reactors` | `max_reactors` | | `max_comments` | `max_comments` | | `filters[].filter_type` | `filters[].field` | | `filters[].value` | `filters[].value` | | `filters[].type` | (removed) | ### Per-post response fields | Legacy field | Current field | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `backend_urn` | `backend_urn` | | `uid` | `uid` | | `share_urn` | `share_urn` | | `share_url` | `share_url` | | `text` | `text` | | `actor_name` | `actor_name` | | `actor_type` (`person` / `company`) | `actor_type` | | `date_posted` | `date_posted` | | `total_reactions` | `total_reactions` | | `total_comments` | `total_comments` | | `num_shares` | `num_shares` | | `is_repost_without_thoughts` | `is_repost_without_thoughts` | | `reactions_by_type.{LIKE,PRAISE,…}` | `reactions_by_type.{LIKE,PRAISE,…}` | | `hyperlinks.company_linkedin_urls` | `hyperlinks.company_professional_network_urls` | | `hyperlinks.person_linkedin_urls` | `hyperlinks.person_professional_network_urls` | | `hyperlinks.other_urls` | `hyperlinks.other_urls` | | (new) | `hyperlinks.media_urls[]` | | (new) | `has_video` (boolean) | | `person_linkedin_urn` | `person_details.person_professional_network_urn` | | `person_name` | `person_details.person_name` | | `person_linkedin_flagship_profile_url` | `person_details.person_professional_network_url` | | (new — when actor is a person) | `person_details.person_title`, `person_details.person_location`, `person_details.current_employers[]` | | (new — when actor is a company) | `company_details.company_name`, `company_details.company_professional_network_url`, `company_details.company_professional_network_id`, `company_details.company_domain`, `company_details.professional_network_industries[]`, `company_details.company_size` | | `reactors[]` | `reactors[]` — opt-in via `fields=reactors` | | `comments[]` | `comments[]` — opt-in via `fields=comments` | For the full search-response field catalog, see [Social Post Search reference](/social-post-docs/search/reference). --- ## 5. Pagination and limits | Topic | Legacy | Current | | --------------------- | -------------------------------------------- | -------------------------------------------------------- | | `page` / `limit` | Mutually exclusive; one is required | Mutually exclusive; one is required (unchanged) | | `limit` range | 1–100 | 1–100 (unchanged) | | Required when | `exact_keyword_match: true` requires `limit` | `exact_keyword_match: true` requires `limit` (unchanged) | | Empty-result behavior | `404` with `"No matching posts found"` | `200` with an empty array `[]` | The legacy endpoint returned `404` when no posts matched. The current endpoint returns `200` with an empty array — handle both during the transition. --- ## 6. Error responses The error envelope changed shape. Status codes are unchanged (`400`, `401`, `402`, `500`). The empty-result `404` case moved to a `200` with empty array — see [Pagination and limits](#5-pagination-and-limits). ```json Legacy 400 { "filters": [ "Invalid filter_type 'BAD_FIELD'. Allowed types are: MEMBER, MENTIONING_MEMBER, COMPANY, MENTIONING_COMPANY, AUTHOR_INDUSTRY, AUTHOR_COMPANY, AUTHOR_TITLE" ] } ``` ```json Current 400 { "error": { "type": "invalid_request", "message": "filters: Invalid filter_type 'BAD_FIELD'. Allowed types are: MEMBER, MENTIONING_MEMBER, COMPANY, MENTIONING_COMPANY, AUTHOR_INDUSTRY, AUTHOR_COMPANY, AUTHOR_TITLE", "metadata": [] } } ``` `401` now uses the same structured envelope as the other status codes — `{ "error": { "type": "unauthorized", "message": "Invalid API key in request.", "metadata": [] } }`. Common error cases: | Status | Cause | | ------ | -------------------------------------------------------------------------------------------- | | `400` | Both `page` and `limit` provided, or neither. | | `400` | `exact_keyword_match: true` without `limit`. | | `400` | Unsupported `filter_type` / `field` value. | | `400` | Unsupported `date_posted`, `sort_by`, or `content_type` value. | | `400` | Invalid filter combination (e.g., `AUTHOR_COMPANY` + `COMPANY`). | | `400` | `max_reactors` set without `reactors` in `fields`, or `max_comments` set without `comments`. | | `400` | Keyword exceeds the supported Boolean-operator count. | | `400` | Sending `filters[].filter_type` instead of `filters[].field`. | | `401` | Missing or invalid `Authorization` header. | --- ## 7. Added features | Feature | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | Optional `keyword` | The keyword field is now optional. Submit only `filters[]` to scope by entity attributes without a free-text query. | | `person_details` / `company_details` | Author firmographics are returned inline alongside each post. | | `hyperlinks.media_urls[]` | Image and media attachments referenced by the post. | | `has_video` | Boolean — true when the post includes embedded video. | | Stable error envelope | All `4xx` / `5xx` errors return `{ "error": { "type", "message", "metadata" } }`. | --- ## 8. End-to-end example Keyword search with a recency window — written against both endpoints. ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/linkedin_posts/keyword_search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "keyword": "LLM Evaluation", "date_posted": "past-month", "limit": 2, "sort_by": "relevance" }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "LLM Evaluation", "date_posted": "past-month", "limit": 2, "sort_by": "relevance" }' ``` Keyword search with an industry filter: ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/linkedin_posts/keyword_search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "keyword": "AI", "date_posted": "past-week", "limit": 1, "filters": [ { "filter_type": "AUTHOR_INDUSTRY", "type": "in", "value": ["Software Development"] } ] }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "AI", "date_posted": "past-week", "limit": 1, "filters": [ { "field": "AUTHOR_INDUSTRY", "value": ["Software Development"] } ] }' ``` Exact-match keyword search (requires `limit`, not `page`): ```bash Legacy curl --request POST \ --url 'https://api.crustdata.com/screener/linkedin_posts/keyword_search' \ --header 'authorization: Token YOUR_API_KEY' \ --header 'content-type: application/json' \ --data '{ "keyword": "chat.com", "limit": 50, "exact_keyword_match": true }' ``` ```bash Current curl --request POST \ --url https://api.crustdata.com/social_post/professional_network/search/live \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "keyword": "chat.com", "limit": 50, "exact_keyword_match": true }' ``` --- ## Migration checklist - [ ] Switch from `POST /screener/linkedin_posts/keyword_search` to `POST /social_post/professional_network/search/live`. - [ ] Change the `Authorization` scheme from `Token` to `Bearer`. - [ ] Add `x-api-version: 2025-11-01` header to every request. - [ ] Rename each filter's `filter_type` key to `field`. - [ ] Drop each filter's `type` field (it was always `"in"`; not accepted by the new endpoint). - [ ] Drop the `result_type` parameter — no longer supported. - [ ] Keep `page` / `limit` mutually exclusive — exactly one of the two is required, and `limit` is required for `exact_keyword_match: true`. - [ ] Update response parsers: rename `hyperlinks.company_linkedin_urls` → `hyperlinks.company_professional_network_urls`, `hyperlinks.person_linkedin_urls` → `hyperlinks.person_professional_network_urls`. - [ ] Move flat person-author fields into the new `person_details` object: `person_linkedin_urn` → `person_details.person_professional_network_urn`, `person_name` → `person_details.person_name`, `person_linkedin_flagship_profile_url` → `person_details.person_professional_network_url`. - [ ] Read author firmographics from `company_details` / `person_details` when present (new on the response). - [ ] Adopt the new optional fields if useful: `hyperlinks.media_urls[]`, `has_video`. - [ ] Continue using `fields=reactors,comments` with `max_reactors` / `max_comments` to opt into engagement payloads. - [ ] Handle empty results as `200` with `[]` (legacy returned `404 "No matching posts found"`). - [ ] Update error handlers for the new `error.type` / `error.message` envelope. --- ## See also - [Search Social Posts](/social-post-docs/search/introduction) — entry point for the new endpoint. - [Search Social Posts reference](/social-post-docs/search/reference) — full request, response, and filter-field catalog. - [Search Social Posts filters](/social-post-docs/search/reference#filters) — filter cookbook. - [Social Post Enrich](/social-post-docs/enrichment/introduction) — per-entity enrichment (replaces `/screener/linkedin_posts`). - [Social-post enrich migration](/migrations/social-post/social-post-enrich-live) — equivalent guide for the per-entity enrich endpoint. # API Introduction Source: https://docs.crustdata.com/openapi-specs/2025-11-01/introduction Learn the base URL, headers, authentication, versioning, and response patterns used across the Crustdata API. Use this page to send your first request and understand the shared conventions across the Company, Person, Job, Web, and Social Post APIs. ## Base URL All API requests are made to: ``` https://api.crustdata.com ``` ## Authentication Every request must include a Bearer token in the `authorization` header. ```bash --header 'authorization: Bearer YOUR_API_KEY' ``` You can get your API key from the [Crustdata dashboard](https://crustdata.com). Keep your API key secret. Do not expose it in client-side code or public repositories. Endpoint availability can vary by plan. Some live Company and Person endpoints are enterprise-only. Review [Pricing](/general/pricing) before you build a production workflow. ## API versioning Include the `x-api-version` header in every request. The current version is `2025-11-01`. ```bash --header 'x-api-version: 2025-11-01' ``` Requests without a version header can default to the latest version, which may introduce breaking changes. Always pin to a specific version. ## Request format All endpoints accept JSON request bodies with `content-type: application/json`. ```bash curl --request POST \ --url https://api.crustdata.com/company/identify \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{"domains": ["retool.com"]}' ``` ## Response format All responses return JSON. Successful requests typically return `200` with the endpoint-specific payload in the response body. ## Error responses Error responses return JSON with an `error` field and a details field explaining the issue. Most APIs use `description` for the details field. The Person API uses `reason` instead. ```json { "error": "Invalid Request", "description": "Exactly one identifier must be provided." } ``` ```json Person API { "error": "Invalid Request", "reason": "Input should be a valid dictionary" } ``` ### HTTP status codes | Status code | Meaning | Description | | ----------- | --------------------- | ------------------------------------------------ | | `200` | Success | Request completed successfully. | | `400` | Bad Request | Invalid request parameters or malformed input. | | `401` | Unauthorized | Missing or invalid API key. | | `403` | Forbidden | API key does not have access to this endpoint. | | `404` | Not Found | Requested resource not found. | | `500` | Internal Server Error | Unexpected server-side failure. Try again later. | ### Error response examples ```json { "error": "bad_request", "description": "Invalid request body." } ``` ```json { "error": "Unauthorized", "description": "Missing or invalid API key." } ``` ```json { "error": "forbidden", "description": "API key does not have access to this endpoint." } ``` ```json { "error": "Not Found", "description": "Company not found." } ``` ```json { "error": "Internal Server Error", "description": "An unexpected error occurred. Please try again later." } ``` ## Rate limits API requests are rate-limited per API key. See [Rate limits](/general/rate-limits) for details on limits and best practices. ## Endpoints This reference currently documents 18 endpoints across five product areas. Endpoint availability varies by plan. Certain live and specialized endpoints are enterprise-only. Check exactly what your API key can currently access with the free [`/account/endpoints`](/api-reference/account-apis/list-endpoint-permissions-and-rate-limits) endpoint, and contact sales to enable more. ### Company API | Method | Path | Description | | ------ | ------------------------------------------- | ------------------------------------------- | | POST | `/company/search` | Search companies with filters | | POST | `/company/identify` | Resolve a domain, URL, or name to a company | | POST | `/company/enrich` | Get a full company profile | | POST | `/company/professional_network/search/live` | Search companies in real time | | POST | `/company/search/autocomplete` | Autocomplete company search fields | ### Person API | Method | Path | Description | | ------ | ------------------------------------------ | ------------------------------------------------------- | | POST | `/person/search` | Search people using filters and sorting | | POST | `/person/enrich` | Enrich person profiles from the cached dataset | | POST | `/person/contact/enrich` | Enrich contact data (emails and phone numbers) for a person | | POST | `/person/professional_network/enrich/live` | Fetch fresh profile data from the web | | POST | `/person/professional_network/search/live` | Search people in real time | | POST | `/person/search/autocomplete` | Get field value suggestions for building search filters | ### Job API | Method | Path | Description | | ------ | --------------------------------------- | ------------------------------------------------------- | | POST | `/job/search` | Search the indexed job dataset with filters, sorting, and aggregations | | POST | `/job/search/autocomplete` | Autocomplete values for a job search field | | POST | `/job/professional_network/search/live` | Fetch job listings in real time for a company | ### Web API | Method | Path | Description | | ------ | ------------------ | ------------------ | | POST | `/web/search/live` | Perform web search | | POST | `/web/enrich/live` | Fetch web content | ### Social Post API | Method | Path | Description | | ------ | ------------------------------------------------ | ----------------------------------------------- | | POST | `/social_post/professional_network/enrich/live` | Fetch recent social posts for a person or company | | POST | `/social_post/professional_network/search/live` | Search social posts by keyword and filters | Search, identify, enrich, and autocomplete company data. Search, enrich, and autocomplete people data. Search indexed job listings and fetch real-time openings for a company. Search the web and fetch page content. Social Post API guides are available to plans with live access — the endpoints above are documented in this reference. To enable access, [contact us](https://crustdata.com/demo). # Best practices Source: https://docs.crustdata.com/openapi-specs/2025-11-01/best-practices How to handle rate limit headers, insufficient credit responses, and denied endpoint access when building production clients against the Crustdata API. Practical patterns for building reliable clients against the Crustdata API. Each section explains the signal the API sends, then shows the code you need to react to it. ## Handle rate limits Crustdata returns rate limit headers on every response. Read them after each call and pace your client accordingly — this is more reliable than guessing limits up front, and it gives you visibility before you hit the cap. Rate limits are enforced on a **sliding window**. The window does not reset at fixed clock intervals — instead, each request counts against the limit for the duration of the window, and `x-ratelimit-reset` tells you how many seconds until the oldest request ages out and capacity frees up. Spreading traffic evenly is safer than bursting and waiting for a "reset." ### Rate limit headers | Header | Meaning | | ----------------------- | ---------------------------------------------------------------- | | `x-ratelimit-limit` | Maximum requests allowed within the sliding window. | | `x-ratelimit-remaining` | Requests you have left right now. | | `x-ratelimit-reset` | Seconds until the next request ages out and capacity frees up. | Example values on a response: ``` x-ratelimit-limit: 30 x-ratelimit-remaining: 29 x-ratelimit-reset: 60 ``` ### Read the headers and pace requests After each request, inspect `x-ratelimit-remaining`. When it gets close to zero, wait `x-ratelimit-reset` seconds before sending the next call. ```python Python import time import requests def call_crustdata(url, payload, api_key): response = requests.post( url, headers={ "authorization": f"Bearer {api_key}", "content-type": "application/json", "x-api-version": "2025-11-01", }, json=payload, ) remaining = int(response.headers.get("x-ratelimit-remaining", 1)) reset = int(response.headers.get("x-ratelimit-reset", 0)) if remaining <= 1 and reset > 0: time.sleep(reset) response.raise_for_status() return response.json() ``` ```javascript Node.js async function callCrustdata(url, payload, apiKey) { const response = await fetch(url, { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify(payload), }); const remaining = Number( response.headers.get("x-ratelimit-remaining") ?? 1, ); const reset = Number(response.headers.get("x-ratelimit-reset") ?? 0); if (remaining <= 1 && reset > 0) { await new Promise((r) => setTimeout(r, reset * 1000)); } if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json(); } ``` ### Retry on 429 with backoff and jitter If you do receive a `429 Too Many Requests`, wait `x-ratelimit-reset` seconds and retry. Add jitter so concurrent workers do not all retry at the same instant. ```python Python import random import time import requests def call_with_retry(url, payload, api_key, max_retries=3): headers = { "authorization": f"Bearer {api_key}", "content-type": "application/json", "x-api-version": "2025-11-01", } for attempt in range(max_retries): response = requests.post(url, headers=headers, json=payload) if response.status_code != 429: response.raise_for_status() return response.json() reset = int(response.headers.get("x-ratelimit-reset", 1)) time.sleep(reset + random.uniform(0, 1)) raise RuntimeError("Exceeded retry budget") ``` ```javascript Node.js async function callWithRetry(url, payload, apiKey, maxRetries = 3) { const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json", "x-api-version": "2025-11-01", }; for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload), }); if (response.status !== 429) { if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json(); } const reset = Number( response.headers.get("x-ratelimit-reset") ?? 1, ); const jitter = Math.random(); await new Promise((r) => setTimeout(r, (reset + jitter) * 1000)); } throw new Error("Exceeded retry budget"); } ``` See [Rate limits](/general/rate-limits) for default per-endpoint limits and how to request higher throughput. ## Handle insufficient credits When your account has no remaining credits, the API returns `402 Payment Required` with a structured error body. Treat this as terminal — retrying will not succeed until credits are added. An API key that has its own monthly cap returns the same status and `error.type` once that cap is exhausted, with a message naming the key's limit and its reset date. Handle both with the same branch — see [Per-key monthly limit](/general/credits#per-key-monthly-limit). ### Response shape ```json { "error": { "type": "insufficient_credits", "message": "Insufficient credits. Please get in touch with the Crustdata sales team.", "metadata": [] } } ``` | Field | Description | | ---------------- | -------------------------------------------------------- | | `error.type` | `insufficient_credits` for this case. | | `error.message` | Human-readable message safe to surface in logs and UI. | | `error.metadata` | Reserved for additional context. May be empty. | ### Detect and stop Catch `402` early, pause the workflow, and alert the operator. Do not feed the same request back into a retry loop. ```python Python import requests class InsufficientCreditsError(Exception): pass def call_crustdata(url, payload, api_key): response = requests.post( url, headers={ "authorization": f"Bearer {api_key}", "content-type": "application/json", "x-api-version": "2025-11-01", }, json=payload, ) if response.status_code == 402: body = response.json() raise InsufficientCreditsError(body["error"]["message"]) response.raise_for_status() return response.json() ``` ```javascript Node.js class InsufficientCreditsError extends Error {} async function callCrustdata(url, payload, apiKey) { const response = await fetch(url, { method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify(payload), }); if (response.status === 402) { const body = await response.json(); throw new InsufficientCreditsError(body.error.message); } if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json(); } ``` Do not retry `402` responses. Repeated calls without adding credits will return the same error and obscure the real problem in your logs. ### Get more credits Reach out to [support@crustdata.co](mailto:support@crustdata.co) to discuss your usage, upgrade your plan, or add credits to your account. ## Handle denied endpoint access A `403 Forbidden` means the API key you used is not allowed to call that endpoint. A workspace admin can restrict an individual key to a subset of the endpoints the account has enabled, so a key that works for one endpoint can be refused on another. ### Response shape ```json { "error": { "type": "permission_error", "message": "This API key does not have access to /person/search. A workspace admin can update the key's endpoint access in the dashboard.", "metadata": [] } } ``` ### Detect and stop Treat `403` the way you treat `402`: terminal for that endpoint, not worth retrying. Nothing you change about the request will alter the outcome. Route it to the operator, who can widen the key's endpoint access in the dashboard. See [Permissions](/general/permissions) for the endpoint that lists what the account has enabled, and [Per-key endpoint access](/general/permissions#per-key-endpoint-access) for how a single key can be narrowed below that. # Search Companies Source: https://docs.crustdata.com/api-reference/company-apis/search-companies-with-indexed-fields-only https://static-assets.crustdata.com/openapi-specs/2025-11-01/company.yaml post /company/search Search the Crustdata company database using filter conditions. Supports complex AND/OR filter logic, cursor-based pagination, sorting, and field selection. Only indexed fields are searchable; use /company/enrich for non-indexed fields like news, people, or web_traffic. Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Identify Companies Source: https://docs.crustdata.com/api-reference/company-apis/identify-a-company-from-name-domain-id-or-profile-url https://static-assets.crustdata.com/openapi-specs/2025-11-01/company.yaml post /company/identify Match a company by name, website domain, profile URL, or Crustdata company ID. Returns one or more matches ranked by confidence score. This endpoint is useful for entity resolution before enrichment. The API returns a top-level array with one result per submitted identifier. Each match currently includes `company_data.crustdata_company_id` and `company_data.basic_info`. Exactly one identifier type must be provided per request. Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Enrich Companies Source: https://docs.crustdata.com/api-reference/company-apis/get-full-company-enrichment https://static-assets.crustdata.com/openapi-specs/2025-11-01/company.yaml post /company/enrich Get comprehensive company data including firmographics, headcount, funding, web traffic, employee reviews, people (founders, CXOs, decision makers), news, and more. Identify the company by domain, name, profile URL, or Crustdata company ID. Use the fields parameter to select specific data groups and reduce response size. The API returns a top-level array with one result per submitted identifier. The same endpoint supports multiple identifiers of one type in a single request. No-match responses currently return `200` with an empty `matches` array. Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Live Search Source: https://docs.crustdata.com/api-reference/company-apis/search-companies-in-real-time https://static-assets.crustdata.com/openapi-specs/2025-11-01/company.yaml post /company/professional_network/search/live Search for companies in real time using live company search filters. Returns 25 results per page. Supports filters for industry, headcount, region, revenue, followers, fortune ranking, account activities, job opportunities, and more. Results include basic company info, headcount, taxonomy, locations, revenue estimates, and decision maker counts. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Autocomplete Source: https://docs.crustdata.com/api-reference/company-apis/get-autocomplete-suggestions-for-company-search-fields https://static-assets.crustdata.com/openapi-specs/2025-11-01/company.yaml post /company/search/autocomplete Returns field value suggestions with document counts for company search fields. Useful for discovering valid filter values before building search queries. Accepts field names from the /company/search response schema using dot notation. Default `rate-limit` is 45 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Search People Source: https://docs.crustdata.com/api-reference/person-apis/search-people-using-filters-and-sorting https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/search Search the Crustdata person database using flexible filter conditions, sorting, and cursor-based pagination. Supports filtering on hundreds of fields including job title, company, location, seniority, industry, education, and more. Use compound conditions with AND/OR logic to build complex queries. Results can be sorted and paginated using cursors. Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Enrich People Source: https://docs.crustdata.com/api-reference/person-apis/enrich-person-profiles-from-cached-dataset https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/enrich Enrich person records using the Crustdata cached dataset. Provide profile URLs to retrieve detailed person data including employment history, education, skills, and developer platform data when available. Supports batch enrichment of up to 25 profiles at once. Default `rate-limit` is 15 requests per minute. Contact support to discuss higher limits if needed for your use case. # Contact Enrich Source: https://docs.crustdata.com/api-reference/person-apis/enrich-only-person-contact-data-from-cached-dataset https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/contact/enrich Enrich only the contact data for a person — business emails, personal emails, and phone numbers — using the Crustdata cached dataset. Provide either a profile URL or a business email. Exactly one identifier type must be provided per request. Supports batch enrichment of up to 25 identifiers at once. This endpoint mirrors `/person/enrich` but is scoped to contact fields: the enrichable fields are `contact.business_emails`, `contact.personal_emails`, `contact.phone_numbers`, and `contact.websites`. Requesting any non-contact field returns a `400` error listing the available contact fields. When `fields` is omitted, the three contact tiers — business emails, personal emails, and phone numbers — are enriched; `contact.websites` is returned only when requested explicitly. Each submitted identifier returns one result entry; an identifier with no contact match returns an empty `matches` array. Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Live Enrich Source: https://docs.crustdata.com/api-reference/person-apis/live-enrich-person-data-when-cached-data-is-missing-or-stale https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/professional_network/enrich/live Fetch fresh person profile data from the web. Use this endpoint when a profile is not yet available in the Crustdata cache or when the most up-to-date information is required. Accepts profile URLs and returns the full person data model with live data. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Live Search Source: https://docs.crustdata.com/api-reference/person-apis/search-people-in-real-time https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/professional_network/search/live Perform a real-time person search using live person search filters. Supports synchronous search with pagination as well as asynchronous background jobs for large result sets. Filter by title, company, headcount, industry, region, seniority, and many other live search fields. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Autocomplete Source: https://docs.crustdata.com/api-reference/person-apis/get-autocomplete-suggestions-for-person-search-fields https://static-assets.crustdata.com/openapi-specs/2025-11-01/person.yaml post /person/search/autocomplete Return ranked field-value suggestions for any supported person search field. Use this endpoint to discover the exact indexed values that `/person/search` filters expect — feed the suggestion `value` back into a search filter verbatim to retrieve the matching profiles. Pass a partial `query` string for type-ahead behaviour, or pass an empty `query` to retrieve the most common values for the field by frequency. An optional `filters` scope lets you compute autocomplete results against a subset of the dataset (for example, top job titles among current Google employees in the United States). The top-level `field` must be one of the supported autocomplete fields — an allowlisted subset of the field names used by `/person/search`, not every field exposed in its request and response schemas. Sending an unsupported field returns a 400 whose error message lists every accepted field. Default `rate-limit` is 45 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Search Source: https://docs.crustdata.com/api-reference/web-apis/web-search https://static-assets.crustdata.com/openapi-specs/2025-11-01/web.yaml post /web/search/live Perform a web search query and return results from multiple sources including web, news, academic articles, academic author profiles, deep research results, and social media. Use this endpoint for competitive intelligence, market research, lead generation, and content discovery. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Fetch Source: https://docs.crustdata.com/api-reference/web-apis/web-fetch https://static-assets.crustdata.com/openapi-specs/2025-11-01/web.yaml post /web/enrich/live Fetch the HTML content of webpages given their URLs. Retrieves the page title and full HTML content for up to 10 URLs in a single request. Use this for content extraction, web content retrieval, data collection, content monitoring, and SEO analysis. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Search Jobs Source: https://docs.crustdata.com/api-reference/job-apis/search-the-indexed-job-dataset https://static-assets.crustdata.com/openapi-specs/2025-11-01/job.yaml post /job/search Search the Crustdata job dataset using filter conditions. Each result includes the job's details (title, category, URL, openings), the hiring company's core firmographics (basic info, headcount, followers, revenue, funding, competitors), the job location, full job description text, and metadata timestamps. Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. Supports complex AND/OR filter logic, cursor-based pagination, sorting, field selection, aggregations (count and group_by), and geographic radius filtering on `location` with the `geo_distance` and `geo_exclude` operators. Only indexed fields are filterable and sortable — see the `field` description on `SearchCondition` for the full list. # Autocomplete Source: https://docs.crustdata.com/api-reference/job-apis/autocomplete-values-for-a-job-search-field https://static-assets.crustdata.com/openapi-specs/2025-11-01/job.yaml post /job/search/autocomplete Return type-ahead suggestions for the values of a single `/job/search` field. Pass the `field` you intend to filter on (for example `title` or `category`) and a partial `query`, and the endpoint returns matching values drawn from the indexed job dataset. Use it to power autocomplete inputs or to discover the exact values a filter will accept before issuing a `POST /job/search` request. This endpoint does not consume credits. Default `rate-limit` is 30 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Live Search Source: https://docs.crustdata.com/api-reference/job-apis/fetch-job-listings-in-real-time-for-a-given-company https://static-assets.crustdata.com/openapi-specs/2025-11-01/job.yaml post /job/professional_network/search/live Fetch job listings in real time for a single company identified by its `crustdata_company_id`. This endpoint goes out to the web on each request and returns the freshest listings currently published for that company, bypassing the indexed dataset. Use this when you need up-to-the-minute hiring data for one company — for example, to detect brand-new requisitions the moment they appear. For querying across many companies, filtering by title/category/date, or running aggregations, use `POST /job/search` against the indexed dataset instead. Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Enrich Source: https://docs.crustdata.com/api-reference/dev-platform-apis/enrich-a-person-with-dev-platform-profile-data https://static-assets.crustdata.com/openapi-specs/2025-11-01/dev_platform.yaml post /dev_platform/enrich Returns the dev platform profiles linked to a person. Specify the person using exactly one of `crustdata_person_id` or `profile_url`. Sending both, or neither, returns a `400` error. The response includes profile attributes retrieved from the web (name, bio, location, followers, etc.), any declared handles the profile points to, org memberships where available, and a confidence score for the match. Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Enrich Employee Reviews Source: https://docs.crustdata.com/api-reference/employee-review-apis/enrich-a-company-with-employee-review-data https://static-assets.crustdata.com/openapi-specs/2025-11-01/employee_review.yaml post /employee_review/enrich Fetch the full employee review profile for a single company identified by its `crustdata_company_id`. Returns company-level firmographics (name, HQ, size, revenue, stock ticker), aggregate rating metrics (overall star rating and category ratings for culture, work/life balance, compensation, management, diversity, business outlook, recommend-to-friend), review and content counts, CEO approval, primary industry, office locations, awards, and the underlying individual reviews (pros, cons, advice, category star ratings, employment status, tenure). Default `rate-limit` is 15 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. Each successful call costs **1 credit** per company. Companies without coverage return `500` with `error.message = "No data found for company_id="` and do not consume a credit. # Enrich Social Posts Source: https://docs.crustdata.com/api-reference/social-post-apis/fetch-social-posts-by-person-post-url-or-company https://static-assets.crustdata.com/openapi-specs/2025-11-01/social_post.yaml post /social_post/professional_network/enrich/live Fetch recent social posts for a single entity. Supply exactly one identifier: a person profile URL, a single post URL, or one of the company identifiers (`company_name`, `company_domain`, `crustdata_company_id`, or `company_professional_network_url`). Either `page` or `limit` must be provided (but not both), unless using `social_post_url` (which always returns one post). Use `fields` to opt into `reactors` and `comments` payloads, and `max_reactors` / `max_comments` to control how many of each are fetched per post. The response wraps the matched posts in a `posts` array. Each post carries a nested `actor`, `engagement`, and `hyperlinks` object; reposts also carry a `reposter` actor. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Search Social Posts Source: https://docs.crustdata.com/api-reference/social-post-apis/search-social-posts-by-keyword-and-filters https://static-assets.crustdata.com/openapi-specs/2025-11-01/social_post.yaml post /social_post/professional_network/search/live Search recent social posts using a keyword (with optional Boolean OR/AND operators) and filters. Supported filter fields include `PROFESSIONAL_NETWORK_MEMBER`, `MENTIONING_PROFESSIONAL_NETWORK_MEMBER`, `COMPANY`, `MENTIONING_COMPANY`, `AUTHOR_INDUSTRY`, `AUTHOR_COMPANY`, and `AUTHOR_TITLE`. Use `date_posted` to scope to a recency window and `content_type` to filter by media type. When `exact_keyword_match` is true, use `limit` (not `page`) since the API scans the first N posts and returns only those that exactly match the keyword phrase. The response is a top-level array of posts (no wrapper object). Each post has the same shape as an enrich post: a nested `actor`, `engagement`, and `hyperlinks` object. Default `rate-limit` is 10 requests per minute. Send an email to [gtm@crustdata.co](mailto:gtm@crustdata.co) to discuss higher limits if needed for your use case. # Batch Enrich Companies Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-company-enrichment-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/company/enrich Enrich up to **10,000 companies** in a single asynchronous job instead of one request per company. Provide exactly one identifier type — `names`, `domains`, `professional_network_profile_urls`, or `crustdata_company_ids` — and optionally the `fields` to include in each record. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each record is wrapped in an `{original_identifier, internal_id, data}` envelope, where `data` holds the company record in the same shape as the non-batch `/company/enrich` response. Identifiers that cannot be resolved are silently dropped from the results file (compare `entities_requested` with `entities_fulfilled`). # Batch Search Companies Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-company-database-search-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/company/search Run one company database search query and receive up to **10,000 matching companies** as a single results file. Where the non-batch `/company/search` returns one cursor page per call, the batch job walks every page for you and delivers the entire result set at once. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Provide one `filters` group (`{op, conditions}`) plus optional `sorts` and `max_results`. The non-batch paging parameters (`limit`, `page`, `preview`) are ignored by batch jobs — result volume is controlled by `max_results` alone. Records in the downloaded results file are flat, identical to the non-batch `/company/search` record shape. # Batch Live Search Companies Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-live-company-search-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/company/professional_network/search/live Run one live company search and receive up to **1,000 matching companies** as a single results file, with data retrieved from the web at request time. The batch job runs asynchronously and auto-paginates across the whole result set up to your requested count (results are fetched internally in pages of 25). An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Provide `filters` as a **flat list** of `{field, type, value}` objects (not the `{op, conditions}` group used by database search), or a `professional_network_search_url` instead. Because live jobs depend on an external, rate-limited source, they are slower than database-backed jobs and are the most likely to surface upstream errors in `error_message`. Records in the downloaded results file are flat, identical to the non-batch `/company/professional_network/search/live` record shape. # Batch Enrich Person Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-person-enrichment-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/enrich Enrich up to **10,000 people** in a single asynchronous job. Provide exactly one identifier type — `professional_network_profile_urls` or `business_emails` — and optionally the `fields` to include in each record. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each record is wrapped in an `{original_identifier, internal_id, data}` envelope, where `data` holds the person record in the same shape as the non-batch `/person/enrich` response. Identifiers that cannot be resolved are silently dropped from the results file (compare `entities_requested` with `entities_fulfilled`). # Batch Identify Person Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-person-identify-reverse-email-lookup-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/identify Resolve a bulk list of **emails** (business or personal) or `professional_network_profile_urls` to the people behind them — reverse email lookup — as a single asynchronous job. This is the only endpoint whose email resolution matches **personal** addresses (for example Gmail): the synchronous `/person/enrich` and `/person/contact/enrich` reverse-look-up business emails only, returning an empty `matches` array for a personal address. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each line is one record `{matched_on, match_type, matches}` — the same match shape as the non-batch person identify/enrich response, not wrapped in the enrich `{original_identifier, internal_id, data}` envelope. `matches` holds the resolved person's `crustdata_person_id` and `basic_profile`. An identifier that resolves to nobody returns an empty `matches` array and is not billed (compare `entities_requested` with `entities_fulfilled`). # Batch Contact Enrich Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-person-contact-enrichment-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/contact/enrich Enrich contact information — business email, personal emails, and phone numbers — for up to **300 people** in a single asynchronous job. Provide `professional_network_profile_urls` (the only identifier type accepted) and a required `fields` list naming which contact kinds to retrieve for each profile. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each record is wrapped in an `{original_identifier, internal_id, data}` envelope, where `data` holds the resolved contact information — `business_email` plus a nested `personal_contact_info` object — in the same shape as the non-batch `/person/contact/enrich` response. Profiles for which no requested contact kind could be found are still listed with empty values (compare `entities_requested` with `entities_fulfilled`). # Batch Search Person Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-person-database-search-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/search Run one person database search query and receive up to **10,000 matching people** as a single results file. Where the non-batch `/person/search` returns one cursor page per call, the batch job walks every page for you and delivers the entire result set at once. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Provide one `filters` group (`{op, conditions}`) plus optional `sorts` and `max_results`. The non-batch paging parameters (`limit`, `page`, `preview`) are ignored by batch jobs — result volume is controlled by `max_results` alone. Records in the downloaded results file are flat, identical to the non-batch `/person/search` record shape. # Batch Live Search Person Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-live-person-search-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/professional_network/search/live Run one live person search and receive up to **1,000 matching people** as a single results file, with data retrieved from the web at request time. The batch job runs asynchronously and auto-paginates across the whole result set up to your requested count (results are fetched internally in pages of 25). An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Provide `filters` as a **flat list** of `{field, type, value}` objects (not the `{op, conditions}` group used by database search), or a `professional_network_search_url` instead. Because live jobs depend on an external, rate-limited source, they are slower than database-backed jobs and are the most likely to surface upstream errors in `error_message`. Records in the downloaded results file are flat, identical to the non-batch `/person/professional_network/search/live` record shape. # Batch Live Enrich Person Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-live-person-enrichment-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/person/professional_network/enrich/live Enrich up to **300 people** in a single asynchronous job, with each profile retrieved fresh from the web at request time. Provide a list of `professional_network_profile_urls` — this is the only identifier type accepted by live enrichment. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each record is wrapped in an `{original_identifier, internal_id, data}` envelope, where `data` holds the person record in the same shape as the non-batch `/person/professional_network/enrich/live` response. For live enrichment, `internal_id` echoes the submitted profile URL. Because live jobs depend on an external, rate-limited source, they are slower than database-backed jobs. # Batch Search Jobs Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-job-listings-search-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/job/search Retrieve every job listing in the dataset for up to **10 companies** in a single asynchronous job. Where the non-batch `/job/search` returns one cursor page per call, the batch job walks every page for you and delivers the complete result set as one file. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Unlike the other batch search jobs, this endpoint does not take a `filters` group — provide a `crustdata_company_ids` list (a JSON array of integers; comma-separated strings are rejected) and the job returns **all** listings for those companies. To search listings by arbitrary filters, use the non-batch `/job/search`. Records in the downloaded results file are flat, identical to the non-batch `/job/search` record shape. Billing is per listing delivered, at the same rate as the non-batch `/job/search` — see [Pricing](/general/pricing#job-endpoints). # Batch Social Posts Source: https://docs.crustdata.com/api-reference/batch-apis/submit-a-batch-social-posts-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml post /batch/social_post/professional_network/enrich/live Retrieve recent social posts for up to **10 identifiers** in a single asynchronous job, with posts retrieved from the web at request time. Provide exactly one identifier type — `professional_network_profile_urls`, `social_post_urls`, `company_professional_network_urls`, `crustdata_company_ids`, `company_names`, or `company_domains`. An account may have at most 5 active (`pending` or `processing`) batch jobs at a time; submitting a sixth returns `429`. Use `limit` to control how many posts are retrieved per identifier. It is required for every identifier type except `social_post_urls`, which always resolves a single post. Set `max_reactors` and/or `max_comments` to also pull per-post engagement; requesting engagement lowers the maximum `limit` from 100 to 25, because each post then triggers additional reactor/comment pagination. Use `post_types` to keep only `original` posts or `repost`s. The job responds immediately with a `batch_id`. Poll `GET /batch/{batch_id}` (or provide a `webhook_url`) and download the gzipped JSONL results file when the job completes. Each record is wrapped in an `{original_identifier, data}` envelope, where `original_identifier` echoes the exact value you submitted and `data` is an array of social-post records in the same shape as the non-batch social posts response. Because live jobs depend on an external, rate-limited source, they are slower than database-backed jobs. # Get Batch Job Status Source: https://docs.crustdata.com/api-reference/batch-apis/get-the-status-and-download-urls-for-a-batch-job https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml get /batch/{batch_id} Poll the current state of a batch job. The `status` moves `pending` → `processing` → `completed` or `failed`. When the job is `completed`, the response includes a `download_url` for the merged results file and `download_urls` for the same data split into parts; both are pre-signed links valid for **5 days**. When the job is `failed`, `error_message` carries a human-readable reason. Results files are gzipped JSONL (one record per line). Enrichment job records are wrapped in an `{original_identifier, internal_id, data}` envelope; search job records are flat and match the corresponding non-batch endpoint's record shape. The `x-api-version` header is not required on this endpoint. # List Batch Jobs Source: https://docs.crustdata.com/api-reference/batch-apis/list-batch-jobs-for-the-authenticated-account https://static-assets.crustdata.com/openapi-specs/2025-11-01/batch.yaml get /batch List your batch jobs, most recent first, with cursor-based pagination and optional status filtering. Job summaries carry the same fields as the job-status endpoint minus the download links — poll `GET /batch/{batch_id}` for a specific job to obtain its `download_url`. The `x-api-version` header is not required on this endpoint. # List endpoint permissions and rate limits Source: https://docs.crustdata.com/api-reference/account-apis/list-endpoint-permissions-and-rate-limits https://static-assets.crustdata.com/openapi-specs/2025-11-01/account.yaml get /account/endpoints Returns one entry per Crustdata API endpoint with your account's access status (`enabled` or `disabled`), the response fields enabled and disabled for your account, and the effective per-endpoint rate limit in requests per minute. **Filter, or the response is hard to read.** Unfiltered, the response carries one entry for every product endpoint (32 on the `2025-11-01` version) and each entry lists every response field the account can and cannot receive, as a dot-path string. That comes to 1,929 field strings, of which `/company/enrich` alone contributes 558 enabled and 58 disabled. As compact JSON on a single line the whole payload is around 76 KB, so pass `path`, `category`, or `status` to get back something you can inspect. Start with `?path=/web/enrich/live`, whose entry is five fields long, to see the shape before you request more. Filters combine with AND. An unknown `path` or `category` returns an empty `endpoints` array, since both act as filters rather than lookups. An unrecognized `status` returns `400`. This endpoint is free and consumes no credits. It is rate limited to 300 requests per minute. It requires the `x-api-version: 2025-11-01` header, and requests without it return `400`. # Get account credit balance Source: https://docs.crustdata.com/api-reference/account-apis/get-account-credit-balance https://static-assets.crustdata.com/openapi-specs/2025-11-01/account.yaml get /account/credits Returns the account's remaining credit balance together with the recurring credit grant: the grant size, how often it refreshes, and the next refresh date. Accounts with credit wallets enabled also receive a `wallets` list — the recurring wallet (refreshed each cycle, spent first) and the top-up wallet (purchased credits that roll over until their expiry date). The key is absent for accounts without wallets. Always gate spending decisions on `account.credits`; `wallets` is informational. Accounts with live top-ups additionally receive a `credit_topups` list — one entry per top-up still holding credits, with its origin (`purchase`, `auto_topup`, or `granted`), original and remaining credits, and expiry date, sorted soonest expiry first. The key is absent otherwise. This endpoint is free and consumes no credits. It is rate limited to 300 requests per minute. It requires the `x-api-version: 2025-11-01` header — requests without it return `400`. # Technographics Source: https://docs.crustdata.com/guides/technographics Detect the tools, platforms, and categories a company uses, directly through the Crustdata Company API. Technographics is currently in **beta**. Field names, category values, and coverage may evolve based on feedback. To share input or request access, write to [support@crustdata.co](mailto:support@crustdata.co). Technographics maps each company to the technologies it uses. Every tool comes with a **name**, the **sources** it was detected from, its **catalog categories**, a **description**, and the **job-posting evidence** behind the detection, so you can both target companies by their stack and read the full stack for a company you already care about. ### Where technographics is available Technographics ships on the existing Company APIs. There is no separate endpoint, and the two endpoints play different roles: | API | Request field | Notes | | ------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------- | | [Company Enrich](/company-docs/enrichment/introduction) | `fields: ["technographics"]` | **Returns** the full detected stack. Never returned by default; request it. | | [Company Search](/company-docs/search/introduction) | `technographics.*` filter columns | **Filters and sorts** companies by tech stack, but does not return the values. | ### At a glance | Detail | Value | | --------------------- | ------------------------------------------------------------------------------------------------- | | **Read endpoint** | `POST https://api.crustdata.com/company/enrich` | | **Filter endpoint** | `POST https://api.crustdata.com/company/search` | | **Auth** | `authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header | | **Enrich field** | `technographics` (request via `fields`); returned under `company_data.technographics` | | **Filter columns** | Each is prefixed with `technographics.`: `technographics.total_technologies`, `technographics.top_technologies`, `technographics.technologies.name`, `technographics.technologies.category`, `technographics.technologies.super_category` | | **Access** | Gated add-on; requires field-level permission on your account | | **Pricing** | **+2 credits** per company that returns technographics data, on top of the base **2 credits** (beta pricing, subject to change) | | **Freshness** | `updated_at`, per company | --- ## The problem: firmographics don't tell you what a company is built on Company data tells you a company's size, funding, and industry, but not the tools it runs. Knowing the stack is what drives the highest-intent motions: - **Account scoring:** prioritize accounts that already run a complementary or competing product. - **Competitive displacement:** find every company using a competitor's technology. - **ICP targeting:** segment by category, like everyone using a data warehouse or an AI model provider. - **Personalization:** reference a prospect's actual stack in outreach. Detecting this yourself means running your own pipeline over each company's web presence and job postings, then normalizing thousands of raw tool names into a consistent taxonomy. ## The solution: a ready-made stack on the Company API Request the `technographics` field group on [Company Enrich](/company-docs/enrichment/introduction) and Crustdata returns the technologies detected for the company, each with its catalog categories and the sources it came from: ```json { "fields": ["technographics"] } ``` To build a list instead of reading one company, filter and sort on the `technographics.*` columns in [Company Search](/company-docs/search/introduction), then enrich the companies you want in full. A typical workflow is **search to narrow, enrich to read**. ## Access and pricing Technographics is a **gated add-on**. It requires field-level permission on your account. Check your access with the [permissions endpoint](/general/permissions), or contact Crustdata to enable it. It is billed at **+2 credits** per company that returns technographics data, on top of the endpoint's base **2 credits** per company. Companies with no technographics data are not charged the add-on. The same pricing applies to [Batch Company Enrich](/company-docs/enrichment/batch). These credit amounts are **beta pricing** and may change as technographics moves toward general availability. ## Example: read a company's stack (Enrich) Request the `technographics` field group explicitly. It is never returned by default. ```bash curl --request POST \ --url https://api.crustdata.com/company/enrich \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "domains": ["stripe.com"], "fields": ["basic_info", "technographics"] }' ``` The `technographics` section appears on each match under `company_data.technographics`: ```json { "technographics": { "total_technologies": 635, "top_technologies": ["SQL", "SaaS", "Python", "API", "Java", "Ruby", "Go", "React", "Salesforce", "Kubernetes"], "technologies": [ { "name": "React", "sources": ["web signals", "job posting"], "posting_count": 3, "categories": [ { "category_slug": "frontend-frameworks", "super_slug": "languages-frameworks", "score": 1.0 } ], "description": "React is an open-source JavaScript library for building user interfaces with reusable components.", "evidence": [ { "source": "job posting", "posting_id": "52239010", "confidence": 0.13, "url": "https://www.linkedin.com/jobs/view/4433506276", "title": "Software Engineer", "posted_at": "2026-06-25T20:30:57Z" }, { "source": "web signals", "posting_id": "stripe.com", "confidence": 0.29, "url": null, "title": null, "posted_at": null } ] }, { "name": "Twitter", "sources": ["web signals"], "posting_count": null, "categories": [ { "category_slug": "community-platforms", "super_slug": "customer-success-support", "score": 1.0 } ], "description": "Twitter, now branded as X, is a microblogging platform for posting and receiving short messages.", "evidence": [ { "source": "web signals", "posting_id": "stripe.com", "confidence": 0.29, "url": null, "title": null, "posted_at": null } ] } ], "updated_at": "2026-07-21T05:18:56Z" } } ``` ### Fields | Field | Type | Description | | ------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------- | | `total_technologies` | integer | Total count of technologies detected for the company. | | `top_technologies` | string[] | The most notable technology names. | | `technologies` | object[] | List of detected technologies, capped at **500 entries** per company. | | `technologies[].name` | string | Technology name. | | `technologies[].sources` | string[] | Where the signal was detected: `"web signals"` (the company's web presence) or `"job posting"`. | | `technologies[].posting_count` | integer, nullable | Number of the company's job postings the technology was detected in. `null` when there is no job-posting signal, never `0`. | | `technologies[].categories` | object[] | Catalog categories for the technology, strongest match first, up to four entries. | | `technologies[].categories[].category_slug` | string | Category slug, a lowercase kebab-case value such as `frontend-frameworks` or `cloud-platforms-iaas` (see [Coverage](#coverage-and-freshness)). The same values power the `technographics.technologies.category` search filter. | | `technologies[].categories[].super_slug` | string | Super-category slug, the broader group the category belongs to. The same values power the `technographics.technologies.super_category` search filter. | | `technologies[].categories[].score` | number | Strength of the category match. Higher is stronger. | | `technologies[].description` | string, nullable | Short catalog description of the technology. | | `technologies[].evidence` | object[], nullable | Evidence entries behind the detection. Each has a `source`, a `posting_id`, and a `confidence`, plus `url`, `title`, and `posted_at`, which are populated for job-posting evidence and `null` for web-signal evidence. | | `updated_at` | date-time string | Timestamp of the last technographics refresh for the company. | The `technologies` list is ordered by notability (`top_technologies` is the head of the same ranking) and is capped at **500 entries** per company. For a large stack, `total_technologies` (the true count) can exceed the number of entries returned. Use `total_technologies` for the count and `top_technologies` for the most notable tools. Because the list is the top 500 by notability, a company with a very large stack may match a [Company Search](#example-target-companies-by-tech-stack-search) filter on a niche technology yet not include that technology in its enriched `technologies` array. Treat Company Search as the authority for whether a company uses a given technology, and use Enrich to read the notable stack. For the complete schema, see the [`technographics` fields reference](/company-docs/enrichment/reference#technographics-fields). ## Example: target companies by tech stack (Search) [Company Search](/company-docs/search/introduction) can **filter and sort** on technographics without returning the values. Use it to build a list, then enrich the companies you want. ```bash curl --request POST \ --url https://api.crustdata.com/company/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": "technographics.technologies.name", "type": "in", "value": ["Snowflake"] } ] } }' ``` To segment by category instead of a specific tool, for example every company using a cloud service, filter on `technographics.technologies.category` with a category slug: ```bash curl --request POST \ --url https://api.crustdata.com/company/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": "technographics.technologies.category", "type": "in", "value": ["cloud-platforms-iaas"] } ] } }' ``` To segment at the broader group level, filter on `technographics.technologies.super_category` with a super-category slug, for example `infrastructure`. Filterable and sortable paths: | Path | Type | Filter | Sort | Description | | ---------------------------------------------- | -------- | ------ | ---- | ---------------------------------------- | | `technographics.total_technologies` | integer | Yes | Yes | Number of detected technologies | | `technographics.top_technologies` | string[] | Yes | No | Most notable technology names | | `technographics.technologies.name` | string[] | Yes | No | Detected technology names | | `technographics.technologies.category` | string[] | Yes | No | Detected technology category slugs | | `technographics.technologies.super_category` | string[] | Yes | No | Detected technology super-category slugs | **Pricing.** Filtering and sorting on technographics uses standard [Company Search](/company-docs/search/introduction) pricing of **0.03 credits per result returned**, with no add-on. The technographics add-on applies only when you return the data through Company Enrich. `technographics.total_technologies` is a raw count that tracks company size and job-posting volume more than stack sophistication. Sort on it for reach, not as a quality signal. Discover valid filter values with the `technology` and `technology_category` [autocomplete fields](/company-docs/autocomplete/introduction#common-fields-to-autocomplete) so your filters match the values Crustdata uses. See the [Company Search reference](/company-docs/search/reference#technographics-filter-only) for the full list of filter-only fields. ## Coverage and freshness - **Signals.** Technologies are detected from two independent sources: the company's **web presence** (`"web signals"`) and its **job postings** (`"job posting"`). A technology can be found by either or both, and the `sources` array tells you which. - **Categories.** Every technology maps to one or more catalog categories, each a lowercase kebab-case category slug (for example `frontend-frameworks`, `cloud-platforms-iaas`) grouped under a broader super-category slug (for example `engineering`, `infrastructure`). Filter at either level with `technographics.technologies.category` and `technographics.technologies.super_category`. Enumerate the category slugs with the `technology_category` autocomplete field. - **Freshness.** `updated_at` reflects the last refresh of the company's technographics as a whole. Individual technologies are not timestamped, so a signal from an older job posting and a recent one look the same. During beta, coverage is expanding, so if a company you expect is thin or missing, let us know at [support@crustdata.co](mailto:support@crustdata.co). ## What to do next - [Company Enrich reference](/company-docs/enrichment/reference#technographics-fields): full response schema. - [Company Search reference](/company-docs/search/reference#technographics-filter-only): filter and sort fields. - [Autocomplete](/company-docs/autocomplete/introduction#common-fields-to-autocomplete): valid technology and category values. - [Permissions](/general/permissions): check whether technographics is enabled on your account. # Job Title Normalization Source: https://docs.crustdata.com/guides/job-title-normalization Turn messy, real-world job titles into a clean, structured, and actionable taxonomy — available directly through the Crustdata Person API. Job Title Normalization (JTN) is currently in **beta**. Field names, taxonomy values, and confidence scoring may evolve based on feedback. To share input or request access, write to [support@crustdata.co](mailto:support@crustdata.co). **Use this when** you need to clean up raw, free-text job titles for targeting, market sizing, segmentation, or downstream analytics — without building your own ML pipeline. JTN transforms each raw title into a rich, structured record using Crustdata's own ML models and a proprietary, data-derived taxonomy. Every title is mapped to a high-level **Department**, a specific **Category (Sub-Department)**, and a standardized **Matched Title**, and is returned inline as `basic_profile.normalized_title`. ### Where JTN is available JTN ships as a field on every person record returned by the Person APIs — no extra endpoint or `fields` flag is required. | API | Field path | Notes | | ---------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------- | | [Person Enrich](/person-docs/enrichment/introduction) | `basic_profile.normalized_title` | Included whenever `basic_profile` is in `fields`. | | [Person Search (PersonDB)](/person-docs/search/introduction) | `basic_profile.normalized_title` | Returned on every match. Also usable as a filter and sort field — see the [Person Search reference](/person-docs/search/reference#basic-profile). | ## The problem: messy title data Raw job titles are wildly inconsistent, which makes accurate targeting, market sizing, and segmentation nearly impossible. Real-world data is full of: - **Generic titles** — ambiguous roles like "Manager," "Director," or "Consultant" that lack any functional context. - **Acronyms & typos** — shorthand such as "MTS" or "SDE," plus misspellings that general-purpose models struggle to interpret. - **Non-English titles** — roles written in many different languages. Without normalization, sales and marketing teams burn time manually cleaning data — which leads directly to poor campaign performance and inaccurate reporting. ## The solution: structured taxonomy mapping Every raw title is mapped to three fields: | Taxonomy field | Description | Example input | Example output | | ----------------------------- | ------------------------------------ | ----------------------------- | --------------------------------------- | | **Department** | High-level function of the role | `Analyst II, Commercial Bank` | `Finance & Accounting` | | **Category / Sub-Department** | Specific functional grouping | `Analyst II, Commercial Bank` | `Credit & Debt Management` | | **Matched Title** | Standardized, canonical title | `Analyst II, Commercial Bank` | `Credit Analyst - Commercial Banking` | ### Why JTN Unlike systems built on rigid public standards (such as ESCO), our taxonomy is data-derived and powered by our own ML models. The model focuses on a role's *functional intent* rather than its seniority or rank — so `Senior Data Engineer` is correctly grouped with `Data Engineer`. JTN returns clean, structured fields (Department, Category, Matched Title) that map cleanly into your CRM, campaign segmentation, and BI tools — making your targeting immediately more granular and effective. ## Example: Person Enrich response JTN is exposed inline on the person record at `basic_profile.normalized_title`. No extra `fields` flag is required — if `basic_profile` is requested, `normalized_title` is included. The same field is also returned on every [Person Search](/person-docs/search/introduction) match. ### Example request ```json { "professional_network_profile_urls": ["https://www.linkedin.com/in/sharroh"], "fields": ["basic_profile", "experience"] } ``` ### Example response (truncated) ```json [ { "matched_on": "https://www.linkedin.com/in/sharroh", "match_type": "professional_network_profile_url", "matches": [ { "confidence_score": 1.0, "person_data": { "basic_profile": { "current_title": "Software Engineer", "name": "Rohit S", "normalized_title": { "confident": true, "department": "Engineering & Technology", "sub_department": "Software Engineering", "matched_title": "Software Engineer - 2", "similarity": 0.9047 } } } } ] } ] ``` ### `normalized_title` field reference | Field | Type | Description | | ---------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | | `department` | string | High-level function of the role. One of the 20 departments in the taxonomy. | | `sub_department` | string | Specific functional grouping (Category). One of the 226 sub-departments in the taxonomy. | | `matched_title` | string | Standardized canonical title that the raw input was mapped to. | | `similarity` | float | Cosine similarity (0–1) between the raw input title and the matched canonical title. Higher = closer match. | | `confident` | boolean | `true` when JTN's internal heuristics consider the mapping high-confidence. Use this as a filter for stricter downstream use. | For strict segmentation use cases, filter on `normalized_title.confident == true`. For broader coverage (e.g. enrichment top-ups), use all results and inspect `similarity`. --- ## The taxonomy The JTN taxonomy covers **20 departments** and **226 sub-departments (categories)**, derived from a normalized set of canonical titles. ### Departments - Consulting & Professional Services - Customer Service & Support - Data & Analytics - Education & Research - Engineering & Technology - Executive Leadership - Finance & Accounting - Government & Public Sector - Healthcare & Medical - Legal & Compliance - Manufacturing & Quality - Marketing - Media & Communications - Operations & Logistics - Other - People & HR - Product & Design - Real Estate & Construction - Retail & Hospitality - Sales & Revenue ### Departments & sub-departments - Advisory & Strategy Consulting - Business Analysis & Operations - Consulting Project & Program Delivery - Executive & Business Coaching - Human Resources Consulting - IT & Technical Consulting - Independent & Fractional Consulting - Management Consulting - Marketing & Sales Consulting - Organizational Change Management - SAP & ERP Consulting - Solutions Architecture - Customer Experience Strategy - Customer Onboarding & Activation - Customer Relationship Management - Customer Success & Retention - Customer Support Operations - Member & Membership Services - Technical Support & Help Desk - Analytics Consulting & Strategy - Business Intelligence & Insights - Data Engineering & Operations - Data Science & Analytics - Geospatial & Product Analytics - Quantitative & Applied Analytics - Academic Advising & Counseling - Athletic Coaching & Administration - Early Childhood & Special Education - Experiential & Clinical Education - Fellowships & Postdoctoral Research - Institutional & Alumni Relations - Library & Information Science - Performing & Fine Arts Instruction - Program & Curriculum Development - Research & Laboratory Science - STEM Education - Student Affairs & Residential Life - Teaching & Instruction - Tutoring & Academic Support - Vocational & Corporate Training - CAD Design & Technical Drafting - Civil & Structural Engineering - Cloud & DevOps Engineering - Cybersecurity & Security Engineering - Data Engineering & Analytics - Database Administration - Embedded Systems & Firmware Engineering - Environmental & Marine Engineering - Field & Technical Service Engineering - Frontend Development - Geoscience & Geology Engineering - IT Infrastructure & Support - IT Leadership & Management - Industrial & Process Engineering - Machine Learning & AI Engineering - Mechanical & Electrical Engineering - Mobile Development - Network & Telecoms Engineering - Product Management Technical - R&D and Innovation Engineering - Software Engineering - Software Quality Assurance & Testing - Systems & Solutions Architecture - Technical Project Management - Board Governance & Advisory - Corporate Strategy & Business Development - Executive & C-Suite Leadership - Executive Support & Coordination - Founder & Entrepreneurship Leadership - General & Regional Management - Investor Relations & Governance - Program & Project Leadership - Sales & Marketing Leadership - Technology & Engineering Leadership - Vice President Leadership - Accounts Payable & Receivable - Actuarial & Cost Analysis - Audit & Assurance - Credit & Debt Management - Financial Controlling & Reporting - Financial Operations & Accounting - Financial Planning & Analysis - Insurance & Underwriting - Investment & Asset Management - Loan Processing & Origination - Mergers & Acquisitions - Payroll & Billing Administration - Retail & Commercial Banking - Risk Management - Tax Accounting & Compliance - Trading & Portfolio Management - Wealth & Relationship Management - Agricultural & Environmental Services - Emergency & Protective Services - Government Policy & Relations - Law Enforcement & Corrections - Military & Defense Operations - Public Administration & Civil Service - Behavioral & Wellness Therapy - Biomedical & Health Informatics - Clinical Laboratory Sciences - Clinical Research & Trials - Dentistry & Oral Health - Emergency & Trauma Services - Healthcare Administration & Operations - Medical Imaging & Radiology - Mental Health & Counseling - Nursing & Clinical Care - Nutrition & Dietetics - Pharmacy & Drug Safety - Physical & Occupational Therapy - Physician & Medical Practice - Revenue Cycle & Medical Coding - Social Work & Community Services - Speech & Audiology Services - Surgical & Procedural Care - Veterinary & Animal Care - Corporate Legal Affairs - Data Privacy & Governance - EHS & Safety Compliance - Financial Crimes & Investigations - Paralegal & Legal Support - Regulatory & Environmental Compliance - Laboratory & Quality Analysis - Maintenance & Reliability - Manufacturing Engineering & Operations - Print & Prepress Operations - Process & Continuous Improvement - Quality Assurance & Control - Skilled Trades & Fabrication - Brand Management & Communications - Campaign & Promotions Management - Content Marketing - Digital & Social Media Marketing - E-Commerce Marketing - Marketing Analytics & Research - Marketing Strategy & Planning - Trade & Channel Marketing - Audio & Music Production - Broadcast & Journalism - Community & Stakeholder Engagement - Corporate Communications & PR - Creative Writing & Storytelling - Curatorial & Exhibition Management - Digital Content & Social Media - Editorial & Copywriting - Event Production & Coordination - Media Strategy & Planning - Performance & Acting - Photography & Visual Media - Technical Writing & Translation - Video & Film Production - Administrative & Office Services - Document & Records Management - Driver & Fleet Operations - Event & Program Coordination - Facilities & Building Management - Field & Technical Operations - Health, Safety & Environment - Inventory & Asset Management - Operational Security Services - Operations Management - Process & Data Management - Procurement & Purchasing - Project & Program Management - Supply Chain & Logistics - Trade & Customs Compliance - Warehouse & Distribution Operations - Arts & Creative Services - Athletic & Sports Professions - Career & Employment Services - Personal & Caregiving Services - Program & Administrative Support - Religious & Spiritual Leadership - Volunteer & Community Services - Compensation, Benefits & Rewards - Diversity, Equity & Inclusion - HR Business Partnering & Generalist - HR Operations & Administration - Learning & Development - Talent Acquisition & Recruiting - Workforce Planning & Analytics - Apparel & Fashion Design - Creative Direction & Art Direction - D Art & Animation - Freelance & Contract Creative - Graphic & Visual Design - Product Development & Innovation - Product Management - UX & UI Design - Web & Digital Design - Architectural & Interior Design - Construction Project Management & Controls - Construction Site Supervision & Trades - Landscaping & Grounds Maintenance - Real Estate Appraisal & Investment - Real Estate Leasing & Property Management - Real Estate Sales & Brokerage - Aviation & Inflight Service - Beauty & Personal Care - Event & Banquet Operations - Food & Beverage Operations - Front Desk & Guest Services - Golf & Club Services - Hotel & Hospitality Management - Kitchen & Culinary Operations - Reservations & Travel Services - Restaurant & Dining Management - Retail Sales & Merchandising - Retail Store Management - Visual Merchandising & Inventory - Account Management & Strategy - Bid & Proposal Management - Channel & Partner Sales - Field & Regional Sales - Healthcare & Insurance Sales - Industry & Specialist Sales - Inside & Outside Sales - International & Global Sales - Philanthropy & Fundraising - Retail & Consumer Sales - Revenue Operations & Analytics - Sales Development & Prospecting - Sales Operations & Enablement - Sales Training & Enablement - Solution Consulting & Presales --- ## What to do next - **Try it on Enrich** — call [Person Enrich](/person-docs/enrichment/introduction) with `fields: ["basic_profile"]` and inspect `basic_profile.normalized_title`. - **Try it on Search** — every [Person Search](/person-docs/search/introduction) result returns `basic_profile.normalized_title`. You can also filter or sort on the nested fields — see the [Person Search reference](/person-docs/search/reference#basic-profile). - **Field references** — [Person Enrich](/person-docs/enrichment/reference) · [Person Search](/person-docs/search/reference). - **Feedback** — JTN is in beta; send taxonomy or accuracy feedback to [support@crustdata.co](mailto:support@crustdata.co). # Person Semantic Search Source: https://docs.crustdata.com/guides/person-semantic-search Use natural-language search on POST /person/search to rank people by role, skills, location, and broader profile context. Person Semantic Search is currently in **beta** on `POST /person/search`. Request fields, ranking behavior, and query-constraint extraction may evolve based on feedback. To share input or request access, write to [support@crustdata.co](mailto:support@crustdata.co). **Use this when** you want to search for people using natural language instead of building every filter by hand, for recruiting searches, persona discovery, market mapping, and exploratory lead lists. Person Semantic Search lets you pass a natural-language `search.query` to [Person Search](/person-docs/search/introduction). Crustdata uses the query to rank matching people by profile context such as title, skills, company history, education, location, and summary text. You can also combine semantic ranking with structured `filters`. This is **current platform behavior** for the live `/person/search` endpoint. The checked-in OpenAPI reference may not yet list the beta `search` object or top-level semantic `mode` field. ### Where semantic search is available | API | Request field | Notes | | ----------------------------------------------------- | -------------- | --------------------------------------------------------------------- | | [Person Search](/person-docs/search/introduction) | `search` | Add a natural-language query to rank and recall people semantically. | | [Person Search](/person-docs/search/introduction) | `mode` | Optional top-level recall mode: `managed` (default) or `exact`. | ### At a glance | Detail | Value | | ---------------------- | ---------------------------------------------------------------- | | **Endpoint** | `POST https://api.crustdata.com/person/search` | | **Auth** | `Authorization: Bearer YOUR_API_KEY` | | **API version** | `x-api-version: 2025-11-01` header | | **Beta request field** | `search.query` | | **Retrieval modes** | `search.mode`: `hybrid` (default), `lexical`, or `semantic` | | **Recall modes** | top-level `mode`: `managed` (default) or `exact` | | **Keyword operators** | `search.query_syntax`: `plain` (default) or `boolean` (lexical + exact only) | | **Query length limit** | `search.query` accepts up to ~32,000 tokens (131,072 characters) | | **Response** | Paginated Person Search shape: `profiles` (each with a relevance `fit` tier), `next_cursor`, `total_count`, `total_count_relation` | | **Pricing** | Same as Person Search: **0.03 credits per result** | | **Rate limit** | Same as Person Search: **30 requests per minute** | --- ## The problem: rigid filter-only search Structured filters are precise, but they force you to know the exact fields, operators, and indexed values before you search. That works for narrow queries, but it breaks down when your intent is broader: - **Persona searches** - "founding engineers at developer tools startups." - **Skill-heavy searches** - "backend engineers with Golang and distributed systems experience." - **Job-description searches** - paste the most important parts of a role and find similar profiles. - **Exploratory recruiting** - start with a plain-language role description, then tighten with filters. Without semantic search, you need to manually translate those ideas into many field-specific filters. ## The solution: natural-language ranking Add a `search` object to a Person Search request: ```json { "search": { "query": "founding engineers at developer tools startups", "mode": "hybrid" }, "limit": 10 } ``` The `search.query` is used to find and rank people whose profiles match the meaning of the query. You can send `search` by itself, or combine it with structured `filters`. Do not send `sorts` with semantic search. Semantic results are already rank-ordered by relevance. ### Limits `search.query` accepts up to ~32,000 tokens (131,072 characters) — enough to paste a full job description. Requests over the limit return **HTTP 400** with an error explaining the limit and the length of the query you sent. ### Retrieval modes The nested `search.mode` field controls which retrieval signals are used for the natural-language query. | `search.mode` | Meaning | Use when | | ------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- | | `hybrid` | Combines lexical keyword matching and semantic vector search. | You want the best default for natural-language people search. | | `lexical` | Uses keyword matching only. | You want exact terms, acronyms, names, or IDs to dominate. | | `semantic` | Uses vector similarity only. | You want concept matching even when profiles use different words. | `hybrid` is the default. Use it unless you have a specific reason to isolate keyword-only or vector-only behavior. ### Recall modes The top-level `mode` field controls how Crustdata combines your natural-language query with explicit `filters`. | `mode` | Behavior | Use when | | ------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------- | | `managed` (default) | Crustdata can extract extra constraints from the query and union them with your filters before reranking. | You want better recall and are comfortable with adaptive results. | | `exact` | Only your explicit `filters` define the hard result set. The query still ranks results inside that set. | You need filters enforced as hard constraints or reproducible counts. | In `managed` mode, explicit `filters` are a recall and ranking signal, not always a hard global constraint. A strong profile can be returned if it matches constraints extracted from the natural-language query. Use `mode: "exact"` when every returned profile must satisfy your explicit filters. ### Boolean keyword operators For precise keyword search, set `search.query_syntax: "boolean"` to read the query as a boolean expression instead of plain text. Punctuation becomes search operators: | Operator | Example | Matches profiles that | | -------------- | ----------------------------- | ----------------------------- | | space (AND) | `golang kubernetes` | contain **both** terms | | `\|` (OR) | `golang \| rust` | contain **either** term | | `+` (require) | `engineer +kubernetes` | must contain the `+` term | | `-` (exclude) | `engineer -manager` | must not contain the `-` term | | `"…"` (phrase) | `"site reliability engineer"` | contain the exact phrase | | `*` (prefix) | `kuber*` | match a term prefix | | `()` (group) | `+(golang \| rust) engineer` | group operators together | A space means **AND** — every space-separated term is required. Use `|` for OR. This follows standard keyword-search conventions. Boolean syntax is honored only with `search.mode: "lexical"` and the top-level recall `mode: "exact"`. Any other combination returns a `400`. The default, `query_syntax: "plain"`, is unchanged — existing queries keep matching as before. ```bash curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "\"site reliability\" +kubernetes (golang | rust) -recruiter", "mode": "lexical", "query_syntax": "boolean" }, "mode": "exact", "fields": ["fit", "basic_profile"], "limit": 5 }' ``` --- ## Example: search from a natural-language query This request finds people who match a plain-language recruiting query. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "backend engineers with Golang and distributed systems experience", "mode": "hybrid" }, "fields": [ "fit", "basic_profile", "experience.employment_details.current" ], "limit": 5 }' ``` ```python Python import os import requests response = requests.post( "https://api.crustdata.com/person/search", headers={ "Authorization": f"Bearer {os.environ['CRUSTDATA_API_KEY']}", "Content-Type": "application/json", "x-api-version": "2025-11-01", }, json={ "search": { "query": "backend engineers with Golang and distributed systems experience", "mode": "hybrid", }, "fields": [ "fit", "basic_profile", "experience.employment_details.current", ], "limit": 5, }, ) response.raise_for_status() profiles = response.json()["profiles"] ``` ```javascript Node.js const response = await fetch("https://api.crustdata.com/person/search", { method: "POST", headers: { Authorization: `Bearer ${process.env.CRUSTDATA_API_KEY}`, "Content-Type": "application/json", "x-api-version": "2025-11-01", }, body: JSON.stringify({ search: { query: "backend engineers with Golang and distributed systems experience", mode: "hybrid", }, fields: [ "fit", "basic_profile", "experience.employment_details.current", ], limit: 5, }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { profiles } = await response.json(); ``` The response uses the normal Person Search shape, plus a per-profile `fit` relevance tier and a top-level `total_count_relation`: ```json Response shape { "profiles": [ { "crustdata_person_id": 123, "fit": "strong", "basic_profile": { "name": "Example Person", "current_title": "Senior Backend Engineer" } } ], "next_cursor": "H4sIA...", "total_count": 1250, "total_count_relation": "eq" } ``` The response above shows shape only. Actual profile values depend on the query, requested `fields`, and account permissions. ### Relevance tier (`fit`) and result counts Semantic responses add two fields on top of the standard Person Search shape: | Field | Where | Meaning | | ---------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- | | `fit` | per profile | Coarse relevance tier for that profile against your query: `strong`, `possible`, or `weak`. Use it to threshold or group results without reading raw scores. | | `total_count_relation` | top level | Whether `total_count` is an exact count (`"eq"`) or a lower bound (`"gte"`). | Use `fit` to keep only high-confidence matches (for example, surface `strong` results and review `possible` ones): ```python strong = [p for p in response.json()["profiles"] if p.get("fit") == "strong"] ``` `fit` is returned by default. If you pass an explicit `fields` list, add `"fit"` to keep it in the response. `fit` can be `null` on deeper result pages, where results are not reranked. `fit` is populated for at most 200 profiles. Any limit set to more than 200 will return fits as 'null'. --- ## Example: semantic ranking inside hard filters Use `mode: "exact"` when your filters must be enforced. This example limits the result set to people in San Francisco, then uses the natural-language query to rank the best machine-learning profiles inside that set. ```bash Request curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "search": { "query": "machine learning engineers who have built recommender systems", "mode": "hybrid" }, "mode": "exact", "filters": { "field": "basic_profile.location.full_location", "type": "(.)", "value": "San Francisco" }, "fields": [ "basic_profile", "experience.employment_details.current" ], "limit": 5 }' ``` Use this pattern when you need deterministic membership for handoffs to agents, workflows, or downstream systems. --- ## When to use each approach | You want to | Use | | -------------------------------------------------------- | -------------------------------------------------------------------- | | Find people from a natural-language role or persona | `search.query` with `search.mode: "hybrid"` | | Match concepts even when profiles use different wording | `search.mode: "semantic"` | | Match exact terms, acronyms, names, or IDs | `search.mode: "lexical"` | | Use boolean operators (`+ - \| "phrase" * ()`) | `search.query_syntax: "boolean"` with `search.mode: "lexical"` + top-level `mode: "exact"` | | Keep explicit filters as hard constraints | top-level `mode: "exact"` | | Maximize recall and let Crustdata improve query parsing | top-level `mode: "managed"` | | Build a fully deterministic filter query | Filter-only [Person Search](/person-docs/search/introduction) | | Discover exact filter values before filtering | [Person Autocomplete](/person-docs/autocomplete/introduction) | --- ## What to do next - **Search with filters** - see [Person Search](/person-docs/search/introduction) for the standard filter workflow. - **Look up operators and fields** - see [Person Search reference](/person-docs/search/reference). - **Discover filter values** - use [Person Autocomplete](/person-docs/autocomplete/introduction). - **Feedback** - Person Semantic Search is in beta; send ranking or query-parsing feedback to [support@crustdata.co](mailto:support@crustdata.co). # Crustdata for Recruiting Source: https://docs.crustdata.com/recruiting/overview Crustdata is a recruiting digital twin. It learns how you think and runs your sourcing workflow inside Claude, on live data covering 800M+ people. From zero to sourcing, market mapping, and pushing candidates into your ATS. Check the [prerequisites](/recruiting/installation/step-0-pre-requisites), follow the [setup](/recruiting/installation/overview), then learn [how to use the skill](/recruiting/how-to-use). ## What this is Most sourcing tools standardize judgment. You type a title, they keyword-match under an AI wrapper, and the best match for a senior engineer comes back as a data analyst. Crustdata is a **recruiting digital twin**: a system that learns how you think and runs your sourcing workflow inside Claude. Brief it on a role the way you would brief a recruiter. It finds the people, reads each full profile, vets every candidate against your criteria, and returns a screen-ready shortlist. It can push results straight into your ATS. The data is why it works. It reads full career history, live, across 800M+ people and 200M+ companies. It can tell whether a person is real and current instead of showing a profile that went stale six months ago. ## What you get Give it a JD or a set of criteria. It returns a vetted, ranked shortlist with the reasoning behind each pick. Before you source, see how deep a pool is, where the people are, and which companies hold them. Reviewed candidates go straight into your ATS, deduped against what is already there. Personal emails, phones, full work history, education, and skills on any profile. Refresh an existing database so employment history is current. Track job changes, open-to-work, and new hires so your pipeline grows on its own. **What it is not:** an autonomous black box. You brief it, you review the shortlist, you decide. ## Results Recruiters using this have closed searches in 20 days that previously took 2.5 months and 15+ conversations. One agency grew sourcing capacity 50% without hiring. The twin does the volume; you do the judgment. ## Get access > Want to get access to the recruiting agent? Visit [app.crustdata.com](https://app.crustdata.com). # Calibrate the Shortlist Source: https://docs.crustdata.com/recruiting/best-practices/calibrate The first list is a draft. Score candidates in the sheet, say why, and let it re-rank. The feedback loop is where the twin gets good. Treat the first list as a draft, then calibrate. The first set of candidates is never 100 percent right. That is expected. Claude does not just create the sheet, it reads it back. The sheet is your feedback channel: - **Write feedback straight into the sheet**, next to each candidate. Score them out of 100 and say why in a few words, for good ones and bad ones both. Then tell Claude to re-rank. It reads your notes from the sheet and refines. - **Use the calibration candidates.** It pulls a pool of a few hundred, shows you a handful to react to, recalibrates, and returns a tighter top 25. - **Say why on the good ones too**, not just the rejects. Otherwise it can over-index on a pattern you did not mean to reward. Each round gets sharper, and it pulls fresh people from the full database when it re-ranks. You are not stuck with the first pool. # Review, Push, Spend Source: https://docs.crustdata.com/recruiting/best-practices/review-push-spend Review the output, push keepers to your ATS deduped, and pull contact details only for the shortlist that survives your review. Skim the list, mark your keepers, and spend credits where they count. ## Review, then push to your ATS The list is verified, but you decide. Skim it, mark your keepers, then have Claude push them into your ATS, deduped against what is already there. Working inside your ATS data? Be direct: tell it to stay on your ATS and read the full resume data, not just surface attributes. It listens when you are specific. ## Spend where it counts Searching and mapping are cheap. Full profiles and contact details cost more per person. 1. Map and iterate on light data first. 2. Pull contact info only for the shortlist that survives your review. 3. **Enrich the twenty you want, not the thousand you do not.** Questions? See the [FAQs](/recruiting/faqs), or jump to [You Are Ready](/recruiting/get-started) and run your first search. # FAQs Source: https://docs.crustdata.com/recruiting/faqs Answers on searching, results, ATS integrations, data freshness, contact info, and Chrome Canary. What recruiters actually ask about searching, results, and data. Setup questions live in the [Installation FAQs](/recruiting/installation/faqs). ## Searching and results Almost always, the skill was not invoked. If you just chatted with Claude instead of starting with `/source-candidates` (or your firm's skill), it did not use your rubric. Start with the skill command and confirm it says it is running the skill. This fixes most "results were off" cases. As much as you can, in the first prompt: JD, ideal profiles with the why, hard negatives, and your must / good / cannot-haves. The best results come from briefs written like you are explaining the role to a new sourcer, not typing a search query. No. Typos and broken English are fine. It only needs to understand what you mean. Yes. The first cut is a draft. Score the candidates in the sheet, say why in a few words for good and bad both, and have it recalibrate. It gets sharper each round and pulls fresh people from the full database when it re-ranks. It reads them. Setup connects Claude to your Google Workspace, so it writes your Sheets and reads them back. That is the feedback loop: type scores and notes into the candidate sheet, tell Claude to refine, and it re-ranks from your edits. Yes. Drop columns you do not care about, add ones you do, and ask for inferred columns, like "has this person worked at a Big Four firm, yes or no." Claude reads the profile and fills it in. No. It does the volume and the first pass so you spend your time deciding, not digging. You stay the decider on every candidate. Bigger searches can run a while; that is normal. If it actually stalled, make sure Chrome Canary is open and your ATS is still logged in, then ask it to continue. Still stuck? Restart Claude and rerun. If that does not fix it, reach out to the Crustdata team. ## Data, ATS, and contact info Any of them. Because the data is structured, we can integrate with whatever ATS you use. We already have integrations across Gem, Loxo, Ashby, Lever, and Cluen Encore, plus home-grown internal systems. If yours is not live yet, tell the Crustdata team what you use; where it has an API we can usually stand it up in one to two weeks. Yes. It can pull candidates from an existing job or list, refresh their data so employment history is current, and score them against your rubric. Refreshing a stale database is one of the highest-value things it does. This is the core of what we do. It reads live career history, so it flags the person who changed jobs last week instead of showing them as a perfect current match. Yes, including personal emails, not just corporate. Pull contact info on your reviewed shortlist rather than the whole raw pool, since it costs more per person. ## Chrome Canary No. Keep it in the background and use your normal Chrome exactly as you do today. Do not reopen it manually. Claude opens it when needed; opening it yourself can stop Claude from driving it correctly. # Setting Up Source: https://docs.crustdata.com/recruiting/installation/overview Eight steps, about fifteen minutes, mostly hands-off. Install the app, connect Claude, and you are ready to source. Eight steps, about fifteen minutes, most of it hands-off. First: what machine are you on? Apple Silicon (M1–M5). Start the eight-step setup below. We set up Windows machines with you on a short guided call. ## The Mac journey 1. [Install the Crustdata app](/recruiting/installation/step-1-install-the-app): three Terminal commands 2. [Log in with your API key](/recruiting/installation/step-2-log-in) 3. [Install all skills](/recruiting/installation/step-3-install-skills): one click, kept up to date for you 4. [Run the prerequisite setup](/recruiting/installation/step-4-prerequisites): green checks all the way down 5. [Install Claude](/recruiting/installation/step-5-install-claude) 6. [Set up Claude Code](/recruiting/installation/step-6-claude-code): project folder, Auto mode, model 7. [Connect Crustdata to Claude](/recruiting/installation/step-7-connect-crustdata) 8. [Restart Claude](/recruiting/installation/step-8-restart) Each step is one page. Do it, hit **Next**, keep going. The longest single step is the Google connection inside step 4: four to six minutes, mostly on its own. Stuck? Check the [Installation FAQs](/recruiting/installation/faqs) first. The common snags (app won't open, repeated Google sign-ins, locked-down work laptop) all have quick fixes. You can always reach out to the Crustdata team. # Prerequisites Source: https://docs.crustdata.com/recruiting/installation/step-0-pre-requisites The five things to have ready before setup: a Mac, a Crustdata API key, a Claude account, a Google account, and your ATS login. Have these ready before you start. Setup covers the rest and takes about fifteen minutes, most of it hands-off. M1 through M5. On Windows? See [Windows setup](/recruiting/installation/windows). We set you up on a short guided call. Signs you in and powers every search. Get your key at [app.crustdata.com/api-keys](https://app.crustdata.com/api-keys). Where you do the work. We suggest the Claude **Max 20x** plan for daily recruiting. The [Installation FAQs](/recruiting/installation/faqs) explain why. For Sheets, Drive, and Docs. Use your **work** account, not a personal one. Only if you want candidates pushed in automatically. Sign in with your work Google account from the start. Starting with a personal Gmail and switching later tangles the setup. Ready? Head to the [setup](/recruiting/installation/overview). # Install the Crustdata App Source: https://docs.crustdata.com/recruiting/installation/step-1-install-the-app Install Homebrew, then the Crustdata app, from the Terminal. Three commands, copy-paste each one. Open the **Terminal** app: press `Command + Space`, type `Terminal`, and press Enter. Run these commands one at a time. Paste, press Enter, wait for each to finish. **1. Install Homebrew** (it may ask for your Mac password): ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` - The password entry is hidden and won't show characters as you type. Type it and press Enter. - If it asks for confirmation, press Enter. **2. Finish the Homebrew installation.** When the first command finishes, it prints a **Next steps** section at the end of the output listing the commands below. Paste these three lines together and press Enter: ```bash echo >> $HOME/.zprofile echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> $HOME/.zprofile eval "$(/opt/homebrew/bin/brew shellenv)" ``` If the **Next steps** output on your machine shows a different path than `/opt/homebrew`, run the lines it prints instead of the ones above. **3. Install the Crustdata app:** ```bash brew install --cask crustdata/tap/crustdata ``` Open **Crustdata** from your Applications folder. ## If that did not work - Download the latest `.dmg` from the [Crustdata releases page](https://github.com/crustdata/homebrew-tap/releases/latest). - Open the downloaded file and drag Crustdata into your Applications folder. - In Terminal, paste this once, then open the app: ```bash xattr -dr com.apple.quarantine /Applications/Crustdata.app ``` If the app won't open the first time, that is macOS's standard security prompt for a fresh app. Go to **System Settings → Privacy & Security**, scroll down, and click **Open Anyway**. It happens once. # Log In With Your API Key Source: https://docs.crustdata.com/recruiting/installation/step-2-log-in Sign in to the Crustdata app with the API key from app.crustdata.com/api-keys. The app uses your Crustdata API key to sign you in. 1. Go to [app.crustdata.com/api-keys](https://app.crustdata.com/api-keys). Sign in with your Crustdata account if asked. 2. Copy your API key. 3. Open the Crustdata app and paste the key into the login field. You should now see your account and remaining credits. # Install All Skills Source: https://docs.crustdata.com/recruiting/installation/step-3-install-skills Install the recruiting skills from the Skills tab in the Crustdata app. The app keeps them up to date for you. Skills are the guided recruiting tools Claude uses: sourcing, market mapping, and more. 1. Open the **Skills** tab in the Crustdata app. 2. Install the skills shown. The app installs them into Claude and keeps them up to date. 3. Wait until every skill shows as installed. # Run the Prerequisite Setup Source: https://docs.crustdata.com/recruiting/installation/step-4-prerequisites The Recruiting tab checks every tool your workflow needs. Work through the list until everything shows a green check. Open the **Recruiting** tab in the Crustdata app. It checks each tool your workflow needs and shows a green check when ready. Most items will already be green. For any that are not, click their button and wait. Go through them **in order**. ### 1. Git Usually already installed and green. If not, follow the prompt to install it. ### 2. Node.js + npm Click **Install** if not green. The runtime the recruiting tools depend on. ### 3. Python 3 Click **Install** or **Update** if not green. ### 4. Google Workspace CLI (gws) Click **Install** if not green. Lets Claude work with your Google Sheets, Drive, Docs, and Gmail. Sign-in happens in the next item. ### 5. Google connection Click **Set up**. A guided wizard installs the Google Cloud tools, signs you in, creates your project, and turns on the Google APIs. Takes **4–6 minutes**. - **One step happens in your browser:** a Google Cloud page opens where you create a login key and download it. The wizard shows exactly what to click. When done, it saves the key and finishes signing you in. - **Asked to sign in 3–4 times?** Normal. Setup authenticates against several Google services in sequence. Let it finish. - **"First time using Google Cloud?" error:** click **Open Console**, sign in, accept the Terms of Service, then click **Retry**. Use your work Google account, not a personal one. Starting with a personal Gmail and switching later tangles the setup. ### 6. Python cryptography Click **Install** if not green. Needed for some recruiting integrations. ### 7. SSL_CERT_FILE Optional. Leave it alone unless the app flags it, then click its button. ### 8. Chrome Canary Click **Launch (debug)**. Chrome Canary, the yellow Chrome, is a separate browser Claude uses for recruiting tasks, kept apart from your everyday Chrome. In the window that opens: - Sign in to your **Google account**. - Sign in to any recruiting software you use, such as Gem, Loxo, Ashby, or Greenhouse. - Do **not** select "Make Chrome Canary your default browser". You do this once. Then leave it in the background; Claude opens it when needed. More in the [FAQs](/recruiting/faqs). # Install Claude Source: https://docs.crustdata.com/recruiting/installation/step-5-install-claude Download Claude for Mac from claude.ai/download and sign in. Claude is where you do your recruiting work. Claude is the app where you do your recruiting work. Your skills and Crustdata run inside it. 1. Go to [claude.ai/download](https://claude.ai/download) and download Claude for Mac. 2. Open the downloaded file and drag Claude into your Applications folder. 3. Open Claude. If prompted, click **Open** to allow it to run. 4. Sign in, or create an account. Which plan? **Max 20x** if you source regularly. A sourcing run reads full career histories over hundreds of profiles, and Pro runs out mid-search. The [Installation FAQs](/recruiting/installation/faqs) have the full comparison. # Set Up Claude Code Source: https://docs.crustdata.com/recruiting/installation/step-6-claude-code Pick a project folder, turn on Auto mode, and select Opus 4.8 on High effort. In the Claude app, click **Code** in the top-left corner. Then set up your workspace: three small things. ## Create and select a project folder 1. Just above the message box, click **Select folder**. 2. In the file picker, choose an easy-to-find location, such as your Desktop. 3. Create a new folder named `Crustdata`. 4. Open the `Crustdata` folder and click **Open**. 5. If prompted, select **Trust Workspace**. You should now see `Crustdata` listed as the active folder. ![Claude Code with the Crustdata folder selected](/images/recruiting/claude-code-folder.png) All files Claude creates during runs land in this folder. Always run Claude from this same folder. The most common setup snag is having things installed in one folder and running Claude from another. ## Turn on Auto mode 1. Look to the bottom-left of the window, below the chat box. 2. Click the mode selector and choose **Auto**. 3. Select **Enable Auto Mode**. ![The mode selector with Auto mode highlighted](/images/recruiting/auto-mode.png) In Auto mode, Claude completes tasks without stopping to ask for each step. Without it, it asks for permission hundreds of times in one run. ## Choose the model 1. Look to the bottom-right of the window, below the chat box. 2. Click the model selector and choose **Opus 4.8**. 3. Confirm Effort is **High** (the default for Opus 4.8). ![The model selector set to Opus 4.8 with High effort](/images/recruiting/model-selector.png) Opus 4.8 on High effort gives Claude more time to think and the best results. ## FAQ Turn on Auto mode. It lets Claude complete tasks without stopping to ask for each step; otherwise it asks hundreds of times in one run. Do **not** use "Dangerously skip permissions," especially once your ATS is connected. Auto mode gives you the speed without the risk. All your sheets and files land in one place, and Claude always runs where things are installed. The most common setup snag is having things installed in one folder and running Claude from another. # Connect Crustdata to Claude Source: https://docs.crustdata.com/recruiting/installation/step-7-connect-crustdata Add the Crustdata connector in Claude and set both tool sections to Always Allow. This lets Claude use your Crustdata data while it works. 1. In Claude, open **Customize** from the top-left. ![The Customize option in Claude's top-left menu](/images/recruiting/claude-customize.png) 2. Click **Connectors** in the left menu. ![The Connectors item in the settings menu](/images/recruiting/connectors-menu.png) 3. Click **Add**, then **Browse connectors** in the top-right. ![The Browse connectors button](/images/recruiting/browse-connectors.png) 4. Find **Crustdata** in the list, click **+**, then **Connect**. 5. A Crustdata login page opens in your browser. Sign in with your Crustdata credentials. If you were given a Crustdata username and password, use those. 6. Return to Claude. The connector should show as connected. ## Allow the Crustdata tools Change permissions so Claude does not stop to ask each time: 1. Find the **Read-only tools** section in the Crustdata connector and turn on **Always Allow**. 2. Find the **Write/delete tools** section and turn on **Always Allow**. ![Both Crustdata tool sections set to Always Allow](/images/recruiting/always-allow.png) When both show Always Allow, Claude can use Crustdata automatically. ## FAQ On company or team Claude plans, connectors need a one-time approval from whoever owns your Claude workspace. Email your admin. One approval unblocks the whole team. Until then, reach out to us for a temporary path. # Restart Claude Source: https://docs.crustdata.com/recruiting/installation/step-8-restart Fully quit and reopen Claude so it picks up your new skills and settings. Fully quit and reopen Claude so it picks up your new skills and settings. 1. Quit Claude completely: press `Command + Q`, or right-click the Claude icon in the Dock and choose **Quit**. 2. Wait a few seconds. 3. Open Claude again from your Applications folder or the Dock. ## You are set up Once every prerequisite in the Crustdata app shows a green check, you are ready to source. Next: [How to Use the Skill](/recruiting/how-to-use). The model, the slash command, Auto mode, and what to put in your first prompt. # Installation FAQs Source: https://docs.crustdata.com/recruiting/installation/faqs Setup questions: how long it takes, which Claude plan, Windows, locked-down laptops, Google accounts, and Chrome Canary. The questions that come up during setup. Step-specific snags are covered on their step pages too. About fifteen minutes. The Google connection step is the longest at four to six minutes, and it runs mostly on its own. Get **Max 20x** if you source regularly. A sourcing run is heavy: Claude reads full career histories over hundreds of profiles, so one deep search burns a lot of usage. On Pro you run out fast, mid-search. Max 5x is fine for light or occasional sourcing. Max 20x gives the headroom to run several deep searches a day without hitting a wall. If you do hit a limit, it resets on a rolling window of a few hours. Use **Opus 4.8** for sourcing runs. It is one of the strongest models for long, multi-step work: reading hundreds of full profiles, holding your rubric, and reasoning about fit. Sonnet is faster and lighter on your usage, and it is fine for quick follow-up questions on a finished list. Set the model yourself in the selector at the bottom-right of the chat box, and check it before a big run. Two different things. **Context** is how much of the current conversation Claude can hold at once. A deep search fills it because every profile it reads takes up space. You do not need to manage this: when the conversation gets long, Claude automatically summarizes the earlier part and keeps going. Starting a new session per role keeps context focused. **Usage limits** are how much you can run on your plan. They work on a rolling window of a few hours, plus a weekly cap. Check where you stand in the Claude app under **Settings → Usage**, or type `/usage` in Claude Code. You are covered. The one-click app is Mac today; a Windows app is on the way. Meanwhile we set Windows machines up with you on a short guided call. Tell the Crustdata team you are on Windows when you start. See [Windows setup](/recruiting/installation/windows). Almost always, with one assist from whoever manages your machines. Locked-down laptops hold back two things: installing new tools, and approving new connectors in Claude. Have someone with admin access reachable during setup to approve the Crustdata connector once and clear any blocked install prompts. Both are one-time. After that, daily sourcing needs nothing special. Not sure what your laptop allows? Reach out to us and we will sort it out. Your **work** Google account, not a personal one. Starting with a personal Gmail and switching later tangles the setup. If your work account says to contact your administrator, that is an admin restriction; loop in whoever manages your Google Workspace. No. The Terminal is used once during install. After that, everything happens inside the Crustdata app and Claude. The yellow Chrome, a testing version that gets the newest updates first. Claude uses it for browser automation, kept separate so your normal browsing is never touched. Set it up once in [step 4](/recruiting/installation/step-4-prerequisites): open it, sign in to Google and your ATS. Then leave it in the background. Do not reopen it manually if it closes; Claude launches it in a special mode that lets it drive the browser, and a window you open yourself does not have that connection, so Claude cannot use it. More questions about searching, results, and ATS pushes: see the [FAQs](/recruiting/faqs). # How to Use the Skill Source: https://docs.crustdata.com/recruiting/how-to-use The core usage concepts: invoke the skill with a slash command, run Opus 4.8 on High effort in Auto mode, and pack your first prompt with context. Four things: invoke the skill, use the right settings, pack your first prompt with context, and keep one search per session. ## 1. Always start with the skill Type the skill command first, **every single time**: `/source-candidates` (or your firm's named skill), then your brief. ``` /source-candidates Head of Engineering for a Series B fintech in London. Must have scaled a team past 30. JD pasted below… ``` If you skip it and just chat with Claude, you lose the whole thing. The skill is the instruction manual. Without it, Claude free-searches the open web and you get nonsense back, like a retired CFO as your top match. With it, you get the rubric matching, the ranking, and the reasoning. Quick check: when a run starts, it should say it is running the skill. If it does not, stop and start over with the command. Two more things about the skill itself: - If we built you a **custom skill**, use that exact name, not the generic one. It carries your search logic. - **Update your skills before a big run.** The Crustdata app keeps them current. ## 2. Use the right model and mode | Setting | Use | Why | | --- | --- | --- | | Model | **Opus 4.8** | If it drops to a smaller model, switch it back. | | Effort | **High** | Best results in our benchmarking. Bump to Extra only for very nuanced searches. | | Mode | **Auto** | Otherwise Claude stops to ask permission hundreds of times per run. | Auto mode, yes. "Dangerously skip permissions," no, especially once your ATS is connected. Auto mode gives you the speed without the risk. ## 3. Pack the first prompt with context Put what you are thinking into words once, up front. Give it: - **The JD**, pasted or linked. JDs are noisy, so say so: *"It says 25 to 35 years, but I am fine with 20."* - **Ideal-candidate profiles, with why they are ideal.** Four good profile URLs alone make it hunt for patterns you do not care about. Say what makes them right. - **Hard negatives.** The one people skip, and the one that matters most. If a type looks perfect on paper but is wrong, drop those profiles in and say why they fool you. Negative examples sharpen it more than positive ones. - **The frame:** must-haves, good-to-haves, cannot-haves. Examples under each are better. You can also attach an Excel of target companies or your own rubric. Grammar does not matter. Typos and broken English are fine as long as the point comes through. Treat the brief like explaining the role to a new sourcer, not typing a search query. ## 4. One session per search A session is one search space. Refine the same role in the same session. Start a new role or new criteria in a new session. Sessions stay in the sidebar, so nothing is lost. --- That is the core loop. The first list is a draft; see [Calibrate the Shortlist](/recruiting/best-practices/calibrate) to sharpen it. # You Are Ready Source: https://docs.crustdata.com/recruiting/get-started Open Claude, type /sour to see your recruiting skills, pick one, and describe the role. Open Claude, type `/sour` to see your recruiting skills, pick one, and describe the role. ``` /source-candidates Find me graduates from Stanford University working at Google with 5-15 years of experience. Location is USA, remote accepted. ``` The more context you give, the closer the shortlist. The loop: 1. **Start with the skill:** `/source-candidates` (or your firm's skill), every time. 2. **Brief it fully:** JD, ideal profiles with the why, hard negatives, must/good/cannot-haves. 3. **Calibrate:** score the draft list in the sheet, say why, re-rank. 4. **Review and push:** mark keepers, send them to your ATS deduped. Stuck at any step? Reach out to the Crustdata team. # Changelog Source: https://docs.crustdata.com/openapi-specs/2025-11-01/changelog Product updates and announcements for the Crustdata API. ## `GET /account/credits`: per-top-up breakdown with `credit_topups` [Account credits](/general/credits) responses now include a **`credit_topups`** array for accounts with live top-ups — one entry per top-up still holding credits, sorted soonest expiry first: - **`type`**: how the top-up entered your account — `purchase` (one-time purchase), `auto_topup` (automatic reload), or `granted` (added by Crustdata). - **`granted_credits`** / **`remaining_credits`**: what the top-up originally added and what's still unspent. - **`granted_at`** / **`expires_at`**: when it was added and when its unspent credits expire (`expires_at` is `null` for top-ups without an expiry). The key is **absent** for accounts without live top-ups, so existing responses are unchanged. Like `wallets`, the list is informational — keep gating spending decisions on `account.credits`. Available on `GET /account/credits` with `x-api-version: 2025-11-01`. ## Company Search: filter by any office location with `locations.all_office_addresses` [Company Search](/company-docs/search/introduction) now accepts **`locations.all_office_addresses`** as a filter field, so you can match companies on any of their offices, not only the headquarters that the other `locations.*` filters target. The field was already returned in responses; it is now filterable too. - **Substring match**: office addresses are free-text strings (e.g. `"HITEC City, Madhapur, Hyderabad, Telangana, 500081, IN"`), so filter with a city or region substring using `(.)` / `contains`. - **Combines with HQ filters**: pair it with `locations.country` in an `and` group to express queries like *headquartered in the USA with an office in Hyderabad*: `locations.country` `=` `"USA"` and `locations.all_office_addresses` `(.)` `"Hyderabad"`. - **Not sortable**: the field is filterable and returned, but not valid in `sorts`. See [Locations](/company-docs/search/reference#locations) in the search reference. Available on `POST /company/search` with `x-api-version: 2025-11-01`. ## `is_null` and `is_not_null` filter operators on Person Search Person Search now accepts the **`is_null`** and **`is_not_null`** filter operators, so you can match on field presence directly instead of fetching fields and checking for null in your own code. - **`is_null`** — matches profiles where the field has no value. On nested-array fields the check is element-based: an empty `experience.employment_details.current` array matches `experience.employment_details.current.name` `is_null`. - **`is_not_null`** — matches profiles where at least one non-null value is present for the field. - **`value` is required** — send a literal `"value": null`; omitting the `value` key returns `400`. Combine the two to find people who left a job and have not started a new one: `experience.employment_details.current.name` `is_null` and `experience.employment_details.past.name` `is_not_null` in one `and` group. See [null checks](/person-docs/search/reference#null-checks-is_null-and-is_not_null) for details. Available on `POST /person/search` with `x-api-version: 2025-11-01`. Company Search and Job Search already accept both operators. ## Every API response now reports what it cost: the `X-Credits-Used` header Every response from the data APIs — search, enrich, identify, autocomplete, web, and batch — now carries an `X-Credits-Used` header with the exact decimal credits that request deducted, read from the same ledger that bills you. Log it per call to attribute spend by customer, agent, or workflow, sum it to keep an automation inside a budget, or reconcile it against invoices — without polling the balance endpoint. - **Exact decimal** — `0.03` for a one-result person search, `3` for a three-company enrich. Fractional costs report exactly. - **Present on error responses too** — errors report `0`, so the header is there on every call you log. - **`0` is meaningful** — endpoints that consume no credits report `0`, and so do asynchronous submissions (batch jobs and background-job searches), which are billed when the job runs rather than at submit time. - **Readable from browsers** — the header is exposed cross-origin, so dashboard-style apps can read it from JavaScript. - **Declared in the specs** — every response in the [OpenAPI specs](/api-reference/company-apis/search-companies-with-indexed-fields-only) documents the header. Responses generated before a request reaches the API, such as rate-limit `429`s, carry no header. See [Per-call usage on the Credits page](/general/credits#per-call-usage-the-x-credits-used-header) for details and examples. Available on every data API endpoint. ## Job Search: company job-board postings, `job_details.source`, and `job_details.employment_type` [Job Search](/job-docs/search/introduction) now includes postings published directly on company job boards — about 3 million postings joining the roughly 45 million existing listings. Two new `job_details` fields expose this: - **`job_details.source`** — the platform each posting comes from. Present on every job: `professional_network` for the existing listings, or one of the job-board platforms — `workday`, `smartrecruiters`, `greenhouse`, `workable`, `lever`, `icims`, `ashby`, `rippling`, `yc`. Filterable (`=`, `in`, …) and groupable via `group_by`; not sortable. See the [Job details catalog](/job-docs/search/reference#job-details-job_details). - **`job_details.employment_type`** — the employment type declared by the posting: `full_time`, `part_time`, `contract`, `intern`, or `temporary`. Currently populated for roughly 85% of job-board postings and `null` for now when `source` is `professional_network`. Filterable and groupable; not sortable. - **Search-only** — both fields are returned by `POST /job/search` only; [Live Search](/job-docs/search/live-search) does not include them. Available on `POST /job/search` with `x-api-version: 2025-11-01`. ## Company Search: `geo_distance` and `geo_exclude` on company headquarters [Company Search](/company-docs/search/introduction) now filters companies by true geographic radius around their headquarters. Pick a point — either a place name like "Palo Alto, CA" or a pair of coordinates — and set a distance. `geo_distance` then returns every company whose headquarters falls within that distance of the point, instead of matching on location text and listing city names one at a time. - **`geo_distance`** — returns companies headquartered inside the radius. - **`geo_exclude`** — the inverse of `geo_distance`. It returns companies headquartered outside the radius and excludes the ones inside it, so you can skip a region you already cover. See the [geographic radius filters reference](/company-docs/search/reference#geo_distance--radius-around-headquarters). - **Setting the point and the distance** — the point is what the radius is measured from. Pass it as `location` (a place name, geocoded server-side) or as `lat_lng` (`[lat, lng]`, which skips geocoding), then add a `distance` and a `unit` such as `km` or `mi`. - **One field path** — apply both operators to `locations.headquarters`, which targets the company's geocoded headquarters point. - **Normalized location facets** — `locations.state` and `locations.city` are newly filterable and match normalized values derived from geocoding the headquarters address, with search responses returning the same normalized values. `locations.country` accepts **both** ISO-3 codes (`"USA"`) and full country names (`"United States"`) — either form matches the same companies, so existing filters keep working — and responses return the normalized full country name. See [Locations](/company-docs/search/reference#locations). Available on `POST /company/search` with `x-api-version: 2025-11-01`. ## Auto top-up: refill credits automatically before the balance runs out Save a card once, set a threshold and a reload amount, and Crustdata tops your balance up on its own. The balance is checked every time credits are spent, and the moment it drops below your threshold the saved card is charged for the reload package and the credits land immediately — instead of buying credits by hand each time the balance runs down. The feature is free on every plan and stays off until you turn it on. - **Turn it on from the [Credits page](https://app.crustdata.com/billing/credits)** — save a card through a Stripe-hosted page and accept the auto-charge consent, then set two numbers: a threshold and a reload amount. Enabling requires a balance above the threshold you pick, so buy credits manually once if you are already below it. Turning it off is one click, any time. - **What a top-up does** — the saved card is charged at the price shown during setup, the live balance refreshes right away, and you get a receipt email plus a real Stripe invoice in the **Invoices** tab, identical to a manual purchase. - **Monthly recharge limit** — an optional cap on how many credits auto top-up can add per calendar month. It resets each calendar month. - **Safety rails** — at most 3 auto top-ups per day per account, and double-charge protection means one threshold breach can only ever produce one charge. - **Failed charges** — a declined or expired card gets an email with the reason and the fix, Crustdata retries after a cooldown, and after 3 consecutive failures auto top-up disables itself and says so. Cards nearing expiry get a warning email before they lapse. - **Thresholds and reload size** — the threshold is an absolute credit balance, and the minimum reload is 500 credits. The exact price is shown before you save. - **Pricing** — the feature is free, with no credit cost and no plan gate. Each top-up buys the selected credit package at the standard self-serve rate — the same volume-tiered pricing as a manual purchase. - **Coming next** — negotiated per-account rates applied to auto top-ups. Every top-up charges the standard self-serve rate today. Manage auto top-up on the [Credits page](https://app.crustdata.com/billing/credits); setup steps are in [Credits — Auto top-up](/general/credits#auto-top-up). Available to all users on all plans. ## Person Search: match conditions across different entries in a profile [Person Search](/person-docs/search/introduction) adds `all_of`, a composite operator whose conditions are each matched by a **different entry** in a person's history array. A person record holds arrays with many entries — one for every past job and every degree — so "Engineer at Company A and Manager at Company B" now returns people who held each title at each company, rather than one job carrying both titles. A plain `and` on the same array path keeps its existing same-entry meaning, so your current searches using this operator don't change. - **`all_of`** — a composite operator whose conditions are each matched independently, by their own array entry. Use it for compound cross-entry queries, like a role at one company and a degree from another school, or a past job plus a current one. See the [operator reference](/person-docs/search/reference#nested-array-matching-all_of-and-has_all). - **`has_all`** — shorthand for one field with several values ("worked at both A and B"). It expands to one `all_of` block per value. - **Works across person history** — employment, education, certifications, and honors. - **Combines with document-level filters** — nest `all_of` inside a plain `and` to add profile-level conditions, so you can ask for a role at a company and a person based in a given country in one query. - **One matching entry per condition** — each condition is satisfied by at least one entry in the array. Conditions do not have to land on the same entry, and they do not have to cover every entry. - **Backward compatible** — a plain `and` on one array path still matches within a single entry, so existing queries return exactly what they returned before. - **Pricing** — included on all plans. These are new operators on an existing endpoint, so there is no new credit cost and the `/person/search` rate limit is unchanged. Available on `POST /person/search` with `x-api-version: 2025-11-01`, for all users, at no extra cost. ## Job Search: `geo_distance` and `geo_exclude` on job location [Job Search](/job-docs/search/introduction) now filters postings by true geographic radius. Pick a point — either a place name like "San Francisco" or a pair of coordinates — and set a distance. `geo_distance` then returns every job posting whose location falls within that distance of the point, instead of matching on exact location text and listing city names one at a time. - **`geo_distance`** — returns job postings whose location falls inside the radius. - **`geo_exclude`** — the inverse of `geo_distance`. It returns job postings whose location falls outside the radius and excludes the ones inside it, so you can skip a region you already cover. See the [geographic radius filters reference](/job-docs/search/reference#geographic-radius-filters-geo_distance--geo_exclude). - **Setting the point and the distance** — the point is what the radius is measured from. Pass it as `location` (a place name, geocoded server-side) or as `lat_lng` (`[lat, lng]`, which skips geocoding), then add a `distance` and a `unit` such as `km` or `mi`. - **Either field path** — use `location.raw` or `location`; both target the same geo point. - **Geocoded on ingest** — each posting's location text is turned into real coordinates when Crustdata adds the posting to the database, so the filter compares coordinates rather than matching location strings, and postings in towns you never thought to list still come back. - **Pricing** — no new credit cost. A radius query bills the same as any other `/job/search` call, and the endpoint's rate limit is unchanged. Available on `POST /job/search` with `x-api-version: 2025-11-01`, for all users, at no extra cost. ## Teams: one workspace with shared credits, API keys, and roles Bring your whole team into one Crustdata account. Invite teammates by email, give each one a role, and work from a shared credit pool and one set of API keys. Credits and keys now belong to the **team**, so everyone works from the same place instead of a separate login each. Free on every plan, and existing accounts were migrated automatically. - **Every account became a team** - your existing account is now a team named **Personal** with you as the owner. Nothing changes until you invite someone. - **Invite by role** - **Owner** (the creator; bypasses every permission check and cannot be reassigned), **Admin** (invites members, buys credits, manages payment methods, and creates and manages API keys, watchers, and integrations), and **Member** (runs playground searches and enrichments against team credits and views usage, keys, watchers, exports, and billing, but cannot purchase, manage keys, or change the team). - **Shared credits and API keys** - everyone on a team draws from one credit pool and one set of `cd_` keys, gated by role. Rate limits are shared across the team. - **Team switcher** - one person can belong to and create several teams and switch between them from the sidebar. Credits never move between teams: each team buys and holds its own, which is a clean way to cap spend for a project, client, or department. - **Per-team billing** - each team has its own wallet, subscription, invoices, and saved cards. A new team starts at **0 credits** and buys its own. - **Default limits** - up to **2 teams** per user and **10 members** per team. Reach out to sales to raise either limit. Manage your team from the **Team** section of the sidebar at [app.crustdata.com](https://app.crustdata.com); teammates accept an invite from [app.crustdata.com/account](https://app.crustdata.com/account). Available to all users on all plans at no extra cost. ## Per-API-key limits: cap credits and restrict endpoints on one key A workspace admin can now attach two independent limits to an individual API key from the dashboard **API Keys** page, without affecting the rest of the account. - **Monthly credit cap** — cap how many account credits one key can spend per month; it resets on the 1st (UTC). Use it for a key you hand to a teammate, a customer, or an unattended job. When the cap is reached, that key returns `402` with `error.type` `insufficient_credits`. Setting the cap to `0` stops the key until an admin raises it. See [Per-key monthly limit](/general/credits#per-key-monthly-limit). - **Endpoint access** — restrict a key to a subset of the endpoints the account has enabled. A call to an endpoint outside that subset returns `403` with `error.type` `permission_error`. See [Per-key endpoint access](/general/permissions#per-key-endpoint-access). Both errors reuse the **same status and `error.type`** as their account-level equivalents, so clients that already handle account-level `402` and `403` need no new code — only the message differs, naming the key's limit so you can tell which one was hit. Handling patterns are in [Best practices](/openapi-specs/2025-11-01/best-practices#handle-insufficient-credits). ## Usage and logs: see and filter activity by API key Your dashboard now records which API key made each request, so you can tell your keys apart in usage and request logs instead of seeing one merged total. - **Per-key attribution** — every request is tagged with the key that made it, including requests from keys you have since deleted, so past activity stays attributed after a key is rotated out. - **Filter by key** — narrow the **Usage** and **Logs** views to one or more keys to see exactly what each key spends and calls. Together with per-key [credit caps](/general/credits#per-key-monthly-limit) and [endpoint access](/general/permissions#per-key-endpoint-access), you can set a key's limits and then confirm what it actually did. View it in the **Usage** and **Logs** sections of your [dashboard](https://app.crustdata.com/). Available to all accounts. ## Technographics: filter by and enrich a company's technology stack Crustdata now exposes the technologies detected for a company across [Company Enrich](/company-docs/enrichment/introduction), [Company Search](/company-docs/search/introduction), and [Company Autocomplete](/company-docs/autocomplete/introduction). - **`technographics` field group on Enrich** — request it explicitly via `fields` (it is never included by default) to get `total_technologies`, `top_technologies`, the full `technologies` list (each entry with `name`, detection `sources`, `posting_count`, catalog `categories`, a `description`, and `evidence`), and `updated_at`. See the [field reference](/company-docs/enrichment/reference#technographics-fields). - **Search filters** — filter on `technographics.total_technologies` (also sortable), `technographics.top_technologies`, `technographics.technologies.name`, `technographics.technologies.category`, and `technographics.technologies.super_category`. Technographics values are not returned in search responses — filter, then enrich. See [searchable fields](/company-docs/search/reference#technographics-filter-only). - **Autocomplete** — two new company-scope fields: `technology` (technology names) and `technology_category` (category slugs) for discovering valid filter values. - **Access and pricing** — `technographics` requires field-level permission on your account. It is a billed add-on: **+2 credits** per company that returns technographics data, on top of the base enrich cost; companies with no technographics data are not charged the add-on. The same add-on applies to [Batch Company Enrich](/company-docs/enrichment/batch). Available on `POST /company/enrich`, `POST /batch/company/enrich`, `POST /company/search`, and `POST /company/search/autocomplete` with `x-api-version: 2025-11-01`. ## Company lookup: more accurate domain matching for large organizations We improved domain matching in [Company Identify](/company-docs/identify/introduction) and [Company Enrich](/company-docs/enrichment/introduction), especially for large organizations and companies with subsidiaries. On these domains a lookup could sometimes surface a related record (a subsidiary, a regional arm, an acquired company, or a showcase page) instead of the parent company. Matching now favors the primary company, automatically. - **Better matches for large organizations** - a domain maps to the parent company rather than a subsidiary, an acquired company, or a showcase page. For example, `amazon.com` resolves to Amazon, `paypal.com` to PayPal, and `schwab.com` to Charles Schwab. - **Nothing to change** - same endpoints, same request shape, and same response shape. No new fields and no confidence score to handle; matches just get better automatically. - **Biggest gains on high-volume domains** - in testing across Fortune 500 US companies, top-1 accuracy by domain rose from 88% to 94%. No extra cost: [Company Identify](/company-docs/identify/introduction) stays free and [Company Enrich](/company-docs/enrichment/introduction) credits are unchanged. Available on `POST /company/identify` and `POST /company/enrich` with `x-api-version: 2025-11-01`. No changes to the legacy identity endpoints. ## Person Search: natural-language `search.query` (beta) [Person Search](/person-docs/search/introduction) adds `search.query`, a natural-language query that returns a ranked list of people instead of requiring you to hand-build every filter. Crustdata pulls the hard constraints out of the query and applies them as exact filters, semantically matches the rest against the full profile, and orders results by fit. - **`search.query`** - describe who you want in plain language; results are ranked by profile context (title, skills, company history, education, location, summary). - **`search.mode`** - `hybrid` (default, blends keyword and concept matching) and `semantic` (concepts). For exact keyword matching, see the keyword search changelog below. - **`mode`** - two recall modes: `managed` (default; the query may add constraints) and `exact` (your filters are hard constraints and the query only ranks within them). - **`fit`** - each profile returns a relevance tier (`strong` / `possible` / `weak`) alongside `total_count_relation`, so you can keep only high-confidence matches. - **Not included** - sorts; semantic results come back already rank-ordered by relevance. - **Pricing** - Person Search parity at **0.03 credits per result**, no premium for semantic ranking, and the same **30 requests/minute** rate limit. [Semantic Search Guide →](/guides/person-semantic-search) ## Person Search: keyword search with Boolean operators [Person Search](/person-docs/search/introduction) now lets you search for keywords across the full profile. Set `search.mode: "lexical"` to match on keywords, so exact terms, acronyms, names, and IDs are found wherever they appear in the profile (title, skills, company history, education, location, summary). - **`search.mode: "lexical"`** - keyword matching across the whole profile, for when you want exact terms to be found rather than concept ranking. - **`search.query_syntax: "boolean"`** - read the query as a Boolean expression instead of plain text: a space means **AND** (every term required), `|` means **OR**, and you can combine terms for precise keyword search. Honored only with `search.mode: "lexical"` and top-level `mode: "exact"`. [Boolean keyword operators →](/guides/person-semantic-search#boolean-keyword-operators) ## Multiple API keys on one account Every Crustdata account used to have a single API key. You can now create and manage several named keys on one account, so you can keep dev and prod apart, give each project or teammate its own key, and switch off a key without affecting the others. Works across the API and MCP. Every existing key was migrated automatically and named **Default**. - **Create and name keys** - add as many keys as you need at [app.crustdata.com/api-keys](https://app.crustdata.com/api-keys), rename them anytime, and copy the generated `cd_` key into any API or MCP request. See [Authentication](/general/introduction). - **Per-key status** - `ACTIVE` (usable across the API and MCP), `INACTIVE` (blocked instantly and reversible), and `DELETED` (permanently retired; the account's other keys keep working). - **Shared credits and rate limit** - all keys on an account draw from one credit pool and one rate-limit bucket today, so keys separate and organize work rather than cap spend or speed per key. - **Coming next** - credit balance and limits set at the individual API-key level. Available to all users on all plans, at no extra cost, across the API and MCP. Manage keys at [app.crustdata.com/api-keys](https://app.crustdata.com/api-keys). ## Person Contact Enrich: business emails, personal emails, and phone numbers [Person Contact Enrich](/person-docs/contact/enrich) is a new endpoint that returns contact data for up to **25 people** in a single request. Supply `professional_network_profile_urls` or `business_emails` as input and get business emails, personal emails, and phone numbers back in the same response — no polling. - **Identifiers** — pass exactly one of `professional_network_profile_urls` (a profile URL) or `business_emails` to reverse-look-up a person, up to 25 values per request. - **`fields`** — select which contact data to return: `contact.business_emails`, `contact.personal_emails`, `contact.phone_numbers`, and `contact.websites` (or `contact` for all). Each email entry includes its deliverability `status`. See the [contact fields reference](/person-docs/contact/enrich#contact-fields). - **Response** — the same record shape as `/person/enrich`, restricted to the `person_data.contact` object. For higher fill rate on bulk lists, use the async [Batch Contact Enrich](/person-docs/contact/batch) job instead. Available on `POST /person/contact/enrich` with `x-api-version: 2025-11-01`. ## Person Search: `geo_exclude` filter operator [Person Search](/person-docs/search/introduction) adds a `geo_exclude` filter operator — the inverse of `geo_distance`. It excludes profiles **inside** a radius and keeps everyone else, so you can carve out a metro you already cover or target candidates outside a region. - **`geo_exclude`** takes the same value object as `geo_distance`: a centre given as `location` (geocoded server-side) or `lat_lng` (`[lat, lng]`, skips geocoding), a required `distance`, and an optional `unit` (`km`, `mi`, `miles`, `m`, `meters`, `ft`, `feet`; defaults to `km`). When both `location` and `lat_lng` are supplied, `lat_lng` wins. See the [operator reference](/person-docs/search/reference#geo_exclude--exclude-a-radius) and a [worked example](/person-docs/search/introduction#exclude-a-geographic-radius). Available on `POST /person/search` with `x-api-version: 2025-11-01`. ## Professional-network name and normalized title on Person profiles [Person Search](/person-docs/search/introduction) and [Person Enrich](/person-docs/enrichment/introduction) now return additional `basic_profile` identity fields. - **`basic_profile.professional_network_name`** — the display name on the person's professional-network profile, returned by both `/person/search` and `/person/enrich`. On search it is also a filterable field (not sortable). See the [search field reference](/person-docs/search/reference#searchable-fields) and the [enrich response reference](/person-docs/enrichment/reference#person_data-sections). - **`basic_profile.normalized_title`** — the normalized job-title object (`matched_title`, `department`, `sub_department`, `similarity`, `confident`) is now also returned by `/person/enrich`, matching the field already available on `/person/search`. Available on `POST /person/search` and `POST /person/enrich` with `x-api-version: 2025-11-01`. ## Normalized titles, education details, and logos on Person Search [Person Search](/person-docs/search/introduction) results now include a normalized job-title classification, richer education entries, and stable Crustdata-hosted logo permalinks for employers and schools. - **`basic_profile.normalized_title`** — an object with `matched_title`, `department`, `sub_department`, `similarity`, and `confident`. Filter on `basic_profile.normalized_title.matched_title`, `.department`, or `.sub_department` (filterable, not sortable). See the [field reference](/person-docs/search/reference#basic-profile). - **Education location and description** — each `education.schools[]` entry now returns a `location` object (`raw`, `city`, `state`, `country`, `continent`) and a `description`. The location sub-fields are filterable via `education.schools.location.*`. See the [education fields](/person-docs/search/reference#education). - **`education.schools[].institute_logo_permalink`** — a stable Crustdata-hosted school logo URL, returned for display. - **`experience.employment_details[].company_profile_picture_permalink`** — a stable Crustdata-hosted employer logo URL, so you can render company logos without resolving image URLs yourself. See the [profile-card example](/person-docs/search/introduction#build-a-profile-card-with-company-and-school-logos). Available on `POST /person/search` with `x-api-version: 2025-11-01`. ## Follower count on Person Search profiles [Person Search](/person-docs/search/introduction) now exposes a `followers` field on each result's `professional_network` block, alongside the existing `connections` field. - **`professional_network.followers`** — integer follower count from the profile. Filterable and sortable: pass it in `filters.conditions[].field` or `sorts[].field`. See the [filter and sort field reference](/person-docs/search/reference). Available on `POST /person/search` with `x-api-version: 2025-11-01`. ## Person Search: new filter operators [Person Search](/person-docs/search/introduction) gains two new filter options for the `2025-11-01` API: - **`(!)` — fuzzy negation.** Excludes profiles whose value contains the given substring (case-insensitive). Multi-word values are matched as a literal phrase, so `(!) "New York"` excludes profiles literally containing `"New York"` but **not** `"New Yorker"`. To exclude on each word independently, send separate `(!)` conditions inside an `and` group. See the [operator reference](/person-docs/search/reference#-fuzzy-negation) and a [worked example](/person-docs/search/introduction#exclude-profiles-matching-a-substring). - **`geo_distance` accepts `lat_lng`.** Supply explicit coordinates as `[lat, lng]` to skip geocoding. When both `location` and `lat_lng` are provided, `lat_lng` wins. See the [`geo_distance` reference](/person-docs/search/reference#geo_distance--radius-around-a-point) and the [`lat_lng` example](/person-docs/search/introduction#search-by-explicit-coordinates). Both operators are available only on `POST /person/search` with `x-api-version: 2025-11-01`. ## 🚀 New API version: `2025-11-01` We are launching a new, versioned API that replaces the legacy `/screener` and `/data_lab` endpoints. Every product — Company, Person, Job, Web, and Social Post — now lives under a consistent, purpose-built surface area. ### What's new - **Versioned endpoints.** Every call requires the `x-api-version: 2025-11-01` header, so future changes ship without breaking existing integrations. - **Bearer authentication.** `Authorization: Bearer ` replaces the legacy `Token` scheme across every endpoint. - **Structured error envelope.** A consistent `{ error: { type, message, metadata } }` shape across every endpoint. - **Cursor-based pagination.** Opaque `next_cursor` replaces numeric `offset` on search endpoints. - **Match-result envelope for enrich.** Enrich responses now return `matches[]` with a `confidence_score` per match. - **Nested, neutral response sections.** Flat top-level keys are grouped under sections like `basic_info`, `employee_reviews`, and `funding`. ### Migrating from the legacy API Every legacy `/screener` and `/data_lab` endpoint has a current replacement. The [Migration guides](/migrations/overview) map each legacy endpoint to its new equivalent and walk through request keys, field renames, type changes, and response-shape differences.