Research runs
Statuses, retries, refunds, depth, and monitoring — the run lifecycle in full
Base URL + auth as everywhere: Authorization: Bearer ds_live_....
Which Blueprint a run researches
Send blueprint_id (an id from GET /v1/blueprints) on the create and on the
estimate. It is required: without it the call is 400 invalid_request with
param: "blueprint_id", before anything runs or is charged. Only a dry_run may
omit it, and then uses your active Blueprint, which GET /v1/me names. The
estimate and a dry_run both return blueprint: {id, name} before
anything is spent, so check it there: a run on the wrong Blueprint polls and
completes exactly like one on the right Blueprint. Price with the same
blueprint_id you will run with. The create's 202 names the Blueprint too, but a
202 means the run has been charged; if it names the wrong one, stop it at once. A
run stopped before it has started is refunded in full. GET /v1/research/runs lists one Blueprint's runs
(?blueprint_id=, else the ambient one), and GET /v1/research/runs/{run_id}
finds a run in any Blueprint your key can read.
Lifecycle
queued → running → completed | failed | cancelled (with cancelling between a
cancel request and its terminal state). The enum is closed — build exhaustive
matches safely.
- completed — results persisted with citations;
run.completedwebhook fires. - failed —
error.code/error.messageon the resource; the run charge is automatically refunded;run.failedwebhook fires. Read the message first; to run it again, retry it rather than creating a new run. - cancelled — partial results may exist. Whether any of the charge came back depends on when you stopped it: see Cost & latency, and the cancel response says which.
Token counts. metrics.llm_input_tokens and llm_output_tokens add up every model call
the run's research made. A chat turn about the run is not counted. metrics.usage_complete
says whether that sum is whole: true when every call reported its usage, false when at least
one did not (the counts are then a floor, and metrics.usage_unreported_stages says where), and
null when unknown (a run from before this field, one still running, or one two API processes
executed, where whether every call reported its usage is not known; the counts themselves
survive a restart). Never read null as complete. Token counts are for your own accounting; a run is billed per run, never by tokens.
Search count. metrics.search_queries is how many web searches the run's research agent
ran, as Google reports them, summed over the run's research. It is null when that is not
known: a run from before this field was sourced from Google, one still running, one you
stopped, one recovered after an interruption by collecting only its finished research, and one where Google gave no count for some part of its research. null never means
zero searches. Searches made by other research steps are not counted yet. Like the token counts,
it is for your own information; a run is billed per run.
What a re-check found. A re-check is its own run. When you ask for one in
chat, the reverify action's reverify_run_id is that run's id. Once it is
done, its check_results has one entry per value this re-check covered, in the order
they were checked:
{"object": "research_run", "run_type": "reverify", "status": "completed", "done": true,
"check_results": [
{"evidence_id": "…", "outcome": "confirmed",
"message": "Checked: the value is still correct.", "followup_run_id": null}],
"update_result": null}Show message to your human as written. outcome is a stable key for choosing a tone. It is an
open string, so read a key you don't know as neutral; the current keys are listed in the API
reference. check_results is null on every other run, until the run finishes, and on a run
that ended before it could record a result (status and error then say why).
Follow-up searches. Some checks start a search for the current value, and then the entry's
followup_run_id names that search's run. Today that happens on re-checks that monitoring
schedules when a value goes stale, and on a re-check started from a value's Re-verify button in
the app; a re-check you ask for in chat does not start one. When a chat re-check finds that the
sources don't confirm a value, that entry's outcome is not_confirmed, and the value's
Re-verify button in the app is what starts the search. Read the follow-up run the same way: once it is done, its update_result ({outcome, message}) says
what the search found. Research that reuses values you already hold can start the same kind of
search, and that run carries an update_result too.
Cost & latency
A run bills prepaid credits when it starts (typically $10 standard / $20 max
depth — see pricing, or /billing for your own account) and takes
15-60 minutes.
One run researches one topic area and fills your Blueprint with what it finds. How many rows that yields depends on the area, so don't budget from an entity count — price the specific run:
curl -s -X POST $BASE_URL/v1/research/runs/estimate \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-d '{"blueprint_id": "<blueprint_id>",
"query": "solid-state battery startups in Japan", "depth": "standard"}'The estimate is the price. It prices the same request on the same steps the create
charges on: seeds that are one entity count once, seeds an additive_to report already holds
are left out, and with reuse_existing the seeds you already hold current data on are priced
out (listed in reuse_predicted). A known free run is priced on the entities it researches.
Send the same reuse_existing and temporal_cutoff you will create with. A request the create
would refuse before charging (everything already held) is refused here with the same 422 and
message, so you find out before you commit.
Bind the run to the estimate. Once your human approves the estimate, send its
run_price_cents as expected_total_cents on the create. If the run would now cost more (a
query read into more subjects, other seeds or depth, a price change), the create is refused
409 run_price_changed with the new total in error.details.total_price_cents, and nothing is
charged or created: ask again, then resend with that total. The same or a lower price starts as
usual. Omit it and the create behaves as before.
A create can also refuse 409 run_funding_changed (retriable), whether or not you bound it.
How the run would be paid changed before it started: it was handled as your free run, and it
would have been paid from plan credits or your balance. Nothing was charged. Estimate again and
confirm the price it states with your human before you create it again.
curl -s -X POST $BASE_URL/v1/research/runs \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"blueprint_id": "<blueprint_id>",
"query": "solid-state battery startups in Japan", "depth": "standard",
"expected_total_cents": 1000}'Seeding with URLs? Check them first. A URL seed is read to find what to
research (each entity found on the page is then researched by name), so a page
that names nothing of your Blueprint's kinds adds nothing, and is still priced as
a seed. Add "check_urls": true to the estimate and url_outcomes says, per URL
and in order, found (with a count), none_found or unreadable. It reads each
page, which can take several fetches and model calls per URL and uses your
organization's model allowance (a free organization's never resets), so it is
opt-in; without it, an estimate with URL seeds carries a check_urls_hint and
reads nothing. It reads at most 25 URLs (a URL sent twice is read once), reads
them without your run's guidance, and never changes the price.
"Use only these URLs." Send them as a query, such as "research these four pages, use
only these URLs: P01: https://…, P02: https://…". The parser keeps each URL as its own
target, in your order, with your label, rather than reading it for names: the run and the
estimate say seed_mode: "as_typed", and a target is named P01 (https://…). A seeds
request is always "discover": each URL is read to find what to research. seed_labels
({"https://…/p01": "P01"}) is stored and returned on either path.
Your first real run is free (one per account, before you subscribe) and isn't consumed if it fails.
Failed runs are refunded automatically. When you stop a run:
- Before it has started (still waiting to begin): refunded in full. An
automatic re-research run counts as not started until it really begins. If it
was paid from your included re-research, the allowance comes back instead, and
cancel.refundedreadsfalse. - Within 30 seconds of starting: refunded for whatever it had not yet delivered. A run that had already delivered everything gets nothing back.
- After that: stopping still stops it, but the run is charged, because the research is already underway.
- A run that crashed and is being retried automatically (it shows as
running, notfailed): stopping it refunds whatever it had not delivered, between retry attempts too. If it was paid from your included re-research, the allowance comes back instead, andcancel.refundedreadsfalse. Stopped between attempts with everything already delivered, it settlescompleted, with nothing refunded.
The cancel response says which: cancel.refunded, and cancel.refund_scope:
full, or partial when part of the run had already delivered results (that part
is kept, and charged). Both describe THIS call. Sending the cancel again, on a run
already stopping, reports refunded: false even when the first call refunded, so
keep the first response's values.
Spending is capped by the prepaid balance and any per-key budget: an agent can hit
402, never an invoice.
Top up a report instead of re-running it
If you already have a report and want to extend it, pass additive_to rather
than starting a fresh run. Entities that report already holds with in-TTL
evidence are dropped before the run is batched, so you pay for the gap, not
the list — re-sending 20 held seeds plus 6 new ones costs about the 6.
There is no cap on how many seeds you send: price scales with the list, so a
longer one simply costs more and you approve it first.
curl -s -X POST $BASE_URL/v1/research/runs \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"blueprint_id": "<blueprint_id>", "seeds": ["newco.example", "otherco.example"],
"additive_to": "<existing_run_id>"}'This is the right shape for a recurring loop — a quarterly refresh, a watchlist
you keep topping up. Without it, re-sending the full list starts a new run that
re-researches, and re-charges for, everything you already had. 404 means that
id isn't a run on the Blueprint you named.
An additive run joins its report's monitoring. It registers no monitoring of
its own, so monitored is ignored with additive_to (silently, in this
version): the report's setting governs it, and you turn monitoring on or off for
the whole report. The dossiers it adds are part of the report's units, and each
additive run keeps its own free 30 days from when it ran (or from when the
report's own window opened, if later), then bills with the report.
GET /v1/research/monitoring shows the split: units_in_free_window and each
run's additive_windows[].free_until.
Price it first. additive_to works on /runs/estimate too, and it narrows
the list exactly the way the charge does — so the quote is the gap, not the list
you sent:
curl -s -X POST $BASE_URL/v1/research/runs/estimate \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-d '{"blueprint_id": "<blueprint_id>", "seeds": ["newco.example", "otherco.example"],
"additive_to": "<existing_run_id>"}'The response carries skipped_already_held when anything was dropped — that is
why the number is lower than the list you sent, and worth logging so a surprise
looks like an explanation rather than a bug.
Reuse what we already have
If the Blueprint already holds current data on some of your seeds, you can keep it instead of paying to research those entities again:
curl -s -X POST $BASE_URL/v1/research/runs \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"blueprint_id": "<blueprint_id>",
"seeds": ["acme.example", "newco.example"], "reuse_existing": true}'Entities we already cover with in-TTL evidence are carried into the new
report as they stand, with their sources, and cost no run credit. Anything stale
is researched again regardless — reuse never passes off out-of-date data as
current. The 202 lists the entities the price left out as reuse_predicted
(and, for compatibility, reused_existing), so a cheaper run than your seed list
implies is explained rather than mysterious.
That list is a prediction, priced with your allowed_sources applied. The run
itself records what reuse actually carried as reuse_carried, and any entity it
skipped because a refresh of it was already running as being_refreshed (with that
run's id). An entity predicted held that reuse cannot carry is researched anyway at
no extra charge, up to a per-run limit; any past it are listed in
reuse_not_researched so you can re-run them. You never pay more than the quote.
This is the lineage-free sibling of additive_to: use reuse_existing when you
just want whatever we have, and additive_to when you specifically mean "extend
that report".
Only cite the sources you name
allowed_sources restricts where a run may take its evidence from. Research is
steered toward the list (its searches are asked to stay on those sources), but
that is a request, not the guarantee. The guarantee is the rule the run enforces
at write time: a cell whose only support is a site outside the list is not
written, and its URLs come back as a lead so you can see what was withheld.
curl -X POST $BASE_URL/v1/research/runs \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"blueprint_id": "<blueprint_id>", "seeds": ["acme.example"],
"allowed_sources": ["*.gov", "https://sec.gov/cgi-bin/*", "blueprint:primary"]}'Entries are a host (subdomains included), a wildcard suffix such as *.gov
(a bare public suffix like *.com is refused), an exact-host URL prefix, or
blueprint:primary / blueprint:trade, which expand to the sources your
Blueprint declares. Seed URLs' hosts are always allowed. An empty list is
refused rather than treated as "no restriction"; omit the field for that. The
run resource returns the resolved list under allowed_sources, and after the
run stage_metrics.persisting.allowed_sources counts what was kept, dropped and
excluded by host. Restricting sources changes what a run returns, never what it
costs, and the estimate warns you of that before you spend. It also warns
(allowed_sources_seed_hosts_only) when the list is only your URL seeds' own
sites and each page is read to find what to research (seed_mode: "discover"):
those finds are researched by name, and keeping only those sites' citations can
leave nothing to write.
Exact phrases and kinds of document
A query that quotes a phrase (pages that say “Request for Information”) or names a kind of
document ("PDFs only") is read into search_hints: {"exact_phrases": [...], "file_types": ["pdf"]}. The research agent runs them as extra searches, never as a filter: a page without
the phrase, or a result that is not a PDF, still counts. They never change the price.
The estimate shows what was read under intake.search_hints. If the parser read one your user
did not mean, send the ones to keep on the estimate and then the create (an explicit value wins
over the parsed one), or {} to keep none:
curl -X POST $BASE_URL/v1/research/runs/estimate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"blueprint_id": "<blueprint_id>", "query": "Acme filings, PDFs only",
"search_hints": {}}'This is the same choice a person makes in the app, where the Review Plan dialog lists each one
and lets them remove it before Start. An unusable entry is a 400 invalid_request with
param: search_hints.
Depth
"depth": "standard" (default) or "max" — max lifts research budgets and
uses the deeper agent preview; it costs more per run.
Monitoring
"monitored": true (default) keeps the run's target re-verified after the
initial research, on a paid plan: every report is covered for its first 30 days,
then while monitoring stays on. That is the "living dataset". Your plan keeps a set number
of units fresh at no charge; each unit beyond that allowance bills per day
(see pricing for your plan's rate, or /billing for your account).
The run resource says whether it is monitored now: monitored is true or false, the
same state PATCH /v1/research/runs/{run_id}/monitoring sets and GET /v1/research/monitoring
lists, so a change since the run was created shows. An additive run reports its report's state.
monitored: null means monitoring does not apply to this run (a cascade rerun, an update child,
or a run from before monitoring existed). It never means monitoring is off; that is false.
The unit is a DOSSIER, not a report — one Deep Research call's returned data.
A 20-entity request is researched as 4 dossiers, so it is 4 run credits AND 4
monitoring units, even though it reads as one report. Budget for the second
number as well as the first: it is the recurring one. When a monitored value is
re-verified and changes, a dataset.updated webhook fires with the
record/column/evidence ids. Toggle per target later in the app.
See everything you're paying to monitor
An agent that creates reports on a schedule is the caller most likely to
accumulate overlapping monitoring: re-researching a watchlist into a fresh
report, rather than topping up the old one, is the natural loop — and it
silently doubles the recurring cost. GET /v1/research/monitoring is how you
audit that in one call.
curl -s $BASE_URL/v1/research/monitoring \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY"{
"object": "monitoring_overview",
"monitored_units": 8,
"billing_units": 3,
"duplicated_units": 3,
"reruns": {"included_per_period": 8, "included_used": 3, "awaiting_count": 0,
"next_period_start": "2026-10-01", "complete": true},
"reports": [
{"run_id": "…", "label": "EU vendors", "units": 4, "entities": 20,
"in_free_window": false, "free_until": "2026-07-30", "billing": true,
"units_in_free_window": 1, "billing_units": 3,
"additive_windows": [{"run_id": "…", "units": 1, "free_until": "2026-10-12",
"in_free_window": true}],
"fully_covered_by": ["<other_run_id>"],
"reruns": {"status": "ok", "included_per_period": 4, "included_used": 3,
"awaiting_count": 0, "next_period_start": "2026-10-01"},
"locked_until": "2026-10-20T00:00:00Z", "stop_scheduled_at": "2026-10-20T00:00:00Z"},
{"run_id": "…", "label": "Watchlist", "units": 4, "entities": 18,
"in_free_window": true, "free_until": "2026-09-04", "billing": false,
"units_in_free_window": 4, "billing_units": 0, "additive_windows": [],
"fully_covered_by": [],
"reruns": {"status": "ok", "included_per_period": 4, "included_used": 0,
"awaiting_count": 0, "next_period_start": "2026-10-01"},
"locked_until": null, "stop_scheduled_at": null}
],
"warnings": [{"level": "info", "code": "duplicate_monitoring", "…": "…"}]
}Three things worth wiring into a scheduled job:
in_free_windowandbillingare separate flags, not one status. The first means the report is inside its free 30 days and costs nothing yet (free_untilis the first day it bills); the second means it is past that and spendingunitsdaily. The word "monitored" hides that difference, and it is the difference that shows up on an invoice next month.billing_unitsper report is what it bills today:unitsminusunits_in_free_window. A report you topped up withadditive_tocan bebillingand still hold free units, because each additive run keeps its own 30 days;additive_windowssays when each one starts to bill.fully_covered_bylists reports whose in-TTL entity set is a superset of this one's — everything here is already kept fresh there, so these units buy nothing. Only reported for reports actuallybilling.locked_untilandstop_scheduled_at: turning a report's monitoring back on after its free 30 days commits 30 UTC days.locked_untilis the exclusive 00:00Z end of that commitment, so the last billed day is the day before it.stop_scheduled_atis set when it was turned off during those 30 days: it stays on, and billing, until then.billing_unitsvsmonitored_unitsis what you are charged for versus what you are watching.duplicated_unitsis the gap you could reclaim.rerunsis the included re-research used this billing period: when a monitored fact stops checking out, its record is re-researched, and each report includes some re-research runs per period (included_per_period, its own: they are not pooled across reports). Past that, a re-research waits for approval (awaiting_count). The total counts each report once. Each report'sstatusisok,no_allowance, orunavailable(it could not be read, so its numbers are null, never 0); the total sayscomplete: falseand areruns_unavailablewarning names the report when one was left out.
Advisory only: warnings carries info entries (and a warning when
re-research usage could not be read), and nothing is turned off on your behalf. Deciding which report keeps the coverage is a judgement call about
which one your pipeline reads from, and we won't guess it.
Turning one off (or on)
PATCH /v1/research/runs/{run_id}/monitoring with {"active": false} stops a
report's monitoring; {"active": true} starts it again. It needs runs:write
and a member role.
curl -s -X PATCH $BASE_URL/v1/research/runs/$RUN_ID/monitoring \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-d '{"active": false}'The response is that report in the same vocabulary as the overview's rows
(active, in_free_window, free_until, billing, units, reruns), read
back after the change. A UTC day bills if monitoring was on at any moment of
it: OFF stops billing from the next day, ON bills today, and days already
counted are never refunded. The free 30 days run on the calendar either way.
Sending the same state twice changes nothing. A run with no monitoring (an
update run) is 404 not_found. The MCP tool is set_monitoring, and the CLI
is deepsieve runs monitoring <run-id> --on|--off.
The same report also says what it costs and whether it can run:
price_cents_per_day is the per-unit daily price past the free window (null
when it could not be read, never 0); dormant: true means monitoring has never
started (an unsubscribed organization's report); paused: true means monitoring
is paused for the whole organization (for example, a zero balance), with
pause_fault saying whose remedy it is: customer (yours, such as adding
credits) or ours (contact support).
Turning it back on after the free 30 days commits 30 UTC days. That ON is
refused with 409 lock_confirmation_required until you send
"commit_lock": true; nothing changes, and error.details.locked_until says
when the 30 days would end (the exclusive 00:00Z end, so the last billed day
is the day before it). Ask your human before you send commit_lock: it
commits 30 days of spend. Send expected_locked_until (that same date) with it:
if a UTC midnight passed while they decided, the call is 409 lock_quote_changed with the new date and nothing is committed, so ask again.
curl -s -X PATCH $BASE_URL/v1/research/runs/$RUN_ID/monitoring \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-d '{"active": true, "commit_lock": true, "expected_locked_until": "2026-10-28T00:00:00+00:00"}'Turned off during those 30 days, monitoring stays on, and billing, until
locked_until (the response has stop_scheduled_at); turning it on again
before then just keeps it. Turned off before grace_until (15 minutes after
the ON that started it), it stops straight away and the commitment is
dropped; that day still counts.
Re-research waiting for approval
When a monitored fact stops checking out, its record is re-researched. Each report
includes some re-research per billing period (included_per_period); past that,
re-research waits for approval. Read it first:
curl -s $BASE_URL/v1/research/runs/$RUN_ID/reruns \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY"awaiting_count entities wait (entities, not the number of runs it buys), costing
awaiting_total_cents today; awaiting_interactions is the Deep Research work they need,
which the price scales with, and per_interaction_cents the price of one (null for a
mixed-depth backlog). items names every one, up to 500: the entity, the fact, and what
the re-check found. quote_id pins exactly those rows at that total. It is published only
beside the full list: when it is null, quote_withheld says why (more than 500 are
waiting, which you approve in the app). included_used and next_period_start say how
much of this period's allowance is used and when it renews; backlog_exceeds_renewal
means more are waiting than one renewal includes; covered_elsewhere counts the
entities another report's rerun took over this period, which need nothing here.
There is no decline. A rerun left waiting costs nothing, but its value is not treated
as verified until it is re-checked after next_period_start and decided again against the
renewed allowance. On a plan that includes no re-research, waiting never clears it.
Approving spends money. Show your human the items and the total; only if they agree,
send that total and the quote_id they saw:
curl -s -X POST $BASE_URL/v1/research/runs/$RUN_ID/reruns/approve \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"expected_total_cents": 900, "quote_id": "…"}'The approval buys exactly the rows they saw. If anything waiting changed since (a rerun
arrived, even at the same total, or they were already approved), it is
409 rerun_price_changed with the new total and count in error.details, and nothing is
charged: read the list again and ask again. Each rerun is charged like any run (plan
credits, then balance); too little credit is 402 insufficient_credits. The
Idempotency-Key is required: a retry with the same key replays the first answer and never
charges twice. The answer says how many were approved, their run_ids (several entities
can share one run), and how many are still_waiting. A charge refused part-way (another run
of the same record already running, or the balance moved) is still a 200: stopped says
why, stop_charged whether that batch took a charge, and the rest keep waiting.
Auto-approve is standing authority to spend.
PATCH /v1/research/runs/{run_id}/reruns/auto-approve with
{"auto_approve": true, "auto_approve_cap_cents": 5000} approves the report's future
re-research automatically, up to that cap a month. Turn it on only when your human asks,
with the cap they chose. It is recorded against the calling key or app: it switches off if
that key is revoked or expires, or its user signs out everywhere, and it pauses while the
key is suspended. (Revoking an app at AuthKit alone does not stop it.)
{"auto_approve": false} turns it off and never spends. The answer is the report's
re-research state, as GET …/reruns returns it: auto_approve, auto_approve_cap_cents,
auto_approve_spent_cents (what it has committed this billing period; null while it is off), and auto_approve_lapsed, which says why the last
automatic approval could not spend when it could not. The approve call also takes
"auto_approve": true (with a cap) to approve now and turn it on. Its answer's
auto_approve is the setting as saved: if saving it failed after the charge went
through, the answer still says 200, shows the setting unchanged, and you can set it again.
These need runs:write and a member role. The MCP tools are get_reruns,
approve_reruns and set_rerun_auto_approve, and the CLI is
deepsieve runs reruns <run-id>.
Retry a failed run
POST /v1/research/runs/{run_id}/retry retries a failed, stopped or empty run, as the
app's Retry does. If the run's research is still alive upstream, it resumes the same run;
otherwise it starts a new run with the same inputs, linked to this one. The 202 is the
retried run, with retry.source_run_id and retry.mode (resumed or restarted).
retry.funded_by is included_allowance when a restart was covered by the report's
included re-research, and null otherwise.
A retry that charges nothing starts at once. A retry that would charge asks first. The
first call is refused 409 retry_confirmation_required, nothing is created, and
error.details is the quote:
{"error": {"code": "retry_confirmation_required", "param": "expected_total_cents",
"details": {"total_price_cents": 1200, "interactions": 2, "shortfall_cents": 0,
"refund": {"kind": "refunded_dollars", "amount_cents": 1200},
"reuse_predicted": [], "skipped_already_held": [],
"funding": {"covered_by": "balance", "plan_credits_needed": 2,
"plan_credits_available": 0, "balance_cents": 5000}}}}total_price_cents is the list price even when the org's free run or plan credits would
pay; funding.covered_by (free_run, plan_credits or balance) says which. Tell your
human what would pay and what became of the failed run's charge (refund). Only if they
agree, send the total back, with the rail they agreed to:
curl -s -X POST $BASE_URL/v1/research/runs/$RUN_ID/retry \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"expected_total_cents": 1200, "expected_funding": "balance"}'expected_funding is optional, but without it only the total is bound: if your human agreed
to "uses 2 plan run credits" and another run spends those credits first, the retry is paid
from the balance. With it, that is refused instead.
A quote can also say covered_by: "mixed": plan run credits pay for the interactions they
cover and the balance pays the rest, funding.prepaid_cents of it. Confirm a mixed quote with
expected_funding: "mixed" AND expected_prepaid_cents set to that prepaid_cents. The rail
alone does not bind a split: credits spent elsewhere meanwhile would move more of the run onto
the balance at the same total. A mixed confirmation without it is 422 validation_failed
(param: expected_prepaid_cents), and a different split is 409 retry_price_changed.
If the price or what pays moved in between, the answer is 409 retry_price_changed with the
new quote, and nothing is charged: ask again. param is expected_funding whenever the rail
moved (even if the total moved too), else expected_total_cents, and the message says which
moved. If what pays moved in the instant between the quote and the debit, the refused attempt
stays in your run list as a failed run that was not charged; the next retry replaces it. A second call after a retry has started returns that
same retry. The charge gate's refusals match the start route's: 402 insufficient_credits (stop and tell your human) and 429 run_concurrency_exceeded (wait
for Retry-After). run_stopping means a cancel is still finishing (retry once the run is
cancelled); run_recovering means the run is already retrying itself; conflict means
it completed with results or has nothing new to research, so start a new run instead. It
needs runs:write and a member role.
Attribution & budgets
Runs created with an API key record that key (api_key_id) for audit and
budget purposes. Keys can carry budget_runs/budget_cents ceilings —
exceeding one returns 402 budget_exceeded.
Questions about a finished run
Ask the research agent directly — see Chat with the research agent. Your questions appear in the customer's own chat history for that report, attributed to your API key.