Quick start

From no key to a first analysis

Five steps. The first two need no account, and none of them needs patient data.

API v1 · docs 1.0 · updated 2026-09-22

1. Get access

Every call except the public ones below needs an API key in the X-API-Key header. Keys are issued by Oxynet on request; there is no self-service signup yet. A key starts with sk_live_ and grants specific analyses (thresholds, oscillation and substrate are licensed separately), so the first call with it should be GET /v1/capabilities.

While you wait, this much works without one:

# No key needed: the synthetic sample and the schema are public
curl -s https://app.oxynet.net/v1/sample | jq '{filename, note, expected}'
curl -s https://app.oxynet.net/v1/openapi.json | jq '.paths | keys'

Not yet available

A self-service sandbox key. Until one exists, a request with a sentence on what you want to build is enough to receive a trial key.

2. Get sample data

Use the synthetic recording before anything of yours. It is generated by the API (GET /v1/sample), 900 seconds of an incremental test at 2 s intervals, with V̇O₂, V̇CO₂, V̇E, breathing frequency, end-tidal gases and heart rate. It is not a person and it is not validation data: the thresholds sit near 55 % and 80 % of the test because the generator placed them there.

Fetch it live, as in step 3, or download the saved copies, including one with an oscillation for the research analysis.

3. Run Oxynet

The shortest route, sample to thresholds with curl and jq:

export OXYNET_API_KEY=sk_live_...   # keep it out of files and logs

# Fetch the synthetic sample and upload it in one pipe
ID=$(curl -s https://app.oxynet.net/v1/sample \
  | jq '{filename, content}' \
  | curl -s -H "X-API-Key: $OXYNET_API_KEY" -H 'Content-Type: application/json' \
      -d @- https://app.oxynet.net/v1/cpet/content \
  | jq -r .cpet_id)

curl -s -H "X-API-Key: $OXYNET_API_KEY" -H 'Content-Type: application/json' \
  -d '{"analyses": ["vt"]}' \
  https://app.oxynet.net/v1/cpet/$ID/analyze | jq '.results[0]'

The complete flow on a file from disk (capabilities, upload, analyses, derived quantities, deletion), in the two forms it is downloadable in:

python/quickstart.py
"""
Oxynet quick start: upload a CPET export, analyse it, read the result.

    pip install requests
    export OXYNET_API_KEY=sk_live_...
    python quickstart.py oxynet_sample.json

Works on any supported vendor export, sent unchanged: do not convert units or
rename columns first. Reference: https://www.oxynet.net/developers/api/v1
"""

import os
import sys

import requests

BASE = "https://app.oxynet.net"

session = requests.Session()
session.headers["X-API-Key"] = os.environ["OXYNET_API_KEY"]


def call(method, path, **kwargs):
    r = session.request(method, BASE + path, timeout=120, **kwargs)
    if not r.ok:
        # Every error has the same shape: {"error": CODE, "message": ..., "context": {...}}
        err = r.json()
        sys.exit(f"{r.status_code} {err['error']}: {err['message']}\n{err.get('context')}")
    return r.json()


def main(path):
    # 1. What this key may do. Analyses are licensed separately, so ask first.
    caps = call("GET", "/v1/capabilities")
    print("analyses on this key:", caps["analyses"])

    # 2. Upload the file exactly as exported. The format is detected from its content.
    with open(path, "rb") as f:
        rec = call("POST", "/v1/cpet", files={"file": (os.path.basename(path), f)})
    cpet_id = rec["cpet_id"]
    print("cpet_id:", cpet_id, "| format:", rec["source"]["format"])
    for name, ch in rec["channels"].items():
        if not ch.get("present"):
            print(f"  {name} absent: {ch.get('reason', 'not in this export')}")

    # 3. Analyse. One envelope per analysis; one that cannot run does not stop the others.
    wanted = [a for a in ("vt", "eov", "substrate") if a in caps["analyses"]]
    out = call("POST", f"/v1/cpet/{cpet_id}/analyze", json={"analyses": wanted})
    for env in out["results"]:
        print(f"\n[{env['analysis']}] status: {env['status']}")
        if env["status"] != "ok":
            # A refusal is information: it says what was missing and why.
            print("  refused:", env["error"], "-", env["message"])
            continue
        # `quality` describes the RECORDING. It is not a confidence in the answer.
        print("  quality of the input:", env["quality"])
        if env["analysis"] == "vt":
            print("  findings:", env["findings"])
        else:
            print("  finding fields:", ", ".join(sorted(env["findings"])))
        for note in env["notes"]:
            print("  note:", note)  # caveats that must travel with the numbers
        print("  provenance:", env["provenance"])

    # 4. Derived quantities and input checks, cheaper than a full analysis.
    m = call("POST", f"/v1/cpet/{cpet_id}/compute",
             json={"metrics": ["vo2max", "gas_quality", "sampling_adequacy"]})
    for name, res in m["metrics"].items():
        print(f"\n[{name}]", res["value"] if res["status"] == "ok" else res)

    # 5. Delete now rather than waiting for the 24 h expiry.
    call("DELETE", f"/v1/cpet/{cpet_id}")
    print("\ndeleted", cpet_id)


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "oxynet_sample.json")
rest/quickstart.sh
#!/usr/bin/env bash
# Oxynet quick start with curl and jq.
#
#   export OXYNET_API_KEY=sk_live_...
#   ./quickstart.sh oxynet_sample.json
#
# Reference: https://www.oxynet.net/developers/api/v1
set -euo pipefail

