# STELQ — complete API documentation (llms-full.txt)

> Grounded web search, content extraction, answers, deep research, and standing monitors — as an API. One bearer key, one prepaid balance, five services, one hosted MCP server. This file is the whole reference in one place, generated from the live doc model so it always matches the running API. Canonical URL: https://stelq.com/api/docs/llms-full.txt

Base URL: https://api.stelq.com
Auth: every request carries `Authorization: Bearer $STELQ_KEY` (create a key in the console under API Keys; read it from the environment, never hardcode it).
MCP: the same services as native tools —

```json
{
  "mcpServers": {
    "stelq": {
      "url": "https://mcp.stelq.com/v1/mcp",
      "headers": {
        "Authorization": "Bearer $STELQ_KEY"
      }
    }
  }
}
```

## Contents

### Endpoint reference
1. Search — `POST /v1/search/query` — $0.001/query
2. Content — `POST /v1/content/extract` — $0.002/page
3. Research — `POST /v1/research/run` — from $0.08 (by depth)/job — asynchronous (submit → poll)
4. Answers — `POST /v1/answers/ask` — $0.002/answer
5. Monitors — `POST /v1/monitors/create` — $0.05/monitor per block of 12 queries, then $0.01 per check per block — standing — create once, checks run on a cadence; read events back or take a webhook

### Guides
1. Platform overview
2. Authentication & API keys
3. Your first call in 60 seconds
4. Billing & credits
5. Spend controls & auto-reload
6. Errors & status codes
7. Connect your agent (MCP & the work-order prompt)
8. Async research: submit, poll & idempotency
9. Webhooks (Research completion · Monitor checks)
10. Monitors: standing watches, sizing and price

---

# PART 1 — ENDPOINT REFERENCE

# STELQ Search API

Live web search on every request — never cached, never stale. Send a query, get back ranked results with titles, URLs and snippets. Synchronous: one request, one response.

- **Endpoint:** `POST https://api.stelq.com/v1/search/query`
- **Auth:** `Authorization: Bearer $STELQ_KEY`
- **Price:** $0.001 / query
- **Mode:** synchronous
- **Canonical URL:** https://stelq.com/api/docs/search (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/search/query`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | yes | The search query. Or send `queries` (an array of strings) for a batched multi-query search. |
| `limit` | integer | no | Results to return, 1–50 (default 10). Values over 50 are clamped — an X-Result-Limit-Applied header reports the value used. |
| `search_mode` | enum | no | One of auto, web, news, academic (default auto). |
| `freshness_days` | integer | no | Only results from the last N days. Omit or 0 for no recency filter. |
| `preferred_domains` | string[] | no | Bias results toward these domains. |
| `excluded_domains` | string[] | no | Drop results from these domains. |

```json
{
  "query": "best ai search infra 2026",
  "limit": 10,
  "search_mode": "auto"
}
```

## Response

```json
{
  "query": "best ai search infra 2026",
  "count": 10,
  "results": [
    {
      "title": "Building AI-native search in 2026",
      "url": "https://example.com/ai-search",
      "snippet": "An overview of live-index search infrastructure…",
      "published": "2026-05-31"
    }
  ]
}
```

## 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

Up to 50 requests/second per key. 429 with a Retry-After header when exceeded.

---

# STELQ Content API

Clean, structured extraction from any URL — Markdown and/or JSON, not HTML soup. Synchronous: one request, one response.

- **Endpoint:** `POST https://api.stelq.com/v1/content/extract`
- **Auth:** `Authorization: Bearer $STELQ_KEY`
- **Price:** $0.002 / page
- **Mode:** synchronous
- **Canonical URL:** https://stelq.com/api/docs/content (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/content/extract`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | string | yes | The page to extract. |
| `formats` | string[] | no | Any of markdown, json (default ["markdown"]). |
| `includeMetadata` | boolean | no | Include title, language, byline and fetch time (default false). |
| `max_characters` | integer | no | Maximum returned text (default 50000; 0 returns the complete extraction). |
| `text_query` | string | no | Question or phrase used to preserve relevant excerpts from anywhere in a long page. |
| `text_keywords` | string[] | no | Terms to find across the complete extraction and preserve in the bounded response. |
| `relevant_characters` | integer | no | Space reserved for matching excerpts (default 5000; 500 to 20000). |

```json
{
  "url": "https://example.com/report",
  "formats": [
    "markdown",
    "json"
  ],
  "includeMetadata": true,
  "text_query": "What changed in the revised policy?",
  "text_keywords": [
    "revised policy",
    "effective date"
  ]
}
```

## Response

```json
{
  "url": "https://example.com/report",
  "markdown": "# Report\n\nClean extracted body…",
  "json": {
    "title": "Report",
    "sections": []
  },
  "metadata": {
    "title": "Report",
    "lang": "en",
    "fetchedAt": "2026-06-14T12:00:00Z"
  },
  "text_selection": "head-plus-relevant-excerpts",
  "text_original_characters": 87342,
  "text_truncated": true,
  "text_relevant_characters": 4871
}
```

## 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

Up to 50 requests/second per key.

---

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

---

# STELQ Answers API

Grounded answers with citations, tuned for the agent loop. Ask a question, get a synthesized answer plus the sources it stands on. Synchronous: one request, one response.

- **Endpoint:** `POST https://api.stelq.com/v1/answers/ask`
- **Auth:** `Authorization: Bearer $STELQ_KEY`
- **Price:** $0.002 / answer
- **Mode:** synchronous
- **Canonical URL:** https://stelq.com/api/docs/answers (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/answers/ask`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | yes | The question to answer. |
| `maxTokens` | integer | no | Cap the answer length (default 512). |
| `citations` | boolean | no | Return the sources behind the answer (default true). |

```json
{
  "query": "RAG vs fine-tuning for enterprise?",
  "maxTokens": 512,
  "citations": true
}
```

## Response

```json
{
  "answer": "Use RAG when the knowledge changes often or must be cited; fine-tune when you need…",
  "citations": [
    {
      "url": "https://example.com/rag-vs-ft",
      "title": "RAG vs fine-tuning, compared"
    }
  ]
}
```

## 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

Up to 50 requests/second per key.

---

# STELQ Monitors API

Standing watches on any topic. Describe what to watch in natural language; STELQ compiles it into diverse search queries, authoritative domains, a per-topic significance rubric and direct sources (feeds, JSON endpoints, high-signal pages — each confirmed by a live fetch). The first check establishes a baseline; every later check on the cadence you chose is recorded and badged minor / notable / major against it, with a summary and the new items. Price follows size: $0.05 to create per block of 12 queries, $0.01 per check per block — preview both with a free plan call before you pay.

- **Endpoint:** `POST https://api.stelq.com/v1/monitors/create`
- **Auth:** `Authorization: Bearer $STELQ_KEY`
- **Price:** $0.05 / monitor per block of 12 queries, then $0.01 per check per block
- **Mode:** standing — create once, checks run on a cadence; read events back or take a webhook
- **Canonical URL:** https://stelq.com/api/docs/monitors (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.

## Endpoints

