The surface
| Method | Path | What it does | Units | Phase |
|---|---|---|---|---|
| GET | /v1/doctor | The manifest: where to start, how to get a key, prices today, sources now | none | live |
| POST | /v1/quote | A ceiling for a search or a catch, valid for a short window | none | 1b |
| POST | /v1/search | One query across the sources you name, deduplicated and ranked | search, result | 1b |
| POST | /v1/capture | One post or page fetched with thread context | capture | 1b |
| POST | /v1/classify | Classify results against caller-defined criteria | classify | 1b |
| GET | /v1/sources | What is live right now and whether deep coverage is possible | none | 1b |
| POST | /v1/keys/trial | A key with no identity and a small allowance | none | 1b |
| POST | /v1/keys/claim | Claim a trial key with an email to keep it and raise allowance | none | 1b |
| POST | /v1/catches | A standing query with a destination | alert, result per firing | 2 |
| GET/PATCH/DELETE | /v1/catches/{id} | Read, update, or remove a catch | none | 2 |
| GET | /v1/events | The account event log from a cursor, with long-poll and SSE | none | 2 |
| POST | /v1/subscriptions | Route events to a webhook, Doozy, GitHub dispatch, or email | none | 2 |
| GET | /v1/usage | Units consumed this period, by kind, with spend | none | 1b |
The result shape
Every result from search, capture, and catch deliveries uses the same shape. The classification field is present only when classify: true was requested.
Result
{
"id": "res_01J...",
"source": "hackernews",
"url": "https://news.ycombinator.com/item?id=42345678",
"title": "Show HN: a tool that catches discourse online",
"excerpt": "We built this because none of the monitoring tools ...",
"body": null,
"author": { "handle": "user", "url": null },
"posted_at": "2026-09-09T14:17:00Z",
"found_at": "2026-09-09T15:30:00Z",
"language": "en",
"prescore": 0.82,
"thread": { "replies": 47, "score": 312, "parent_url": null },
"classification": null,
"matched": { "query": "\"your product\"", "terms": ["your product"] }
}The error envelope
Error
{
"error": {
"code": "budget_exceeded",
"message": "This search would cost $0.09 and your budget_usd is $0.05.",
"next_action": "Raise budget_usd, narrow sources, or set coverage to fast."
}
}Codes: unauthorized, scope_missing, budget_exceeded, quote_expired, source_unavailable, invalid_query, rate_limited, destination_failed. Every error carries a next_action so the caller knows what to do without reading docs.
The quote flow
Every call that consumes units can be quoted first. The quote is a ceiling with locked unit prices. The charge is actual units at those prices and never exceeds the ceiling.
- POST /v1/quote with the same body you would send to /v1/search. The response carries a quote_id, the ceiling in USD, and unit prices locked for a short window.
- POST /v1/search with the quote_id to lock those prices. Or call without it and the quote is computed inline.
- Read the quote on the response to see what was charged.
Scoped keys
Keys are scoped: read, search, catches:write, billing:read. A trial key starts with read and search. Claiming it with an email adds catches:write. The quote endpoint needs no scope because it charges nothing.
What works today
GET /v1/doctor is live. It returns the manifest with prices today, sources now, and how to get a key. Every other endpoint on this page is the proposed interface from docs/api.md. The manifest reports what is live and what is planned. Unknown routes return the manifest with a next_action so a wrong URL still lands the caller in the right place.