> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crustdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Search API

> Ask for people, companies, or jobs in plain words and get back the matching records, each with the reasons it matches. In beta.

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](https://docs.crustdata.com/person-docs/search/introduction.md),
[Company search](https://docs.crustdata.com/company-docs/search/introduction.md), or
[Job search](https://docs.crustdata.com/job-docs/search/introduction.md) when you already know the exact
filters, because a direct search is faster and costs less.

## Endpoint essentials

| Detail | Value |
| - | - |
| **API key and headers** | Send `authorization: Bearer YOUR_API_KEY` and `x-api-version: 2025-11-01` with every request, and `content-type: application/json` with every `POST`. |
| **Routes** | `POST /agent/searches` starts a search. `GET /agent/searches/{search_id}` reads it. `GET /agent/searches/{search_id}/events` reads its progress events. `POST /agent/searches/{search_id}/cancel` stops it. |
| **Run time** | A search runs for tens of seconds to a few minutes. In Crustdata's tests on 2026-11-13, searches took 19 to 39 seconds. |
| **Credit cost** | Each search is charged once, by the first read or stream that sees it end, for the agent's work, the data it searched, the records it fetched in full, and the contact data it looked up. The charge grows with the effort of the search. A search that finds nothing, fails, or is cancelled costs 0. See [Pricing](#pricing). |
| **Access** | Your account needs Agent Search and the Person, Company, and Job search endpoints. See the `403` in [Errors](#errors). |
| **Rate limit** | For each account: 6 starts per minute, and 300 requests per minute shared by the read, events, and cancel routes. |

## 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](#repeat-a-start-safely)). The response gives the
`search_id` and the state of the search.

```bash Request theme={"theme":"vitesse-black"}
curl --request POST \
  --url https://api.crustdata.com/agent/searches \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --header 'Idempotency-Key: YOUR_IDEMPOTENCY_KEY' \
  --data '{"query": "The founders of Crustdata"}'
```

```json Response theme={"theme":"vitesse-black"}
{
  "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
  "state": "running"
}
```

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`.

```bash Request theme={"theme":"vitesse-black"}
curl --request GET \
  --url https://api.crustdata.com/agent/searches/1318-07fcae93f67c961a96c8758766b2fea1 \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'x-api-version: 2025-11-01'
```

```json Response theme={"theme":"vitesse-black"}
{
  "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
  "query": "The founders of Crustdata",
  "state": "succeeded",
  "created_at": "2026-11-13T11:32:26.860998+00:00",
  "output": {
    "note": null,
    "reason": null,
    "person": {
      "constraints": [
        { "id": "c1", "text": "Founded Crustdata.", "importance": "hard" }
      ],
      "columns": [
        { "key": "name", "label": "Name", "field": "basic_profile.name" },
        { "key": "profile_url", "label": "Profile URL", "field": "social_handles.professional_network_identifier.profile_url" },
        { "key": "basic_profile_headline", "label": "Headline", "field": "basic_profile.headline" }
      ],
      "rows": [
        {
          "id": 1068035,
          "values": {
            "basic_profile_headline": "Co-founder at Crustdata (YC F24) | Infra for building AI employees",
            "name": "Abhilash Chowdhary",
            "profile_url": "https://www.linkedin.com/in/abhilashchowdhary"
          },
          "constraints": [
            {
              "constraint_id": "c1",
              "assessment": "satisfied",
              "evidence": "Headline identifies him as Crustdata co-founder, and current employer title is Co-Founder & CEO.",
              "fields": ["basic_profile.headline"]
            }
          ]
        },
        {
          "id": 1148585,
          "values": {
            "basic_profile_headline": "Co-Founder at Crustdata (YC F24) | Building most accurate search engine for people search",
            "name": "Manmohit Grewal",
            "profile_url": "https://www.linkedin.com/in/manmohitgrewal"
          },
          "constraints": [
            {
              "constraint_id": "c1",
              "assessment": "satisfied",
              "evidence": "Headline and current role identify him as a Crustdata co-founder.",
              "fields": ["basic_profile.headline"]
            }
          ]
        },
        {
          "id": 402126,
          "values": {
            "basic_profile_headline": "Co-founder at Crustdata (YC F24) | Real-time B2B data for AI Agents",
            "name": "Chris Pisarski",
            "profile_url": "https://www.linkedin.com/in/chris-pisarski"
          },
          "constraints": [
            {
              "constraint_id": "c1",
              "assessment": "satisfied",
              "evidence": "Headline identifies him as co-founder, and his summary says he is building Crustdata.",
              "fields": ["basic_profile.headline", "professional_network.summary"]
            }
          ]
        }
      ]
    }
  },
  "credits_used": {
    "agent_cost": 0.66,
    "search_cost": 0.1385,
    "enrich_cost": 0.0,
    "contact_cost": 0.0,
    "total_cost": 0.7985
  },
  "error": null
}
```

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](#stream-the-events).

## Search states and the charge

A search moves through these states. The last three are end states.

| `state` | Meaning |
| - | - |
| `queued` | Not returned today. A later release adds it for a search that waits in a queue. Treat it as in progress. |
| `running` | The agent works on the search. |
| `succeeded` | The search ended with an answer in `output`. The answer can have no rows, for example when nothing matches. |
| `failed` | The search ended without an answer. `error` says why. |
| `cancelled` | You stopped the search. It costs 0. |

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](#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:

| `error.message` | `retryable` |
| - | - |
| `The search did not finish in time. A narrower query may finish.` | `true` |
| `The search failed. Starting it again may work.` | `true` |
| `Your Team lost access to a search endpoint, so the rows of this search could not be fetched.` | `false` |
| `The rows of this search could not be fetched.` | `false` |

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](#stream-the-events)) takes the same header.

## Read the answer

`output` is set only when `state` is `succeeded`. It holds:

| Key | Meaning |
| - | - |
| `note` | Plain text about the whole answer, for a person to read, at most 512 characters. It is set when `reason` is set. |
| `reason` | Why the answer is short or empty. It is `null` on a full answer. Values today: `partial`, `unsupported_data`, `no_match`, `unsupported_entity`, `not_a_search`, `malformed`, `restricted`. New values can appear. Read `note` for the reason in plain words. |
| `person`, `company`, or `job` | The answer about that entity. Today `output` has exactly one of these keys, and the agent picks it. Find the entity by its key. |

The entity object has three lists:

| Key | Meaning |
| - | - |
| `constraints` | The requirements that the agent read in your query, at most 8. Each has an `id`, a `text`, and an `importance` of `hard`, `core`, or `soft`. |
| `columns` | The columns of the answer, at most 9. Each has a `key`, a `label`, and a `field`. |
| `rows` | One row per record, best first, at most 200. There is no paging. The list is empty when the agent found nothing. |

### 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`](https://docs.crustdata.com/general/permissions) 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:

| Entity | Identity columns (`key`: `field`) |
| - | - |
| `person` | `name`: `basic_profile.name`, `profile_url`: `social_handles.professional_network_identifier.profile_url` |
| `company` | `name`: `basic_info.name`, `profile_url`: `basic_info.professional_network_url`, `domain`: `basic_info.primary_domain` |
| `job` | `title`: `job_details.title`, `url`: `job_details.url`, `company_name`: `company.basic_info.name` |

### Rows

Each row has:

| Key | Meaning |
| - | - |
| `id` | The Crustdata id of the record: `crustdata_person_id`, `crustdata_company_id`, or `crustdata_job_id`, as the entity key says. |
| `values` | The cells of the columns, by column key. A missing key means `null`. |
| `constraints` | The agent's verdicts, at most one per constraint. Each has a `constraint_id`, an `assessment` of `satisfied`, `unknown`, or `violated`, the `evidence` in plain words, and the `fields` that the evidence reads. |

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.

```bash Request theme={"theme":"vitesse-black"}
curl -N --request POST \
  --url https://api.crustdata.com/agent/searches \
  --header 'authorization: Bearer YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --header 'x-api-version: 2025-11-01' \
  --header 'accept: text/event-stream' \
  --data '{"query": "Fintech companies in Berlin with 50 to 200 employees"}'
```

```text Response theme={"theme":"vitesse-black"}
id: 1
event: search.created
data: {"seq":1,"type":"search.created","message":"Search created.","version":1,"search_id":"1318-c379345672f34397a53fafff6dcfaa83","created_at":"2026-11-13T11:32:55.577Z"}

id: 2
event: run.started
data: {"seq":2,"data":{"run":1,"max_runs":2},"type":"run.started","message":"Run 1 of 2 started.","version":1,"search_id":"1318-c379345672f34397a53fafff6dcfaa83","created_at":"2026-11-13T11:32:55.583Z"}

id: 3
event: tool_call.started
data: {"seq":3,"data":{"run":1,"call":1,"step":0,"tool":"review"},"type":"tool_call.started","message":"Checking candidates against your criteria\u2026","version":1,"search_id":"1318-c379345672f34397a53fafff6dcfaa83","created_at":"2026-11-13T11:32:58.524Z"}
```

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.

## 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](#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

> [accordion] A list of companies
>
> The streaming request in [Stream the events](#stream-the-events) started
> this search. A read after the stream ended returned:
>
> ```bash Request theme={"theme":"vitesse-black"}
> curl --request GET \
>   --url https://api.crustdata.com/agent/searches/1318-c379345672f34397a53fafff6dcfaa83 \
>   --header 'authorization: Bearer YOUR_API_KEY' \
>   --header 'x-api-version: 2025-11-01'
> ```
>
> ```json Response theme={"theme":"vitesse-black"}
> {
>   "search_id": "1318-c379345672f34397a53fafff6dcfaa83",
>   "query": "Fintech companies in Berlin with 50 to 200 employees",
>   "state": "succeeded",
>   "created_at": "2026-11-13T11:32:55.584699+00:00",
>   "output": {
>     "note": null,
>     "reason": null,
>     "company": {
>       "constraints": [
>         { "id": "c1", "text": "The company is a fintech company.", "importance": "core" },
>         { "id": "c2", "text": "The company is headquartered in Berlin.", "importance": "hard" },
>         { "id": "c3", "text": "The company has between 50 and 200 employees.", "importance": "hard" }
>       ],
>       "columns": [
>         { "key": "name", "label": "Name", "field": "basic_info.name" },
>         { "key": "profile_url", "label": "Profile URL", "field": "basic_info.professional_network_url" },
>         { "key": "domain", "label": "Domain", "field": "basic_info.primary_domain" },
>         { "key": "locations_headquarters", "label": "Headquarters", "field": "locations.headquarters" },
>         { "key": "headcount_total", "label": "Headcount", "field": "headcount.total" }
>       ],
>       "rows": [
>         {
>           "id": 6034604,
>           "values": {
>             "name": "Qwist",
>             "domain": "qwist.com",
>             "profile_url": "https://www.linkedin.com/company/qwist",
>             "headcount_total": 96,
>             "locations_headquarters": "Berlin, Berlin, Germany"
>           },
>           "constraints": [
>             {
>               "constraint_id": "c1",
>               "assessment": "satisfied",
>               "evidence": "Categories include FinTech and description details financial-data and financing services.",
>               "fields": ["taxonomy.categories", "basic_info.description"]
>             },
>             {
>               "constraint_id": "c2",
>               "assessment": "satisfied",
>               "evidence": "Headquarters location is listed as Berlin, Germany.",
>               "fields": ["locations.headquarters"]
>             },
>             {
>               "constraint_id": "c3",
>               "assessment": "satisfied",
>               "evidence": "Description states the company has around 120 employees.",
>               "fields": ["basic_info.description"]
>             }
>           ]
>         }
>       ]
>     }
>   },
>   "credits_used": {
>     "agent_cost": 0.96,
>     "search_cost": 0.2255,
>     "enrich_cost": 0.0,
>     "contact_cost": 0.0,
>     "total_cost": 1.1855
>   },
>   "error": null
> }
> ```
>
> 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.

> [accordion] 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.
>
> ```bash Request theme={"theme":"vitesse-black"}
> curl --request POST \
>   --url https://api.crustdata.com/agent/searches \
>   --header 'authorization: Bearer YOUR_API_KEY' \
>   --header 'content-type: application/json' \
>   --header 'x-api-version: 2025-11-01' \
>   --header 'Idempotency-Key: YOUR_IDEMPOTENCY_KEY' \
>   --data '{"query": "The founders of Crustdata"}'
> ```
>
> ```json Response theme={"theme":"vitesse-black"}
> {
>   "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
>   "state": "running"
> }
> ```
>
> 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`:
>
> ```json 409 - same key, different query theme={"theme":"vitesse-black"}
> {
>   "error": {
>     "type": "invalid_request",
>     "message": "This Idempotency-Key belongs to a search with a different query.",
>     "metadata": []
>   }
> }
> ```

> [accordion] Cancel a search
>
> This search asked for `Software engineers in Bangalore who write Rust`.
>
> ```bash Request theme={"theme":"vitesse-black"}
> curl --request POST \
>   --url https://api.crustdata.com/agent/searches/1318-ce76b669c98d42849a0c1d4239040e03/cancel \
>   --header 'authorization: Bearer YOUR_API_KEY' \
>   --header 'x-api-version: 2025-11-01'
> ```
>
> ```json Response theme={"theme":"vitesse-black"}
> {
>   "search_id": "1318-ce76b669c98d42849a0c1d4239040e03",
>   "state": "cancelled"
> }
> ```
>
> 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`.

> [accordion] Read the events as one list
>
> ```bash Request theme={"theme":"vitesse-black"}
> curl --request GET \
>   --url 'https://api.crustdata.com/agent/searches/1318-07fcae93f67c961a96c8758766b2fea1/events?after=50' \
>   --header 'authorization: Bearer YOUR_API_KEY' \
>   --header 'x-api-version: 2025-11-01'
> ```
>
> ```json Response theme={"theme":"vitesse-black"}
> {
>   "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
>   "events": [
>     {
>       "seq": 51,
>       "data": {
>         "run": 1,
>         "state": "succeeded",
>         "next_run": false,
>         "completion": "answered"
>       },
>       "type": "run.finished",
>       "message": "Run 1 finished: answered.",
>       "version": 1,
>       "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
>       "created_at": "2026-11-13T11:32:44.442Z"
>     },
>     {
>       "seq": 52,
>       "data": {
>         "state": "succeeded",
>         "completion": "answered",
>         "search": {
>           "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
>           "state": "succeeded",
>           "...": "..."
>         }
>       },
>       "type": "search.finished",
>       "message": "Search finished: 3 people found.",
>       "version": 1,
>       "search_id": "1318-07fcae93f67c961a96c8758766b2fea1",
>       "created_at": "2026-11-13T11:32:44.444Z"
>     }
>   ]
> }
> ```
>
> 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:

| Part | What it pays for |
| - | - |
| `agent_cost` | The agent's work on the search. |
| `search_cost` | The data that the agent searched. |
| `enrich_cost` | The records that the agent fetched in full with an enrich call. |
| `contact_cost` | The contact data that the agent looked up. Each type is charged once for each person who has a value of that type, and a person with no value of a type costs 0 for it. |

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`](https://docs.crustdata.com/general/permissions).
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](https://docs.crustdata.com/general/credits.md#per-key-monthly-limit). When
the balance does not cover the check, the start returns `402` (see
[Errors](#errors)).

## Limits

These limits count for each account:

| Limit | Value |
| - | - |
| `POST /agent/searches` | 6 requests per minute. A request that gets an error counts toward it too. |
| Read, events, and cancel | 300 requests per minute, shared by the three routes. |
| `query` | 1 to 10,000 characters, with no NUL character. |
| An answer | At most 8 constraints, 9 columns, and 200 rows. |

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
`X-RateLimit-Reset`, the seconds until the limit resets.

See [Rate limits](https://docs.crustdata.com/general/rate-limits.md) 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.

| Status | `error.type` | `error.message` | Cause |
| - | - | - | - |
| `400` | `invalid_request` | ``The body must be a JSON object with `query` as its only key.`` | The start body has another key, or is not a JSON object. |
| `400` | `invalid_request` | `` `query` must have 1 to 10,000 characters and no NUL character. `` | The query is empty or too long. |
| `400` | `invalid_request` | `` `Idempotency-Key` must have 1 to 255 printable ASCII characters. `` | The `Idempotency-Key` header is not valid, for example it has a space. |
| `400` | `invalid_request` | `` `after` and `Last-Event-ID` must be a whole number from 0 to 2147483647. `` | The events cursor is not valid. |
| `401` | `unauthorized` | `Invalid API key in request.` | The API key is missing or not valid. |
| `404` | `not_found` | `Unknown search_id.` | No search of your account has this `search_id`. |
| `405` | none | `{"detail": "Method \"GET\" not allowed."}` is the whole body. | You sent `GET` to `/agent/searches`, which takes only `POST`. It counts toward the start limit. |
| `409` | `invalid_request` | `This Idempotency-Key belongs to a search with a different query.` | You sent a used `Idempotency-Key` with a different query. |
| `429` | `rate_limit_error` | `Rate limit exceeded for this endpoint. Please write to gtm@crustdata.co.` | You sent more than 6 requests to `POST /agent/searches`, or more than 300 to the other routes, in one minute. The response has no `Retry-After`: wait for the seconds in `X-RateLimit-Reset`. |
| `429` | `rate_limit_error` | `Too many event streams are open. Read the events list instead, or try again in 5 seconds.` | Too many event streams are open. `Retry-After` is 5. |

The following errors come from the API source and are not yet confirmed in a
live response:

| Status | `error.type` | `error.message` | Cause |
| - | - | - | - |
| `402` | `insufficient_credits` | `Your credits do not cover a new search. Each search needs 8 credits until it is charged.` | Your balance does not cover 8 credits for each search that has no charge yet. See [Balance needed to start](#balance-needed-to-start). |
| `402` | `insufficient_credits` | `This API key's monthly credit limit has been exhausted. ...` | The monthly limit of your API key does not cover the start check. See [Per-key monthly limit](https://docs.crustdata.com/general/credits.md#per-key-monthly-limit). |
| `403` | `permission_error` | `Agent Search needs access to /agent/query, /person/search, /company/search and /job/search. Missing: ...` | Your account cannot call one of the endpoints that the agent uses. |
| `500` | `internal_error` | `An unexpected error occurred.` | The start could not be recorded. |
| `503` | `service_unavailable` | `Agent Search is at capacity. Try again in 60 seconds.` | Agent Search cannot take a new search now. Nothing started. `Retry-After` is 60. |
| `503` | `service_unavailable` | `Agent Search did not answer. Try again in 60 seconds.` | Agent Search did not respond to the start. Nothing started. `Retry-After` is 60. |
| `503` | `service_unavailable` | `Agent Search did not answer. Try again in a few seconds.` | Agent Search did not respond to a read, events, or cancel request. `Retry-After` is 5. |
| `503` | `service_unavailable` | `The rows of this search are not ready. Try again in a few seconds.` | The search ended, and its rows are not ready yet. `Retry-After` is 5. |


## Related topics

- [Web Search API](https://docs.crustdata.com/web-docs/search/introduction.md)
- [Company Search API](https://docs.crustdata.com/company-docs/search/introduction.md)
- [Semantic People Search API](https://docs.crustdata.com/guides/person-semantic-search.md)
- [People Search API](https://docs.crustdata.com/person-docs/search/introduction.md)
- [Search](https://docs.crustdata.com/api-reference/web-apis/web-search.md)