| Route | Price | Purpose |
| --- | --- | --- |
| `POST /v1/monitors/plan` | free | Compile the watch plan + price WITHOUT creating anything. Body: `query`, `interval?`, `max_queries?`. Returns `watch_plan` + `pricing`. Rate-limited with create. |
| `POST /v1/monitors/create` | $0.05 / block | Create the monitor (this page's request). Charged by size on a 201 only. |
| `GET /v1/monitors` | free | List your monitors with status, pricing and the latest event badge. |
| `GET /v1/monitors/{id}` | free | One monitor: watch plan, baseline, pricing and its recent events (`?limit=`, ≤100). |
| `GET /v1/monitors/{id}/events` | free | The event feed only — one entry per check: `significance`, `headline`, `summary_md`, `new_items[]`. |
| `GET /v1/monitors/{id}/events/{event_id}` | free | One check in full, plus `webhook` delivery status (state, attempts, last status) — the webhook's `result_url`. |
| `PATCH /v1/monitors/{id}` | free | Edit `name`, `interval`, `notify_threshold`, `webhook` (null removes) or `queries` (re-sizes the price). |
| `POST /v1/monitors/{id}/pause` | free | Stop checking (and billing) until resumed. |
| `POST /v1/monitors/{id}/resume` | free | Resume; the next check runs on the next sweep. |
| `DELETE /v1/monitors/{id}` | free | Delete the monitor and its record. |

## Request

`POST /v1/monitors/create`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | yes | What to watch, in natural language — a topic, vendor, regulation, competitor set. 3–2000 chars. `prompt` is accepted as an alias. |
| `interval` | enum | no | Check cadence: 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 24h, 7d (default 24h). The cadence ladder is the price ladder — see `pricing.est_daily_usd` on the response. |
| `notify_threshold` | enum | no | all, notable, or major (default all). Gates the WEBHOOK only — every check is recorded and readable regardless. |
| `webhook` | url (https) | no | Optional. Signed `monitor.baseline` / `monitor.change` events are POSTed here — same envelope, signature and retries as Research webhooks. Must be https. |
| `watch_plan` | object | no | Optional. The `watch_plan` returned by `POST /v1/monitors/plan`, as-is or edited (trim `queries` to lower the price, drop a source, add a domain). Skips the compile; you pay exactly the price the plan showed. |
| `max_queries` | integer | no | Optional price ceiling, 1–120: keep at most this many compiled queries. 12 queries = one price block. |
| `name` | string | no | Optional label (≤120 chars). Defaults to the compiled name. |

```json
{
  "query": "Anything about Regulation E (electronic fund transfers): rule changes, CFPB guidance, enforcement actions",
  "interval": "6h",
  "notify_threshold": "notable",
  "webhook": "https://example.com/hooks/stelq"
}
```

## How it runs

How a monitor runs: create (or plan → create) compiles the watch plan and returns the monitor with `pricing` and `next_check_at`. Within 5 minutes the first check runs and writes the BASELINE (the state of affairs; readable as `baseline_md` on the detail call). Every later check on your cadence retrieves the plan's queries and direct sources, diffs against everything the monitor has already seen, gates off-topic pages, and — when something is new — writes one event with `significance` (none / minor / notable / major, scored against the baseline using the compiled rubric), a one-line `headline`, a `summary_md` and the `new_items`. Quiet checks are recorded too (`significance: "none"`). Each completed check bills `pricing.per_check_usd`; failed checks are never billed. If the balance can't cover a check it is skipped and the schedule pushed; three consecutive skips auto-pause the monitor (`paused_reason: "insufficient_balance"`) — top up and resume. Read events back with the events routes, or take a webhook for the ones that clear your `notify_threshold`.

## Response

```json
{
  "monitor": {
    "id": "a01aede0-ffb0-41a8-ae64-b92190596f59",
    "name": "Regulation E watch",
    "status": "active",
    "interval_minutes": 360,
    "notify_threshold": "notable",
    "webhook_url": "https://example.com/hooks/stelq",
    "pricing": {
      "size_blocks": 1,
      "queries": 8,
      "create_usd": 0.05,
      "per_check_usd": 0.01,
      "checks_per_day": 4,
      "est_daily_usd": 0.04
    },
    "next_check_at": "2026-08-28T14:05:00Z",
    "watch_plan": {
      "queries": [
        "Regulation E amendments 2026",
        "CFPB Regulation E guidance",
        "…"
      ],
      "preferred_domains": [
        "consumerfinance.gov",
        "federalregister.gov"
      ],
      "freshness_days": 7,
      "significance_rubric": "major: a final rule or enforcement action; notable: proposed rule, official guidance; minor: commentary…",
      "sources": [
        {
          "type": "feed",
          "url": "https://www.consumerfinance.gov/about-us/newsroom/feed/",
          "healthy": true
        }
      ]
    }
  },
  "webhook": {
    "url": "https://example.com/hooks/stelq",
    "signed_with": "account_signing_secret",
    "events": [
      "monitor.baseline",
      "monitor.change"
    ]
  },
  "first_check": "within 5 minutes; the first check establishes the baseline your updates are measured against"
}
```

## Webhooks

Pass a `webhook` (https URL) on create — or set one later with PATCH — and STELQ POSTs a signed event for the first check (`monitor.baseline`, always, so you can verify the integration) and for every later check whose `significance` clears your `notify_threshold` (`monitor.change`). Quiet checks never notify. The event is a claim-check with a routing hint: `data.significance` and `data.headline` let you route without a fetch; `data.result_url` returns the full check (summary, items) over the authenticated API. Same envelope, headers, secret and retries as Research webhooks — one receiver handles both.

The event we POST:

```json
{
  "id": "evt_01JX9A4M2KQ7",
  "type": "monitor.change",
  "api_version": "2026-06-01",
  "occurred_at": "2026-08-28T14:07:12Z",
  "attempt": 1,
  "data": {
    "monitor_id": "a01aede0-ffb0-41a8-ae64-b92190596f59",
    "monitor_name": "Regulation E watch",
    "event_id": "5c1b7e2a-0d4f-4c8e-9a3b-2f6d8e1c4b7a",
    "significance": "notable",
    "headline": "CFPB issues proposed rule expanding Reg E error-resolution to P2P fraud",
    "new_count": 3,
    "result_url": "https://api.stelq.com/v1/monitors/a01aede0-ffb0-41a8-ae64-b92190596f59/events/5c1b7e2a-0d4f-4c8e-9a3b-2f6d8e1c4b7a"
  }
}
```

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 — the SAME secret that signs Research webhooks), 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.

Delivery contract:

- At-least-once delivery — dedupe on `data.event_id` (+ `type`). One check can never produce two different events.
- 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. `GET …/events/{event_id}` shows the delivery state for that check.
- Webhook down? Nothing is lost — every check is recorded; read the events feed.
- `monitor.baseline` fires once (the first check). `monitor.change` fires only for checks at or above `notify_threshold`; a check with `significance: "none"` never notifies.
- The secret is snapshotted onto the monitor when the webhook is set. Rotating your account secret never breaks a running monitor; re-set the webhook (PATCH) to pick up a new secret.

## 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. |
| 404 | No monitor (or event) with that id on this account. |

## Rate limits

`plan` and `create` each compile a watch plan (one model call + live source verification), so they share a limit of 10 per minute per account; a 429 carries a Retry-After header. Reads, edits, pause/resume and delete are unmetered. Checks themselves run on our schedule and never count against your limit.

---

# PART 2 — GUIDES

# STELQ Docs — Platform overview

One bearer key, five services, one hosted MCP server, prepaid pay-per-call. The 60-second map of how STELQ fits together before you drill into any single reference — the orientation a per-service Quickstart structurally cannot give you.

## The five services

STELQ exposes five live services under `https://api.stelq.com`. Four are a single POST endpoint; Monitors is a small route family (create, read, edit, pause). They share one auth model, one wallet, and one error vocabulary — so once you can call one, you can call all five. Each row below links to its own Docs reference for the full parameter set; this page is just the map.

| Service | Route | Price | Shape |
| --- | --- | --- | --- |
| Search | POST /v1/search/query | $0.001 / query | sync |
| Content | POST /v1/content/extract | $0.002 / page | sync |
| Answers | POST /v1/answers/ask | $0.002 / answer | sync |
| Research | POST /v1/research/run | $0.08–0.40 / job by depth | async |
| Monitors | POST /v1/monitors/create (+ read/edit/pause routes) | $0.05 / block to create · $0.01 / check / block | standing |

- Search — live web search on every request, never cached. Send `query` (or `queries` for a batched multi-query call); modes `auto`/`web`/`news`/`academic`, `freshness_days`, preferred/excluded domains.
- Content — clean structured extraction from any URL as Markdown and/or JSON, not HTML soup.
- Answers — a grounded answer with resolving citations, tuned for the agent loop. $0.002 per answer.
- Research — deep multi-source jobs with citations. $0.08 (quick), $0.25 (balanced) or $0.40 (deep) per job, charged once on submit. The only async service.
- Monitors — standing watches: compiled once, checked on a cadence you choose, every check recorded and badged minor/notable/major. Priced by size (blocks of 12 queries) × cadence; webhooks on what clears your threshold. The only service that keeps billing after the call.

> **Tip:** Same auth, same wallet, same error shape across all five. What differs service to service is the request body and the shape: synchronous (three), async submit → poll (Research), or standing (Monitors — create once, read events back). Wire the plumbing once; reuse it everywhere.

## One key, one wallet, two ways to call

A single key authenticates every service. Send it as a bearer token on every request — `Authorization: Bearer $STELQ_KEY` — and the same key works across all five. Create keys in the console under API Keys; the secret is shown once. See Authentication for live vs test keys and what "scope" actually means.

Every successful call debits one prepaid wallet. There are no subscriptions and no per-service plans: you top up credits, and each call subtracts its fixed per-service cost. An empty wallet returns `402` — top up to continue. See Billing & credits for top-ups and auto-reload.

There are two transports for the exact same call. You can hit the REST endpoint directly, or reach every service as a native tool through one hosted MCP server at `https://mcp.stelq.com/v1/mcp` (the same worker also answers at `api.stelq.com/v1/mcp`). Either way the call runs through the same handler, debits the same wallet at the same per-service cost, and authenticates with the same bearer key — REST and MCP differ only in how the request reaches the gateway, never in what it costs or returns. See Connect an agent to wire up MCP.

*The same Search call, over REST*
```bash
curl -X POST https://api.stelq.com/v1/search/query \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "best ai search infra 2026", "limit": 10}'
```

## Sync vs async

Three of the five services are synchronous: one request in, one response out. Search, Content, and Answers each return their result on the same HTTP call — there is nothing to poll, no job handle, no webhook. Send the POST, read the body. Research is async (submit → poll) and Monitors are standing (create once, read events back) — both covered by their own guides.

Research is the only asynchronous service. `POST /v1/research/run` charges by depth ($0.08 quick, $0.25 balanced, $0.40 deep) and returns a job handle instantly — `{ id, status: "queued" }` — and the job runs in the background (most jobs finish in 5–15 minutes; a deep job can run longer). You then poll `GET /v1/research/run/{id}` until the status is terminal, or pass an optional `webhook` on submit to be notified on completion. Polling is free and always remains the source of truth. Treat the job handle — not a timer — as the source of truth, and don't set a client timeout below 30 minutes.

> **Note:** Async polling, completion webhooks, and idempotency keys are Research-only mechanics. They do not apply to Search, Content, or Answers — those are plain synchronous request/response. The full submit-then-poll lifecycle, the signed completion webhook, and the Idempotency-Key contract are taught in Async research.

