Skip to main content
You send a question in plain words, for example “Fintech companies in Berlin with 50 to 200 employees”. An agent searches the Crustdata person, company, and job datasets in the background. The agent is an AI model that runs searches for you. It returns the matching records with the columns that it picks, and for each record it gives the reasons it matches the requirements in your question. Use it when you cannot express your question as the filters of one search request, or when you do not want to build those filters yourself. Use Person search, Company search, or Job search when you already know the exact filters, because a direct search is faster and costs less.

Endpoint essentials

Quickstart

1

Start a search

Send your question as query. The Idempotency-Key header is optional: send 1 to 255 printable ASCII characters that identify the task in your own system, so that a retry does not start a second search (see Repeat a start safely). The response gives the search_id and the state of the search.
A start returns 202 with the new search_id and its state, which is running. If you send the same Idempotency-Key with the same query again, the response is 200 with the current state of the first search, and that search can already be over. Always read state from the body, not from the status code. Treat every state that is not succeeded, failed, or cancelled as still in progress, because a later release adds queued. The start costs nothing.
2

Read the search until it ends

Read the search every 10 seconds until state is succeeded, failed, or cancelled. While the search runs, a read returns state: running, output: null, and 0 for every key of credits_used. At the end, the answer is in output.
The first read after the end returns the charge in the X-Credits-Used response header, 0.7985 for this search. Later reads return X-Credits-Used: 0.
To get each step of the search as it happens, stream the events instead of polling. See Stream the events.

Search states and the charge

A search moves through these states. The last three are end states. The charge happens once. The first read of GET /agent/searches/{search_id} after the end charges the search, and that read carries the charge in the X-Credits-Used header and in credits_used. Every later read returns X-Credits-Used: 0 and shows the same credits_used. Until you read an ended search, it has no charge yet, which counts for the balance needed to start. When you stream a search to its end, the stream charges it, and the charge is in credits_used of the search.finished event. No header carries it in that case. A search that finds nothing costs 0. It ends succeeded with no rows, a reason such as no_match or not_a_search, a note that says why, and 0 for every key of credits_used. A read of a failed search returns 200 with state: failed and an error object with type, message, and metadata. A failed search costs 0. The error.metadata[0].retryable value tells you whether to start the search again. To start it again, send a new Idempotency-Key, because the old key returns the failed search: In these messages, Team means your account. These messages come from the API source and are not yet confirmed in a live response.

Repeat a start safely

Send an Idempotency-Key header with the start: 1 to 255 printable ASCII characters. When a network error hides the response, send the same request again. The same key with the same query returns 200 with the first search’s search_id and its current state, so you do not start a second search. That state can already be succeeded, failed, or cancelled. The same key with a different query returns 409. A key belongs to your account, so all API keys of the account share it. The streaming start (see Stream the events) takes the same header.

Read the answer

output is set only when state is succeeded. It holds: The entity object has three lists:

Columns

The agent picks the columns. A column is one of two kinds:
  • A field column names a field path of the record in field, for example headcount.total. The response shows it only when your account may receive that field. Otherwise the response leaves out the column and its cells. Your account may receive a field when it is in fields.enabled of GET /account/endpoints for the entity’s search endpoint, or for its enrich endpoint.
  • An agent column has field: null. The agent wrote the text of each cell, for example a “why it matches” column. Every account receives agent columns.
A contact column is a field column on contact.business_emails, contact.personal_emails, or contact.phone_numbers. Only a person answer has one, and only when the query asks for that contact data. Each cell is a list of strings. Contact columns show for every account, whatever fields.enabled says, and credits_used.contact_cost bills them. The first columns are the identity columns, which say who or what each row is. An identity column is a field column, so it shows only when your account may receive its field:

Rows

Each row has: A cell is a string, a number, a boolean, a list of those, or null. A field under a list, such as experience.employment_details.current.title, gives a list. null, "", and [] all mean empty. A number larger than 2^53 - 1 comes as a string. A constraint that the agent did not get to check before the search ended has no verdict. Match verdicts by constraint_id, not by their position in the list. The evidence can quote fields that are not columns of the answer, such as basic_info.description. When the evidence quotes a field that your account may not receive, evidence is "" and that field is left out of fields. A person who asked Crustdata to remove their data has no row.

Stream the events

