Endpoints
Agent runs
Start a long-running agent search, follow it, read its outcome and page through its results: six routes, one billing lifecycle.
/v1/agents/runsStart an agent run
Start a run. 202 with a handle; stream /events or poll the run.
Ordered so that nothing irreversible happens before something reversible has been checked. The replay lookup comes first because a replay must never place a second hold; the hold comes before the engine call because an unaffordable run must not start; and the run row is written only once the engine has accepted the run, so a refusal leaves nothing behind but a released hold.
Scope: dsp.agents.write
Use it when a question needs more work than one search request can do: several criteria to check, columns to fill, or a market to measure. Outcome: a run that works in the background. You get a handle straight away, then read the run's outcome and page through its rows when it finishes. For a step-by-step walkthrough, see the agent runs guide.
Six routes make up the surface:
| Route | What it does |
|---|---|
POST /v1/agents/runs (this one) | Starts a run and places a credit hold. Needs Idempotency-Key |
GET /v1/agents/runs/{run_id} | The run's status and outcome. The first read of a finished run settles its charge |
GET /v1/agents/runs | Your workspace's recent runs, newest first |
POST /v1/agents/runs/{run_id}/cancel | Stops a run. A cancelled run is charged nothing |
GET /v1/agents/runs/{run_id}/results | The run's rows, in the standard envelope. Free |
GET /v1/agents/runs/{run_id}/events | The run's progress as server-sent events, resumable with Last-Event-ID |
Starting and cancelling a run need dsp.agents.write. The four reads accept
dsp.agents.read or dsp.agents.write. Runs belong to the workspace: any
credential in the workspace that holds one of those scopes can read them.
Start a run#
category"company" | "person"default: companyWhat the run looks for: companies or people.
criteriaarray of string, ≤ 10effortEffortdefault: mediumHow hard the run works, and what it costs once it succeeds: minimal 1.02 credits, low 2.125, medium (the default) 8.5, high 42.5, xhigh 85. auto holds 85 while it chooses, and is charged the price of the tier it ran. A failed or cancelled run is charged nothing.
enrichmentsarray of string, ≤ 20exclusionarray of string, ≤ 1000querystringrequired≥ 1 chars, ≤ 4000 chars.
Headers
Idempotency-Keystring | nullrequiredRequired. A unique value per intent, at most 255 characters. Reuse it when retrying the same request, and you get the original run back.
| Field | Notes |
|---|---|
query | Required. What you are looking for, in words. 1 to 4,000 characters |
category | company (default) or person: what each row of the answer is |
effort | How hard the run works, and what it costs. See the price ladder |
criteria | Up to 10 extra requirements, written as sentences. A row must satisfy them to be kept |
enrichments | Up to 20 column names to fill on every row. A column no data source can support comes back empty rather than guessed |
exclusion | Up to 1,000 record ids to leave out of the answer. Coverage is still measured against the whole matching set |
Unknown fields are refused with a 422.
Effort and price
A run is priced by its effort tier, whatever number of rows it returns. It is charged only when it succeeds. A run that fails or is cancelled costs nothing.
effort | Credits per successful run |
|---|---|
minimal | 1.02 |
low | 2.125 |
medium (default) | 8.5 |
high | 42.5 |
xhigh | 85 |
auto | The price of the tier it ran. It holds 85 while it decides |
The run reports the tier it actually ran as effort_ran, and that is the tier
it is charged at. See Credits & usage for the
full price list.
Idempotency-Key is required
Starting a run holds credits and the work can take minutes, so a retry must be
recognisable. Send a unique Idempotency-Key header for every new run, and
send the same value again when you retry the same request.
- Missing, empty, or longer than 255 characters:
400 request.invalid. Nothing is started or held. - A key you have used before: you get the original run's handle back with
200instead of202, whatever the body says. No second run starts, no second hold is placed and nothing is charged. - Scope: keys belong to your workspace, not to one credential or one day, and they never expire. Two credentials in the same workspace that send the same key get the same run. Generate a fresh key (a UUID works well) for each new run.
Response
metaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.
credits_chargednumber | nullWhat this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.
environment"live" | "test"Echoed on every response so a developer can never mistake a test-key result for a live one.
request_idstringrun_idstringstatusstring202 Accepted with a handle: the run_id you use on every other route, and
the run's status at that moment. A 200 carries the same shape and means the
key was a replay.
Billing: hold, then settle#
A run's cost is only known when it ends, so creating a run holds credits rather than charging them, like a hold on a payment card.
- Create: the hold
POST /v1/agents/runsholds the tier's price (85 forauto). If your balance cannot cover the hold, the request fails with402 credits.insufficientand the run does not start. While the hold is in place, the credits show asallocated.reservedonGET /v1/credits. - First read of the finished run: the settle
The charge is settled the first time the finished run is read with
GET /v1/agents/runs/{run_id}, or withPOST .../cancel. A run that succeeded is charged the price ofeffort_ranand the rest of the hold is released. A run that failed or was cancelled releases the whole hold and is charged nothing. Reading the results, listing runs and the events stream do not settle a run. - After 35 minutes: the hold lapses
The hold lasts 35 minutes from the start of the run. If nobody reads the finished run before the hold lapses, the run is never charged, and a later read reports
credits_charged: 0. Until then, usage history lists the run asfailedwitherror_code: "unsettled".
credits_charged on a run is null until the run has settled. It is never 0
as a stand-in for "not known yet": 0 always means the run cost nothing.
Statuses and polling#
status | Meaning |
|---|---|
pending | Accepted, not yet running |
running | Working |
completed | Finished with an answer. Terminal |
failed | Finished without an answer. Terminal, charged nothing |
cancelled | Stopped by a cancel. Terminal, charged nothing |
Poll GET /v1/agents/runs/{run_id} every few seconds until the status is
terminal, or follow the events stream and read the run once
the stream closes. Either way, end with a GET of the run: that read gives you
the outcome and credits_charged, and it is what settles the charge.
Read a run#
curl "https://api.sproutsdata.ai/v1/agents/runs/{run_id}" \
-H "X-API-Key: $SPROUTS_API_KEY"Path parameters
run_idstringrequiredcoverageCoverage | nullauditedbooleandefault: falseWhether the query was checked against the executed filter for dropped requirements. Reported because an empty constraints_unmet only means something if something looked.
constraints_unmetarray of stringRequirements in your query that the search could not express, in your own words. Non-empty means part of the question was not answered -- most often because it needs a dataset this search cannot reach (job postings, employment history, patents, news, funding dates). When this is non-empty, universe_exhausted is withheld: 'we returned everything that matches' cannot be true of a question that was never asked.
criteriaarray of objectPer criterion: evaluated, passed, success_rate, and whether it was *proven* by the query or *judged* by a model. Without this a thin result leaves the caller guessing which requirement thinned it.
enumeratedintegerHow many of them this answer carries. ≥ 0.
enumeration_recallnumber | nullenumerated / universe. Null when the universe was not exact, because a ratio over an estimate is not a recall figure. Note this measures coverage of the *resolved query*, not of the user's intent -- a wrong predicate can be exhaustively enumerated. ≥ 0, ≤ 1.
linkagearray of objectOne entry per other dataset this answer joined against: corpus, join_key, observable, of_constrained, ratio, is_lower_bound. ratio is the fraction of your matching accounts that dataset can even see -- measured at 5-60% depending on the dataset and how large the companies are, and never 100%. Non-empty therefore means universe_exhausted is withheld by construction: we returned everything reachable, and a dataset cannot see the rest.
linkage_coveragenumber | nullThe joins combined, as a product rather than a minimum: two datasets seeing 27% and 24% of your accounts jointly see about 6.5%, not 24%. Null when no join was performed or when any leg's coverage is unknown.
linkage_coverage_is_lower_boundbooleandefault: falseTrue when a key set was capped or coverage was estimated rather than measured, making linkage_coverage a floor. Narrowing the question makes it exact.
similarityarray of objectNarrowings done by ranking rather than by predicate. Each entry carries the concept, how many ids it resolved to, the k asked for, whether the list was cut at that ceiling, and the fraction of candidates that could be compared at all. Non-empty means universe_exhausted is withheld on purpose: a cosine is a degree, so a top-k ranking has no complete state, and 2.9% of companies carry no embedding to compare.
truncated_bystring | nullWhich limit stopped the enumeration short, when one did.
universeintegerRecords matching the resolved query, counted, not sampled. ≥ 0.
universe_exhaustedbooleandefault: falseEvery matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.
universe_is_exactbooleanFalse when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.
credits_chargednumber | nullWhat this run cost, in credits, once settled. Null while it is still running — never 0, which would read as free.
degradationarray of DegradationWays this run is less than it should be, in a closed vocabulary a client can branch on. billing_bypassed means the work was done and nothing was charged -- either a test credential or a deployment configured to run on an empty wallet. Stated rather than left to be inferred from a zero.
detailstringimpact"recall" | "precision" | "freshness" | "fields"reasonDegradationReasonWhy an answer is less than it should be. Closed, because a client that cannot branch on the reason can only show the user a shrug. These are the honest constraints — partial vector coverage, smry mapped as keyword so it cannot be scored lexically, a federated corpus being unreachable — surfaced at runtime instead of in a footnote.
source_corpusSourceCorpus | nulleffortstring | nullThe effort tier requested: minimal to xhigh, or auto.
effort_ranstring | nullThe effort tier the run actually ran at, and was charged at, once it has settled. For an auto run this is the tier it chose. Null until then.
errorstring | nullexclusionsarray of stringWhat was left out, and why -- a criterion a row failed, a constraint no reachable dataset can express, accounts a joined corpus cannot see. Named here rather than implied by a smaller row count.
insight_sourcesarray of objectThe documents the insights rest on, so a citation number resolves to something you can show. Present even when insights is empty -- that means we found relevant documents and would not assert a statement from them, which is a result worth rendering rather than an empty panel.
insightsarray of objectCited statements about the market or technology behind your question, from research reports, papers and trend records. A separate channel from the rows and from coverage: those corpora have no company linkage, so an insight is never a matching account and cannot have changed universe. Each carries the sources it rests on; a statement we could not cite is not returned.
metaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.
credits_chargednumber | nullWhat this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.
environment"live" | "test"Echoed on every response so a developer can never mistake a test-key result for a live one.
request_idstringrun_idstringstatusstringstop_reasonstring | nullsummarystringdefault: Two or three sentences on what came back and the bar actually applied, which is often not the bar that was asked for. Empty when the run could not produce one.
The fields worth reading on a finished run:
summary: two or three sentences on what came back and the bar that was actually applied.coverage: how much of the matching set the answer represents, the same object as on the envelope.exclusions: what was left out and why, stated rather than implied by a smaller row count.insightsandinsight_sources: cited statements about the market behind your question, and the documents they rest on. They are separate from the rows and never count as matching accounts.effortandeffort_ran: the tier you asked for and the tier that ran (and was charged).error: what went wrong, on a failed run.
A run id that does not exist, or belongs to another workspace, answers
404 request.invalid.
List runs#
curl "https://api.sproutsdata.ai/v1/agents/runs?limit=25" \
-H "X-API-Key: $SPROUTS_API_KEY"limitintegerdefault: 25≥ 1, ≤ 100.
Your workspace's runs, newest first. limit is 1 to 100 (default 25). There is
no cursor: this route returns the most recent runs only.
metaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.
credits_chargednumber | nullWhat this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.
environment"live" | "test"Echoed on every response so a developer can never mistake a test-key result for a live one.
request_idstringresultsarray of RunListItemcreated_atstringfinished_atstring | nullquerystring | nullrun_idstringstatusstringCancel a run#
curl -X POST "https://api.sproutsdata.ai/v1/agents/runs/{run_id}/cancel" \
-H "X-API-Key: $SPROUTS_API_KEY"Path parameters
run_idstringrequiredCancelling stops the work, releases the hold and charges nothing. It needs
dsp.agents.write, and it is safe to repeat. The response has the same shape as
reading the run.
Page through the results#
curl "https://api.sproutsdata.ai/v1/agents/runs/{run_id}/results?limit=25" \
-H "X-API-Key: $SPROUTS_API_KEY"cursorstring | nulllimitintegerdefault: 25≥ 1, ≤ 100.
Path parameters
run_idstringrequiredThe run's rows, in the standard envelope. This route is free: the run is charged once, when it settles, and reading its rows again costs nothing.
- Paging:
limitis 1 to 100 (default 25). Follownext_cursoruntilhas_moreisfalse. A cursor is tied to its run and cannot be used on another. See Pagination. idon each row is the company's domain when it has one, otherwise an opaque 16-character hex hash. It is not a record-lookup id. To fetch a full record, send a company'sdomainto/v1/entities/companieswith"by": "domain", or a person'slinkedin_urlto/v1/entities/personswith"by": "li_url".totalis the size of the answer. When the run measured the exact matching set,totalis that number, which can be larger than the number of rows stored for the run.columnslists the extra columns, in order, when you asked forenrichments. Each value sits on its row under the column's name.
The rows you read count towards your credential's entity ceiling, in the same way search results do.
attributionarray of Attributionas_ofstring | nulldomainstring | nullquotestring | nullRESERVED for per-fact citation. Always null in v1.
source_corpusSourceCorpusWhere content came from. Closed set — it appears in attribution[]. The first four are the GTM entity graph. The rest are the document corpora behind the non-entity categories, each a distinct index with its own refresh cadence — which is the reason they are named separately rather than lumped under one label: attribution[].as_of means nothing if two corpora updated a month apart share a name.
urlstring | nullRESERVED for per-fact citation. Always null in v1.
columnsarray of object | nullThe answer table's columns, in order, when a run carries extra columns beyond the entity's own fields -- a job-opening link, a value joined from another dataset. Each entry is {name, description, source}; the value for each sits on every row in results keyed by the column name. Null means a plain entity table. Additive, so it breaks no existing client.
coverageCoverage | nullHow much of the addressable set this answer represents. Null means coverage was not measured for this request -- fast and auto do not enumerate -- and never that coverage was zero. Additive, so it breaks no existing client.
auditedbooleandefault: falseWhether the query was checked against the executed filter for dropped requirements. Reported because an empty constraints_unmet only means something if something looked.
constraints_unmetarray of stringRequirements in your query that the search could not express, in your own words. Non-empty means part of the question was not answered -- most often because it needs a dataset this search cannot reach (job postings, employment history, patents, news, funding dates). When this is non-empty, universe_exhausted is withheld: 'we returned everything that matches' cannot be true of a question that was never asked.
criteriaarray of objectPer criterion: evaluated, passed, success_rate, and whether it was *proven* by the query or *judged* by a model. Without this a thin result leaves the caller guessing which requirement thinned it.
enumeratedintegerHow many of them this answer carries. ≥ 0.
enumeration_recallnumber | nullenumerated / universe. Null when the universe was not exact, because a ratio over an estimate is not a recall figure. Note this measures coverage of the *resolved query*, not of the user's intent -- a wrong predicate can be exhaustively enumerated. ≥ 0, ≤ 1.
linkagearray of objectOne entry per other dataset this answer joined against: corpus, join_key, observable, of_constrained, ratio, is_lower_bound. ratio is the fraction of your matching accounts that dataset can even see -- measured at 5-60% depending on the dataset and how large the companies are, and never 100%. Non-empty therefore means universe_exhausted is withheld by construction: we returned everything reachable, and a dataset cannot see the rest.
linkage_coveragenumber | nullThe joins combined, as a product rather than a minimum: two datasets seeing 27% and 24% of your accounts jointly see about 6.5%, not 24%. Null when no join was performed or when any leg's coverage is unknown.
linkage_coverage_is_lower_boundbooleandefault: falseTrue when a key set was capped or coverage was estimated rather than measured, making linkage_coverage a floor. Narrowing the question makes it exact.
similarityarray of objectNarrowings done by ranking rather than by predicate. Each entry carries the concept, how many ids it resolved to, the k asked for, whether the list was cut at that ceiling, and the fraction of candidates that could be compared at all. Non-empty means universe_exhausted is withheld on purpose: a cosine is a degree, so a top-k ranking has no complete state, and 2.9% of companies carry no embedding to compare.
truncated_bystring | nullWhich limit stopped the enumeration short, when one did.
universeintegerRecords matching the resolved query, counted, not sampled. ≥ 0.
universe_exhaustedbooleandefault: falseEvery matching record was returned and the count agrees. The claim an unbounded-corpus search engine cannot make.
universe_is_exactbooleanFalse when the count hit a cap. Always serialized: a capped count presented as exact is the same lie as an unmeasured one.
degradationarray of Degradationdetailstringimpact"recall" | "precision" | "freshness" | "fields"reasonDegradationReasonWhy an answer is less than it should be. Closed, because a client that cannot branch on the reason can only show the user a shrug. These are the honest constraints — partial vector coverage, smry mapped as keyword so it cannot be scored lexically, a federated corpus being unreachable — surfaced at runtime instead of in a footnote.
source_corpusSourceCorpus | nullhas_morebooleanmetaResponseMetaPresent on every 2xx, including the ones that are not paginated.
as_ofstringcredential"api_key" | "session"default: api_keyWhich credential produced this response. session means the dashboard playground ran it as the signed-in user rather than as an API key — so it is billed to the user, subject to interactive rate limits, and carries no integration compatibility guarantee. Additive and defaulted, so it breaks no existing client.
credits_chargednumber | nullWhat this request cost, in credits, to at most three decimals. 0 when it cost nothing: a Basic or AI search that found nothing, rows this search already paid for (a cursor sent again, or a request repeated with the same Idempotency-Key on the same UTC day), or a test key. A request without a cursor or a repeated key is a new search, so keep the first page rather than asking for it again. null on a free endpoint.
environment"live" | "test"Echoed on every response so a developer can never mistake a test-key result for a live one.
request_idstringnext_cursorstring | nullOpaque and signed. Invariant: has_more == (next_cursor is not None). There is no page or offset parameter — not accepted, not deprecated, never present.
resolved_searchResolvedSearch | nullcategoryEntityCategory*What* to search, and what a returned row is. One parameter carries both, because they are the same fact: a search of patent returns patents. The alternative — a source/vertical selector beside a narrower category — lets a caller ask for patents from the people index, a combination that has no meaning and would need rejecting at runtime. A closed set that cannot express the contradiction is better than a validator that catches it. Values are exactly prism's own row types, so category in the request and results[].category in the response are always the same string. company and person are the GTM index; the rest are separate corpora with their own filters, freshness and coverage. Not everything applies everywhere: see SearchRequest, which rejects the GTM-only parameters rather than ignoring them. job_opening is a document corpus like patent/news_article/etc — a posting, not an entity — but its records carry a domain (the posting company's) all the same. It is deliberately not in GTM_CATEGORIES (that set is about the entity/document distinction, and a job posting is still a document), but it *is* in DOMAIN_FILTERABLE_CATEGORIES, which exists specifically to separate those two axes.
conflictsarray of objectfiltersFiltersfilters_source"caller" | "parser" | "caller+parser" | "none"default: nonemode"fast" | "auto" | "deep"How hard to work on the query. The distinction is *what the words mean*, not how many results come back: - fast — match my words. The query is matched by meaning (vector retrieval, falling back to keywords where vectors are unavailable); no LLM in the path, so nothing is extracted into filters. - auto — understand my intent. The parser extracts filters. - deep — reason about it. Multi-step, slowest, most expensive. fast was very nearly defined as "filters only, no LLM", which would have made query — the primary parameter — silently inert. A parameter that does nothing is worse than one that is missing.
overridesarray of objectparser_versionstringquerystring | nullsemantic_querystring | nullsemantic_scope"company" | "person" | nullsortarray of objectresultsarray of objectsuggestionSearchSuggestion | nullA better way to have asked this question -- a different category (kind: search), or agentic search via POST /v1/agents (kind: agentic). Advisory: these results came from the search that was asked for, whatever this says. Null when there is nothing worth suggesting, which is the common case. Additive, so it breaks no existing client.
categoryEntityCategory | nullThe category to search instead. Set iff kind is search.
kind"search" | "agentic"search — a different category fits. agentic — no single search fits; use POST /v1/agents.
messagestringOne ready-to-show sentence.
totalinteger≥ 0.
total_is_estimatebooleanAlways serialized, never defaulted away. A field clients can forget exists is a field that renders an estimate as exact.
Stream progress#
GET /v1/agents/runs/{run_id}/events streams the run's progress as
server-sent events
(text/event-stream).
curl -N "https://api.sproutsdata.ai/v1/agents/runs/{run_id}/events" \
-H "X-API-Key: $SPROUTS_API_KEY" \
-H "Accept: text/event-stream"Path parameters
run_idstringrequiredHeaders
Last-Event-IDstring | nullThe id of the last frame you received, to resume after it.
Each frame carries an id, an event name and a JSON data payload:
id: 7
event: step.started
data: {...}
: ping
idis the run's own sequence number for that event.- The stream ends after the run's last event (
run.done, orerror). Opening the stream for a run that has already finished replays its events and closes. - Lines starting with
:are keep-alive comments, sent when the run is quiet. Ignore them. - Other event names report progress. Treat a name you don't recognise as informational rather than as an error.
To resume after a dropped connection, reconnect with a Last-Event-ID header
holding the last id you received. You get every later event once, in order.
Ownership is checked before the stream opens, so an unknown run is a 404.
Once the stream is open the status code is already sent: if something fails
after that, the stream ends and you reconnect.
Limits on starting runs#
Two limits apply when a run is started. Neither applies to a replayed
Idempotency-Key, because a replay starts no work.
| Limit | Error | What to do |
|---|---|---|
| Entity ceiling. A credential that has been shown 5,000,000 distinct entities in the last 30 days cannot start another run. Search results and run results both count | 429 quota.entity_ceiling, with extra.seen and extra.limit | Contact us to raise it |
| Start concurrency. At most 5 runs can be in the middle of being started at the same moment per credential | 429 quota.concurrency_exceeded, with Retry-After: 30 | Wait the Retry-After seconds and retry with the same Idempotency-Key |
Errors#
| Status | Code | Summary |
|---|---|---|
| 400 | request.invalid | A run was started without an Idempotency-Key |
| 401 | auth.unauthorized | No usable credential (cause deliberately unspecified) |
| 402 | credits.insufficient | The allocation cannot fund this action |
| 403 | auth.browser_origin | A secret key was used from a browser |
| 403 | auth.insufficient_scope | The credential does not hold the required scope |
| 422 | pagination.cursor_invalid | The cursor could not be read (e.g. a filter changed mid-pagination) |
| 422 | request.invalid | A field failed validation (extra.errors has the detail) |
| 429 | quota.burst_exceeded | Interactive burst limit hit — wait Retry-After seconds |
| 429 | quota.concurrency_exceeded | Too many runs are being started at once for this credential |
| 429 | quota.entity_ceiling | The credential has been shown its 30-day ceiling of distinct entities |
| 502 | upstream.unavailable | The agent engine failed to process the request |
| 503 | credits.unavailable | The billing service could not confirm the charge; retry with the same Idempotency-Key |
| 503 | upstream.unavailable | The retrieval engine is unreachable (not charged) |
Reading and cancelling a run answer:
| Status | Code | Summary |
|---|---|---|
| 401 | auth.unauthorized | No usable credential (cause deliberately unspecified) |
| 403 | auth.browser_origin | A secret key was used from a browser |
| 403 | auth.insufficient_scope | The credential does not hold the required scope |
| 404 | request.invalid | The agent run does not exist, or belongs to another workspace |
| 502 | upstream.unavailable | The agent engine failed to process the request |
| 503 | credits.unavailable | The billing service could not confirm the charge; retry with the same Idempotency-Key |
| 503 | upstream.unavailable | The retrieval engine is unreachable (not charged) |
The results route answers the same, plus 422 for a limit out of range or a
cursor it cannot use. The events stream answers 401, 403 and 404 before it
opens. The list route answers 401, 403 and 422.
404 request.invalid: the run id does not exist, or belongs to another workspace.502 upstream.unavailable: the agent engine answered with an error. Retry shortly.503: a dependency could not be reached. Retry shortly.