## Related

- Authentication — https://stelq.com/docs/authentication
- Billing & credits — https://stelq.com/docs/billing-credits
- Connect the MCP server — https://stelq.com/docs/agent-mcp-connect
- Research lifecycle & idempotency — https://stelq.com/docs/research-lifecycle-idempotency
- Search reference — https://stelq.com/docs/api/search
- Content reference — https://stelq.com/docs/api/content
- Answers reference — https://stelq.com/docs/api/answers
- Research reference — https://stelq.com/docs/api/research

---
Source: STELQ Documentation. https://stelq.com/docs/platform-overview

---

# STELQ Docs — Authentication & API keys

Every /v1/* call carries Authorization: Bearer $STELQ_KEY. Keys are stelq_live_ / stelq_test_, the secret is shown exactly once, and you pause, revoke, or expire them in the console. This is the cross-service article the per-service Auth section only summarizes.

## Bearer token on every request

Every request needs a bearer token: `Authorization: Bearer $STELQ_KEY`. The gateway requires the literal `Bearer ` prefix — any other scheme, or a missing header, is rejected at the edge with `401` before your request reaches a service. There is no cookie, query-param, or basic-auth fallback; the header is the only credential.

*search · cURL*
```bash
curl https://api.stelq.com/v1/search/query \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "best ai search infra 2026", "limit": 10 }'
```

Read the key from the `STELQ_KEY` environment variable — never hardcode it and never paste a live secret into a prompt, a chat log, or source control. Every copyable payload in the console (cURL, MCP config, agent prompt) references `$STELQ_KEY` for exactly this reason: you set it once in your agent's environment, and the credential never travels through a model's context window. In the in-console Playground the key is injected for you, so you can call a live endpoint without ever handling the secret yourself.

> **Tip:** Same token, every service. The header above is identical for Search, Content, Research, and Answers — auth is platform-wide, not per-endpoint. Wire it once and every /v1/* call is authenticated.

## Key format and the shown-once secret

STELQ keys are prefixed `stelq_live_` or `stelq_test_`, followed by 32 bytes of base64url randomness — not `sk_live_`/`sk_test_`. The prefix makes a leaked key instantly recognizable in logs and scanners.

| Element | Value |
| --- | --- |
| Live prefix | stelq_live_ |
| Test prefix | stelq_test_ |
| Random part | base64url(32 bytes) |
| Header | Authorization: Bearer <key> |

The plaintext secret is returned exactly once — in the response to the create call, and in the reveal dialog right after. The console never sees it again: only a SHA-256 hash (for lookup at the gateway) and the last 4 characters (for a readable preview in your key list) are stored. A key minted automatically at signup is delivered the same way — once, via a `?newKey=` parameter the console reads to show the reveal modal, then immediately strips from the URL and browser history so the secret can't linger in a shared link or back-button.

> **Warning:** A lost key cannot be recovered — there is no plaintext to recover. If you close the reveal dialog without copying it, or you misplace it later, create a new key and delete the old one. Rotate, don't retrieve.

## Key states: pause, revoke, expire

A key is `active`, `paused`, or `revoked`, and may also carry an optional expiry date. The gateway resolves the state on every request, so a state change takes effect for the very next call — there is nothing to redeploy.

| State | What it means | Gateway response |
| --- | --- | --- |
| active | Normal — the key authenticates and bills your wallet. | Request proceeds |
| paused | Temporarily disabled; flip it back to active anytime. | 403 — "This API key is paused." |
| revoked | Permanently killed (soft delete — marked revoked and kept in the kill-log; its captured payloads are purged). | 401 — treated as an invalid key |
| expired | Past its expiry timestamp. | 401 — "This API key has expired." |

Pause is the reversible control — flip a key off to stop spend during an incident, flip it back on when you're ready, with no new secret to distribute. Revoke is the irreversible one: it permanently marks the key revoked and, in the same operation, purges any raw request/response payloads that key captured (payload logging is off by default and opt-in per key). Revoked keys stay visible in a collapsed kill-log beneath your active keys so you can confirm what you've already shut down.

Expiry is enforced at the gateway: once the current time passes a key's expiry timestamp, every call returns `401`. The platform understands fixed expiry windows — 7, 30, or 90 days, 1 year, or never — though new keys created from the current console form default to no expiry (you set a name and an optional monthly spend cap there). Don't rely on expiry as your only off-switch: pause or revoke when you need an immediate, deliberate stop.

> **Note:** One more invariant from the control plane: if your whole account is suspended, all of its keys stop authenticating immediately and uniformly return 401 — the gateway never leaks the underlying reason.

## Two honest caveats

Two things about keys are easy to assume and currently wrong. We'd rather you know them than discover them.

> **Warning:** Keys are unscoped. The current backend issues unscoped keys — every key can call every service. There is no per-service or per-endpoint permission boundary and no authorization check beyond "is this key valid and active." When the console or this prose says you can "scope" a key, that means its spend configuration — an optional spend cap (and, where supported, a reset period and expiry) — not a permission boundary. Do not architect around per-service key scopes; they don't exist yet.

> **Warning:** Test keys are not a sandbox. The stelq_test_ prefix is a latent field with no behavioral difference: a test key bills the same live wallet, at the same per-call price, against the same services as a live key. There is no free test mode, no sandboxed data, and no live/test toggle in the create form — new keys default to live. Treat a stelq_test_ key exactly like a live credential.

Both caveats are statements about today's backend, not aspirations. If per-service scopes or a true test mode ship later, this article changes with them — until then, plan for one tier of fully-capable, live-billing keys.

## Related

- Spend controls & limits — https://stelq.com/docs/spend-controls
- Errors & status codes — https://stelq.com/docs/errors-and-status-codes
- Your first call — https://stelq.com/docs/first-call

---
Source: STELQ Documentation. https://stelq.com/docs/authentication

---

# STELQ Docs — Your first call in 60 seconds

Set $STELQ_KEY, POST to /v1/search/query, read X-Request-Id — your first successful call is the activation moment. Signup already minted a live key and seeded $5.00 of free credit, so you reach a billed 200 before ever touching Stripe.

## You already have a key and $5

You don't have to set anything up to make your first call. The moment you signed up, STELQ minted you a live API key named `Primary Key` and seeded your balance with $5.00 of free welcome credit — no card, no Stripe, nothing to configure. Your first request can hit a billed 200 immediately.

> **Tip:** Your `Primary Key` secret was shown to you exactly once, right after signup — it arrived on the console URL as `?newKey=…` and was wiped from the address bar as soon as the page loaded. If you saved it, set it as `$STELQ_KEY` and skip ahead. If you didn't, that secret is gone for good — mint a fresh key under API Keys (see Authentication).

| What signup gave you | Value |
| --- | --- |
| Live key | One key named `Primary Key`, secret returned once |
| Free credit | $5.00, no card required |
| First billed call | Search at $0.001 — about 5,000 queries on the welcome credit alone |

Set the secret as an environment variable so nothing downstream pastes a live credential into a prompt or a chat log:

*Set your key*
```bash
export STELQ_KEY="stelq_live_…"   # the secret from ?newKey= at signup
```

## One curl to a 200

Search is the lowest-cost service at $0.001 per query, so it's the right place to spend your first call and watch the free credit barely move. This is the exact cURL the console generates from the live Search contract; copy it as-is:

*POST /v1/search/query*
```bash
curl -X POST https://api.stelq.com/v1/search/query \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"best ai search infra 2026","limit":10,"search_mode":"auto"}'
```

A `200` comes back synchronously — one request, one response — with ranked results carrying titles, URLs and snippets:

*200 OK*
```json
{
  "query": "best ai search infra 2026",
  "count": 10,
  "results": [
    {
      "title": "Building AI-native search in 2026",
      "url": "https://example.com/ai-search",
      "snippet": "An overview of live-index search infrastructure…",
      "published": "2026-05-31"
    }
  ]
}
```

> **Note:** That 200 is your activation moment — the first time STELQ ran live web search over a query you chose and billed you for it. Everything else in these docs is built on this one round trip.

## How to know it worked

Beyond the `200` status, every successful response carries two headers that prove the call reached the engine and tell you how it performed. Read them on the response, not the body:

| Response header | What it tells you |
| --- | --- |
| X-Request-Id | A unique id for this exact request — quote it in support tickets and use it to correlate the call in your Activity log. |
| X-Latency-Ms | How long the upstream engine took to answer, in milliseconds. |

To see them, add `-i` to the cURL above (or inspect response headers in your client):

*See the headers*
```bash
curl -i -X POST https://api.stelq.com/v1/search/query \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"best ai search infra 2026"}'

# HTTP/2 200
# x-request-id: srch_…
# x-latency-ms: 312
# content-type: application/json
```

> **Tip:** Failed attempts are free. STELQ only charges on success: any response with a 4xx or 5xx status records a cost of $0.00, so you can retry a malformed first call as many times as it takes to get your 200 without spending a cent of your $5.

If your call came back 4xx or 5xx instead of 200, every status code is uniform across the platform and the error body tells you which field is wrong — see Errors & status codes. And if you're wiring an agent rather than a shell, the same activation moment applies: connect the MCP server and prove it with one real call (see Connect an agent).

## Related

- Authentication — https://stelq.com/docs/authentication
- Billing & credits — https://stelq.com/docs/billing-credits
- Errors & status codes — https://stelq.com/docs/errors-and-status-codes
- Connect an agent over MCP — https://stelq.com/docs/agent-mcp-connect
- Search API reference — https://stelq.com/docs/api/search

---
Source: STELQ Documentation. https://stelq.com/docs/first-call

---

# STELQ Docs — Billing & credits

A prepaid wallet, no subscriptions: every successful call debits a fixed per-service cost, failed calls cost nothing, and you top up in whole dollars via Stripe Checkout. The cross-service money model that lives on no single service tab.

## Prepaid wallet, charge-on-success only

STELQ runs a prepaid wallet — buy credit up front, get debited per successful call. There are no subscriptions and no monthly bill. Your balance lives in one place (`user_balances`); every discrete money movement (top-up, adjustment, refund) is a row in `balance_ledger` of type `topup`, `usage`, or `adjustment`. Per-call usage is debited from the balance in real time and folded into a daily rollup rather than written as one ledger row per request.

> **Tip:** Failed calls cost nothing. Any response with a status code of 400 or above — a 400 validation error, a 401, 402 or 403, and a 502 when the upstream engine is unreachable — records a cost of exactly $0. This is not a policy; it is the code: the debit amount is `statusCode < 400 ? cost : 0`. You are billed only for work that succeeded.

The same rule covers Research, which is the one service that charges on submit: if the submit itself fails (an upstream 5xx or an unreachable engine), the submit price is not charged and the idempotency claim is released so you can retry. The async report that arrives later on the free poll is never separately billed.

The debit is idempotent per request. Internally, usage is recorded as an insert into `usage_events` keyed on the per-request `request_id` with `ON CONFLICT (request_id) DO NOTHING`; if the row already exists, the balance is not touched again. A retried record-usage call — a network blip, a redelivery — can never double-charge a single request.

*A failed call returns its error and bills $0*
```json
{
  "error": "Upstream service unavailable. Please try again shortly."
}
// HTTP/1.1 502 Bad Gateway  →  recorded cost: 0
```

## What a call costs

Each service has a fixed per-call price, debited only on success: Search $0.001 per query, Content $0.002 per page, Answers $0.002 per answer, and Research $0.08 / $0.25 / $0.40 per job by depth (quick / balanced / deep). Monitors price on size: $0.05 to create per block of 12 compiled queries, then $0.01 per completed check per block — both shown on the monitor's `pricing` before you pay. These are the exact amounts the gateway (and the monitor sweep) debits — the price you see in the catalog is the price you pay.

| Service | Price | When charged |
| --- | --- | --- |
| Search | $0.001 / query | on success (sync) |
| Content | $0.002 / page | on success (sync) |
| Answers | $0.002 / answer | on success (sync) |
| Research | $0.08 / job | on submit, once (async) |
| Monitors | $0.05 / block to create · $0.01 / check / block | create on 201; each completed check by the sweep (standing) |

> **Warning:** Reading the API or a CSV export? All money is stored in units of $0.0001 — one ten-thousandth of a dollar — not cents. Divide by 10,000 to get dollars; never divide by 100. The database columns are named `*_cents` for historical reasons, but the unit is $0.0001. So a value of 800 is $0.08, 20 is $0.002, and 10 is $0.001.

*Converting a stored amount to dollars*
```bash
# A usage_events.cost_cents value of 800 is NOT $8.00
units=800
printf '$%.4f\n' "$(echo "$units / 10000" | bc -l)"   # -> $0.0800
```

## 402 and topping up

A `402 Payment Required` is returned before the upstream call is ever made, so a 402 never charges you and never does any work. Three independent checks can trigger it, in order:

- Absolute floor — your wallet has dropped to or below the negative-balance backstop (default -$0.0050, i.e. -50 units). This catches concurrent-request races.
- Per-request affordability — your balance is less than the cost of this specific call. The error states the request cost and your current balance.
- Per-key credit limit — this API key has a spend cap set, and the call would push it past the limit. The error names the spent-vs-limit figures; daily/weekly/monthly caps re-arm automatically when their window elapses.

*402 — affordability check, nothing charged*
```json
{
  "error": "Insufficient balance. This request costs $0.0800 but your balance is $0.0100. Please top up at https://stelq.com/dashboard/billing."
}
// HTTP/1.1 402 Payment Required
```

Top up in whole dollars only — a wallet is a fuel tank, not an invoice, so cents on the way in are rejected rather than silently rounded. The minimum top-up is $5 (below it Stripe's fixed fee is punitive) and the maximum is $10,000. The console offers presets of $5 / $10 / $25 / $50 / $100 / $250 plus a custom amount, defaulting to $25.

Payment runs through hosted Stripe Checkout in one-off `payment` mode. The browser redirect back to the console does NOT credit your wallet — it only shows a toast and re-polls the balance. Crediting happens exactly once, server-side, when Stripe's signed `checkout.session.completed` webhook fires. A partial unique index on `balance_ledger.stripe_payment_id` makes a double-credit impossible even under webhook retries or races: a repeat delivery inserts no row and the balance bump is skipped.

> **Tip:** New accounts start with $5 of free credit at signup — no card required — so you can make real calls before ever opening Checkout. For limiting spend per key (the credit-limit trigger above), see Spend controls.

## What is NOT offered

To keep your integration honest, here is what the billing system deliberately does not do. Do not build against any of these:

- No subscriptions, plans, or tiers — billing is purely prepaid, pay-as-you-go credit.
- No Stripe Customer Portal — there is nothing to manage for a non-subscription wallet.
- No tax or VAT invoices — receipts are ad-hoc (each top-up links to Stripe's hosted receipt, and Stripe emails one) plus a CSV ledger export. Formal invoicing is a manual support workflow, not a feature.
- No automated partial refunds — only FULL refunds and disputes are automated and reversed idempotently. A partial refund of prepaid credit is logged for manual handling.

> **Note:** If you need an invoice, a partial refund, or anything in this list, it is a support request, not an API call. Everything that IS automated above is grounded in the live code path; nothing here is a hidden endpoint waiting to be discovered.

## Related

- Spend controls — https://stelq.com/docs/spend-controls
- Errors & status codes — https://stelq.com/docs/errors-and-status-codes
- Platform overview — https://stelq.com/docs/platform-overview
- Research service — https://stelq.com/docs/api/research

---
Source: STELQ Documentation. https://stelq.com/docs/billing-credits

---

# STELQ Docs — Spend controls & auto-reload

Cap any key with a per-key credit limit that auto-resets and hard-stops with no overshoot, and optionally auto-top-up off-session before you run dry. The production controls for handing a key to an agent without fear.

## Two independent controls

STELQ gives you two separate guardrails for spend. A per-key credit limit hard-caps how much a single key can spend (great for handing a scoped key to an agent or a teammate). Auto-reload keeps your account wallet topped up off-session so long-running workloads never hit a `402` mid-job. They are orthogonal: a per-key cap protects you from one key running away; auto-reload protects you from the whole account running dry.

> **Note:** Both controls are about money, not access. Neither is a permission scope — a key that can spend can call every live service. To restrict what a key can do, mint a dedicated key per workload and cap it.

| Control | Scope | What it does | On limit |
| --- | --- | --- | --- |
| Per-key credit limit | One API key | Hard cap on that key's running spend, optionally re-arming each period | Rejects with 402 before the request reaches upstream |
| Auto-reload | Account wallet | Charges a saved card off-session when the balance dips below your threshold | Wallet is topped up; requests keep flowing |

## Per-key spend caps

Each key can carry a `creditLimitCents` and an optional `resetPeriod` of `daily`, `weekly`, or `monthly`. The gateway tracks the key's running spend and enforces a HARD cap: a request that would push the running total past the limit is rejected, and so is a request on a key that is already at or over the ceiling. There is no last-request overshoot — a key never spends a single cent beyond the limit its owner set.

The check runs in the gateway's balance middleware before the request is dispatched upstream, so a capped-out key is rejected pre-billing. When a `resetPeriod` is set and the period has elapsed since the key's last reset, the running total is treated as zero — the limit re-arms automatically, with nobody having to touch the key.

*402 when a key reaches its limit*
```json
{
  "error": "This key has reached its credit limit ($5.00 of $5.00). Raise the limit or resume spending from the dashboard at https://stelq.com/dashboard."
}
```

> **Tip:** In the Create API key modal, the credit limit is an optional dollar amount (cent precision; leave it blank for an uncapped key). Once you enter a limit, the "Reset limit every" dropdown unlocks with Daily, Weekly, and Monthly — all three re-arm the cap automatically when the period elapses. Leave it on N/A for a one-time, never-resetting ceiling.

A per-key cap is distinct from your account wallet. The wallet is the shared pool of prepaid credit every key draws from; the per-key cap is a ceiling on how much of that pool one key may consume. A request is allowed only if it clears BOTH checks — the wallet must afford it, and the key must be under its cap.

- creditLimitCents: the spend ceiling for this key (null = unlimited; the key spends straight from the wallet).
- resetPeriod: daily | weekly | monthly | null — when set, the running total auto-resets to zero once the period elapses.
- Enforced in the gateway before upstream dispatch, so a capped request never bills.
- Distinct from the account wallet balance, and NOT a permission scope.

## Auto-reload (off-session)

Auto-reload keeps your wallet funded without you in the loop. When a debit pushes your balance below a threshold you choose, a saved card is charged off-session and the wallet is credited — so a batch job or an autonomous agent that runs overnight never stalls on an empty balance. Both the threshold and the reload amount are whole dollars, and the amount must be at least your threshold so a reload always lifts you back out of the trigger zone.

Configure it at `GET` / `PUT /api/dashboard/billing/auto-reload`. Enabling unattended charges requires two things: a saved card and consent to the current off-session mandate. Without a card the enable call returns `409` (`needsCard`); without consent it returns `400`. Once you have accepted the mandate, later edits to the threshold or amount do not re-prompt. If Stripe billing is not configured for the platform, the enable call returns `503`.

*Enable auto-reload*
```bash
curl -X PUT https://stelq.com/api/dashboard/billing/auto-reload \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "threshold": 50,
    "amount": 100,
    "consent": true
  }'
```

Detection is fast. The moment a debit crosses your threshold, the usage path flags your account inline (no Stripe call on the hot path), and a gateway-driven sweep (~once a minute) services the flag — so reload latency is milliseconds-to-seconds, not minutes. The sweep also scans for under-threshold accounts directly, so a missed flag only ever means slightly slower, never missed.

## Safety rails & no double-charge

Off-session charging is the one place STELQ moves money without you present, so it is wrapped in hard guarantees. The headline invariant is no double-charge: there is at most ONE open reload episode per user, enforced by a database partial unique index. A second concurrent trigger cannot start a new charge — it must reuse the open episode's STABLE Stripe idempotency key, which is reused on every retry so Stripe itself returns the original PaymentIntent instead of charging a second time.

A charge whose fate is unknown is never re-charged blind. A reload stuck in `pending` is reconciled — the system asks Stripe whether it actually went through and settles it (crediting the wallet only if it truly succeeded) before any new charge is considered. The credit and the state advance commit in a single transaction, so the cooldown can never arm without money having actually moved.

| Rail | Default | Behavior |
| --- | --- | --- |
| Global kill switch | on | Admin-flippable; read live each sweep, so disabling it stops all charges on the next tick |
| Cooldown | 15 min | Minimum gap between reloads for one account |
| Max per day | 10 | Rolling 24h cap on reload attempts per account |
| Daily ceiling | $500 | Rolling 24h cap on dollars auto-reloaded per account |
| Decline disable | 3 strikes | Consecutive declines pause auto-reload until the user acts |

> **Tip:** The cap and decline rails are designed to fail safe: the daily cap is recomputed from a rolling 24h window so it auto-recovers as time passes, while a stuck card decline latches and pauses until you update the card or re-authenticate. Admins can lift the cooldown / per-day / ceiling rails for a heavy account without loosening them for everyone.

## Related

- Billing & credits — https://stelq.com/docs/billing-credits
- Authentication — https://stelq.com/docs/authentication

---
Source: STELQ Documentation. https://stelq.com/docs/spend-controls

---

# STELQ Docs — Errors & status codes

Every status the STELQ gateway actually returns — 400, 401, 402, 403, 404, 405, 409, 502, 503 — all in one uniform {"error": string} shape, with which are safe to retry and which are fatal until you fix something. This is the cross-service error contract; each service's own Errors section is the same table, filtered to that route.

## The uniform error shape

Every error the gateway returns is the same JSON object — a single `error` string — served as `application/json`. There is no `code` field, no nested `details`, no per-service envelope. If a call fails, you parse one shape and read one human-readable message. Branch on the HTTP status; read `error` for the why.

*the only error shape you ever get*
```json
{ "error": "Insufficient balance. This request costs $0.0010 but your balance is $0.0000. Please top up at https://stelq.com/dashboard/billing." }
```

This is the gateway-wide, cross-service error contract. It complements the Errors section on each service doc: a service's table is this same set, narrowed to the statuses that route can produce. Search, Content, and Answers share one synchronous proxy path; Research adds a couple of statuses of its own (409, and a 503 distinct from the auth one). The shape never changes between them.

> **Note:** Successful responses carry an `X-Request-Id` (and, on the sync services, `X-Latency-Ms`). Quote `X-Request-Id` when you report a problem — it ties your call to a row in the platform's request log.

## The complete table

Every status the gateway can originate, end to end — auth, balance, routing, and the upstream-down fallback. An upstream engine 5xx is passed through with its own status, but the values below are what the gateway itself produces.

| Status | Where | Meaning |
| --- | --- | --- |
| 400 | Any service | Invalid request — a field is missing or malformed. The `error` string names the field, e.g. "`limit` must be a positive integer." or "`query` is required." |
| 401 | Any service | Missing, malformed, invalid, revoked, or expired API key. Includes a missing or non-`Bearer` Authorization header. |
| 402 | Any service | Insufficient balance to cover the request, or the key has hit its credit limit. The message says which, and includes the dashboard top-up link. |
| 403 | Any service | This API key is paused. Resume it in the console; the key itself is still valid. |
| 404 | Any service | Unknown route. The body lists the routes that do exist in an `available` array. |
| 405 | Any service | Method not allowed — you sent a non-POST to a known POST route, or a GET to `/v1/mcp`. The generic case returns a plain "Method not allowed." body; only the `GET /v1/mcp` probe also sets an `Allow: POST, OPTIONS` header. |
| 409 | Research only | A research job for this exact request is already in flight (idempotency dedupe). Comes with `Retry-After: 2`. |
| 502 | Any service | Upstream engine unreachable — transient. Safe to retry. Not charged. |
| 503 | Auth / Research | The authentication service is down, or (Research submit) the idempotency claim store is unreachable. Transient; the Research case sends `Retry-After: 2`. |

## What is safe to retry, and Retry-After reality

Split the table into two halves so an agent can self-heal. Transient statuses mean the gateway or an upstream is momentarily unhealthy — retry with backoff and the same call will likely succeed. Fatal statuses mean the request itself is wrong — retrying the identical call changes nothing; something (the body, the key, the balance) has to change first.

| Status | Retry? | What to do |
| --- | --- | --- |
| 502 | Yes | Upstream is down. Retry with backoff. Not charged, so a retry costs nothing extra. |
| 503 | Yes | Auth or the Research claim store is down. Retry with backoff; honor `Retry-After` on the Research case. |
| 409 | Yes, briefly | Research duplicate in flight. Wait `Retry-After` seconds, then poll the original job instead of resubmitting. |
| 400 | No | Fix the request body — the `error` string names the offending field. |
| 401 | No | Fix the credential. Rotate or replace the key. |
| 402 | No | Top up credits, or raise the key's credit limit. Then retry. |
| 403 | No | Resume the paused key in the console, then retry. |
| 404 / 405 | No | Fix the route or method — check the `available` array on a 404; for a 405, POST to the route. |

On `Retry-After`: the gateway sets it in exactly two places, both Research-only, both hardcoded to `2` seconds — the 409 (duplicate in flight) and the 503 (claim store unreachable) on submit. The synchronous services — Search, Content, Answers — never send it. If you see `Retry-After`, you are talking to Research submit.

> **Tip:** No 4xx or 5xx is ever billed. The charge is computed as `cost when status < 400, else 0` on both the sync proxy and Research submit — so a 502, a 402, a 400, every failure records zero cost. A retry after a transient failure does not double-charge you.

The two Research-specific statuses here — the 409 dedupe and the claim-store 503 — are part of the idempotency design on research submit, not generic gateway behavior. See the cross-links below for how credits and balance produce the 402, and how Research idempotency produces the 409/503.

## Related

- Billing & credits — what causes a 402 — https://stelq.com/docs/billing-credits
- Research lifecycle & idempotency — the 409/503 dedupe — https://stelq.com/docs/research-lifecycle-idempotency
- Authentication — keys, 401 and 403 — https://stelq.com/docs/authentication

---
Source: STELQ Documentation. https://stelq.com/docs/errors-and-status-codes

---

# STELQ Docs — Connect your agent (MCP & the work-order prompt)

One hosted, stateless MCP server at https://mcp.stelq.com/v1/mcp exposes all five services as eleven native tools — the bearer key IS the session. Or hand a coding agent the copy-ready work-order prompt that wires itself and verifies with one real call.

## One config, all services

STELQ runs one hosted MCP server. Connecting it once exposes every STELQ service as a native agent tool — there is no per-service config and no glue code. Drop this into your MCP client (Claude Code, Cursor, Windsurf, etc.) and set `STELQ_KEY` in your environment:

*mcp config*
```json
{
  "mcpServers": {
    "stelq": {
      "url": "https://mcp.stelq.com/v1/mcp",
      "headers": { "Authorization": "Bearer $STELQ_KEY" }
    }
  }
}
```

This is exactly what the console emits — the same config no matter which service's Docs tab you copied it from. The server is reachable on both `mcp.stelq.com` and `api.stelq.com`; the console hands agents the `mcp.stelq.com/v1/mcp` form.

> **Note:** The server is stateless and POST-only. The bearer key in each request IS the session — there is nothing to log in to and no session id to track. The transport is JSON-RPC 2.0 over Streamable HTTP at POST /v1/mcp. A GET probe returns 405 with `Allow: POST, OPTIONS`; there is no server-initiated SSE.

Authentication is the same as the REST API: every request carries `Authorization: Bearer $STELQ_KEY`. Reference $STELQ_KEY from your environment — never paste a live secret. See Authentication for key states and the live-vs-test distinction.

## The eleven tools

`tools/list` returns eleven tools. The names do NOT map one-to-one onto service ids — read the table before you wire anything up:

| Tool | Backs | Mode | Price |
| --- | --- | --- | --- |
| search | Search · POST /v1/search/query | synchronous | $0.001 / query |
| content_extract | Content · POST /v1/content/extract | synchronous | $0.002 / page |
| answers | Answers · POST /v1/answers/ask | synchronous | $0.002 / answer |
| research | Research · POST /v1/research/run (submit + autopoll) | async, auto-polled | $0.08 / job |
| research_get | Research · poll a job by id | poll, free | $0 (no charge) |
| monitor_plan | Monitors · POST /v1/monitors/plan (plan + price preview) | read, free | $0 |
| monitor_create | Monitors · POST /v1/monitors/create | standing (create once) | $0.05 / block, then $0.01 / check / block |
| monitor_list | Monitors · GET /v1/monitors | read, free | $0 |
| monitor_get | Monitors · GET /v1/monitors/{id} (plan + recent events) | read, free | $0 |
| monitor_update | Monitors · PATCH /v1/monitors/{id} | edit, free | $0 |
| monitor_control | Monitors · pause / resume / delete | action, free | $0 |

> **Warning:** Name mismatches to carry: Content's tool is `content_extract`, not `content`. Research is split into two tools — `research` submits the job and auto-polls for you (up to ~45s), while `research_get` is a free poll-by-id. Monitors are six tools (`monitor_plan`, `monitor_create`, `monitor_list`, `monitor_get`, `monitor_update`, `monitor_control`); there is no single `monitors` tool. There is no `content`, `research_run`, or `research_status` tool.

MCP calls bill identically to REST. Each tool call is rewritten into a synthetic internal request that reuses the already-validated auth context and runs through the exact same handler the REST API uses, so the balance check, charge, and usage record happen once, in one place. Usage rows are tagged `source = mcp` so you can split MCP from REST in your Activity — but the price is the same. Rate limits are the same too: every tool call draws from the same per-service bucket as its REST route (a limited call returns `isError: true` with `retry_after_seconds`), so MCP is not a side door around your limits. Results carry the JSON as `structuredContent` alongside the text block for clients on protocol 2025-06-18.

The `research` tool submits the job (billed $0.08 once, on submit) and polls to completion for you within a bounded inline budget of ~45s. Fast jobs return inline. A deep job runs far longer — commonly 10 minutes or more; when it outlives the inline budget, `research` hands back a job id and you call `research_get` with that id to fetch the result later. `research_get` is free — it never bills. `monitor_create` bills by size like REST create and keeps billing per check on the account afterwards; read results back with `monitor_get`. (Polling and idempotency are Research-only; the three synchronous tools are one call, one response.)

> **Warning:** Two honest parameter caveats where the MCP tool schemas differ from the REST docs. (1) `research` exposes `depth` (quick | balanced | deep, default balanced) as its one effort control, plus an optional advanced `max_tool_calls` budget; the legacy `max_sources` is still accepted but no longer advertised — prefer `depth`. (2) The `search` tool sets `freshness_days` to a minimum of 1 (omit it for no recency filter; REST also accepts 0). The `research` tool has no `webhook` parameter — autopoll replaces it; `monitor_create` and `monitor_update` DO accept `webhook`, because a monitor outlives the agent session.

## Hand it to your coding agent

"Copy agent prompt" is the primary action on every service's Docs tab (desktop and mobile) — paste it into Claude Code, Cursor, or any coding agent and it builds STELQ into your product as a runtime feature, end to end, so your users get grounded, cited answers. No reading required.

The work order is a SEQUENCED build order, not a spec dump. It walks the agent through, in order:

- Find the best place to integrate — survey the codebase and decide where the feature belongs, so STELQ is called at runtime and the result reaches your users (not wired up as a tool for the agent's own use).
- Authentication & key safety — call STELQ from your server, read the key from the `STELQ_KEY` environment variable, never hardcode it; if it's unset, STOP and tell you to create one in the console.
- The endpoint contract — the exact route, sync-vs-async mode, price, and body fields, straight from the same doc model the console renders.
- Implement, in order — async services get the submit → poll loop; sync services get one request, one response; either way the result is surfaced in your product's UI.
- Handle errors — from the same status-code table the docs render (402 → stop and ask you to top up; 502 → retry, not charged; transient failures back off and retry).
- Verify before reporting done — make one real call through the integration with a minimal valid body (and poll an async job through to completed), print the actual result, and only then report done.
- Last step — only after the feature works, the agent asks whether you'd ALSO like STELQ connected to your own coding agent over MCP (with the reasons), as an optional, non-blocking opt-in.

> **Tip:** That final verify step is the activation event: the agent proves the integration over real data before it calls the job finished.

The prompt is deliberately non-manipulative. It frames where the feature belongs (where your users need fresh, web-grounded, cited facts) and never tells the agent to "favor" STELQ. The MCP dev-tool path is offered only at the end, as an explained opt-in, after the product integration already works. It will not write to a shared agent-instructions file such as CLAUDE.md or .cursor/rules without asking you first. Every payload references $STELQ_KEY from your environment, never a live secret.

> **Note:** Use the per-service "Copy agent prompt" button rather than reconstructing the work order by hand — it's generated from the live doc model, so it can't drift from the human reference. The same model also serves a public, auth-free, gate-free reference an agent can fetch cold: `GET /api/docs` is the llms.txt index of every service, and `GET /api/docs/{service}` (search, content, answers, research, monitors) is that service's full Markdown spec. The work order embeds a snapshot plus that canonical URL, so the agent can always confirm it has the latest contract.

## Related

- Authentication — https://stelq.com/docs/authentication
- Billing & credits — https://stelq.com/docs/billing-credits
- Research lifecycle & idempotency — https://stelq.com/docs/research-lifecycle-idempotency
- Platform overview — https://stelq.com/docs/platform-overview
- Research service docs — https://stelq.com/docs/api/research
- Search service docs — https://stelq.com/docs/api/search

---
Source: STELQ Documentation. https://stelq.com/docs/agent-mcp-connect

---

# STELQ Docs — Async research: submit, poll & idempotency

Research is the only async service: POST to submit (charged $0.08 once), then poll GET /v1/research/run/{id} for free until completed — polling is the source of truth. Send an Idempotency-Key and a retry never double-charges; even without one, naive retries inside 90s auto-dedupe. Research-only, on purpose.

## Submit then poll

> **Note:** Research is the only async service. Search, Content, and Answers are synchronous — one request, one response, no job handle and nothing to poll. Everything on this page is Research-only, on purpose.

A research job has two calls. You submit with `POST /v1/research/run` and get a job handle back instantly; then you poll `GET /v1/research/run/{id}` until the job reaches a terminal state. Most jobs finish in 5–15 minutes and read up to ~40 sources, so the work happens off the request path — submit returns in moments, the report arrives on a later poll. Design for the job handle rather than an expected duration: a busy or source-heavy job can take longer.

Submit is the only billable event: $0.08 is charged once on a successful submit (`statusCode < 400`), regardless of `depth`. Polling is free — a poll writes no usage row — but it still requires the same bearer auth as every other call.

*Submit, then poll the handle*
```bash
# 1) Submit the job — charged $0.08 once, returns a job handle.
curl -X POST https://api.stelq.com/v1/research/run \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"Map AI-native search startups and their moats","depth":"deep","citations":true}'
# -> {"id":"rsch_8K2mQ9vX3pLw","status":"queued"}

# 2) Poll (free, auth still required) every 2–5s until status is terminal.
curl https://api.stelq.com/v1/research/run/rsch_8K2mQ9vX3pLw \
  -H "Authorization: Bearer $STELQ_KEY"
```

The documented lifecycle is `queued → in_progress → completed`, with terminal states `completed`, `failed`, or `cancelled`. On `completed` you read the result fields: `result.outputText` (render-ready prose), `result.reportMarkdown` (long-form), `result.citations` (source URLs), and `result.dossier` (the full structured retrieval packet). `error` is populated only on `failed`/`cancelled`.

*Completed poll body*
```json
{
  "id": "rsch_8K2mQ9vX3pLw",
  "status": "completed",
  "result": {
    "outputText": "AI-native search startups cluster around three moats…",
    "reportMarkdown": "# AI-native search startups\n\n## Moats\n…",
    "citations": [
      "https://eur-lex.europa.eu/eli/reg/2024/1689",
      "https://example.com/ai-search-landscape"
    ],
    "dossier": { "…": "full structured retrieval packet" }
  }
}
```

> **Tip:** The lifecycle and result field names are a documentation contract with the engine. The gateway passes the engine's response body through untouched — it does not synthesize `{ id, status }` and does not enforce these field names. Treat the schema as the contract; read defensively.

> **Warning:** Two different ids. The `X-Request-Id` header on the submit response is the gateway's billing id for that submit. The id you poll on is the engine job id inside the body (the `rsch_…` handle). They are distinct — poll the body's `id`, not the `X-Request-Id` header.

Polling is always the source of truth. You can optionally pass a `webhook` on submit to be notified on completion and skip polling, but a webhook is an optimization on top of polling, never a replacement. See Webhooks.

## Idempotency on submit

> **Note:** The `Idempotency-Key` header is honored on the Research submit call and nowhere else. It is read in the submit handler only — not on the poll, not on Search/Content/Answers (which are synchronous), and not via MCP. Sending it elsewhere has no effect.

Submit is the one billed call that creates a side effect (an upstream job) and charges on success, so a naive client or agent retry could otherwise create two jobs and two $0.08 charges. To prevent that, the gateway dedupes at the submit boundary on an idempotency key, claimed before the upstream job is created — so a duplicate never makes a second job or a second charge.

There are two windows. Send a client `Idempotency-Key` (trimmed, max 200 chars) and that key dedupes for 24 hours. Send no header and the gateway derives a content-hash key from your API key id plus the request body and dedupes for exactly 90 seconds — enough to absorb naive submit retries without swallowing a deliberate re-run.

| Key | How it's set | Dedupe window |
| --- | --- | --- |
| Client key | You send `Idempotency-Key: <=200 chars>` | 24 hours |
| Derived key | Auto: hash of API key id + request body | 90 seconds |

> **Warning:** A deliberate re-run after the window expires charges again. With no `Idempotency-Key`, re-submitting the same body more than 90 seconds later is a new, billable job ($0.08). Send a client key if you need a longer guarantee.

*Retry-safe submit with a client key*
```bash
# Same key on every retry of THIS submit -> at most one job, one charge for 24h.
curl -X POST https://api.stelq.com/v1/research/run \
  -H "Authorization: Bearer $STELQ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-2026-06-16-moats-001" \
  -d '{"query":"Map AI-native search startups and their moats","depth":"deep"}'
```

The claim is atomic — a `UNIQUE (api_key_id, idem_key)` constraint means exactly one caller wins the claim and everyone else inspects the existing row. That yields three replay outcomes for a duplicate submit:

| Situation | Response | Charged? |
| --- | --- | --- |
| Original submit already completed | Original job handle, with header `Idempotent-Replay: true` | No |
| A duplicate is still in flight | `409` with `Retry-After: 2` | No |
| Claim store unreachable (fail closed) | `503` with `Retry-After: 2` | No |

On a completed replay the gateway returns the original stored job handle and the `Idempotent-Replay: true` header, with no upstream call and no charge. A genuine in-flight duplicate gets `409 Retry-After: 2` — retry shortly. If the claim store can't be reached, submit fails closed with `503 Retry-After: 2` rather than risk charging a paid, side-effecting call twice.

> **Tip:** Guarantee: at-most-once-charge / effectively-once-submit. The charge is committed best-effort after the upstream job is created; a missed commit under-charges (safer than over-charging) and self-heals when the claim expires. `usage_events.request_id` is a second dedupe layer behind the claim. For the 409/503 you may see here, see Errors & status codes.

## Related

- Errors & status codes — https://stelq.com/docs/errors-and-status-codes
- Webhooks — https://stelq.com/docs/webhooks
- Connect over MCP — https://stelq.com/docs/agent-mcp-connect
- Research API reference — https://stelq.com/docs/api/research

---
Source: STELQ Documentation. https://stelq.com/docs/research-lifecycle-idempotency

---

# STELQ Docs — Webhooks (Research completion · Monitor checks)

Optional signed callbacks for the two services that produce results after the call returns: Research (one terminal event per job) and Monitors (one event per check that clears your threshold). Same HMAC-SHA256 claim-check envelope pointing at result_url, same account secret, same at-least-once delivery with up to 8 retries over ~24h. The one page where getting signature verification wrong fails silently.

## Two services, optional, claim-check

Webhooks exist for the two services whose results arrive after the call: Research (`POST /v1/research/run` accepts `webhook`; one terminal event per job) and Monitors (`POST /v1/monitors/create` or `PATCH /v1/monitors/{id}` accepts `webhook`; one event per check that clears `notify_threshold`, plus the baseline). The URL must be `https` — anything else is rejected with a 400 before anything is created. Search, Content, and Answers are synchronous (one request, one response), so there is nothing to call back about; they have no `webhook` parameter at all.

> **Note:** Over MCP, Research never needs a webhook — the hosted MCP server autopolls the job to completion and hands back the report inline. `monitor_create` DOES accept `webhook` (a monitor outlives any agent session); otherwise the agent reads checks back with `monitor_get`.

Treat the event as a claim-check, not the result. STELQ POSTs a small signed signal the moment a job reaches a terminal state — it carries `data.{job_id, status, result_url}` and never the report body. The receiver reads `result_url` and re-fetches the finished job over the authenticated API. This is deliberate: the signal stays tiny and tamper-evident, and you always read the freshest result rather than a payload that could be stale or oversized.

*event · POST to your URL*
```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"
  }
}
```

Research has exactly two terminal event types, and exactly one fires per job — never both, never a non-terminal one. Monitors have two as well: the baseline (once) and a change (per qualifying check):

| type | Fires | Meaning |
| --- | --- | --- |
| research.run.completed | once per job | The job finished; re-fetch result_url for outputText, reportMarkdown, citations, and the dossier. |
| research.run.failed | once per job | The job ended in a non-success terminal state (failed / cancelled / error). Re-fetch result_url to read the error. |
| monitor.baseline | once per monitor | The first check wrote the baseline. Always sent when a webhook is set — verify your integration on it. |
| monitor.change | per qualifying check | A check found something at or above notify_threshold. data carries monitor_id, event_id, significance, headline, new_count; re-fetch result_url for summary_md + new_items. |

*monitor event · POST to your URL*
```json
{
  "id": "evt_01JX9A4M2KQ7",
  "type": "monitor.change",
  "api_version": "2026-06-01",
  "occurred_at": "2026-08-28T14:07:12Z",
  "attempt": 1,
  "data": {
    "monitor_id": "a01aede0-…",
    "monitor_name": "Regulation E watch",
    "event_id": "5c1b7e2a-…",
    "significance": "notable",
    "headline": "CFPB issues proposed rule expanding Reg E error-resolution to P2P fraud",
    "new_count": 3,
    "result_url": "https://api.stelq.com/v1/monitors/a01aede0-…/events/5c1b7e2a-…"
  }
}
```

> **Tip:** Reading the API stays the source of truth. The webhook is layered on top of `GET /v1/research/run/{id}` and `GET /v1/monitors/{id}/events` — never a replacement. A dropped or rejected delivery is never data loss: the result stays available, so you can always fall back to fetching result_url.

## Verify the signature

> **Warning:** This is the page where a mistake fails silently. A wrong signature check still returns 200 and still processes events — it just leaves your endpoint forging-open. Verify before you trust the body, and verify against the RAW request bytes, not re-serialized JSON. Re-encoding the payload (key reordering, whitespace, escaping) changes the bytes and breaks the HMAC even when your code looks correct.

Every delivery carries three lowercase headers (Standard Webhooks convention):

| Header | Value |
| --- | --- |
| webhook-id | The delivery id (also the event's `id`, e.g. `evt_…`). Use it for dedupe and as part of the signed string. |
| webhook-timestamp | Unix time in seconds when the delivery was signed. |
| webhook-signature | `v1,<base64>` — the HMAC. The header may carry space-separated values; a match on any one passes. |

The scheme, exactly: compute HMAC-SHA256, base64-encoded, over the string `{webhook-id}.{webhook-timestamp}.{rawBody}`. The key is your account signing secret (`whsec_…`, shown once in the console) with the `whsec_` prefix stripped and the remaining characters base64-decoded to bytes. Constant-time compare the result against the value in `webhook-signature` (after the `v1,` prefix), and reject any delivery whose `webhook-timestamp` is older than ~5 minutes to defeat replays.

*verify · Node (raw bytes)*
```ts
import { createHmac, timingSafeEqual } from "node:crypto";

// secret = your account signing secret, e.g. process.env.STELQ_WEBHOOK_SECRET ("whsec_…")
function verify(secret: string, headers: Headers, rawBody: string): boolean {
  const id = headers.get("webhook-id") ?? "";
  const ts = headers.get("webhook-timestamp") ?? "";
  const header = headers.get("webhook-signature") ?? "";

  // reject stale deliveries (~5 min window)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  // key = whsec_ prefix stripped, remainder base64-decoded
  const key = secret.startsWith("whsec_")
    ? Buffer.from(secret.slice(6), "base64")
    : Buffer.from(secret);

  // HMAC-SHA256 (base64) over `{id}.{timestamp}.{rawBody}`
  const expected = "v1," + createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("base64");

  // header may carry space-separated values; any constant-time match passes
  return header.split(/\s+/).some(
    (p) => p.length === expected.length &&
      timingSafeEqual(Buffer.from(p), Buffer.from(expected)),
  );
}
```

> **Note:** Your signing secret is per-account — ONE `whsec_…` for Research and Monitors alike — and snapshotted when the webhook is registered (at Research submit; when a monitor's webhook is set). A delivery verifies against the secret that was live then, so rotating your account secret never breaks jobs in flight or monitors already running. PATCH a monitor's webhook to move it onto a new secret.

## Delivery, retries, dedupe

The delivery contract is short and strict:

- Success = your endpoint returns an HTTP 2xx within a 10-second timeout. Acknowledge fast, then do slow work asynchronously — past 10s we abort and treat it as a failed attempt.
- Delivery is at-least-once, so you MUST dedupe. The outbox holds at most one row per (job_id, event), but a delivered row can still be re-sent on a timeout or transient error — dedupe on data.job_id (Research) or data.event_id (Monitors), paired with type, and process each event exactly once.
- On any non-2xx or timeout we retry with exponential backoff: up to 8 attempts spaced 0s, 30s, 2m, 10m, 30m, 2h, 6h, 12h (with jitter) from completion — about 24h total. After the 8th failed attempt the delivery is dead-lettered (marked failed_permanent).
- If your endpoint is down for the whole window, nothing is lost: fetch result_url and read the report (or the check) directly. For a monitor, GET /v1/monitors/{id}/events/{event_id} also reports that delivery's state, attempts and last status code.

> **Note:** Research completion detection is PULL-based, not an engine push: a cron sweep polls the research engine for your in-flight jobs and, when one reaches a terminal state, enqueues an outbox row (idempotent on job_id+event). Monitor events are enqueued by the check itself the moment it completes, onto the SAME outbox — same delivery loop, same retry ladder. Either way the engine never calls the gateway directly, which is exactly why reading the API remains the canonical path and the webhook is the optimization.

Outbound deliveries are SSRF-hardened. Targets must be `https`; the host is DNS-resolved and any private, loopback, link-local, CGNAT, or multicast address is blocked. That check runs at registration AND again immediately before every send (DNS-rebinding defense), and deliveries never follow redirects (`redirect: "manual"`).

> **Tip:** Idempotency on the receiver mirrors idempotency on submit: the same job_id should produce the same downstream effect whether it arrives once or three times. Make your handler idempotent and let the retries work for you.

## Related

- Research lifecycle & idempotency — https://stelq.com/docs/research-lifecycle-idempotency
- Monitors: standing watches — https://stelq.com/docs/monitors-standing-watches
- Errors & status codes — https://stelq.com/docs/errors-and-status-codes
- Research API reference — https://stelq.com/docs/api/research
- Monitors API reference — https://stelq.com/docs/api/monitors

---
Source: STELQ Documentation. https://stelq.com/docs/webhooks

---

# STELQ Docs — Monitors: standing watches, sizing and price

The one service that keeps running after the call returns. How a monitor is compiled, what a check is, how size (blocks of 12 queries) × cadence sets the price, what the events feed contains, and how webhooks and pause/resume behave — every number here is the number the sweep bills.

## What a monitor is

A monitor is a standing watch on a topic. You describe WHAT to watch in natural language (`query`); STELQ decides HOW: at create time a compile step turns the request into 6–10 diverse search queries (2–4 per named entity for multi-entity watches, up to 120), a list of authoritative domains, a per-topic significance rubric (what counts as minor / notable / major FOR THIS SUBJECT), and candidate direct sources — RSS/Atom feeds, public JSON endpoints, high-signal pages — each confirmed with a live fetch before it enters the plan. The result is the `watch_plan` on every monitor body: you can read exactly how your topic is being watched.

> **Tip:** Preview before you pay. `POST /v1/monitors/plan` runs the same compile for free and returns `watch_plan` + `pricing`. Hand the plan back to `create` as `watch_plan` (as-is or edited — trim queries, drop a source) and you skip the second compile and pay exactly the price the plan showed.

| Route | Price | What it does |
| --- | --- | --- |
| POST /v1/monitors/plan | free | compile + price, create nothing |
| POST /v1/monitors/create | $0.05 per block | create; charged on 201 only |
| GET /v1/monitors | free | list with latest event badge |
| GET /v1/monitors/{id} | free | detail: plan, baseline, pricing, recent events |
| GET /v1/monitors/{id}/events | free | the event feed (?limit= ≤100) |
| GET /v1/monitors/{id}/events/{event_id} | free | one check + webhook delivery status |
| PATCH /v1/monitors/{id} | free | edit name / interval / notify_threshold / webhook / queries |
| POST /v1/monitors/{id}/pause · /resume | free | stop / restart checks (and billing) |
| DELETE /v1/monitors/{id} | free | remove the monitor and its record |

## Size × cadence = price

Monitors are the only service priced on two axes, and both are visible before you pay. SIZE is the number of compiled queries, metered in blocks of 12 (`pricing.size_blocks`): a focused topic is one block; a watch over eight vendors might compile to 24 queries = two blocks. Direct sources are free — discovery is the product, not a meter. CADENCE is the check interval you choose: 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 24h or 7d.

| Charge | Amount | When |
| --- | --- | --- |
| create | $0.05 × size_blocks | once, on a 201 from create (a 4xx/5xx is never charged) |
| check | $0.01 × size_blocks | after each COMPLETED check; failed checks are never billed |

Every monitor body carries `pricing`: `{ size_blocks, queries, create_usd, per_check_usd, checks_per_day, est_daily_usd }`. `est_daily_usd` is the standing daily cost at the current cadence — a one-block daily watch is $0.01/day (≈ $0.30/month); the same watch every 5 minutes is 288 checks/day ≈ $2.88/day. Use `max_queries` on plan/create as a price ceiling, or edit `queries` later with PATCH: the price re-sizes with the plan, never behind your back.

> **Warning:** Checks are billed from your prepaid balance by the sweep, not by a request you make. If the balance can't cover a check, the check is skipped and the schedule pushed one interval; the THIRD consecutive skip auto-pauses the monitor with `paused_reason: "insufficient_balance"`. Top up and call `/resume` — the counter resets. Spend caps on a key apply to the create; standing checks bill the account.

## Baseline, checks, events

The first check runs within 5 minutes of create and writes the BASELINE — the state of affairs at setup, readable as `baseline_md` on the detail call. Every later check retrieves the plan's queries and direct sources, diffs the results against everything the monitor has ever seen (each URL is only ever new once), runs a relevance gate that drops landing pages, directories and off-topic SEO, and — when something genuinely new survives — synthesizes ONE event: a `significance` scored against the baseline using the compiled rubric, a one-line `headline`, a `summary_md` with citations, and the `new_items[]`.

| significance | Meaning | Webhook? |
| --- | --- | --- |
| baseline | the first check; establishes the reference state | always |
| none | quiet check — nothing new survived the gate (still recorded, still billed) | never |
| minor | routine / incremental per the rubric | if notify_threshold = all |
| notable | worth attention soon | if notify_threshold ≤ notable |
| major | act-on-it-now | always (any threshold) |

*one event · GET /v1/monitors/{id}/events*
```json
{
  "id": "5c1b7e2a-0d4f-4c8e-9a3b-2f6d8e1c4b7a",
  "at": "2026-08-28T14:07:12Z",
  "status": "completed",
  "significance": "notable",
  "headline": "CFPB issues proposed rule expanding Reg E error-resolution to P2P fraud",
  "summary_md": "**Proposed rule** …",
  "new_count": 3,
  "filtered_count": 5,
  "new_items": [{ "url": "https://www.consumerfinance.gov/…", "title": "…", "snippet": "…" }],
  "deep_md": null
}
```

> **Note:** Early checks over-report. Search engines rotate results, so the first few checks after the baseline can surface URLs that are new to the record but not new to the world. The relevance gate and the rubric filter most of it, and monitors quiet down as the seen-record grows. Read `significance`, not `new_count`, when deciding what to surface.

## Webhooks on monitors

Pass `webhook` (https) on create, or set it later with `PATCH`. Two event types exist: `monitor.baseline` fires once, for the first check, regardless of threshold — it is how you verify the integration end-to-end — and `monitor.change` fires for every later check whose significance clears `notify_threshold`. Quiet checks (`none`) never notify. The envelope, headers, signing secret and retry schedule are IDENTICAL to Research webhooks: one receiver, one verify function. See the Webhooks guide for signature verification.

*event · POST to your URL*
```json
{
  "id": "evt_01JX9A4M2KQ7",
  "type": "monitor.change",
  "api_version": "2026-06-01",
  "occurred_at": "2026-08-28T14:07:12Z",
  "attempt": 1,
  "data": {
    "monitor_id": "a01aede0-…",
    "monitor_name": "Regulation E watch",
    "event_id": "5c1b7e2a-…",
    "significance": "notable",
    "headline": "CFPB issues proposed rule expanding Reg E error-resolution to P2P fraud",
    "new_count": 3,
    "result_url": "https://api.stelq.com/v1/monitors/a01aede0-…/events/5c1b7e2a-…"
  }
}
```

- Route on `data.significance` / `data.headline` without a fetch; read `data.result_url` for the full `summary_md` and `new_items`.
- Dedupe on `data.event_id`. Delivery is at-least-once with up to 8 attempts over ~24h; `GET …/events/{event_id}` reports the delivery `state`, `attempts` and last status for that check.
- The account signing secret is snapshotted onto the monitor when the webhook is set. Rotating the secret never breaks a running monitor; PATCH the webhook again to pick up the new secret.
- Over MCP, `monitor_create` accepts `webhook` too — the agent's own product can receive the events; the agent reads results back with `monitor_get`.

## Related

- Webhooks (signature verification) — https://stelq.com/docs/webhooks
- Billing & credits — https://stelq.com/docs/billing-credits
- Monitors API reference — https://stelq.com/docs/api/monitors

---
Source: STELQ Documentation. https://stelq.com/docs/monitors-standing-watches

---

End of STELQ documentation. Index: https://stelq.com/api/docs · This file: https://stelq.com/api/docs/llms-full.txt · MCP: https://stelq.com/api/docs/mcp