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"
MethodPathPurpose
GET/api/v1/capabilitiesRead API version and deployment feature availability.
GET/api/v1/devicesList device metadata.
GET/api/v1/devices/:deviceIdGet one device.
GET/api/v1/devices/:deviceId/motion-historyRead 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/pcmDownload 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/usageRead 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.

FieldMeaning / unit
speed_mpmSpeed 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_5sSteps during five seconds. setps_5s is the canonical firmware spelling; steps_5s is accepted on ingestion only.
running, walking, falling, turningBoolean flags, or null when absent from this sample.
motionBehaviorsAll 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, observedAtsampleAt 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.