API documentation
API documentation
Send your API key with every request. API keys work only over HTTPS and are restricted to data your account owns.
Cloud and self-hosted deployments share this API. Change the HTTPS domain and use that deployment's API key; paths, payloads, scopes and responses stay the same. Both X-API-Key and Authorization: Bearer are accepted.
For cloud devices, grant permissions under Devices → More → API access. For self-hosted devices, select permissions and devices when creating or editing the instance key.
GET /api/v1/capabilities reports available features. Self-hosted deployments return HTTP 501 with code API_FEATURE_UNAVAILABLE for transcriptions and billing. They do not proxy those requests to cloud.
curl -H "X-API-Key: YOUR_API_KEY" "https://YOUR_SERVER/api/v1/devices"
curl -H "X-API-Key: YOUR_API_KEY" --output recording.pcm "https://YOUR_SERVER/api/v1/devices/DEVICE_ID/recordings/RECORDING_ID/audio/pcm"
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/capabilities | Read API version and deployment feature availability. |
GET | /api/v1/devices | List device metadata. |
GET | /api/v1/devices/:deviceId | Get one device. |
GET | /api/v1/devices/:deviceId/motion-history | Read speed, acceleration, gyroscope, steps and motion history (requires motion.history.read). |
GET | /api/v1/recordings?deviceId=&from=&to=&limit= | List archived recording metadata. Audio bytes are deliberately not exposed. |
GET | /api/v1/transcriptions?deviceId=&from=&to=&limit= | Get completed transcript text and timestamp spans. |
GET | /api/v1/devices/:deviceId/recordings/:recordingId/audio/pcm | Download one archived recording as decrypted raw PCM (requires audio.pcm.read). |
DELETE | /api/v1/devices/:deviceId/recordings/:recordingId | 删除归档录音及对应转写(需要 recordings.delete)。 |
POST | /api/v1/devices/:deviceId/recording/start | |
POST | /api/v1/devices/:deviceId/recording/stop | |
POST | /api/v1/devices/:deviceId/local-recordings/:recordingId/upload | |
GET | /api/v1/devices/:deviceId/local-recordings | |
POST | /api/v1/devices/:deviceId/local-recordings/refresh | |
DELETE | /api/v1/devices/:deviceId/local-recordings/:recordingId | |
GET | /api/v1/account/usage | Read the key account's current quota and billing status. |
Timestamps are ISO 8601 UTC. Device, recording and transcript lists accept a maximum limit of 100; motion history defaults to 1,000 samples and allows up to 5,000 per page. HTTP 402 means access is paused because the unpaid request limit was reached; contact your administrator to record payment.
录音控制与 SD 卡录音上传
curl --fail-with-body -X POST \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
--data '{"acknowledged":true,"expectedDeviceId":"DEVICE_ID","work_mode":"sd_only","length":600}' \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/recording/start"
curl --fail-with-body -X POST \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
--data '{"acknowledged":true,"expectedDeviceId":"DEVICE_ID"}' \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/recording/stop"
curl --fail-with-body -X POST \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
--data '{"acknowledged":true,"devicePassword":"DEVICE_PASSWORD","expectedRecordingId":"RECORDING_ID","expectedFileName":"FILE_NAME","expectedFileSize":4096,"expectedReportTimestamp":1790572800000}' \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/local-recordings/RECORDING_ID/upload"
删除归档录音
curl --fail-with-body -X DELETE \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data '{"acknowledged":true,"expectedRecordingId":"507f1f77bcf86cd799439011"}' \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/recordings/507f1f77bcf86cd799439011"
{"deleted":true,"recordingId":"507f1f77bcf86cd799439011","deviceId":"DEVICE_ID"}
删除设备 SD 卡录音
curl --fail-with-body -X POST -H "X-API-Key: YOUR_API_KEY" \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/local-recordings/refresh"
curl --fail-with-body -H "X-API-Key: YOUR_API_KEY" \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/local-recordings"
curl --fail-with-body -X DELETE \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
--data '{"acknowledged":true,"devicePassword":"DEVICE_PASSWORD","expectedRecordingId":"RECORDING_ID","expectedFileName":"FILE_NAME","expectedFileSize":4096,"expectedReportTimestamp":1790572800000}' \
"https://YOUR_SERVER/api/v1/devices/DEVICE_ID/local-recordings/RECORDING_ID"
{"requested":true,"deviceId":"DEVICE_ID","recordingId":"RECORDING_ID","fileName":"FILE_NAME","requestedAt":"2026-09-28T00:00:00.000Z"}
Motion and sensor history
Create a key on the APIs usage page, then open Devices → More → API access for the device. Enable Motion and sensor history (motion.history.read) for that key and save. Existing keys need this explicit grant; telemetry.read alone does not allow history. Device discovery also requires device.read.
Use the hardware deviceId from GET /api/v1/devices, not the portal database id. Examples use placeholders for the server, device and secret. Keep API keys on your backend; never embed them in public browser code.
start is inclusive and end is exclusive; both are required ISO 8601 timestamps with a timezone. Select up to 7 days per query. limit defaults to 1,000 (1–5,000). Results are sorted by sampleAt. If next is non-null, send it as after with the same start/end; stop when next is null. Each page is a separate metered API request.
Samples use server receipt time in five-second buckets. Missing/invalid fields return null; missing intervals are omitted rather than filled with zeros. Repeated reports in one bucket update that sample. Step counts are for the reported five-second interval, not cumulative totals. Sum non-null setps_5s values for received steps; gaps mean incomplete totals. History begins when the server first receives motion readings.
| Field | Meaning / unit |
|---|---|
speed_mpm | Speed in meters per minute (m/min). |
accel_g.{x,y,z} | X/Y/Z acceleration in gravitational units (g). |
accel_m.{x,y,z} | X/Y/Z acceleration in SI units (m/s², meters per second squared). This field is acceleration, not velocity. |
gyro_dps.{x,y,z} | X/Y/Z angular velocity in degrees per second (°/s). |
setps_5s | Steps during five seconds. setps_5s is the canonical firmware spelling; steps_5s is accepted on ingestion only. |
running, walking, falling, turning | Boolean flags, or null when absent from this sample. |
motionBehaviors | All true flags appear in this array. stationary means all four flags are false; unknown means no flag is true and at least one is missing. These states describe the historical sample. |
sampleAt, observedAt | sampleAt is the UTC five-second bucket start; observedAt is the latest server receipt time for the bucket. |
Fetch one page (cURL)
export OX_API_BASE_URL="https://YOUR_SERVER/api/v1"
export OX_API_KEY="YOUR_API_KEY"
export OX_DEVICE_ID="YOUR_HARDWARE_DEVICE_ID"
curl --fail-with-body --get \
-H "X-API-Key: $OX_API_KEY" \
"$OX_API_BASE_URL/devices/$OX_DEVICE_ID/motion-history" \
--data-urlencode "start=2026-09-08T12:00:00Z" \
--data-urlencode "end=2026-09-08T13:00:00Z" \
--data-urlencode "limit=1000"
Example response
{
"deviceId": "YOUR_HARDWARE_DEVICE_ID",
"start": "2026-09-08T12:00:00.000Z",
"end": "2026-09-08T13:00:00.000Z",
"intervalSeconds": 5,
"units": {
"speed_mpm": "m/min",
"accel_g": "g",
"accel_m": "m/s²",
"gyro_dps": "°/s",
"setps_5s": "steps/5s"
},
"data": [
{
"sampleAt": "2026-09-08T12:00:00.000Z",
"observedAt": "2026-09-08T12:00:03.000Z",
"accel_g": {
"x": 0.01,
"y": 0.02,
"z": 1
},
"accel_m": {
"x": 0.1,
"y": 0.2,
"z": 0
},
"gyro_dps": {
"x": 0,
"y": 0,
"z": 2.5
},
"speed_mpm": 65,
"setps_5s": 8,
"running": false,
"walking": true,
"falling": false,
"turning": false,
"motionBehaviors": [
"walking"
]
}
],
"count": 1,
"next": null
}
Fetch every page (Node.js 18+)
Save the JavaScript as motion-history.mjs and run it with OX_API_BASE_URL, OX_API_KEY and OX_DEVICE_ID. It fetches the previous hour and prints the measurements and received step total.
const { OX_API_BASE_URL, OX_API_KEY, OX_DEVICE_ID } = process.env;
if (!OX_API_BASE_URL || !OX_API_KEY || !OX_DEVICE_ID) {
throw new Error('Set OX_API_BASE_URL, OX_API_KEY and OX_DEVICE_ID');
}
const end = new Date();
const start = new Date(end.getTime() - 60 * 60 * 1000);
const endpoint = `${OX_API_BASE_URL.replace(/\/$/, '')}/devices/${encodeURIComponent(OX_DEVICE_ID)}/motion-history`;
let after = null;
let receivedSteps = 0;
let stepSamples = 0;
do {
const url = new URL(endpoint);
url.search = new URLSearchParams({
start: start.toISOString(), end: end.toISOString(), limit: '1000',
...(after ? { after } : {}),
}).toString();
const response = await fetch(url, { headers: { 'X-API-Key': OX_API_KEY } });
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const page = await response.json();
for (const sample of page.data) {
console.log({
time: sample.sampleAt,
speedMetersPerMinute: sample.speed_mpm,
accelerationG: sample.accel_g,
accelerationMetersPerSecondSquared: sample.accel_m,
gyroDegreesPerSecond: sample.gyro_dps,
stepsInFiveSeconds: sample.setps_5s,
motion: sample.motionBehaviors,
});
if (sample.setps_5s !== null) {
receivedSteps += sample.setps_5s;
stepSamples++;
}
}
after = page.next;
} while (after);
console.log({ receivedSteps, stepSamples });
node motion-history.mjs
Errors: 400 invalid range/cursor/limit; 401 invalid or revoked API key; 403 missing device permission or unavailable account; 404 device outside the key account or not found; 402 billing access paused. An empty range returns data: [], count: 0, next: null.