What the API does
You send a patient's intake (age, sex, goals, symptoms, medications, history) and their lab files. The API runs them through a 12-stage pipeline and returns two reports: a clinical report in Markdown and a visual report as a complete HTML page, with the knowledge base gaps it found and the text it read from your files.
| # | Stage | What it does | Runs on |
|---|---|---|---|
| 1 | IP Guard | Classifies the intake. A prompt injection or an attempt to extract how the system works ends the run with a polite refusal. | Claude Haiku 4.5 |
| 2 | Philosophy Router | Turns intervention_philosophy into search and writing directives. | Code |
| 3 | File OCR | Reads the text of every file. Scanned PDFs without a text layer go through Claude vision OCR. | pdf-parse, Claude |
| 4 | Query Expansion | Builds one broad search across pathways, interventions and biomarkers. | Gemini 3.5 Flash |
| 5 | Broad KB Retrieval | Three parallel searches of the NGM knowledge base. | Gemini File Search |
| 6 | Gap Analysis | Finds what the broad search missed and writes the targeted query. | Gemini 3.5 Flash |
| 7 | Targeted KB Retrieval | Three parallel searches of the gaps. | Gemini File Search |
| 8 | Clinical Report | Writes the report in Markdown. | The writer model |
| 9 | Protocol Reconciliation | With protocols on: rewrites the draft to follow the clinic's protocols. | Gemini, then the writer model |
| 10 | Reference Retrieval | Looks up real papers for the report's claims in Europe PMC and sets the REFERENCES section. | Gemini, Europe PMC |
| 11 | Visual Report | Writes the HTML report from the final Markdown. | The writer model |
| 12 | Output Routing | Assembles the outputs. | Code |
The writer model is Claude Opus 4.8 unless the request names another with writer_model. Two surfaces serve the same pipeline:
| Surface | Paths | Key | Files | Progress |
|---|---|---|---|---|
| Client API | /api/files/*, /api/pipeline/*, /api/reports/* | Authorization: Bearer <API_KEY> | Uploaded to object storage first, then passed as file_keys | Stage by stage |
| Direct API (v1) | /v1/biomarker/* | x-api-key: <KB_API_KEY> | Inline, base64, in Other_files | Status only |
The Aevum website uses the client API. Use it too, unless you need a synchronous call. This guide follows the client API; the direct API has its own section.
Base URL and authentication
Base URL: https://aevum.nextgenerationmedicine.co
Send the API key as a Bearer token on every /api/* request:
Authorization: Bearer <your API key>
| Situation | Response |
|---|---|
No Authorization header, or not in the form Bearer <key> | 401, "Missing or invalid Authorization header" |
| The key is wrong | 403, "Invalid API key" |
The server has no API_KEY set | 503 |
A /api/files or /api/reports call on a server without object storage | 503, after the key check |
The direct API takes its own key in the x-api-key header. A missing or wrong key there is a 401.
These pages need no key: GET /health, / (the playground), /docs (this guide), /docs/reference (every endpoint with example requests and responses) and /docs/openapi.json (the OpenAPI document). The Swagger UI of the whole service, its agent endpoints included, is at /api.
Quick start
BASE=https://aevum.nextgenerationmedicine.co
API_KEY='<your API key>'
# 1. Upload the lab file
curl -s -X POST "$BASE/api/files/upload" \
-H "Authorization: Bearer $API_KEY" \
-F "files=@labs.pdf"
# {"message":"1 file(s) uploaded successfully","files":[{"fileId":"<fileId>","name":"labs.pdf",
# "mimeType":"application/pdf","size":78042,"key":"uploads/<fileId>.pdf"}]}
# 2. Start the report
curl -s -X POST "$BASE/api/pipeline/start" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"inputs":{"input_0":"Patient 1, 46-year-old man. Goals: heart health and better sleep.",
"file_keys":["uploads/<fileId>.pdf"],"Report_Type":"baseline","intervention_philosophy":"balanced"}}'
# {"taskId":"<taskId>","status":"started","pipeline_id":"biomarker-analysis-v2","totalStages":12}
# 3. Poll every 5 to 15 seconds until status is completed or failed
curl -s "$BASE/api/pipeline/status/<taskId>" -H "Authorization: Bearer $API_KEY"
# {"status":"running","completed":false,"stage":"Broad KB Retrieval","stageIndex":5,"totalStages":12}
# 4. The completed status carries the outputs. Save the visual report:
curl -s "$BASE/api/pipeline/status/<taskId>" -H "Authorization: Bearer $API_KEY" \
| jq -r '.result.Visual_Report' > report.html
The playground builds these same requests from a form, shows each one before it is sent, and follows the run to its outputs.
1. Upload the lab files
In one step: POST /api/files/upload
A multipart/form-data request with the files in the field files: up to 10 files, each at most 200 MB. The answer is 201:
{
"message": "1 file(s) uploaded successfully",
"files": [
{ "fileId": "<fileId>", "name": "labs.pdf", "mimeType": "application/pdf", "size": 78042, "key": "uploads/<fileId>.pdf" }
]
}
Pass each key, exactly as returned, in inputs.file_keys. Storage takes PDF, JPEG, PNG, GIF, WebP, TXT, CSV and XLSX (any other type is a 400), but the pipeline reads the text of PDF, TXT and CSV files. Send lab reports in those: images are read only by a server with the platform fixes on, and XLSX files are not parsed.
In two steps: a presigned upload
POST /api/files/presign-upload with {"name": "labs.pdf", "mimeType": "application/pdf", "size": 78042} answers 200:
{ "uploadUrl": "<presigned URL>", "key": "uploads/<fileId>.pdf", "fileId": "<fileId>", "expiresIn": 900 }
Then PUT the file bytes to uploadUrl with the same Content-Type and no Authorization header. The link works for 15 minutes. A type outside the list above is a 400, and a size over 200 MB a 413. From a browser, the bucket's CORS rules decide which origins may upload.
Manage stored files
| Call | Answer |
|---|---|
GET /api/files/list?prefix=uploads/ | { "prefix", "files": [{ "key", "size", "lastModified" }], "total" } |
GET /api/files/signed-url/{key}?expires=3600 | { "key", "signedUrl", "expiresIn" } |
GET /api/files/download/{key} | The file's bytes, or 404 |
DELETE /api/files/{key} | { "message": "File deleted", "key" } |
Uploads stay in storage until you delete them.
2. Start a report
POST /api/pipeline/start with the run inputs in inputs:
{
"inputs": {
"input_0": "Patient 1, 46-year-old man. Goals: heart health, energy and sleep. No medications. Father had a heart attack at 52.",
"file_keys": ["uploads/<fileId>.pdf"],
"Previous_Data": "",
"Report_Type": "baseline",
"language": "English",
"overall_instructions": "",
"User_Preference_Level": "Default",
"intervention_philosophy": "balanced",
"design": "classic",
"protocols_active": "false",
"protocols_store": "",
"protocols_thisrun": "",
"writer_model": "claude-opus-4-8"
}
}
The answer comes at once, 200:
{ "taskId": "<taskId>", "status": "started", "pipeline_id": "biomarker-analysis-v2", "totalStages": 12 }
Every input is a string except the two file lists. None is required by the endpoint, but a useful report needs input_0 and at least one file.
| Field | Default | What it does |
|---|---|---|
input_0 | "" | The primary input: the intake, and any question or lab text. The IP guard screens it first. primary_input is used when it is empty. |
file_keys | [] | Keys from step 1. Each file is read from storage and added to Other_files. Also accepted at the top level of the body. |
Other_files | [] | Files sent inline: [{ "metadata": { "name", "mime_type" }, "raw_bytes": "<base64, no data: prefix>", "type": "file" }]. They count against the 100 KB body limit. |
Previous_Data | "" | Earlier results or a previous report, for the longitudinal comparison. |
Report_Type | "baseline" | The report template. See Report options. |
language | "English" | Every word of both reports is written in it. |
overall_instructions | "" | The clinic's own instructions, placed first in the clinical and visual report prompts. |
User_Preference_Level | "Default" | Detail depth: how technical the explanations are. |
intervention_philosophy | "balanced" | conservative, balanced, progressive or aggressive. Unknown values run as balanced. |
design | "classic" | The visual report's design kit: classic or modern. Unknown values run as classic. |
protocols_active | "false" | "true" turns on stage 9, protocol reconciliation. |
protocols_store | "" | The clinic's protocol library: a Gemini File Search store name, fileSearchStores/…. |
protocols_thisrun | "" | A one-time physician directive, applied verbatim with the highest priority. |
writer_model | "claude-opus-4-8" | The Claude model that writes the report: claude-opus-4-8, claude-opus-5-5 or claude-sonnet-5-5. Anything else is a 400. |
Fields not listed here are ignored, as is the variant the website sends beside inputs.
| Error | When |
|---|---|
400 | The body is not valid JSON, or writer_model is not one of the three models. |
413 | The body is over 100 KB. Files sent inline in Other_files count toward it. |
503 | file_keys were sent, but the server has no object storage. |
500 | A key in file_keys is not in storage. The message is the storage error. |
3. Poll the run
GET /api/pipeline/status/{taskId}, every 5 to 15 seconds. A run takes 6 to 8 minutes (with Opus 5.5 writing, about 13). While it runs:
{ "status": "running", "completed": false, "stage": "Broad KB Retrieval", "stageIndex": 5, "totalStages": 12 }
When it finishes, result holds the outputs (next section):
{ "status": "completed", "completed": true, "result": { … }, "stage": "Output Routing", "stageIndex": 12, "totalStages": 12 }
When it fails, error says why and stage where:
{ "status": "failed", "completed": false, "error": "<message>", "stage": "Clinical Report", "stageIndex": 8, "totalStages": 12 }
- An id the server does not know answers
runningatstageIndex: 0,stage: "Initializing", never404, because the website's poller expects that. A run the server knows is at stage 1 or later from its first moment, so treat stage 0 as "not found": the server kept only its last 200 runs, or it restarted. - A blocked intake completes at stage 2, with the IP guard's refusal message in place of both reports.
4. Read the outputs
Field of result | What it holds |
|---|---|
Visual_Report | The visual report: a complete HTML page with its own fonts and styles. When the IP guard blocks the intake, the refusal message. |
output_0 | The clinical report in Markdown, ending with its ## REFERENCES section of papers found in Europe PMC. When blocked, the refusal message. |
gap_analysis | Stage 6: what the broad knowledge base search missed, and the targeted query. |
processed_texts | The text read from each file, as <document name="…"> blocks. |
credits | { "report", "visual" }, as text. The token counts of every call go to the telemetry table instead. |
db_write_status | The telemetry write, as JSON text: success, error, or skipped when the server has no database. |
checks | Only on a server with the platform fixes on: { "version", "codes", "rewritten" }, what the checks found in the visual report and whether its one rewrite replaced it. The codes are value_mismatch, target_unit_mismatch, timing_conflict and context_conflict. |
truncated | Only when it happened: the outputs the model stopped at its output limit, "output_0" or "Visual_Report", so their end is missing. Opus 5.5 and Sonnet 5.5 think before they write, and that thinking shares the limit; their reports get 64K tokens, Opus 4.8's 32K. A protocol rewrite that stops at the limit is dropped for the complete draft instead. |
Show Visual_Report in a sandboxed frame without scripts, as the playground does: it is HTML written by a model.
GET /api/reports/{taskId} lists report files stored under reports/{taskId}/, each with a one-hour link. This service returns its reports in the status answer and stores none itself, so it answers 404 unless another service stored files for the id.
Report options
Report template: Report_Type
Given to the writers as text, so any value works. These are the four the Aevum website offers:
| Value | Template | What it asks for |
|---|---|---|
baseline | Comprehensive | Every panel, mechanism and recommendation: the full baseline analysis. The default. |
focused | Focused | The top 3 to 5 findings. |
progress | Progress | An interval comparison. Send the earlier results in Previous_Data. |
snapshot | Snapshot | A quick scorecard. |
Detail depth: User_Preference_Level
It sets how technical the explanations are, never how aggressive the interventions are. The report prompt reads it against its scale: Basic (lay-friendly), Standard (clinical detail), Detailed (full analysis) and Expert (maximum depth, with PMIDs only when they appear verbatim in the knowledge base). The website sends one of two directives, which the playground sends too:
DETAIL DEPTH: DEFAULT — Clear, concise summaries in plain language. Focus on the most important findings and actionable takeaways. …
DETAIL DEPTH: DETAILED — Comprehensive clinical analysis with full pathway mechanisms, differential diagnoses, molecular crosstalk, evidence-based dosing protocols, and PMIDs. …
Intervention philosophy: intervention_philosophy
| Value | The report recommends |
|---|---|
conservative | Lifestyle, diet, exercise, sleep and stress only. Standard medications only where conventional thresholds are clearly met. |
balanced | Lifestyle first, plus well-established supplements and standard-of-care medications where indicated. Peptides and hormones only when strongly indicated. The default. |
progressive | The highest-impact options first: peptides, hormone optimization and advanced supplements where the data supports them, each with dosing, rationale and an evidence label. |
aggressive | The most comprehensive protocol from commercially available compounds, phased over six months, with dosing, monitoring and evidence labels. Never investigational compounds. |
Design and language
design swaps the visual report's fonts and palette: classic (Cormorant Garamond and DM Sans on warm taupe) or modern (Zen Old Mincho and Familjen Grotesk on warm linen with slate accents). language is any language name, such as Spanish or Hindi; the website offers 20.
Writer model: writer_model
The model that writes the clinical report (stage 8), the protocol rewrite (stage 9) and the visual report (stage 11). The IP guard stays on Claude Haiku 4.5, and query expansion, gap analysis and the searches on Gemini.
| Value | Model | Per million tokens, in / out |
|---|---|---|
claude-opus-4-8 | Claude Opus 4.8, production's own. The default. | $5 / $25 |
claude-opus-5-5 | Claude Opus 5.5 | $4 / $20 |
claude-sonnet-5-5 | Claude Sonnet 5.5 | $2 / $10 |
Opus 5.5 and Sonnet 5.5 think before they answer. That adds time and output tokens, within the same 32,000-token output limit per stage.
Physician protocols: protocols_*
With protocols_active: "true", stage 9 builds a query from the draft report and the intake, searches protocols_store, and has the writer model apply what it finds, and protocols_thisrun, to the draft. Protocol directives take priority; each conflict with the draft is marked ⚠ Protocol override: and each applied directive names its source. With nothing found and no directive, the draft stands and the rewrite is not paid for. Any failure in this stage also leaves the draft as it was.
The direct API (v1)
The same pipeline, with the files inside the request. Every call takes x-api-key: <KB_API_KEY>.
| Call | Answer |
|---|---|
GET /v1/biomarker/status | { "service": "biomarker-analysis-v7", "ok": true } |
POST /v1/biomarker/run | Holds the request for the whole run, then 201 with the outputs (the result fields above). From 30 seconds on it sends a space every 30 seconds to keep the request open; JSON parsers skip them. A run that fails after that still answers 201, with the error body. |
POST /v1/biomarker/run-async | 201, { "runId", "status": "running" } at once. |
GET /v1/biomarker/runs/{id} | { "id", "status", "createdAt", "updatedAt", "result"?, "error"? }, with status one of running, completed or failed; 404 for an id the server does not know. These runs do not report their stage. |
The body is the inputs themselves, not wrapped in inputs. input_0 and at least one file in Other_files are required, the other fields are optional strings, and fields not listed are dropped. A missing or malformed field is a 400 whose message lists each problem.
curl -s -X POST "$BASE/v1/biomarker/run-async" \
-H "x-api-key: $KB_API_KEY" -H "Content-Type: application/json" \
-d '{"input_0":"Patient 1, 46-year-old man. Goals: heart health.",
"Other_files":[{"metadata":{"name":"labs.txt","mime_type":"text/plain"},
"raw_bytes":"TERMLUM6IDE0MiBtZy9kTCAocmVmIDwxMDApCkhiQTFjOiA1LjYgJQo=","type":"file"}],
"writer_model":"claude-sonnet-5-5"}'
Inline files count against the server's 100 KB body limit, so the direct API suits short text files. Send PDFs through the client API. Prefer run-async to run: this host closes any request after 15 minutes, which an Opus 5.5 run can pass, and a run that fails after its first 30 seconds answers 201 with the error body, so check it for statusCode.
Errors
Every error is JSON:
{
"statusCode": 400,
"error": "Bad Request",
"message": "writer_model must be one of: claude-opus-4-8, claude-opus-5-5, claude-sonnet-5-5",
"timestamp": "2026-10-23T09:30:00.000Z",
"path": "/api/pipeline/start"
}
message is a string, or a list of strings for the direct API's field checks.
A body the server cannot read is refused before the key is checked: 400 when it is not valid JSON, 413 when it is over 100 KB, and 415 when its charset is not UTF-8. message says what was wrong. Rely on statusCode: error is only a label, such as Bad Request or PAYLOAD_TOO_LARGE.
{
"statusCode": 413,
"error": "PAYLOAD_TOO_LARGE",
"message": "request entity too large",
"timestamp": "2026-10-23T09:30:00.000Z",
"path": "/api/pipeline/start"
}
| Status | Meaning |
|---|---|
400 | An input is invalid: a body that is not valid JSON, a field check, a file type, or writer_model. |
401 | The key is missing; on the direct API, also a wrong key. |
403 | The client API key is wrong. |
404 | A direct API run, a stored file or stored report files that do not exist. |
413 | The JSON body is over 100 KB, or a presigned upload's size is over 200 MB. |
415 | The body's charset is not UTF-8. |
500 | An unexpected failure, such as a file key not in storage. |
503 | The server is missing a key or object storage. |
A run that fails after it started is not an HTTP error: its status answer says failed, with the error.
Limits and behavior
- Time. A run takes 6 to 8 minutes and is stopped after
BIOMARKER_DEADLINE_MS, 10 minutes by default; it then reads asfailed, with an error that names the deadline. With Opus 5.5 writing, a run takes about 13 minutes and gets at least 20 (writerDeadlinesMsinGET /api/pipeline/setup). - Memory. Runs and their outputs live in the server's memory: the last 200, until it restarts or redeploys. Save what you need when a run completes, and poll the same server that started the run.
- Size. A JSON body may be at most 100 KB; a larger one is answered
413. An upload takes up to 10 files of at most 200 MB each. - Failures inside a run. Knowledge base reads are retried, and a read that still fails leaves its part of the context out. Protocol reconciliation and reference retrieval never stop a run. A failed Claude or Gemini call does.
- The IP guard. An intake that tries to extract how the system works, or to inject instructions, ends the run at stage 2 with a refusal message in place of both reports.
Data handling
- Uploads stay in object storage under
uploads/until you delete them. - Runs are not stored: their inputs and outputs live only in the server's memory, as above.
- Telemetry. When the server has a database, every run, playground runs included, writes one row per model call to the table
ngm_biomarker_multi_report: the provider, model, stage and token counts. No patient text. - Processors. The intake and the file text go to Anthropic (Claude) and Google (Gemini). Searches built from them go to the NGM knowledge base, and short queries for the report's claims to Europe PMC.
Use synthetic or de-identified data. The playground's sample patient is fictional.