API reference
The endpoint list, the complete error table, and the limits your client has to live inside. Paths are relative to https://app.bioskepsis.ai/api/v1, and every request carries an API key. For the machine-readable contract, see the interactive BioSkepsis API reference at app.bioskepsis.ai/api/v1/docs.
#Research runs
| Endpoint | What it does |
|---|---|
POST /research-runs | Starts a run. Body takes question (10 to 2000 characters) and an optional output_format. Accepts an optional Idempotency-Key header.202 on first accept, with run_id, status, and eta_hint. 200 on an idempotent replay, carrying the run's current state in the status shape. 422 idempotency_conflict when a key is replayed with a different body. |
GET /research-runs/{run_id} | Run status: run_id, status, phase, papers_found, and a human-readable message. |
GET /research-runs/{run_id}/events | Server-Sent Events stream of the run. Emits progress and status events, then exactly one of completed (carrying brief_id), failed, or declined. Resume with Last-Event-ID. |
GET /research-runs/{run_id}/brief | 200 once the run is done, carrying run_id, brief_id, headline, brief_markdown, sources, coverage, and trust.202 while the run is still working, carrying the status shape (run_id, status, phase, papers_found, message) so you can show progress from the same call. Its only errors for a run that ended without a brief are 409 run_declined and 409 run_failed; this endpoint never returns run_not_finished. |
output_format accepts answer, review, interpretation, summary, or extraction. Omit it to let BioSkepsis choose the shape that fits the question.
#Saved briefs
| Endpoint | What it does |
|---|---|
GET /briefs?limit=10 | The key owner's saved briefs, newest first. limit caps at 50, and v1 has no pagination, so 50 is the ceiling on a single listing. |
GET /briefs/{brief_id} | One saved brief by id. |
sources or coverage. If your product needs the ranked source list or the sub-question coverage, read them from /research-runs/{run_id}/brief when the run completes and store them on your side. You cannot recover them from the saved-brief endpoints later.#Research feeds
| Endpoint | What it does |
|---|---|
POST /feeds | Creates a scheduled feed that follows a finished run's papers by email. Body takes run_id, a frequency of daily, weekly, or monthly, and an optional name.The referenced run has to be finished and have papers, so this endpoint carries three 409 codes: run_not_finished (still queued or running), run_declined, and run_failed. It also returns 409 run_has_no_papers when the finished run has nothing followable. |
409 run_not_finished is the one 409 that clears on its own: the run is simply still working, and the same call succeeds once it finishes. Wait for the run to complete, then retry it unchanged.
Feeds count against the plan's feed allowance. Once that is used up, further creates return 403 feed_limit_reached. A run whose sources carry no usable identifiers to follow cannot become a feed and returns 409 run_has_no_papers, which is deterministic: that run will never be followable.
#Error responses
Every error uses the same envelope: {"error": {"code", "message", "details"?}}. Branch on code, never on message.
| Status | Codes |
|---|---|
| 401 | invalid_api_key: missing, invalid, revoked, or presented to the wrong environment. |
| 402 | credits_exhausted, budget_limit_reached, credits_not_eligible, credits_unavailable, limits_exceeded. details carries pricing and credits_available_cents. |
| 403 | feed_limit_reached. |
| 404 | not_found. |
| 409 | run_not_finished (POST /feeds only, when the referenced run is still queued or running; clears once the run finishes). run_declined, run_failed, run_has_no_papers: deterministic, retrying the same call returns the same answer. |
| 422 | validation_error, idempotency_conflict. |
| 429 | too_many_active_runs, rate_limited, too_many_streams. Honour Retry-After. |
| 500 | internal_error: an unexpected fault on our side. Safe to retry once, then back off. |
| 503 | feature_disabled, backend_unavailable. |
#Which errors are worth retrying
- Retry with backoff:
429(afterRetry-After),503 backend_unavailable, and500 internal_erroronce. - Retry unchanged once the run finishes:
409 run_not_finishedonPOST /feeds. Nothing about the request is wrong; the run it names is just not done yet. - Do not retry, fix the request:
401,404,422, and the deterministic409codes (run_declined,run_failed,run_has_no_papers). Retrying these burns rate limit and returns the same failure. - Do not retry, tell the account owner: every
402, plus403 feed_limit_reached. These clear when someone tops up, upgrades, or waits for the allowance to reset, not when your client tries again.
GET /research-runs/{run_id}/brief, a 202 means the run is still working: keep polling, it is not an error. On POST /feeds, 409 run_not_finished means the same thing: wait for the run to finish, then retry the identical call. Reserve your error path for run_declined, run_failed, and run_has_no_papers, which are final for that run.#Limits
| Limit | Value |
|---|---|
| Concurrent runs | 2 per account, shared with the Claude connector. A third start returns 429 too_many_active_runs. |
| Start requests | 10 per minute, per key. |
| Read requests | 60 per minute, per key. |
| Concurrent SSE streams | 5 per key. Exceeding it returns 429 too_many_streams. |
| Question length | 10 to 2000 characters. Over-length questions are rejected with 422 validation_error, never truncated. |
The concurrency limit is per account, not per key, and it is shared with runs started from the Claude connector and from the app itself. If your integration serves many of your own users behind one BioSkepsis account, queue their runs on your side and feed them in two at a time rather than letting 429 too_many_active_runs be your queue.
Stream limits are the one place where a bug leaks quietly: a client that does not close its connection on completed, failed, or declined holds a slot open, and five of those exhaust the key. Close terminal streams explicitly rather than relying on the server to hang up.
#Versioning
The version lives in the path, and everything documented here is /api/v1. New optional response fields can appear within a version, so parse defensively and ignore fields you do not recognise rather than failing on them. The OpenAPI document at /api/v1/openapi.json is generated from the running service, so it is the place to check whether a field you depend on is still there.
If you need something the v1 surface does not cover, or you are planning a high-volume integration, reach the team at [email protected].
