API quickstart
This is a working start-to-brief integration in four calls: start a run, follow it while the agent works, and fetch the finished brief. Everything below assumes a key in $BSK_KEY and the base URL https://app.bioskepsis.ai/api/v1.
#Start a run
A run starts with one plain-language life-science question. You do not build a query or pick sources; the agent plans the work from the question itself.
curl -X POST https://app.bioskepsis.ai/api/v1/research-runs \
-H "Authorization: Bearer $BSK_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "Does KRAS G12C inhibition improve survival in NSCLC?"}'The response is 202 Accepted. The run is now queued on BioSkepsis and will keep going whether or not your connection stays open.
{
"run_id": "9f0c2a4e8b1d4c76a3e5f60718d2b9aa",
"status": "queued",
"eta_hint": "typically 3 to 8 minutes, sometimes longer"
}Ids are 32-character lowercase hex strings with no prefix, so treat them as opaque and store them as given.
Questions must be between 10 and 2000 characters. An over-length question is rejected with 422 validation_error; it is never silently truncated, because a half-question would quietly produce a brief that answers something you did not ask.
You can also pass an optional output_format to shape what comes back: answer, review, interpretation, summary, or extraction. Leave it out and BioSkepsis picks the shape that fits the question.
#Make the start idempotent
Network timeouts are the one place where a retry can quietly cost a user a brief. Send an Idempotency-Key header on the start call and retries become safe.
curl -X POST https://app.bioskepsis.ai/api/v1/research-runs \
-H "Authorization: Bearer $BSK_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711" \
-d '{"question": "Does KRAS G12C inhibition improve survival in NSCLC?"}'The semantics are worth being precise about, status codes included:
- Same key, same body. You get the original run back rather than a second run, as
200rather than the202of the first accept. The body is the status shape (run_id,status,phase,papers_found,message), so a replay tells you where the run has got to rather than just echoing the acceptance. Retry as often as you need to. - Same key, different body. The request is rejected with
422 idempotency_conflict. The key is already bound to a different question, so honouring it would silently return the wrong brief. - No key. Every accepted start creates a new run and consumes allowance.
Read the two status codes as a signal: 202 means you just created a run, 200 means you found the one you already had. A client that treats only 202 as success will throw away perfectly good replays.
402 and can never consume a second slice of allowance or credits. That makes the header the correct answer to an ambiguous timeout: retry the start rather than guessing whether the first attempt got through.Use a value your own system already owns and will reproduce on retry: an order id, a job id, a hash of the request. A fresh random value generated per attempt gives you nothing, since the retry will carry a different key.
#Follow the run by polling
The simplest way to follow a run is to poll its status every few seconds. This is usually the right choice for a queue worker or anything that only needs to know when the brief is ready.
curl https://app.bioskepsis.ai/api/v1/research-runs/$RUN_ID \
-H "Authorization: Bearer $BSK_KEY"{
"run_id": "9f0c2a4e8b1d4c76a3e5f60718d2b9aa",
"status": "running",
"phase": "reading full text",
"papers_found": 34,
"message": "Reading full text for 34 papers"
}Branch on status. phase is a short, human-readable label for the stage the run is in: starting, searching, reading full text, mapping the literature, composing brief, or checking citations. Note the spaces: these are display strings, not identifiers, so render them rather than switching on them. message is likewise written for a person, and papers_found climbs as the run gathers sources.
Reads are limited to 60 per minute per key, so poll on an interval of a few seconds rather than in a tight loop, and back off when a call returns 429.
#Stream progress over SSE
If you want a live activity feed rather than a status field, stream the run over Server-Sent Events instead.
curl -N https://app.bioskepsis.ai/api/v1/research-runs/$RUN_ID/events \
-H "Authorization: Bearer $BSK_KEY" \
-H "Accept: text/event-stream"Four event types arrive on the stream:
progresscarries the live activity feed entries, the same steps a user watches in the app.statusmarks phase changes.completedends the stream successfully and carries thebrief_id.failedanddeclinedend the stream without a brief. A declined run is one BioSkepsis judged out of scope for life-science research.
EventSource cannot set an Authorization header, so it cannot authenticate to this endpoint, and putting a key somewhere a browser can read it would expose the whole account. Consume the stream server-side with a plainfetch and Accept: text/event-stream, then relay whatever your own front end needs over your own transport.#Entry ids are stable, not unique
This is the detail that catches most integrations. Activity feed entries are re-sent under their original id whenever their content changes, for example when a step moves from running to done. A repeated id is an update to an entry you already hold, not a new one, so key your feed by id and replace in place rather than appending. An id that stops appearing in a later replay was withdrawn by the run and should be dropped from your view.
#Reconnecting and closing
If the connection drops, reconnect with the Last-Event-ID header and the stream resumes rather than starting over. Just as importantly, close the stream yourself as soon as you see completed, failed, or declined. Stock SSE clients reconnect automatically on close, and a reconnect replays the feed, so a client that does not close after a terminal event will happily re-consume a finished run forever. Each key may hold 5 concurrent streams, and abandoned streams count against that.
#Fetch the finished brief
Once the run is finished, whether you learned that from a poll or from a completed event, fetch the brief.
curl https://app.bioskepsis.ai/api/v1/research-runs/$RUN_ID/brief \
-H "Authorization: Bearer $BSK_KEY"{
"run_id": "9f0c2a4e8b1d4c76a3e5f60718d2b9aa",
"brief_id": "51ba9e07c8f34d21b6ac0d9e73f4128c",
"headline": "...",
"brief_markdown": "...",
"sources": [ ... ],
"coverage": { ... },
"trust": { ... }
}brief_markdown is the full brief with its numbered, verified citations. sources is the ranked and tiered source list behind them, coverage reports how well each sub-question was answered, and trust carries the Trust Index score and its facet checks.
Three responses are worth handling explicitly:
202means the run is still working, and the body is the status shape rather than a brief, so one call can both poll and collect. Keep waiting; this is not an error. This endpoint never returns409 run_not_finished, so do not write a branch for it here.409 run_declinedmeans the question was out of scope. Show the message; do not retry the same question.409 run_failedmeans the run ended without producing a brief. A fresh run is a reasonable response; retrying this endpoint is not.
/api/v1/briefs/{brief_id}. Persist brief_id alongside whatever record triggered the run so you can re-read the brief later without holding the run id.#Putting it together
The whole integration, as a shell sketch. It polls the brief endpoint directly and keys on the HTTP status code: 202 while the run is still working, 200 when the brief is ready.
BASE=https://app.bioskepsis.ai/api/v1
AUTH="Authorization: Bearer $BSK_KEY"
RUN_ID=$(curl -s -X POST "$BASE/research-runs" \
-H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711" \
-d '{"question": "Does KRAS G12C inhibition improve survival in NSCLC?"}' \
| jq -r .run_id)
for _ in $(seq 1 120); do
CODE=$(curl -s -o /tmp/brief.json -w '%{http_code}' \
"$BASE/research-runs/$RUN_ID/brief" -H "$AUTH")
[ "$CODE" = "202" ] || break
sleep 10
done
jq -r '.brief_markdown // .error.code' /tmp/brief.jsonA real client should do three more things: honour Retry-After whenever a call returns 429, treat 409 run_declined and 409 run_failed as final rather than retrying them, and give up after a sensible ceiling instead of polling forever. The endpoints, errors & limits page has the full error table and the concurrency ceilings your client has to respect.
