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.completed webhook fires.
  • failed — error.code/error.message on the resource; the run charge is automatically refunded; run.failed webhook 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:

Response· json
{"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:

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

bash
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.refunded reads false.
  • 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, not failed): 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, and cancel.refunded reads false. Stopped between attempts with everything already delivered, it settles completed, 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.

bash
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:

bash
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:

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

bash
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:

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

bash
curl -s $BASE_URL/v1/research/monitoring \
  -H "Authorization: Bearer $DEEPSIEVE_API_KEY"
json
{
  "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_window and billing are separate flags, not one status. The first means the report is inside its free 30 days and costs nothing yet (free_until is the first day it bills); the second means it is past that and spending units daily. The word "monitored" hides that difference, and it is the difference that shows up on an invoice next month.
  • billing_units per report is what it bills today: units minus units_in_free_window. A report you topped up with additive_to can be billing and still hold free units, because each additive run keeps its own 30 days; additive_windows says when each one starts to bill.
  • fully_covered_by lists 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 actually billing.
  • locked_until and stop_scheduled_at: turning a report's monitoring back on after its free 30 days commits 30 UTC days. locked_until is the exclusive 00:00Z end of that commitment, so the last billed day is the day before it. stop_scheduled_at is set when it was turned off during those 30 days: it stays on, and billing, until then.
  • billing_units vs monitored_units is what you are charged for versus what you are watching. duplicated_units is the gap you could reclaim.
  • reruns is 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's status is ok, no_allowance, or unavailable (it could not be read, so its numbers are null, never 0); the total says complete: false and a reruns_unavailable warning 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.

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

bash
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:

bash
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:

bash
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:

json
{"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:

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

Research runs — DeepSieve API