# STELQ Research API

Deep multi-source research jobs with resolving citations. Asynchronous: submit a job, get a handle back instantly, then poll (or take a webhook) until it's done. Most jobs finish in 5–15 minutes and read up to ~40 sources, returning a synthesized report plus the sources it stands on; a deep or source-heavy job can run longer.

- **Endpoint:** `POST https://api.stelq.com/v1/research/run`
- **Auth:** `Authorization: Bearer $STELQ_KEY`
- **Price:** from $0.08 (by depth) / job
- **Mode:** asynchronous (submit → poll)
- **Canonical URL:** https://stelq.com/api/docs/research (public, no auth, always current)

## Authentication

Every request needs a bearer token: `Authorization: Bearer $STELQ_KEY`. Create and scope keys in the console under API Keys — the secret is shown once. In the in-console Playground the key is injected for you; copied payloads reference the `$STELQ_KEY` environment variable, so set it in your agent's environment instead of pasting a live secret into a prompt.

## Request

`POST /v1/research/run`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | yes | The research question, in natural language. `prompt` is accepted as an alias. |
| `depth` | enum | no | How thorough the job is: quick, balanced, or deep (default balanced). quick is bounded and the most predictable; deep reads the most sources and takes longest. This is the only effort control most callers need. `speed_mode` is accepted as an alias. |
| `max_tool_calls` | integer | no | Optional, advanced. An upper bound on research steps (4–120), applied within the chosen depth — higher reads more sources and runs slower, and depth still caps it. The budget knob you'll know from other research APIs. Omit to let depth decide. |
| `max_sources` | integer | no | Optional, legacy. A soft floor on sources considered (clamped ≤50); it never reduces a run below what your depth already gathers. Prefer `depth` (and `max_tool_calls` for finer control). |
| `citations` | boolean | no | Return resolving source citations on the result (default true). |
| `webhook` | url (https) | no | Optional. We POST a signed completion event to this URL when the job finishes, so you can skip polling. Must be https. Polling stays the source of truth — see Webhooks. |

```json
{
  "query": "Map AI-native search startups and their moats",
  "depth": "balanced",
  "citations": true
}
```

## The job lifecycle

Lifecycle: queued → in_progress → completed (the terminal states are completed, failed, or cancelled). The submit call returns `{ id, status: "queued" }` and is the billable event — The depth sets the price — quick $0.08, balanced $0.25, deep $0.40 — charged once on submit; polling is free. Poll `GET /v1/research/run/{id}` every 2–5 seconds until the status is terminal, then read `result.outputText` (fast, render-ready), `result.reportMarkdown` (long-form), and `result.citations` (source URLs). `result.dossier` carries the full structured sources for advanced use; `error` is populated only on failed/cancelled. A 502 on submit is safe to retry and is not charged; a 402 means top up your balance. To skip polling, pass a `webhook` and we'll notify you on completion — but polling always remains the source of truth.

Poll `GET /v1/research/run/{id}` until `status` is `completed`:

```json
{
  "id": "rsch_8K2mQ9vX3pLw",
  "status": "completed",
  "result": {
    "outputText": "AI-native search startups cluster around three moats: live indexing, citation fidelity, and agent-native delivery…",
    "reportMarkdown": "# AI-native search startups\n\n## Moats\n…full long-form report…",
    "citations": [
      "https://eur-lex.europa.eu/eli/reg/2024/1689",
      "https://example.com/ai-search-landscape"
    ],
    "dossier": {
      "…": "full structured retrieval packet — sources, synthesis, threads, gaps, stats"
    }
  }
}
```

### Submit response

```json
{
  "id": "rsch_8K2mQ9vX3pLw",
  "status": "queued"
}
```

## Webhooks

Pass a `webhook` (https URL) on submit and STELQ POSTs a small signed event there the moment the job reaches a terminal state — so you don't have to poll. The event is a signal, not the result body: read `data.result_url` to fetch the finished job. Different jobs can point at different URLs (the URL is your return address for that one job). Webhooks are an optimization on top of polling, never a replacement.

The event we POST:

```json
{
  "id": "evt_01JX7Z9C2QH4",
  "type": "research.run.completed",
  "api_version": "2026-06-01",
  "occurred_at": "2026-06-14T10:32:04Z",
  "attempt": 1,
  "data": {
    "job_id": "rsch_8K2mQ9vX3pLw",
    "status": "completed",
    "result_url": "https://api.stelq.com/v1/research/run/rsch_8K2mQ9vX3pLw"
  }
}
```

Signature headers: `webhook-id`, `webhook-timestamp`, `webhook-signature`.

**Verify the signature.** Verify before you trust the body: recompute HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{rawBody}` with your account signing secret (`whsec_…`, revealed any time under API Keys in the console), then constant-time compare it against the `webhook-signature` header (format `v1,<base64>`). Reject timestamps older than ~5 minutes, and always verify against the raw request bytes — not re-serialized JSON.

Delivery contract:

- At-least-once delivery — you may occasionally receive a duplicate. Dedupe on `data.job_id` (+ `type`) and process each completion exactly once.
- Acknowledge fast: return a 2xx within 10 seconds, then do any slow work asynchronously — otherwise we time out and retry.
- On any non-2xx or timeout we retry with exponential backoff, up to 8 attempts over ~24h, then dead-letter.
- Webhook down? Nothing is lost — the result stays available; just poll `result_url`.
- Exactly one terminal event per job: `research.run.completed` or `research.run.failed`. Treat it as final.

## Errors

| Status | Meaning |
| --- | --- |
| 400 | Invalid request — a field is missing or malformed (the body says which). |
| 401 | Missing or invalid API key. |
| 402 | Insufficient balance — top up credits to continue. |
| 429 | Rate limited — back off and retry after the Retry-After header. |
| 502 | Upstream temporarily unavailable — safe to retry; not charged. |

## Rate limits

Research is queue-backed: submit as many jobs as you need and they run as worker capacity frees up — submit returns instantly with a handle either way. If you submit faster than the queue accepts, you'll get a 429 with a Retry-After header; back off and retry.
