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.

POST/v1/ask

The 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
POST/v1/protocol

A 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
GET/v1/public/stats

Library counters: how many papers and compounds the engine covers and how the papers spread across study-design grades. No key required.

GET/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
GET/v1/contraindications/{compound_slug}

Label-stated contraindications, each tied to the span of the source label it came from.

Path and query

  • compound_slugstringpathrequired
GET/v1/interactions/{compound_slug}

Known interactions for one compound, each with the label span behind it.

Path and query

  • compound_slugstringpathrequired
POST/v1/interactions/check

Check 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
GET/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.

POST/v1/chat/query

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

POST/v1/chat/protocol

Generate 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
POST/v1/chat/dossier

A full evidence dossier on one compound for one indication.

Body

  • compoundstringrequired
  • indicationstringoptional
  • detail_levelstringoptional

Composition

Several compounds assembled into one plan.

POST/v1/chat/program

A 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
POST/v1/chat/journey

A programme laid out over time, week by week.

Body

  • goalstringrequired
  • selected_compoundsstring[]optional
  • patientobjectoptional
  • biomarkersobject[]optional
  • duration_weeksintegeroptional
  • detail_levelstringoptional
POST/v1/chat/compare

Two or more compounds compared on the evidence behind each.

Body

  • compoundsstring[]required

Discovery

Finding candidates for a case, floor-graded.

POST/v1/chat/discover

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

GET/v1/chat/protocols/{slug}

One stored protocol by slug.

Path and query

  • slugstringpathrequired
POST/v1/chat/protocols/batch

Several stored protocols in one round trip.

Body

  • slugsstring[]required

Knowledge Base

The graded library itself, browsable and searchable.

GET/v1/chat/kb

Browse the knowledge base: filter by entity type, grade or class, and page through it.

Path and query

  • entity_typestringqueryoptional
  • gradestringqueryoptional
  • classstringqueryoptional
  • offsetintegerqueryoptional
  • limitintegerqueryoptional
GET/v1/chat/kb/search

Search the knowledge base by text.

Path and query

  • qstringqueryrequired
GET/v1/chat/kb/{slug}

One knowledge base entry.

Path and query

  • slugstringpathrequired

Interventions

Single intervention records.

GET/v1/chat/interventions/{slug}

One intervention record.

Path and query

  • slugstringpathrequired

Evidence Books

Long-form evidence writing.

POST/v1/chat/book

A structured evidence book on a topic, chapter by chapter.

Body

  • topicstringrequired
  • subject_typestringoptional
  • chaptersstring[]optional
  • depthstringoptional

Platform

Liveness for the platform surface.

GET/v1/platform/health

Liveness for the platform surface.