How to call the Aroga Biomarker Analysis API sandbox.
1. Base URL and authentication
Base URL: https://aroga-sandbox.nextgenerationmedicine.co
Send your 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 with "error": "unauthorized" |
| The key is unknown, disabled or expired | 403 with "error": "forbidden" |
| The service is unavailable | 503 with "error": "service_unavailable" |
Three pages need no key: GET /health, GET /docs (this guide) and GET /api-json (the OpenAPI document).
Upload links and report links work without the header. Keep them secret.
Errors are JSON:
{
"statusCode": 400,
"error": "validation_failed",
"message": "One or more inputs are invalid.",
"fields": ["inputs.patient.age: must be an integer between 18 and 120"]
}
statusCode is the HTTP status, error is a stable code, message is for people, and fields is present when single fields are at fault.
2. Quick start
BASE=https://aroga-sandbox.nextgenerationmedicine.co
KEY=<your api key>
# 1. Ask for an upload link
curl -s -X POST "$BASE/api/files/presign-upload" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name":"labs.pdf","mimeType":"application/pdf","size":78042}'
# {"uploadUrl":"<uploadUrl>","key":"aroga/sandbox/uploads/<fileId>.pdf","fileId":"<fileId>","expiresIn":900}
# 2. Upload the file bytes to uploadUrl (no Authorization header)
curl -s -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @labs.pdf
# {}
# 3. Start the report
curl -s -X POST "$BASE/api/pipeline/start" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"idempotency_key":"case-0042-2026-03-16","inputs":{"report_type":"general_practice","patient":{"name":"Patient 1","age":47,"sex":"female"},"files":[{"file_key":"aroga/sandbox/uploads/<fileId>.pdf","kind":"labs","collected_on":"2026-03-16"}]}}'
# {"taskId":"<taskId>","status":"started", ...}
# 4. Poll every 15 to 30 seconds until status is completed, held or failed
curl -s "$BASE/api/pipeline/status/<taskId>" -H "Authorization: Bearer $KEY"
# 5. List the report files, then download one with its report link
curl -s "$BASE/api/reports/<taskId>" -H "Authorization: Bearer $KEY"
curl -s "<signedUrl>" -o output_0.md
3. Requests
Upload a lab file
POST /api/files/presign-upload
| Field | Required | Notes |
|---|---|---|
name |
yes | File name, 1 to 200 characters. |
mimeType |
yes | application/pdf, image/png, image/jpeg, text/plain or text/csv. |
size |
no | Size in bytes, at most 4 MB. |
{ "uploadUrl": "https://aroga-sandbox.nextgenerationmedicine.co/api/files/upload/<upload link id>", "key": "aroga/sandbox/uploads/<fileId>.pdf", "fileId": "<fileId>", "expiresIn": 900 }
PUT <uploadUrl> with the file bytes as the body and no Authorization header. At most 4 MB, and the bytes must be of the type given in mimeType. Success is 200 with {}. The link works for 15 minutes and can be used again until then; each upload replaces the file. Pass key, exactly as returned, as file_key in an inputs.files entry.
POST /api/files/upload is the one-step alternative: multipart/form-data, field files, up to 10 files and 4 MB in total. Response 201:
{ "message": "1 file(s) uploaded successfully", "files": [{ "fileId": "<fileId>", "name": "labs.pdf", "key": "aroga/sandbox/uploads/<fileId>.pdf", "mimeType": "application/pdf", "size": 78042 }] }
DELETE /api/files/{key} deletes a file you uploaded. Send the key as it is, or with its slashes percent-encoded (%2F). The response is always 200:
{ "deleted": true, "message": "File deleted", "key": "aroga/sandbox/uploads/<fileId>.pdf" }
GET /api/files/list, GET /api/files/signed-url/{key} and GET /api/files/download/{key} return 501 not_supported.
Start a report
POST /api/pipeline/start
{
"idempotency_key": "case-0042-2026-03-16",
"inputs": {
"report_type": "general_practice",
"patient": { "name": "Patient 1", "age": 47, "sex": "female", "birth_year": "1978", "height_cm": 165, "weight_kg": 77.4, "fasting_confirmed": true },
"files": [
{ "file_key": "aroga/sandbox/uploads/<fileId>.pdf", "kind": "labs", "collected_on": "2026-03-16" },
{ "file_key": "aroga/sandbox/uploads/<fileId>.pdf", "kind": "labs", "collected_on": "2025-03-12" },
{ "inline": { "name": "visit.txt", "mime_type": "text/plain", "raw_bytes": "<base64>" }, "kind": "encounter_notes" },
{ "file_key": "aroga/sandbox/uploads/<fileId>.pdf", "kind": "body_composition", "collected_on": "2026-03-16" }
],
"context": { "medications": ["Ramipril 5 mg daily"], "devices": ["Hormonal IUD (levonorgestrel)"], "conditions": ["Hypertension"], "allergies": ["Penicillin (rash)"] },
"clinician_notes": "<free text from the clinician>",
"care_team_notes": [{ "role": "nutritionist", "written_on": "2026-02-10", "text": "<the nutritionist's note>" }],
"plan_exclusions": ["fasting"],
"overall_instructions": "<your instruction package>"
}
}
Top level: inputs (required) and idempotency_key (optional, at most 200 characters, see section 6).
inputs:
| Field | Required | Notes |
|---|---|---|
report_type |
yes | general_practice or firefighter. |
patient |
yes | See the next table. |
files |
yes | 1 to 10 files, each with its kind. At least one has kind labs. See the table after next. |
context |
no | Lists of short texts: medications, devices, conditions and allergies, each at most 30 entries of 1 to 200 characters. |
clinician_notes |
no | Free text from the clinician. Email addresses and phone numbers are removed. At most 60,000 characters. |
care_team_notes |
no | Up to 10 notes from the patient's care team. See the table after the files. |
plan_exclusions |
no | Kinds of advice the report leaves out for this patient, by id. See the switch table. |
overall_instructions |
no | Your instruction package. Optional; the report always follows the Aroga clinical profile, and these instructions are added to it. They may set tone, voice, reading level, wording and emphasis; they cannot change the clinical rules, the sections, the results, the units, the recommendations, the supplements, the sources or the patient's context. A start whose instructions ask for one of those is answered 400 validation_failed, naming what was asked, and a report that breaks a rule is held either way. Email addresses and phone numbers are removed. When it is missing or empty, the report follows the Aroga clinical profile alone. At most 120,000 characters. |
language |
no | Reports are in English. |
Fields that are not listed here are accepted and ignored. A request that sends member, input_0, file_keys or Other_files is answered with 400 contract_changed, and its fields name what to send in their place.
patient:
| Field | Required | Notes |
|---|---|---|
name |
yes | A display name, 1 to 120 characters. |
age |
yes | Whole number, 18 to 120. |
sex |
yes | male or female. |
birth_year |
no | YYYY. |
height_cm |
no | 100 to 250. |
weight_kg |
no | 30 to 300. |
fasting_confirmed |
no | true or false. |
Each entry of files:
| Field | Required | Notes |
|---|---|---|
kind |
yes | labs, molecular_you, encounter_notes, body_composition, ecg, treadmill, spirometry, chest_xray or other. |
collected_on |
no | The date the specimen was collected, YYYY-MM-DD, from 1990-01-01 to today. Send it with every lab file. |
file_key |
no | A key from the upload endpoints, exactly as returned, once per request. Send exactly one of file_key and inline. |
inline |
no | The file itself: { "name": "labs.pdf", "mime_type": "application/pdf", "raw_bytes": "<base64>" }. The whole request must stay under 4 MB. |
Send each lab report as its own file of kind labs, earlier reports included, with its collected_on. Each lab file is read on its own, so lab_results holds every test by the date of its own report. Encounter notes and a Molecular You report, as PDF, image or text, are read on their own too, and the report summarizes them.
A body composition report and the report of an ECG, treadmill test, spirometry or chest X-ray are read on their own as well. The report shows body composition values and classification as printed, and each test's conclusion as reported, without the test report's recommendations. Send the date of the measurement or test as collected_on.
Each entry of care_team_notes:
| Field | Required | Notes |
|---|---|---|
role |
yes | nutritionist, dietitian or exercise_physiologist. |
written_on |
no | The date of the note, YYYY-MM-DD, from 1990-01-01 to today. |
text |
yes | 1 to 2,000 characters. Email addresses and phone numbers are removed. |
The report builds on the care team's notes and never contradicts them. It shows its own points from them, not the notes themselves.
plan_exclusions takes distinct ids from this switch table. The report suggests nothing an excluded id covers, anywhere:
| Id | Leaves out |
|---|---|
fasting |
Fasting or time-restricted eating. |
calorie_restriction |
Eating less to lose weight (a calorie deficit or count, or a weight-loss target). |
vigorous_exercise |
Vigorous or high-intensity exercise. Also left out whenever an ecg or treadmill file is sent. |
heat_cold_exposure |
Saunas, hot tubs or cold exposure. |
supplements |
Supplements. |
Response 200:
{
"taskId": "0f8c2e1a-5b7d-4e2a-9c31-7a1d2b3c4d5e", "status": "started",
"pipeline_id": "aroga-biomarker-analysis", "environment": "sandbox", "totalStages": 4,
"template_id": "aroga-biomarker-analysis", "template_version": "v1.1.0", "ranges_version": "draft-2026-09-29", "build": "82ad61c"
}
status is existing when the request repeated an idempotency_key. A start with overall_instructions also carries instructions_sha256, as the task status does.
4. Responses
Task status
GET /api/pipeline/status/{taskId}
{
"status": "running", "completed": false,
"stage": "Writing", "stageIndex": 2, "totalStages": 4,
"pipeline_id": "aroga-biomarker-analysis", "report_type": "general_practice", "environment": "sandbox",
"template_id": "aroga-biomarker-analysis", "template_version": "v1.1.0", "ranges_version": "draft-2026-09-29", "build": "82ad61c",
"created_at": "2026-10-02T15:04:05.000Z", "updated_at": "2026-10-02T15:07:41.000Z", "expires_at": "2026-11-01T15:04:05.000Z",
"progress": { "message": "Working on the report", "elapsed_seconds": 216 }
}
| Field | Meaning |
|---|---|
status |
running, completed, held or failed. |
completed |
true only when status is completed. |
stage, stageIndex, totalStages |
Queued (0), Reading results (1), Writing (2), Assembly (3), Quality checks (4). |
report_type |
The report_type of the request. |
progress |
A short status line and the seconds since the task started. |
created_at, updated_at, expires_at |
ISO timestamps. |
instructions_sha256 |
The SHA-256, in hex, of overall_instructions as sent, trimmed. Present only when the task was started with instructions. |
validation, result |
Present when status is completed or held. |
review |
The latest clinician review, once one is recorded. See Clinician review. |
error, error_category |
Present only when status is failed. |
A failed task:
{
"status": "failed", "completed": false, "stage": "Writing", "stageIndex": 2, "totalStages": 4,
"error": "The run exceeded its time limit and was stopped. Start a new run.", "error_category": "deadline_exceeded",
"progress": { "message": "Stopped", "elapsed_seconds": 1741 }
}
error is a sentence. error_category is deadline_exceeded, input_rejected or pipeline_error.
A held task is final, like a completed one, and carries validation and result: the report was produced, but a check still found an error after one rewrite. validation.errors lists what held it. Show the report only after clinician review.
A completed task carries validation and result:
{
"status": "completed", "completed": true, "stage": "Quality checks", "stageIndex": 4, "totalStages": 4,
"validation": { "passed": true, "errors": [], "warnings": [] },
"result": {
"report_type": "general_practice",
"output_0": "Aroga Lifestyle Medicine\n\n*Your health assessment*\n\n# Aroga Biomarker Report\n\n...",
"Visual_Report": "<!doctype html>...",
"outputs": { "markdown": true, "html": true, "pdf": true },
"validation": { "passed": true, "errors": [], "warnings": [] },
"panel": {
"ranges_version": "draft-2026-09-29",
"categories": [{
"key": "nutrients-iron-status", "name": "Nutrients & Iron Status",
"rows": [{
"key": "vitamin-d-25-oh-total-ia", "display_name": "Vitamin D, 25-OH, Total, IA",
"value": 21, "value_text": "21", "unit": "ng/mL",
"reference_range": { "lo": 60, "hi": 80 }, "reference_range_text": "30-100 ng/mL (optimal 60-80)",
"lab_range": { "lo": 30, "hi": 100 }, "lab_range_text": "30-100 ng/mL",
"lab_flag": "L", "target": "optimal 60-80", "status": "Below Target", "source": "measured"
}]
}]
},
"lab_results": {
"catalog_version": "labs-2026-10-06.1", "dates": ["2025-03-12", "2026-03-16"], "current_date": "2026-03-16",
"analytes": [{
"key": "apob", "name": "ApoB", "group": "lipids", "specimen": "blood",
"results": [
{ "collected_on": "2025-03-12", "value": 1.12, "value_text": "1.12", "unit": "g/L", "value_as_printed": "112", "unit_as_printed": "mg/dL", "converted": true, "reference_range": "<90", "lab_flag": "H", "file_index": 1, "found_on_report": true },
{ "collected_on": "2026-03-16", "value": 0.94, "value_text": "0.94", "unit": "g/L", "value_as_printed": "0.94", "unit_as_printed": "g/L", "converted": false, "reference_range": "<1.05", "lab_flag": null, "file_index": 0, "found_on_report": true }
]
}]
},
"unmapped_biomarkers": ["OMEGA-3 INDEX"], "regenerations": 0,
"references": [{ "n": 1, "doi": "10.1000/example", "pmid": "12345678", "title": "..." }],
"usage": [], "credits": { "report": "0", "visual": "0" },
"template_id": "aroga-biomarker-analysis", "template_version": "v1.1.0", "ranges_version": "draft-2026-09-29", "build": "82ad61c"
}
}
| Field | Meaning |
|---|---|
report_type |
The report_type of the request. |
output_0 |
The report in Markdown, in the template of its report_type: general practice or firefighter. |
Visual_Report |
The same report as one HTML document, laid out for A4 printing. |
outputs |
Which of the Markdown, HTML and PDF versions are present. |
validation |
The same object as the top-level validation. |
panel |
The results as data, grouped by category. |
lab_results |
Every lab result by test and collection date. null when the task has none. |
unmapped_biomarkers |
Results on the lab files that are not in lab_results, by their name on the file. |
regenerations |
1 when the report was rewritten once after failing a check, otherwise 0. |
references |
The numbered sources of the report. |
usage |
Always empty. |
credits |
Always { "report": "0", "visual": "0" }. |
template_id, template_version, ranges_version, build |
The release that produced the report. |
instructions_sha256 |
As in the task status: present only when the task was started with instructions. |
Panel row fields: key, display_name, value, value_text, unit, reference_range, reference_range_text, lab_range, lab_range_text, lab_flag, target, status, source.
Lab result fields: collected_on, value, value_text, unit, value_as_printed, unit_as_printed, converted, reference_range, lab_flag, file_index, found_on_report. unit is the SI unit, or the unit on the lab file when no single conversion applies. reference_range is as it appears on the lab file. file_index is the position of the file in inputs.files.
A general practice report shows how each result moved across the lab dates within 400 days of the latest, with charts, and groups its plan by Aroga's six pillars. Both report types show the body composition and test tables, and the care team's points when notes were sent.
Validation: passed is false when a check reported an error. errors and warnings are lists of code: message. The report is returned either way. New codes can appear.
Report files
GET /api/reports/{taskId}
{
"jobId": "0f8c2e1a-5b7d-4e2a-9c31-7a1d2b3c4d5e",
"reports": [
{ "key": "aroga/sandbox/reports/<taskId>/output_0.md", "type": "output_0", "contentType": "text/markdown", "size": 31874, "lastModified": "2026-10-02T15:12:31.000Z", "signedUrl": "https://aroga-sandbox.nextgenerationmedicine.co/api/reports/<taskId>/files/output_0.md?exp=1791216751&sig=<hex>" },
{ "key": "aroga/sandbox/reports/<taskId>/Visual_Report.html", "type": "Visual_Report", "contentType": "text/html", "size": 58210, "lastModified": "2026-10-02T15:12:31.000Z", "signedUrl": "..." },
{ "key": "aroga/sandbox/reports/<taskId>/Visual_Report.pdf", "type": "Visual_Report_PDF", "contentType": "application/pdf", "size": 41210, "lastModified": "2026-10-02T15:12:31.000Z", "signedUrl": "..." },
{ "key": "aroga/sandbox/reports/<taskId>/meta.json", "type": "meta", "contentType": "application/json", "size": 9120, "lastModified": "2026-10-02T15:12:31.000Z", "signedUrl": "..." }
]
}
- Each
signedUrlis a report link that works for 1 hour without the Bearer header. Call this endpoint again for fresh links. - While the task is running the response is
404with"error": "not_ready". - A failed task lists
meta.jsononly.Visual_Report.pdfis listed when the PDF could be produced; otherwise a warning invalidationsays so.
GET /api/reports/{taskId}/files/{name} serves output_0.md, Visual_Report.html, Visual_Report.pdf or meta.json, with a report link or with your Bearer key.
meta.json is the record of the task: its status, report_type, versions, instructions_sha256 when the task was started with instructions, validation, panel, references and, once recorded, its latest review. A failed task has error and error_category in place of validation. /api-json lists every field.
Clinician review
POST /api/reports/{taskId}/review records the review of a completed or held report by the patient's clinician:
{ "decision": "released", "reviewer": "clinician-17", "report_sha256": "<SHA-256 of the file reviewed>", "note": "Read with the lab report." }
| Field | Required | Notes |
|---|---|---|
decision |
yes | released or returned. |
reviewer |
yes | Your code for the clinician, 1 to 64 letters, digits, ., _ or -. Not a name. |
report_sha256 |
yes | The SHA-256, in hex, of the file the clinician read: Visual_Report.pdf, or output_0.md as downloaded. |
note |
no | At most 2,000 characters. Email addresses and phone numbers are removed. |
Response 200:
{ "taskId": "<taskId>", "review": { "decision": "released", "reviewer": "clinician-17", "reviewed_at": "2026-10-02T16:20:00.000Z", "report_file": "Visual_Report.pdf", "report_sha256": "<hex>", "note": "Read with the lab report." } }
The latest review shows as review in the task status and in meta.json, and the last 20 are kept. Recording one never changes the task: a held task stays held. A running or failed task answers 409 not_reviewable. A report_sha256 that is not the hash of the task's PDF or Markdown answers 409 report_mismatch.
Show a report, completed or held, to the patient only when its latest review has decision released and a report_sha256 that is the SHA-256 of the file shown. A returned report is not shown.
Health
GET /health
{
"ok": true, "status": "ok", "service": "aroga-biomarker-analysis-api", "client": "aroga", "environment": "sandbox",
"region": "us-west2", "tier": "standard", "time": "2026-10-02T15:00:00.000Z",
"template_id": "aroga-biomarker-analysis", "template_version": "v1.1.0", "ranges_version": "draft-2026-09-29", "build": "82ad61c"
}
5. Errors
| HTTP | error |
Meaning |
|---|---|---|
| 400 | invalid_json |
The body is not valid JSON. |
| 400 | validation_failed |
A field is missing or invalid, or overall_instructions asks for what the clinical rules do not allow. See fields. |
| 400 | contract_changed |
The request sends a field this API does not take. fields names what to send in its place. |
| 400 | no_lab_file |
No file of kind labs. |
| 400 | file_not_found |
A file_key is unknown, expired or not yours. fields names the file. |
| 400 | unsupported_file_type |
The file type is not accepted. |
| 400 | file_content_mismatch |
The bytes are not of the type given, or the file is empty. |
| 400 | invalid_body |
The upload body could not be read. |
| 401 | unauthorized |
Missing or malformed Authorization header. |
| 403 | forbidden |
The key is unknown, disabled or expired. |
| 403 | upload_url_invalid |
The upload link is wrong or expired. |
| 403 | signature_invalid |
The report link is wrong or expired. |
| 404 | job_not_found |
No task with that id for your key. |
| 404 | not_ready |
The task is still running. |
| 404 | artifact_not_found |
The task has no file with that name. |
| 409 | not_reviewable |
The task is running or failed, so there is no report to review. |
| 409 | report_mismatch |
report_sha256 is not the SHA-256 of the task's PDF or Markdown. |
| 413 | request_too_large |
The request body is over 4 MB. |
| 413 | file_too_large |
A file, or the files for one report, are too large. |
| 413 | notes_too_long |
clinician_notes is over 60,000 characters. |
| 413 | instructions_too_long |
overall_instructions is over 120,000 characters. |
| 429 | daily_limit |
The key has used its report starts for the day. |
| 429 | concurrency_limit |
Too many reports are running for the key. |
| 429 | upload_limit |
The key has used its uploads for the day. |
| 501 | not_supported |
The endpoint is not available. |
| 503 | service_unavailable |
Try again shortly. |
A 429 carries retry_after_seconds and a Retry-After header.
6. Limits and timing
| Limit | Value |
|---|---|
| Request body | 4 MB |
| One file | 4 MB |
| Files per report | 10, and 20 MB of documents in total |
| Text files per report | 500,000 characters in total |
clinician_notes |
60,000 characters |
care_team_notes |
10 notes of 2,000 characters each |
overall_instructions |
120,000 characters |
| Report starts per key | 40 per UTC day |
| Reports running at once per key | 3 |
| Uploads per key | 500 per UTC day |
- Poll every 15 to 30 seconds.
- Stop polling after 33 minutes.
- A
404on the status route means the task does not exist for your key.
Send idempotency_key on every start. A repeat with the same key and the same API key within 24 hours returns the original taskId with status: "existing" and starts nothing.
A task and its report can be fetched for 30 days. An uploaded file can be used for 7 days. Each is deleted at the first daily sweep after its period ends. A task's reviews are deleted with it.
7. Data rule
- Sandbox is synthetic-data only. Do not send real member data until the BAA is executed.
patient.nameis a display name. Use a placeholder such asPatient 1.- The files,
context,clinician_notesandcare_team_noteshold synthetic content only. - Do not publish upload links or report links.
8. Support
Reply to your NGM contact and include the task id, plus the template_version and build from the response.