AROGA

Aroga Biomarker Analysis API sandbox: integration guide

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": "..." }
  ]
}

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

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

8. Support

Reply to your NGM contact and include the task id, plus the template_version and build from the response.