BASE=https://app.oxynet.net
FILE=${1:-oxynet_sample.json}
KEY="X-API-Key: ${OXYNET_API_KEY:?set OXYNET_API_KEY first}"
JSON='Content-Type: application/json'

# 1. What this key may do (analyses are licensed separately) and its limits.
curl -sS --fail-with-body -H "$KEY" "$BASE/v1/capabilities" | jq '{analyses, limits}'

# 2. Upload the export unchanged. The response names the detected format and
#    every channel that is absent, with the reason.
ID=$(curl -sS --fail-with-body -H "$KEY" -F "file=@$FILE" "$BASE/v1/cpet" | jq -r .cpet_id)
echo "cpet_id: $ID"

# 3. Thresholds. Add "eov" and "substrate" if the key holds them.
curl -sS --fail-with-body -H "$KEY" -H "$JSON" \
  -d '{"analyses": ["vt"]}' \
  "$BASE/v1/cpet/$ID/analyze" | jq '.results[]'

# 4. Derived quantities and input-quality checks.
curl -sS --fail-with-body -H "$KEY" -H "$JSON" \
  -d '{"metrics": ["vo2max", "gas_quality", "sampling_adequacy"]}' \
  "$BASE/v1/cpet/$ID/compute" | jq .metrics

# 5. Delete now rather than waiting for the 24 h expiry.
curl -sS --fail-with-body -X DELETE -H "$KEY" "$BASE/v1/cpet/$ID" | jq .

Note

Your own files go in the same way: the vendor export, unchanged. Remove names and dates of birth from the file and the filename first; Oxynet needs no identifiers. For a file on disk reached through an agent, use an upload ticket (POST /v1/uploads) so the bytes never pass through the conversation.

4. Inspect the result

analyze returns {cpet_id, results: [...]}, one envelope per analysis, all the same shape. This is the threshold envelope as recorded from the live API on 2026-09-04 for the manual's reference recording (a de-identified real test, not the synthetic sample):

analyze → results[0] · analysis_version 2026.08
{
  "status": "ok",
  "findings": {
    "vt1_time_s": 619,
    "vt2_time_s": 763,
    "vt1_vo2_ml_min": 1605.2,
    "vt2_vo2_ml_min": 2028.9
  },
  "quality": "good",
  "provenance": {
    "model": "gandalf",
    "model_version": "v0.1.0",
    "model_tier": "free",
    "analysis_version": "2026.08",
    "latency_ms": 97.41
  }
}

As stored for the manual, which kept only these fields. A live response also carries analysis, notes, and schema_version and computed_at in provenance.

status"ok", or "not_analysable" with an error code and the reason. Other analyses in the same call still run.
findingsVT1 and VT2 in seconds from the start and in mL/min of oxygen uptake.
qualitygood, acceptable or poor: how complete the RECORDING is. Not a confidence in the thresholds.
notesCaveats that belong in any report built on these numbers.
provenanceWhich model and version ran, the analysis version, when, and how long it took.

Every field, and the other two analyses →

5. Build

Clinical application

Store the envelope beside the test. Show findings, print notes verbatim in the report, keep provenance for audit.

Research software

One row per recording: findings as columns, analysis_version and model_version as columns too, so a re-run on a new version is a visible difference and not a silent one.

Report generator

Template the numbers; never template away the notes. A refused analysis becomes a sentence saying why, not an empty cell.

AI agent

Connect over MCP. The agent reasons over the structured results; the thresholds come from the model, not from the agent reading a trace.

Exercise physiology workflow

Thresholds in time and in V̇O₂, substrate use against %V̇O₂peak (or watts when a load channel exists), for prescription tools downstream.