The whole API, on one page.
Graded answers with their citations, over plain HTTP. Twenty-three endpoints, one key.
Base URL
https://novolab.io
Public endpoints
23 across 10 groups
One key, one header
Every call except the public counters carries an API key in the X-API-Key header. The chat endpoints also accept it as Authorization: Bearer <key>. Keys are issued per organisation from the portal and rate-limited per key across per-minute, daily and monthly windows. A key can be limited to chosen endpoints, to IP addresses and to an expiry date. Rotating a key issues a new secret: the old one stops at once, or keeps working for 1 hour, 24 hours or 7 days while you switch over. An expired key is a 401 with the code api_key_expired; a key used outside its endpoints or addresses is a 403 with insufficient_scope or ip_not_allowed. Every response, errors included, carries an x-request-id header: send your own (letters, digits and . _ : -, up to 128 characters) and it is echoed back, otherwise one is generated. Quote it when you report a problem.
curl "https://novolab.io/v1/ask" \
-H "X-API-Key: $NOVOLAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "question": "Does creatine harm the kidneys?" }'What /v1/ask returns
The answer body itself, at the top level: the prose, the claims it makes, the citation behind each claim, a grade per claim and one overall, the numbered sources the inline [n] markers point to, and the outcome: answered, abstained or refused. The verdict is one of ADOPT, PROMISING, WATCH, AVOID or INSUFFICIENT_EVIDENCE.
{
"scope": { "in_scope": true },
"abstained": false,
"outcome": "answered",
"answer_markdown": "…",
"claims": [
{
"text": "…",
"citations": [
{
"pmid": "13129991",
"verified": true,
"title": "…",
"journal": "…",
"year": 2003,
"claim_support_score": 0.91
}
],
"grade": { "letter": "B", "verdict": "PROMISING", … },
"population": "human",
"source_refs": [1]
}
],
"overall_grade": {
"letter": "B",
"verdict": "PROMISING",
"rationale": "…",
"provisional": false,
"factors": {
"downgrading": { "risk_of_bias": { "level": "none", "reason": "…" }, … },
"upgrading": { "large_effect": { "level": "not_assessed", "reason": "…" }, … }
}
},
"confidence": 0.82,
"clinical_risk_tier": 2,
"sources": [ { "number": 1, "id_type": "pmid", "id": "13129991", "url": "…", … } ],
"intent": { … },
"pico": { … },
"ai_generated": { "generated": true, "disclosure": "…", "standard": "EU AI Act Art. 50" }
}What /v1/chat/query returns
The same answer body, wrapped: it sits under data, next to the request id and a copy of the abstained flag. The other /v1/chat endpoints each have their own shape. /v1/chat/protocol, for one, puts the graded answer under evidence, beside biomarker_flags, cautions, monitoring and compounds.
{
"data": {
"scope": { "in_scope": true },
"abstained": false,
"outcome": "answered",
"answer_markdown": "…",
"claims": [ … ],
"overall_grade": { "letter": "B", "verdict": "PROMISING", … },
"sources": [ … ],
"ai_generated": { "generated": true, … },
…
},
"request_id": "…",
"abstained": false
}And when it refuses
A refusal is a normal 200, not an error, and outcome says which kind it is. When the evidence is too thin the outcome is abstained. A question stopped before the search (an emergency, patient identifiers, a topic out of scope, a question too long) is refused; abstained stays false on it. Neither has claims, and neither has a grade letter: overall_grade.letter is null. Render a grade only when outcome is answered and the letter is not null.
{
"abstained": true,
"outcome": "abstained",
"answer_markdown": "…",
"overall_grade": {
"letter": null,
"verdict": "INSUFFICIENT_EVIDENCE",
"rationale": "…",
"provisional": true
},
"claims": []
}Streaming
With stream set to true, /v1/chat/query sends server-sent events in a fixed order: meta at once, then a progress event as each stage starts (understanding, searching, grading, writing, checking; each at most once, some may be skipped), then answer, an optional sidechannel with label data, and done. The answer is not streamed token by token: it arrives whole, in the answer event, with the same body as data above. If the request fails you get one error event with an error_id to quote to support, and the stream closes.
curl -N "https://novolab.io/v1/chat/query" \
-H "X-API-Key: $NOVOLAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "Does creatine harm the kidneys?", "stream": true }'
event: meta
data: {"request_id": "…", "intent": { … }, "pico": { … }}
event: progress
data: {"step": "searching", "label": "Searching literature"}
…
event: answer
data: {"scope": { … }, "abstained": false, "answer_markdown": "…", …}
event: done
data: {}
# on failure, instead of answer:
event: error
data: {"code": "pipeline_error", "message": "…", "error_id": "…"}Conventions
- Base URL
- https://novolab.io. Every path below hangs off it.
- Format
- JSON in, JSON out, UTF-8. Send Content-Type: application/json on any call with a body.
- Versioning
- The version is in the path. /v1 stays as it is; a breaking change would land on a new prefix.
- 401 and 403
- A missing or unknown key is a 401. A key that is valid but not entitled to the route is a 403.
- 422
- The body did not validate. The response names the field and what it expected. An empty or blank question is a 422 too, and costs no unit.
- 429
- A rate window was exhausted. Every 429 carries Retry-After, the seconds until it resets, and X-RateLimit-Scope: minute, day or month. The error code stays rate_limited for all three, so read the scope to tell a burst from a spent monthly quota. Limits are per key, and an organisation can also have a monthly cap across its keys.
- Errors
- The /v1/chat endpoints answer an auth or rate error as {"error": {"code", "message"}}, and a 429 there adds limit_scope (minute, day or month) beside them. The other /v1 endpoints answer {"detail": "…"}. A 422 from any endpoint lists each failing field under detail.
- Request id
- Every /v1 response carries an x-request-id header, errors included. Send your own x-request-id (letters, digits and . _ : -, up to 128 characters) and it is echoed back; otherwise the engine makes one. Quote it to support.
- Latency
- A graded answer is not a lookup: the evidence is found, graded and checked against its citations before you get a reply. Budget in seconds, not milliseconds, and stream where your interface allows it.
- What never goes in
- Do not send patient identifiers. The engine needs none: a case is described by its clinical facts. Request bodies are never written to any log.
Evidence API
Ask a question, check a stack, read a grade. The core surface, and where most products start.
/v1/askThe main call. Send a clinical question, get back a grounded answer in markdown, the claims it makes, the citation behind each claim and an overall GRADE letter. When the retrieved evidence cannot support an answer the engine abstains and says why rather than guessing.
Body
- questionstringrequired
- detail_levelstringoptional
- physician_idstringoptional
/v1/protocolA compound protocol with its safety notes, graded step by step. Dosing stays withheld until a clinical advisor has signed it off.
Body
- compoundstringrequired
- goalstringrequired
- constraintsstringoptional
- physician_idstringoptional
/v1/public/statsLibrary counters: how many papers and compounds the engine covers and how the papers spread across study-design grades. No key required.
/v1/grade/{compound_slug}The stored GRADE record for a compound and indication, with the verdict and the rationale behind the letter.
Path and query
- compound_slugstringpathrequired
/v1/contraindications/{compound_slug}Label-stated contraindications, each tied to the span of the source label it came from.
Path and query
- compound_slugstringpathrequired
/v1/interactions/{compound_slug}Known interactions for one compound, each with the label span behind it.
Path and query
- compound_slugstringpathrequired
/v1/interactions/checkCheck a whole stack pairwise. Returns label-stated interactions with citations, and an explicit no-data note where the literature is silent. Silence is never returned as an all-clear.
Body
- compoundsstring[]required
/v1/regulatory/{compound_slug}Current FDA, 503A and 503B status for a tracked compound.
Path and query
- compound_slugstringpathrequired
Evidence Query
The conversational form, for products that hold a thread.
/v1/chat/queryThe conversational form of ask: carries context from earlier turns. Set stream to true for server-sent events: progress while the answer is prepared, then the whole answer in one frame (see Streaming above).
Body
- querystringrequired
- contextobjectoptional
- detail_levelstringoptional
- streambooleanoptional
- optionsobjectoptional
Generation
Longer documents built from the same graded evidence.
/v1/chat/protocolGenerate a protocol for a case, from the goal and whatever context you hold: biomarkers, current medications, contraindications, allergies, experience.
Body
- goalstringrequired
- patientobjectoptional
- biomarkersobject[]optional
- specific_compoundsstring[]optional
- current_medicationsstring[]optional
- contraindicationsstring[]optional
- allergiesstring[]optional
- experience_levelstringoptional
- route_preferencestringoptional
- detail_levelstringoptional
- physician_idstringoptional
- audiencestringoptional
/v1/chat/dossierA full evidence dossier on one compound for one indication.
Body
- compoundstringrequired
- indicationstringoptional
- detail_levelstringoptional
Composition
Several compounds assembled into one plan.
/v1/chat/programA multi-phase programme built from selected compounds.
Body
- goalstringrequired
- patientobjectoptional
- biomarkersobject[]optional
- selected_compoundsstring[]optional
- phasesintegeroptional
- experiencestringoptional
- route_preferencestringoptional
- constraintsstringoptional
- detail_levelstringoptional
- physician_idstringoptional
- audiencestringoptional
/v1/chat/journeyA programme laid out over time, week by week.
Body
- goalstringrequired
- selected_compoundsstring[]optional
- patientobjectoptional
- biomarkersobject[]optional
- duration_weeksintegeroptional
- detail_levelstringoptional
/v1/chat/compareTwo or more compounds compared on the evidence behind each.
Body
- compoundsstring[]required
Discovery
Finding candidates for a case, floor-graded.
/v1/chat/discoverCandidate compounds for a case, filtered by a minimum grade so thin evidence never surfaces as an option.
Body
- casestringrequired
- patientobjectoptional
- biomarkersobject[]optional
- current_medicationsstring[]optional
- goalsstring[]optional
- max_candidates_per_goalintegeroptional
- min_gradestringoptional
- detail_levelstringoptional
- synthesizebooleanoptional
Protocols
Stored protocols, read back by slug.
/v1/chat/protocols/{slug}One stored protocol by slug.
Path and query
- slugstringpathrequired
/v1/chat/protocols/batchSeveral stored protocols in one round trip.
Body
- slugsstring[]required
Knowledge Base
The graded library itself, browsable and searchable.
/v1/chat/kbBrowse the knowledge base: filter by entity type, grade or class, and page through it.
Path and query
- entity_typestringqueryoptional
- gradestringqueryoptional
- classstringqueryoptional
- offsetintegerqueryoptional
- limitintegerqueryoptional
/v1/chat/kb/searchSearch the knowledge base by text.
Path and query
- qstringqueryrequired
/v1/chat/kb/{slug}One knowledge base entry.
Path and query
- slugstringpathrequired
Interventions
Single intervention records.
/v1/chat/interventions/{slug}One intervention record.
Path and query
- slugstringpathrequired
Evidence Books
Long-form evidence writing.
/v1/chat/bookA structured evidence book on a topic, chapter by chapter.
Body
- topicstringrequired
- subject_typestringoptional
- chaptersstring[]optional
- depthstringoptional
Platform
Liveness for the platform surface.
/v1/platform/healthLiveness for the platform surface.