REST API · v1
API reference
Base URL https://app.oxynet.net. Every request and response is JSON except the multipart upload. The authoritative, generated reference is the service's own OpenAPI schema and app.oxynet.net/docs; this page adds what a schema cannot say.
API v1 · docs 1.0 · updated 2026-09-22
The one rule
Hand Oxynet the file, work with the handle. Upload the vendor export unchanged and receive a cpet_id; every later call names it, and the signals never travel back through the caller unless a picture is being drawn. The lifecycle is the same for a hospital integration, a script and an agent:
- 1GET/v1/capabilities# what may this key do
- 2POST/v1/cpet# upload → cpet_id
- 3POST/v1/cpet/{id}/analyze# thresholds, substrate, oscillation
- 4POST/v1/cpet/{id}/compute# derived quantities, input checks
- 5DELETE/v1/cpet/{id}# or let it expire
Authentication
An API key in the X-API-Key header on every request. Keys are issued by Oxynet (request one). Each key carries a tier, which governs the models it can reach, and a set of products, which governs the analyses it can run, plus an optional monthly limit and expiry. Records are partitioned per key: one key cannot read another's uploads.
Without a key, only these answer: /v1/openapi.json, /v1/sample, /health, /llms.txt, /llms-full.txt. An upload URL minted by POST /v1/uploads carries its own short-lived credential and takes no key.
curl -s -H "X-API-Key: $OXYNET_API_KEY" https://app.oxynet.net/v1/capabilitiesEndpoints
Read from https://app.oxynet.net/v1/openapi.json (Oxynet CPET API, info.version v1, OpenAPI 3.0.3) when this page was built.
POST/v1/cpet/contentUpload a CPET recording as JSON (no multipart)
JSON upload for callers that cannot send multipart (GPT Actions, function calling). Body: filename (keep the real extension), content (text) or content_base64 (XLS/XLSX), optional retain_hours.
POST/v1/uploadsGet a one-shot URL to POST a file to
The right call when the file is on disk. Returns a one-shot upload_url (and a ready-to-run curl line) valid for 5 minutes; POST the file to it with no API key, because the ticket is the credential. {"count": N} mints up to 200 at once for a folder.
GET/v1/cpet/{cpet_id}Summary of a stored recording
The upload summary again: detected format, channels with coverage and the reason for any that are absent, sampling, external load, parser flags, and thresholds the metabolimeter itself embedded, where present.
DELETE/v1/cpet/{cpet_id}Delete a stored recording now
Delete the parsed record now rather than at expiry.
GET/v1/cpet/{cpet_id}/seriesDownsampled series, for plotting
Downsampled signals for plotting only. Every measurement runs server-side on the full-resolution record; never compute from these.
POST/v1/cpet/{cpet_id}/analyzeRun models on a stored recording
Body: {"analyses": ["vt", "eov", "substrate"]}. Returns {cpet_id, results: [envelope, ...]}, one envelope per analysis. Omit "model" unless you need a specific one; the default matches the web application. Unknown fields are a 422.
GET/v1/metricsThe CPET function registry
The derived-quantity registry: each metric with its unit, required channels, minimum duration and reading notes.
POST/v1/cpet/{cpet_id}/computeRun registry functions
Body: {"metrics": ["vo2max", "ve_vco2_slope", "gas_quality"]}. Each metric returns {status: "ok", value} or {status: "unavailable", error, message, context}. Never a bare null.
GET/v1/capabilitiesWhat this key can do
Call first. The analyses and metrics this key may run, the models it can reach, its limits (upload size, monthly limit, retention) and the retention policy.
GET/v1/sampleA synthetic CPET you can analyse immediately
A synthetic recording as {filename, content, note, expected}. No key needed. ?oscillating=true for one that exercises the oscillation detector.
GET/v1/formatsSupported metabolimeter formats
Every format id with a label saying what it is, how detection works, and if_unsupported: the canonical shape to convert a file into when nothing reads it.
POST/v1/cpetMultipart upload (field "file"), optional retain_hoursnot in the importer schema
The upload to use from your own code. Returns the same summary as the JSON upload.
POST/v1/uploads/{ticket}Post a file to a one-shot upload URLnot in the importer schema
No API key: the signed ticket is the credential. You receive this URL from POST /v1/uploads.
The public schema is shaped for importers such as GPT Actions, which reject multipart operations, so the two multipart uploads are documented here and omitted there. The schema declares request bodies; response shapes are documented below and in Outputs.
Upload and input
Three ways in, one result. POST /v1/cpet takes multipart field file from your own code. POST /v1/cpet/content takes {filename, content | content_base64} for callers that cannot send multipart; keep the real extension in filename, because it is part of the evidence for detection. POST /v1/uploads returns a one-shot URL for a file on disk, so an agent never holds the bytes. Every route returns the upload summary: detected format, each channel with its coverage and, if absent, the reason; the sampling the file arrived at; the external load; parser flags.
Detection reads the content and refuses when no candidate is clearly best. If nothing reads a file, the UNSUPPORTED_FORMAT error returns the canonical JSON shape to convert into; that is the only case in which converting a file yourself is correct. See Supported formats.
Analyses and the envelope
POST /v1/cpet/{id}/analyze with {"analyses": ["vt", "eov", "substrate"]} returns {cpet_id, results: [...]}, one envelope per analysis in the order asked. An analysis the recording cannot support comes back as not_analysable inside the 200, with the reason, and the others still run. "signal" is not an analysis name: signal processing is read from GET /v1/cpet/{id} and the input-quality metrics.
analysis | "vt", "eov" or "substrate". |
|---|---|
status | "ok", or "not_analysable" when the recording cannot support it. |
findings | The measurements. Shape depends on the analysis; null when not analysable. |
quality | "good", "acceptable" or "poor". Describes the INPUT recording (coverage of VE, VO2, VCO2 and whether the sampling interval is known). It is not a confidence in the result, and no field is. |
notes | Plain-language caveats that must travel with the numbers into any report built on them. |
error, message, context | Present when status is "not_analysable": the code, the reason, and what was needed. |
provenance | analysis_version, schema_version, computed_at, the model that ran and latency_ms; for thresholds also model_version and model_tier. |
Refusal codes inside a 200
vt | INSUFFICIENT_SIGNAL, MISSING_CHANNEL, TIER_INSUFFICIENT, MODEL_NOT_FOUND, INFERENCE_FAILED |
|---|---|
eov | NO_USABLE_DATA, INSUFFICIENT_DURATION, MISSING_CHANNEL, EOV_FAILED |
substrate | MISSING_CHANNEL, INSUFFICIENT_SIGNAL, SUBSTRATE_FAILED |
Errors
Every error response has one shape, and context says what would have worked:
{
"error": "MISSING_CHANNEL",
"message": "...",
"context": { "required": [...], "missing": [...] }
}| HTTP | error | Meaning |
|---|---|---|
| 401 | MISSING_API_KEY | No X-API-Key header. |
| 403 | INVALID_API_KEY | The key is not recognised. |
| 403 | KEY_EXPIRED | The key has passed its expiry date. |
| 403 | PRODUCT_NOT_ENABLED | The key does not include that analysis. Analyses are licensed separately. |
| 403 | TIER_INSUFFICIENT | The requested model needs a higher tier. |
| 403 | MODEL_NOT_ALLOWED | The key is restricted to other models. |
| 403 | UPLOAD_TICKET_INVALID | The upload URL is expired, used, or malformed. |
| 404 | CPET_NOT_FOUND | Unknown id, or the record passed its expiry. |
| 404 | MODEL_NOT_FOUND | No model by that name or version. |
| 413 | FILE_TOO_LARGE | Over 25 MB. |
| 413 | RECORDING_TOO_LONG | More than 20,000 samples. |
| 422 | UNSUPPORTED_FORMAT | Not recognised as any supported export. context.recovery gives the canonical shape to convert into. |
| 422 | PARSER_ERROR | Format recognised, contents unreadable. |
| 422 | UNKNOWN_ANALYSIS | Not one of vt, eov, substrate. "signal" is not an analysis: use GET /v1/cpet/{id} and compute. |
| 422 | VALIDATION_ERROR | Malformed body, or an unrecognised field. context.errors says which. |
| 429 | RATE_LIMITED | Back off and honour Retry-After. |
| 429 | QUOTA_EXCEEDED | Monthly analysis limit reached. |
| 500 | INFERENCE_FAILED | The model could not run. Worth reporting. |
| 503 | UPLOAD_TICKETS_UNAVAILABLE | Upload tickets are temporarily unavailable; use POST /v1/cpet. |
Limits, quotas and retention
| Upload size | 25 MB (413 FILE_TOO_LARGE) |
|---|---|
| Samples per recording | 20,000 (413 RECORDING_TOO_LONG) |
| Upload ticket | valid 5 minutes, one file each, up to 200 per request |
| Retention, default | 24 hours after upload |
| Retention, maximum | 168 hours (7 days), when requested with retain_hours |
| Rate limiting | per key; a 429 RATE_LIMITED carries a Retry-After header |
| Monthly quota | set per key, reported in capabilities.limits.monthly_limit (429 QUOTA_EXCEEDED) |
| Billing units | one per analysis that returns status "ok"; one per compute request with any metric ok. Uploads and refusals are not billed |
Your key's own values are in GET /v1/capabilities under limits. Uploaded bytes are parsed and discarded, never stored; the parsed record is deleted at expiry or on DELETE. See Data handling.
Not yet documented
A published rate-limit figure and a latency or availability commitment. Neither is stated today; the API reports latency per result in provenance.latency_ms.
Versioning
| API | v1 in the path and in the schema's info.version. Breaking changes would ship as /v2 beside it, documented at /developers/api/v2. |
|---|---|
| Analysis | provenance.analysis_version, currently 2026.08. |
| Response schema | provenance.schema_version and the upload summary's schema_version. |
| Model | provenance.model and model_version. Omit model in requests to get the default, which is what the web application uses. |
| Determinism | The same recording returns the same numbers on the same model version. |
| Request strictness | An unrecognised request field is a 422 naming it, so a typo cannot silently run on defaults. |
Not yet documented
A public changelog and a deprecation policy for model and analysis versions. Until they exist, pin on the provenance fields and store them with every result.