Send accept: text/event-stream to POST /agent/searches, and the response is the event stream of the search, from its first event to search.finished. Use -N with curl to see the events as they arrive.
The response above shows 3 of its 50 events. Each frame has id (the event’s seq), event (its type), and data (the event as JSON). A line that starts with : is a comment, so skip it. Each event has seq, type, version, search_id, created_at, message, and, on most types, data. seq grows by 1 for each event, and it is your cursor. message is one plain sentence that you can show to a person. The types today are search.created, run.started, step.started, step.running, step.finished, tool_call.started, tool_call.finished, answer.chosen, run.finished, and search.finished. New types can appear. Only search.finished has a fixed data key, data.search. The rest of data, on every event, is progress detail that can change, so show message to a person instead. search.finished is the last event. Its data.search is the search object, the same body that GET /agent/searches/{search_id} returns, with the charge in credits_used. The response to a streaming start has an X-Search-Id header. Closing the stream does not stop the search; use the cancel route for that. To resume a broken stream, call GET /agent/searches/{search_id}/events with that id, accept: text/event-stream, and the last seq you received as the Last-Event-ID header or as ?after=. The stream continues after that event. Without accept: text/event-stream, the same route returns one JSON object: the search_id, and an events list of the events stored so far. POST /agent/searches/{search_id}/cancel stops a search, and the search costs 0. The response is 202 with state: cancelled when this request stopped the search, and 200 with the current state when the search had already ended.

Best practices

  • Write the query like a brief. Name the entity (people, companies, or jobs) and each requirement, for example “Heads of data at fintech companies in Berlin”. The agent turns each requirement into a constraint and checks every row against it.
  • Ask only for the data you need. The charge grows with the data that the agent looks up, contact data included (see Pricing).
  • Store each search_id when you start the search. No route lists your searches, and you need the id to read, resume, or cancel a search.
  • Check reason before you use rows. A partial answer covers only part of what the query asked for, and note says why.
  • Cancel the searches that you no longer need. A cancelled search costs 0.
  • Queue your starts, because POST /agent/searches allows 6 requests per minute and a request that gets an error counts toward the limit too.
  • Wait before you retry. The rate-limit 429 has no Retry-After header, so wait for the seconds in X-RateLimit-Reset. A 503, and the 429 for too many event streams, carry Retry-After and "retryable": true in error.metadata.

Examples

The streaming request in Stream the events started this search. A read after the stream ended returned:
The response above is trimmed to 1 of its 25 rows. The stream charged the search, so this read returned X-Credits-Used: 0. The c3 evidence quotes basic_info.description, which is not a column, so it says around 120 employees while headcount_total is 96.
This is the quickstart request, sent a second time with the same key. Pick a key that identifies the task in your own system.
The first request returned 202. This second request returned 200 with the same search_id. A repeat after the search ends also returns 200, with the end state, for example cancelled. The same key with the query Software engineers in Bangalore who write Rust returned 409:
409 - same key, different query
This search asked for Software engineers in Bangalore who write Rust.
The cancel returned 202. A second cancel returned 200 with the same body. A read of the search then showed output: null and 0 for every key of credits_used.
The quickstart search had 52 events, so ?after=50 returns the last 2. data.search in the response above is trimmed. It holds the full search object, as in the quickstart.

Pricing

Each search is charged once, by the first read or stream that sees it end. The charge is in credits, and it grows with the effort of the search. credits_used splits the charge into four parts, and total_cost is their sum: So one person with 3 business emails is charged once for business email. contact_cost also counts people whose contact data the agent looked up while it searched, even when they are not in the final rows. On 2026-11-13, the quickstart query (3 people) cost 0.7985 credits, and the company list (25 companies) cost 1.1855 credits. A search that finds nothing, fails, or is cancelled costs 0. There is no spend limit for one search. To see your own prices, call GET /account/endpoints. That endpoint shows Agent Search listed under the path /agent/query, not /agent/searches. That path is a name for billing and permissions, not a route to call. There, base_credits is the price behind agent_cost. premium_fields lists data_search (behind search_cost), data_enrich (behind enrich_cost), and contact_business_email, contact_personal_email, and contact_phone (behind contact_cost).

Balance needed to start

A start checks your balance before the search begins. The balance must cover 8 credits for each search of your account that has no charge yet, the new one included. A search has no charge yet while it runs, and after it ends until someone reads it. The check takes no credits, and 8 credits is not the price of a search: a search usually costs much less, as the two examples above show. The same check runs against the monthly limit of your API key. When the balance does not cover the check, the start returns 402 (see Errors).

Limits

These limits count for each account: Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset, the seconds until the limit resets. See Rate limits for the limits of the other endpoints.

Errors

Almost every error response has the body {"error": {"type", "message", "metadata"}}. When Agent Search is busy, its error has metadata set to [{"retryable": true, "retry_after_seconds": N}] and a Retry-After header. Two error responses have another body: a method that a path does not take returns 405 with a detail key, and a path that does not exist returns 404 with an HTML page. The following errors come from the API source and are not yet confirmed in a live response: