For developers
Data import API
Send training sessions, morning wellness and jump tests from your own systems straight into a VitalForg workspace, and read them back.
Getting a key
A workspace admin creates keys in Settings › API keys, choosing only the permissions a system needs:
| Permission | Allows |
|---|---|
athletes:read | List athletes |
athletes:write | Add athletes |
data:read | Read sessions, wellness and jump tests |
data:write | Add sessions, wellness and jump tests |
The key is shown once. VitalForg keeps only a fingerprint of it, so store it in your system’s secret settings. A key works only for the workspace it was created in. Revoking it stops it at once.
Making requests
Send every request over HTTPS to https://www.vitalforg.com/api/v1/ with the key in the Authorization header. Request and response bodies are JSON.
curl https://www.vitalforg.com/api/v1/status \
-H "Authorization: Bearer vf_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
If your server cannot use short paths, use /api/v1/index.php?resource=status instead.
| Request | Permission | What it does |
|---|---|---|
GET status | any | Shows the workspace, the key’s permissions and the limits |
GET athletes | athletes:read | Lists athletes: code, name, team, position |
POST athletes | athletes:write | Adds athletes. Teams that do not exist yet are created |
POST sessionsPOST wellnessPOST tests | data:write | Adds records for athletes already in the workspace |
GET sessionsGET wellnessGET tests | data:read | Reads records by date: from, to (YYYY-MM-DD, default the last 30 days), limit (up to 1,000) and after_id for the next page |
Sending records
Send up to 1,000 records per request in a records array. Field names and rules are the same as the CSV import columns in the app.
curl https://www.vitalforg.com/api/v1/sessions \
-H "Authorization: Bearer vf_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gps-export-2026-09-10" \
-d '{"records": [
{"athlete_code": "P01", "date": "2026-09-10", "start_time": "2026-09-10T17:00:00+01:00",
"type": "Training", "duration_min": 75, "rpe": 6, "distance_m": 6400, "hsr_m": 520,
"source_gps": "GPS vest 12"}
]}'
HTTP/1.1 201 Created
{"received": 1, "inserted": 1, "duplicates": [], "warnings": [], "import_id": 418}
| Type | Required fields | Optional fields |
|---|---|---|
| athletes | athlete_code, name, team, date_of_birth | position |
| sessions | athlete_code, date, start_time, type, duration_min, rpe (0–10) | distance_m, hsr_m, source_gps |
| wellness | athlete_code, date, recorded_at, and at least one of the metrics | hrv_rmssd_ms, resting_hr_bpm, sleep_h, soreness_1to5, source_sensor |
| tests | athlete_code, date, recorded_at, cmj_height_cm | trials, source |
Timestamps use ISO 8601 with a time zone. Numbers must be within plausible ranges, for example RPE 0–10 and sleep 2–14 hours. VitalForg is for adults only: athletes under 18 are refused.
All or nothing, and no duplicates
- If any record in a request is invalid, nothing from that request is saved, and the error lists every problem by record position (
index, counting from 0) and field. - Records that are already stored (same athlete and start time and type for sessions, same athlete and date for wellness and tests, same code for athletes) are skipped and listed in
duplicates. Sending the same batch twice never stores it twice. - Add an
Idempotency-Keyheader (8–100 letters, numbers, dots, colons, dashes or underscores) to make retries completely safe: the same key with the same body returns the original answer withIdempotent-Replayed: true. The same key with a different body is refused.
Limits
Each key can make 60 requests a minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Bodies can be up to 2 MB. Repeated requests with an invalid key from one address are blocked for the rest of the minute.
Errors
Errors always look like this:
HTTP/1.1 422 Unprocessable Entity
{"error": {"code": "validation_failed", "message": "Some records are not valid. Nothing was saved.",
"details": [{"index": 3, "field": "rpe", "message": "14 CR-10 is outside the plausible range 0–10."}]}}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_idempotency_key | The body or header could not be read |
| 401 | unauthorized | Missing, wrong or revoked key |
| 403 | insufficient_scope | The key does not have the permission named in required_scope |
| 403 | real_data_disabled | This VitalForg installation accepts synthetic test data only; include "synthetic": true and use generated data |
| 404 | not_found | Unknown address |
| 409 | idempotency_conflict, duplicate_conflict | Key reused with a different body, or another request saved the same records at the same moment (retry it) |
| 413 | payload_too_large, too_many_records | Over 2 MB or over 1,000 records |
| 415 | unsupported_media_type | Send Content-Type: application/json |
| 422 | validation_failed | See details; nothing was saved |
| 429 | rate_limited | Wait for the number of seconds in Retry-After |
| 500 | server_error | Nothing was saved; try again later |
Wearables and other systems
VitalForg does not connect directly to wearable or force-plate vendors’ accounts. Most systems can export CSV files, which you can import in the app, or a short script can read your vendor’s export and send it to this API.