Devices and ingest keys
How a member's watch, phone or a device aggregator writes steps, heart rate and workouts into Kinora with a personal key.
For the Full Stack package
How data gets in
Kinora does not pull from Garmin, Fitbit, Apple Health or any other service. Data arrives because something sends it: a member's own key lets a device, an app or a device aggregator write that member's movement figures and workouts through one door, /api/ingest.
- Aggregators such as Terra, Rook and Spike link a member's watch account once, then send its data to an address you give them. A small adapter of your own maps their payload onto the shape below and forwards it with that member's key.
- The adapter is not in the template: it is the one part that depends on which provider you choose.
In the dashboard
- In Settings, the Connected Devices card lists the device catalogue, each one connected or not, with its last sync and battery. The seed ships a GPS sports watch, the Kinora Band Pro and a smart body scale.
- Connect a data source creates a key: the member names it, and the full key is shown once, to copy. After that only its first 12 characters are shown.
- A key can be revoked at any time, and stops working at once.
The keys
- A key names exactly one account. It writes that account's movement figures and sessions and nothing else.
- Creating one needs
fitness.edit, which only the Member role holds: a staff token answers403. - A key is
kin_followed by 64 hexadecimal characters. The server keeps only its SHA-256 hash and its first 12 characters, so a leaked database does not hand anyone a working key.
| Method | Path | Permission | What it does |
|---|---|---|---|
GET | /api/fitness/devices/keys | fitness.view | The account's keys that are not revoked, newest first |
POST | /api/fitness/devices/keys | fitness.edit | Creates one ({ name }, 1 to 80 characters) and answers with the full key, readable this once |
DELETE | /api/fitness/devices/keys/:id | fitness.edit | Revokes one; 404 api_key_not_found for a key that is not the account's |
GET | /api/fitness/devices | fitness.view | The device catalogue with the account's connection to each |
POST | /api/fitness/devices/:key/toggle | fitness.edit | Connects or disconnects a device |
POST | /api/fitness/devices/:key/sync | fitness.edit | Always 400 device_sync_unavailable: the server pulls nothing |
Sending data
Every /api/ingest route reads the key from the X-Api-Key header and nothing else; a bearer token is not accepted there. A missing key answers 401 api_key_missing, an unknown or revoked one 401 api_key_invalid.
Check the key
Terminalcurl http://localhost:8000/api/ingest/whoami -H "X-Api-Key: kin_…"Expected result: It answers with the account id and the key's name.
Send a day's figures
POST /api/ingest/daily-metricswith a list of days. Onlydateis required; a figure you leave out keeps its value, so a heart-rate strap and a step counter can report the same day without erasing each other. Sending a date again corrects that day.Body{ "days": [ { "date": "2026-08-23", "steps": 15342, "distanceKm": 11.4, "activeKcal": 941, "totalKcal": 2680, "floorsClimbed": 14, "exerciseMinutes": 78, "activeMinutes": 96, "standHours": 11, "restingHr": 58, "avgHr": 74 } ] }Expected result:
{ "written": 1 }. A value out of range (a negative figure,standHoursover 24, a heart rate outside 20 to 250) answers400.Send workouts
POST /api/ingest/sessionswith a list of sessions.workoutSlugmust name a workout in the catalogue (404 workout_not_foundotherwise). A session is known by its account, workout and start, so sending it again does not count it twice. Every session sent is marked done.Body{ "sessions": [ { "workoutSlug": "conditioning", "startedAt": "2026-08-23T07:12:00.000Z", "finishedAt": "2026-08-23T07:49:00.000Z", "minutes": 37, "kcal": 412 } ] }
Each day written records the key's name as its source, which keeps a synced day apart from one entered by hand. Each accepted request stamps the key's last use.