Endpoint essentials
Quickstart
1
Start a search
Send your question as A start returns
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.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 The first read after the end returns the charge in the
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.X-Credits-Used
response header, 0.7985 for this search. Later reads return
X-Credits-Used: 0.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 anIdempotency-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 exampleheadcount.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 infields.enabledofGET /account/endpointsfor 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.
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
Sendaccept: 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.
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.
Cancel a search
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_idwhen you start the search. No route lists your searches, and you need the id to read, resume, or cancel a search. - Check
reasonbefore you userows. Apartialanswer covers only part of what the query asked for, andnotesays why. - Cancel the searches that you no longer need. A cancelled search costs 0.
- Queue your starts, because
POST /agent/searchesallows 6 requests per minute and a request that gets an error counts toward the limit too. - Wait before you retry. The rate-limit
429has noRetry-Afterheader, so wait for the seconds inX-RateLimit-Reset. A503, and the429for too many event streams, carryRetry-Afterand"retryable": trueinerror.metadata.
Examples
A list of companies
A list of companies
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.Repeat a start with an Idempotency-Key
Repeat a start with an Idempotency-Key
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
Cancel a search
Cancel a search
This search asked for The cancel returned
Software engineers in Bangalore who write Rust.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.Read the events as one list
Read the events as one list
?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 returns402 (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:

