# Your first research run

> The async job contract — create, poll, read results, retry safely


Research runs are **asynchronous jobs**: creating one returns immediately; the
research itself takes minutes. Polling is the contract — never hold a
connection open waiting.

## Test mode — iterate for free

`{"dry_run": true}` behaves exactly like a real run — `202` + `Location`,
`status: "running"` — except it's free, instant to set up, and completes in
~15 simulated seconds. Poll the returned id like a real run; on completion it
carries sample rows + per-cell citations in your Blueprint's real shape
(`"test": true` throughout). Nothing is billed, persisted, or researched.
Build your **entire** create → poll → parse loop against it first.

## Create

```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 '{"query": "European industrial-automation companies using AI vision"}'
```

Send **either** `query` (a natural-language research intent) **or**
`seeds` (a list of URLs or names). Optional: `guidance` (steering text),
`depth` (`"standard"` | `"max"`), `monitored` (keep the results fresh after the
run; default true).

Response: `202` with a `Location` header and the run resource.

## Always send Idempotency-Key

Runs bill real money. With an `Idempotency-Key` header, retrying the same
request replays the original response instead of starting (and billing) a
second run. Reusing a key with a *different* body is a `422`; retrying while
the original is in flight is a `409` with `Retry-After`.

## Poll

```bash
curl -s $BASE_URL/v1/research/runs/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY"
```

```json-response
{"object": "research_run", "id": "…",
 "status": "running", "done": false, "error": null,
 "progress": {"phase": "investigate", "entities_found": 4, "entity_counts": {"companies": 4}},
 "metrics": {"search_queries": 41, "sources_read": 12, "…": "…"}}
```

`status` is a closed enum: `queued | running | cancelling | completed | failed |
cancelled`. `done: true` means terminal. On `failed`, `error.code` /
`error.message` say why — a failed run is automatically refunded. Poll every
~15 seconds (the `Retry-After` header on non-terminal responses is the hint).

`POST /v1/research/runs/{id}/cancel` stops an in-flight run.

## List

`GET /v1/research/runs?limit=&cursor=&status=` returns the standard list
envelope `{"data": [...], "has_more": bool, "next_cursor": "…"}`.

## Read the results

Results land in your Blueprint's entity tables, not in the run object — see
[Dataset sync](/developers/dataset-sync). Webhooks fire on
`run.completed` / `run.failed` / `run.cancelled` if you'd rather not poll —
see [Webhooks](/developers/webhooks).
