curl https://api.bota.dev/v1/devices/dev_abc123/state \
-H "Authorization: Bearer sk_live_..."
const response = await fetch('https://api.bota.dev/v1/devices/dev_abc123/state', {
headers: {
'Authorization': 'Bearer sk_live_...',
},
});
const state = await response.json();
const isOnline = state.last_heartbeat_at &&
Date.now() - new Date(state.last_heartbeat_at).getTime() < 15 * 60 * 1000;
const isRecording = state.recording_state?.device_state === 'recording';
import requests
from datetime import datetime, timezone
response = requests.get(
'https://api.bota.dev/v1/devices/dev_abc123/state',
headers={'Authorization': 'Bearer sk_live_...'},
)
state = response.json()
last_hb = state.get('last_heartbeat_at')
is_online = (
last_hb is not None
and (datetime.now(timezone.utc) - datetime.fromisoformat(last_hb.replace('Z', '+00:00'))).total_seconds() < 900
)
is_recording = (state.get('recording_state') or {}).get('device_state') == 'recording'
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "recording",
"recording_id": "rec_xyz789",
"started_at": "2025-01-20T10:30:00Z",
"initiated_by": "remote",
"pending_recordings": 2,
"flags": 24,
"wifi_status": "connected",
"wifi_ssid": "office-wifi",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 85,
"storage_used_mb": 256,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": "2025-01-20T09:50:00Z",
"last_seen_by": "wifi",
"last_synced_by": "wifi"
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 8,
"wifi_status": "connected",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 92,
"storage_used_mb": 128,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 0
},
"battery_percent": 60,
"storage_used_mb": 100,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-19T15:20:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"error": {
"code": "not_found",
"message": "Device not found"
}
}
Devices
Get Device State
Live snapshot of a device — battery, storage, recording state, connectivity, last heartbeat
GET
/
devices
/
{id}
/
state
curl https://api.bota.dev/v1/devices/dev_abc123/state \
-H "Authorization: Bearer sk_live_..."
const response = await fetch('https://api.bota.dev/v1/devices/dev_abc123/state', {
headers: {
'Authorization': 'Bearer sk_live_...',
},
});
const state = await response.json();
const isOnline = state.last_heartbeat_at &&
Date.now() - new Date(state.last_heartbeat_at).getTime() < 15 * 60 * 1000;
const isRecording = state.recording_state?.device_state === 'recording';
import requests
from datetime import datetime, timezone
response = requests.get(
'https://api.bota.dev/v1/devices/dev_abc123/state',
headers={'Authorization': 'Bearer sk_live_...'},
)
state = response.json()
last_hb = state.get('last_heartbeat_at')
is_online = (
last_hb is not None
and (datetime.now(timezone.utc) - datetime.fromisoformat(last_hb.replace('Z', '+00:00'))).total_seconds() < 900
)
is_recording = (state.get('recording_state') or {}).get('device_state') == 'recording'
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "recording",
"recording_id": "rec_xyz789",
"started_at": "2025-01-20T10:30:00Z",
"initiated_by": "remote",
"pending_recordings": 2,
"flags": 24,
"wifi_status": "connected",
"wifi_ssid": "office-wifi",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 85,
"storage_used_mb": 256,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": "2025-01-20T09:50:00Z",
"last_seen_by": "wifi",
"last_synced_by": "wifi"
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 8,
"wifi_status": "connected",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 92,
"storage_used_mb": 128,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 0
},
"battery_percent": 60,
"storage_used_mb": 100,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-19T15:20:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"error": {
"code": "not_found",
"message": "Device not found"
}
}
Returns the live runtime snapshot of a device. Same field shape as
Fields the device reports — all optional. The device may not report every field on every heartbeat, so clients should default missing values defensively.
GET /v1/devices/{id} for runtime fields,
just curated (no settings, no metadata, lighter payload — designed for
polling at a few-second cadence).
The recording_state object is forwarded verbatim from the device’s most
recent heartbeat. Whether the device is currently reachable is not
returned as a server field; derive it client-side from last_heartbeat_at
(Date.now() - last_heartbeat_at < 15 * 60 * 1000 is the convention).
string
required
The device’s unique identifier (e.g.,
dev_abc123).curl https://api.bota.dev/v1/devices/dev_abc123/state \
-H "Authorization: Bearer sk_live_..."
const response = await fetch('https://api.bota.dev/v1/devices/dev_abc123/state', {
headers: {
'Authorization': 'Bearer sk_live_...',
},
});
const state = await response.json();
const isOnline = state.last_heartbeat_at &&
Date.now() - new Date(state.last_heartbeat_at).getTime() < 15 * 60 * 1000;
const isRecording = state.recording_state?.device_state === 'recording';
import requests
from datetime import datetime, timezone
response = requests.get(
'https://api.bota.dev/v1/devices/dev_abc123/state',
headers={'Authorization': 'Bearer sk_live_...'},
)
state = response.json()
last_hb = state.get('last_heartbeat_at')
is_online = (
last_hb is not None
and (datetime.now(timezone.utc) - datetime.fromisoformat(last_hb.replace('Z', '+00:00'))).total_seconds() < 900
)
is_recording = (state.get('recording_state') or {}).get('device_state') == 'recording'
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "recording",
"recording_id": "rec_xyz789",
"started_at": "2025-01-20T10:30:00Z",
"initiated_by": "remote",
"pending_recordings": 2,
"flags": 24,
"wifi_status": "connected",
"wifi_ssid": "office-wifi",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 85,
"storage_used_mb": 256,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": "2025-01-20T09:50:00Z",
"last_seen_by": "wifi",
"last_synced_by": "wifi"
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 8,
"wifi_status": "connected",
"lte_status": "off",
"connection_type": ["wifi"]
},
"battery_percent": 92,
"storage_used_mb": 128,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-20T10:35:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"device_id": "dev_abc123",
"recording_state": {
"device_state": "idle",
"pending_recordings": 0,
"flags": 0
},
"battery_percent": 60,
"storage_used_mb": 100,
"storage_total_mb": 1024,
"last_heartbeat_at": "2025-01-19T15:20:00Z",
"last_synced_at": null,
"last_seen_by": "wifi",
"last_synced_by": null
}
{
"error": {
"code": "not_found",
"message": "Device not found"
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
device_id | string | The device’s unique identifier |
recording_state | object | Snapshot from the device’s last heartbeat — see below |
battery_percent | number | null | Battery level (0-100), null if unknown |
storage_used_mb | number | null | Storage used in MB, null if unknown |
storage_total_mb | number | null | Total storage in MB, null if unknown |
last_heartbeat_at | string | null | ISO 8601 timestamp of the most recent heartbeat. Derive online from this — convention is now - last_heartbeat_at < 15 minutes |
last_synced_at | string | null | ISO 8601 timestamp of the last recording sync |
last_seen_by | string | null | Channel the last heartbeat arrived on: "bluetooth", "wifi", or "cellular" |
last_synced_by | string | null | Channel of the last sync |
recording_state (jsonb passthrough)
Fields the device reports — all optional. The device may not report every field on every heartbeat, so clients should default missing values defensively.
| Field | Type | Description |
|---|---|---|
device_state | string | What the device is doing right now — "idle", "recording", "syncing". The canonical signal for “is the device recording” — there is no separate active boolean. |
recording_id | string | ID of the current recording when device_state === "recording" |
started_at | string | ISO 8601 timestamp when recording started |
initiated_by | string | "local" (button press) or "remote" (API command) |
pending_recordings | number | Number of recordings on the device awaiting sync |
flags | number | Bitfield: 0x01 charging, 0x02 low battery, 0x04 storage full, 0x08 wifi connected, 0x10 lte connected, 0x20 sync active |
connection_type | string[] | Channels the device is currently using: subset of ["bluetooth", "wifi", "cellular"] |
wifi_status | string | "connected", "connecting", "disconnected", "off" |
wifi_ssid | string | Currently connected WiFi network name |
lte_status | string | "connected", "connecting", "disconnected", "off" |
lte_operator | string | Currently connected cellular operator |
Notes on shape
This endpoint returns the same field names asGET /v1/devices/{id} for everything in the runtime snapshot. The two endpoints differ only in what they omit:
GET /v1/devices/{id}includes the full device entity —status,model,firmware_version,serial_number,settings(with hierarchical config resolved),mac_address,metadata, etc.GET /v1/devices/{id}/stateincludes only the runtime snapshot — cheaper to poll, no settings resolution cost.
/state for live UI polling (1–10 s cadence). Use /devices/{id} once per session for device identity + configuration.Was this page helpful?

