← Back to User Guide

PigStick API Reference

REST API and WebSocket interface for programmatic access to PigStick devices.

Base URL: All endpoints are served from the device at http://10.0.0.1 or http://pigstick-XXXX.local. All responses are application/json unless otherwise noted.

Table of Contents

1. Overview

The PigStick exposes a REST API over HTTP and three WebSocket endpoints for real-time streaming and shared-state control. The API requires no authentication—access is controlled by WiFi connectivity to the device's network.

Conventions

Quick Reference

MethodEndpointDescription

2. Device & Status

GET /api/v1/device

Returns device identity, firmware version, and hardware capabilities.

Response

{
  "device": {
    "serial": "PS2609A0110",
    "name": "pigstick-0110",
    "firmware": "2.2.72",
    "uptime": 3621,
    "bootCount": 47,
    "capabilities": {
      "audio": true,
      "magnetic": true,
      "gps": true,
      "battery": true
    },
    "limits": {
      "maxWsClients": 2,
      "maxEvents": 103
    }
  }
}
FieldTypeDescription
serialstringDevice serial number (PS format)
namestringDevice name (matches WiFi AP SSID)
firmwarestringFirmware version
uptimenumberSeconds since boot
bootCountnumberTotal boot cycles
capabilitiesobjectAvailable hardware features
limits.maxWsClientsnumberMaximum concurrent WebSocket clients
limits.maxEventsnumberMaximum stored events before FIFO eviction
GET /api/v1/status

Comprehensive device state snapshot including recorder, GPS, power, and connectivity.

Response

{
  "status": {
    "timestamp": 1773868800000,
    "device": "pigstick-0110",
    "serial": "PS2609A0110",
    "firmware": "2.2.72",
    "app": "2.2.95",
    "uptime": 3621,
    "storage": {
      "eventCount": 12,
      "total": 10379264,
      "used": 1245184,
      "free": 9134080,
      "evictionCount": 0
    },
    "job": {
      "client": "Acme Pipeline",
      "jobName": "Run-042",
      "operator": "J. Smith"
    },
    "recorder": {
      "state": "armed",
      "eventCount": 12,
      "freeSpace": 9134080,
      "captureProgress": null,
      "holdoffRemainingMs": null,
      "lastTriggerTime": 1773868500000,
      "capturesSinceBoot": 3,
      "autoArm": false,
      "audioEnabled": true
    },
    "gps": {
      "timeSynced": true,
      "fix": true,
      "fixQuality": 1,
      "satellites": 7,
      "lat": 30.267153,
      "lon": -97.743057,
      "altitude": 149.20,
      "speedKnots": 0.04,
      "speedMps": 0.0206,
      "utcEpochMs": 1773868800000
    },
    "power": {
      "voltage": 4.05,
      "percent": 82,
      "charging": false,
      "source": "usb"
    },
    "temperature": 42.5,
    "wifiConnected": false,
    "mqttConnected": false,
    "iotJobsBusy": false,
    "startupBehavior": {
      "armOnBoot": false
    }
  }
}
FieldTypeDescription
timestampnumberCurrent UTC epoch milliseconds
jobobject|nullActive job info, or null if none
recorder.statestringOne of: idle, armed, capturing, writing, holdoff
recorder.captureProgressnumber|null0–100 during capture, null otherwise
recorder.holdoffRemainingMsnumber|nullMilliseconds remaining in hold-off, null otherwise
gps.lat, gps.lonnumberConditional: only present when fix is true
gps.utcEpochMsnumberConditional: only present when GPS time is valid
temperaturenumberMCU temperature in °C (omitted if sensor error)
otaobjectConditional: only present during OTA update
GET /api/v1/power

Battery voltage, charge percentage, and power source.

Response

{
  "power": {
    "voltage": 4.05,
    "percent": 82,
    "charging": false,
    "source": "usb"
  }
}
GET /api/v1/accel

Cached accelerometer axes, activity, and device-owned orientation state. Reading this endpoint does not poll the sensor hardware.

GET /api/v1/temperature

Internal MCU temperature reading.

Response

{
  "temperatureC": 42.50,
  "temperatureF": 108.50
}

Error Response

{
  "error": "sensorReadFailed"
}
GET /api/v1/time

System clock status. Time is synced from GPS satellites.

Response (synced)

{
  "synced": true,
  "time": "2026-03-17T14:30:00"
}

Response (not synced)

{
  "synced": false
}

3. Recorder Control

The event recorder follows a state machine: IDLE → ARMED → CAPTURING → WRITING → HOLDOFF → ARMED. Configuration changes are rejected while a capture is in progress.

GET /api/v1/recorder/status

Current recorder state, capture statistics, and active job context.

Response

{
  "recorder": {
    "state": "armed",
    "eventCount": 12,
    "freeSpace": 9134080,
    "captureProgress": null,
    "holdoffRemainingMs": null,
    "lastTriggerTime": 1773868500000,
    "capturesSinceBoot": 3,
    "autoArm": false,
    "audioEnabled": true,
    "activeJob": {
      "client": "Acme Pipeline",
      "jobName": "Run-042",
      "clientDir": "Acme_Pipeline",
      "jobDir": "Run-042",
      "createdAt": 1773782400,
      "notes": ""
    }
  }
}
GET /api/v1/recorder/config

Current trigger, audio, and speed estimation configuration.

Response

{
  "config": {
    "trigger": {
      "mode": "zeroCrossing",
      "thresholdPercent": 50,
      "holdoffMs": 5000,
      "preDurationMs": 10000,
      "postDurationMs": 10000
    },
    "audio": {
      "enabled": true,
      "sampleRate": 8000
    },
    "speed": {
      "intervalWindow": 5,
      "segmentLengthFt": 40,
      "pigThreshold": 30
    },
    "autoArm": false,
    "activeJob": { ... }
  }
}
FieldTypeDescription
trigger.modestringzeroCrossing, threshold, or combined
trigger.thresholdPercentnumber0–100, trigger level for threshold mode
trigger.holdoffMsnumberMinimum time between captures (ms)
trigger.preDurationMsnumberPre-trigger buffer duration (ms)
trigger.postDurationMsnumberPost-trigger capture duration (ms)
audio.enabledbooleanWhether audio is captured with events
speed.segmentLengthFtnumberPipeline segment length in feet
POST /api/v1/recorder/config

Update recorder configuration. All fields are optional—only provided fields are changed.

Request Body

{
  "triggerMode": "threshold",
  "threshold": 40,
  "holdoffMs": 3000,
  "audioEnabled": true,
  "autoArm": false
}
FieldTypeDescription
triggerModestringzeroCrossing, threshold, or combined
thresholdnumber0–100 percent of full scale
holdoffMsnumberHold-off time in milliseconds
preDurationMsnumberPre-trigger buffer (ms)
postDurationMsnumberPost-trigger capture (ms)
audioEnabledbooleanEnable/disable audio capture
autoArmbooleanArm on boot
intervalWindownumberSpeed estimation window
segmentLengthFtnumberSegment length in feet
pigThresholdnumberSpeed detection threshold
browserTimenumberEpoch seconds—sets device clock if GPS not synced

Response

{"status": "ok"}

Errors

Broadcast: Sends {"event":"configChanged"} to all WebSocket clients.
POST /api/v1/recorder/arm

Arm or disarm the event recorder.

Request Body

{"armed": true}

Response

{
  "recorder": {
    "state": "armed",
    "armed": true
  }
}
Broadcast: Sends {"event":"armState","armed":true} to all WebSocket clients.
POST /api/v1/recorder/grab

Start a device-side manual passage capture using the PigStick's magnetic and audio pre-trigger ring buffers. The normal post-trigger capture, LittleFS storage, job export, and multi-viewer notification pipeline is used.

Response (202 Accepted)

{"accepted":true,"captureOrigin":"manual","audioPartial":false}
Busy response: Returns 409 Conflict while the recorder is capturing, writing, or in holdoff.
GET /api/v1/sensitivity

Returns the active field magnetic sensitivity level, profile name, dirty state, and preset availability.

POST /api/v1/sensitivity

Apply a field sensitivity preset. Rejected while the recorder or update service is busy.

Request Body

{"level":"medium"}
GET /api/v1/calibration/mfl-offsets

Returns value-blind MFL offset calibration state, running flag, and operator message.

POST /api/v1/calibration/mfl-offsets/start

Starts local MFL offset calibration. Rejected while the recorder or update service is busy.

4. Events

Events are recorded pig passages containing magnetic waveform data and optional geophone audio. Each event has a numeric ID and is stored on the device's flash filesystem. The device can store up to 103 events; older events are automatically evicted when storage is full.

GET /api/v1/events

List all event metadata for the active job.

Response

{
  "events": [
    {
      "id": 1,
      "timestamp": 1773868500000,
      "triggerSample": 2000,
      "magSamples": 4000,
      "magRate": 200,
      "audioSamples": 160000,
      "audioRate": 8000,
      "peakMagnetic": 1842,
      "triggerMode": 0,
      "threshold": 50,
      "latitude": 30.267153,
      "longitude": -97.743057,
      "hasAudio": true,
      "magDataSize": 8000,
      "audioDataSize": 40000
    }
  ],
  "total": 1,
  "freeSpace": 9134080
}
FieldTypeDescription
idnumberEvent ID
timestampnumberUTC epoch milliseconds at trigger
triggerSamplenumberSample index of trigger point
magSamplesnumberTotal magnetic samples
magRatenumberMagnetic sample rate (Hz)
audioSamplesnumberTotal audio samples (0 if no audio)
audioRatenumberAudio sample rate (Hz)
peakMagneticnumberPeak magnetic reading (raw ADC)
triggerModenumber0 = zero crossing, 1 = threshold, 2 = combined
hasAudiobooleanWhether audio data is available
magDataSizenumberMagnetic data size in bytes
audioDataSizenumberADPCM audio data size in bytes
GET /api/v1/events/{id}

Single event metadata with additional fields.

Response

{
  "id": 1,
  "timestamp": 1773868500000,
  "triggerSample": 2000,
  "magSamples": 4000,
  "magRate": 200,
  "audioSamples": 160000,
  "audioRate": 8000,
  "peakMagnetic": 1842,
  "triggerMode": 0,
  "threshold": 50,
  "latitude": 30.267153,
  "longitude": -97.743057,
  "hasAudio": true,
  "magDataSize": 8000,
  "audioDataSize": 40000,
  "deviceSerial": "PS2609A0110",
  "agmSiteNumber": "AGM-001",
  "cursorSample": 2150
}
FieldTypeDescription
deviceSerialstringSerial of device that recorded the event
agmSiteNumberstringUser-assigned site identifier
cursorSamplenumber|nullSaved cursor position (sample index), or null

Errors

GET /api/v1/events/{id}/magnetic

Raw magnetic waveform data as a binary array.

Response

Content-Type: application/octet-stream

Errors

GET /api/v1/events/{id}/audio

Geophone audio data encoded as IMA ADPCM.

Response

Content-Type: application/octet-stream

Errors

POST /api/v1/events/{id}/cursor

Set or clear the cursor position on a recorded event.

Request Body

{"sample": 2150}

Set to null to clear: {"sample": null}

Response

{"ok": true}
Broadcast: Sends {"event":"cursorUpdated","id":1,"sample":2150} to all WebSocket clients.
POST /api/v1/events/refresh

Force rebuild of the event manifest from disk. Use after manual file operations.

Response

Same format as GET /api/v1/events.

DELETE /api/v1/events/{id}

Delete a single event.

Response

{"status": "ok"}
Broadcast: Sends {"event":"eventDeleted","id":1} to all WebSocket clients.
DELETE /api/v1/events

Delete all events in the active job.

Response

{"status": "ok"}
Broadcast: Sends {"event":"eventsCleared"} to all WebSocket clients.

5. Job Management

Events are organized under jobs, grouped by client. One job is active at a time—new events are recorded into the active job. Jobs are stored on-device and persist across reboots.

GET /api/v1/jobs

List all jobs grouped by client, with the active job indicated.

Response

{
  "active": "Acme_Pipeline/Run-042",
  "clients": [
    {
      "client": "Acme_Pipeline",
      "jobs": [
        {
          "jobName": "Run-042",
          "operator": "J. Smith",
          "pipelineSegment": "Segment-A",
          "siteNameTemplate": "AGM-{n:3}",
          "notes": "",
          "createdAt": 1773782400,
          "siteNameCounter": 4
        }
      ]
    }
  ]
}
FieldTypeDescription
activestring|nullPath of active job (Client/Job), or null
clients[].clientstringClient directory name
jobs[].jobNamestringJob directory name
jobs[].operatorstringOperator/technician name
jobs[].siteNameTemplatestringAuto-numbering template (e.g. AGM-{n:3})
jobs[].createdAtnumberEpoch seconds when job was created
jobs[].siteNameCounternumberNext site number for auto-generation
POST /api/v1/jobs

Create a new job. Becomes the active job by default.

Request Body

{
  "client": "Acme Pipeline",
  "jobName": "Run-043",
  "operator": "J. Smith",
  "pipelineSegment": "Segment-B",
  "siteNameTemplate": "AGM-{n:3}",
  "notes": "",
  "setActive": true
}
FieldTypeRequiredDescription
clientstringYesClient or company name
jobNamestringYesJob identifier
operatorstringNoOperator name
pipelineSegmentstringNoPipeline segment
siteNameTemplatestringNoSite number template
notesstringNoJob notes
siteNameCounternumberNoStarting site number
setActivebooleanNoActivate immediately (default: true)

Response

{
  "status": "ok",
  "clientDir": "Acme_Pipeline",
  "jobDir": "Run-043"
}
PUT /api/v1/jobs/{client}/{job}

Update job metadata. All fields are optional—only provided fields are changed.

Request Body

{
  "operator": "A. Jones",
  "notes": "Updated notes"
}

Response

{"status": "ok"}

Errors

POST /api/v1/jobs/{client}/{job}/activate

Switch the active job. New events will be recorded into this job.

Response

{"status": "ok"}

Errors

DELETE /api/v1/jobs/{client}/{job}

Delete a job and all its events. Cannot delete the currently active job.

Response

{"status": "ok"}

Errors

Destructive: All events within the job are permanently deleted.
GET /api/v1/jobs/{client}/{job}/export

Stream the entire job as a ZIP archive.

Response

Content-Type: application/zip

The archive contains:

Errors

6. GPS

GET /api/v1/gps

GPS receiver status, movement-aware duty cycling, EASY ephemeris, fix quality, and position (when a fix is available).

Response (with fix)

{
  "gps": {
    "timeSynced": true,
    "fix": true,
    "fixQuality": 1,
    "satellites": 7,
    "hdop": 1.2,
    "duty": "rest",
    "dutyEnabled": true,
    "fixAgeS": 91,
    "dutyStatus": {
      "state": "rest",
      "enabled": true,
      "movementAware": true,
      "relocating": false,
      "orientationKnown": true,
      "upright": true,
      "quietForS": 184,
      "nextWakeS": 209,
      "qualityQualified": true,
      "qualityFallback": false,
      "qualityStreak": 3,
      "qualityRequired": 3,
      "minSatellites": 5,
      "maxHdop": 3.0,
      "quietRequiredS": 15,
      "navSeen": true,
      "lastNavAgeS": 1,
      "recoveryPhase": "idle",
      "wakeReason": "scheduled",
      "wakeAttempts": 3,
      "hotRestarts": 0,
      "faultCount": 0,
      "fallbackContinuous": false
    },
    "easy": {
      "state": "enabled",
      "known": true,
      "enabled": true,
      "predictionDays": 3
    },
    "lat": 30.267153,
    "lon": -97.743057,
    "altitude": 149.20,
    "speedKnots": 0.04,
    "speedMps": 0.0206,
    "utcEpochMs": 1773868800000
  }
}

Response (no fix)

{
  "gps": {
    "timeSynced": false,
    "fix": false,
    "fixQuality": 0,
    "satellites": 0,
    "hdop": 0.0,
    "duty": "acquire",
    "dutyEnabled": true
  }
}
FieldTypeDescription
timeSyncedbooleanWhether system clock is GPS-synced
fixbooleanWhether GPS has a position fix
fixQualitynumber0 = no fix, 1 = GPS, 2 = DGPS
satellitesnumberSatellites in use
hdopnumberReceiver-reported horizontal dilution of precision
dutystringoff, acquire, settle, rest, refix, relocate, or fault
dutyEnabledbooleanWhether standby duty cycling is enabled
dutyStatusobjectMovement/orientation, next wake, quality gate, and receiver recovery state. Normal gate: 15 s without movement, 3 fresh RMC/GGA pairs, 5+ satellites, HDOP ≤3.0. Recovery reports navigation age, wake reason/attempts, hot restarts, faults, and fail-open continuous mode. Orientation is informational and does not block standby.
easyobjectL96 EASY enable/check state and 0-3 day prediction extension
fixAgeSnumberSeconds since the last valid RMC position
latnumberLatitude (conditional: fix = true)
lonnumberLongitude (conditional: fix = true)
altitudenumberAltitude in meters (conditional)
speedKnotsnumberSpeed in knots (conditional)
speedMpsnumberSpeed in m/s (conditional)
utcEpochMsnumberGPS UTC time in ms (conditional: time valid)

7. WiFi

GET /api/v1/wifi

WiFi mode, coexist state, access point info, and station connection status. internet is true when the device has proven connectivity (cloud session or completed time sync); cloud is true while the AWS IoT MQTT session is live. mdnsUrl is the address where this interface is reachable from either network.

Response (station connected)

{
  "wifi": {
    "mode": "AP+STA",
    "coexist": true,
    "internet": true,
    "cloud": true,
    "hostname": "pigstick-2001",
    "mdnsUrl": "http://pigstick-2001.local",
    "ap": {
      "ssid": "PigStick-2001",
      "channel": 1,
      "clients": 1
    },
    "sta": {
      "connected": true,
      "enabled": true,
      "pausedForAp": false,
      "reconnectHeld": false,
      "subnetConflict": false,
      "ssid": "FieldOffice",
      "rssi": -52,
      "channel": 1,
      "ip": "192.168.1.105"
    }
  }
}

When the station is disconnected, sta instead carries lastDisconnectReason (e.g. "NO_AP_FOUND", "AUTH_FAIL") when one is known. subnetConflict is true when the uplink lease collides with the access point's own 10.0.0.0/24 subnet. reconnectHeld means the station link is being kept alive while reconnect/scan activity is held for an access-point client.

POST /api/v1/wifi/coexist

Enable or disable Starlink / Local Network Mode (simultaneous AP + station operation with the interface and cloud served over the uplink network). Persisted in NVS; takes effect on the next restart. Requires at least one user-saved network to become active.

Request Body

{"enabled": true}

Response

{"success": true, "coexist": true, "rebootRequired": true}
GET /api/v1/wifi/scan

Scan for available WiFi networks. This is a blocking operation that takes a few seconds.

Response

{
  "networks": [
    {
      "ssid": "FieldOffice",
      "rssi": -52,
      "secure": true,
      "channel": 1,
      "bssid": "c0:6f:98:9b:58:c6"
    },
    {
      "ssid": "OpenNetwork",
      "rssi": -78,
      "secure": false,
      "channel": 11,
      "bssid": "aa:bb:cc:dd:ee:ff"
    }
  ]
}
GET /api/v1/wifi/networks

List saved WiFi networks. Passwords are not returned. factory:true marks the built-in factory/bench network, which the interface hides outside admin mode. channel/bssid are the cached identity from the last successful connection (used for fast reconnects and access-point channel pinning), 0/null when unknown.

Response

{
  "networks": [
    {
      "ssid": "FieldOffice",
      "priority": 0,
      "channel": 1,
      "factory": false,
      "bssid": "c0:6f:98:9b:58:c6"
    }
  ]
}
POST /api/v1/wifi/networks

Save a WiFi network. The device stores up to 5 networks.

Request Body

{
  "ssid": "FieldOffice-5G",
  "password": "secretpass"
}

Response

{"success": true}

Errors

DELETE /api/v1/wifi/networks

Delete a saved WiFi network.

Request Body

{"ssid": "FieldOffice-5G"}

Response

{"success": true}

Errors

8. System

GET /api/v1/system/info

Firmware version and storage statistics.

Response

{
  "system": {
    "firmware": "2.2.72",
    "storage": {
      "total": 10379264,
      "used": 1245184,
      "free": 9134080
    },
    "uptime": 3621
  }
}
GET /api/v1/system/ws-diagnostics

Returns bounded, in-memory connection ownership evidence: HTTP and stream registry counts, active viewer UUIDs and fd generations, close-cause counters, and the 16 most recent lifecycle events. The history resets when the device restarts.

Add the same page-scoped ?viewer=<uuid> value to the audio, magnetic, and control WebSocket URLs so one viewer's sockets can be correlated.

POST /api/v1/system/restart

Restart the device. The restart occurs after a 1-second delay to allow the response to be sent.

Response

{
  "restart": {
    "scheduled": true,
    "delayMs": 1000
  }
}
Warning: The device will be unreachable for several seconds during restart. Reconnect to the WiFi network after the device reboots.
POST /api/v1/service/check-updates

Check for software updates. Behavior depends on connectivity:

Response (in-place check)

{"service": {"scheduled": true, "mode": "checkUpdates", "rebooting": false, "online": true}}

Response (restart flow)

{"service": {"scheduled": true, "mode": "checkUpdates", "rebooting": true, "delayMs": 1500}}

Errors

GET /api/v1/service/status

Service-mode state and the most recent update-check result. updateHint.status is checking, available, none, skipped, or error; the interface polls this after an in-place check.

Response

{
  "service": {
    "active": false,
    "request": 0,
    "lastStatus": "complete",
    "lastDetail": "Checking for updates over WiFi network",
    "updateHint": {
      "status": "none",
      "detail": "No update indicated by fleet manifest",
      "available": false,
      "firmware": false,
      "app": false,
      "firmwareVersion": "2.2.72",
      "appVersion": "2.2.95",
      "checkedBootMs": 108721
    }
  }
}
GET /api/v1/ota/status

Lightweight OTA phase and byte progress for polling while install activity temporarily stops live streams. The active flag remains true during post-reboot validation.

Response

{"ota":{"phase":"downloading","component":"firmware","downloaded":512000,"total":1024000,"percent":50,"active":true}}
POST /api/v1/ota/accept

Accept an update job currently advertised by updateHint. Stale or arbitrary job IDs are rejected.

Request Body

{"jobId":"pigstick-update-2-2-72-20260726"}
POST /api/v1/provision

Run AWS IoT fleet provisioning with a valid PigStick serial number. Intended for manufacturing and authorized service workflows.

POST /api/v1/setserial

Set the device serial without cloud provisioning. A restart is required before the new identity and WiFi name are fully applied.

POST /api/v1/app-version

Report the connected browser app version for device status and cloud shadow reporting.

Request Body

{"version":"2.2.95"}

9. WebSocket Streams

The PigStick provides audio and magnetic sensor streams plus a shared-state control channel. Audio and magnetic allow two clients each; control allows four, subject to the global HTTP socket budget.

WS /api/v1/ws/audio

Real-time geophone audio stream.

Binary Frames (device → client)

Text Commands (client → device)

FieldTypeDescription
freqnumberTest tone frequency (Hz)
ampnumberTest tone amplitude (0–100%)
preampnumberHardware pre-amp gain multiplier
armednumber1.0 = arm, 0.0 = disarm
thresholdnumberTrigger threshold (0–100%)

Send as JSON text frame, e.g.: {"preamp": 4.0}

WS /api/v1/ws/magnetic

Real-time magnetic sensor stream with JSON event notifications.

Binary Frames (device → client)

Format 8 frames contain a 12-byte v2 shared-stream header, aligned MFL and ELF-envelope int16 arrays, and a final format byte.

JSON Event Frames (device → client)

Text frames carry real-time event notifications:

EventDescription
triggerTrigger detected: {"event":"trigger","peak":1842,"time":1773868500000}
eventWrittenPassage saved: {"event":"eventWritten","id":5}
armStateArm state changed: {"event":"armState","armed":true}
configChangedRecorder config updated
cursorUpdatedCursor set on event: {"event":"cursorUpdated","id":1,"sample":2150}
eventDeletedEvent removed: {"event":"eventDeleted","id":1}
eventsClearedAll events deleted
jobCreatedNew job: {"event":"jobCreated","client":"...","job":"..."}
jobActivatedActive job switched
jobUpdatedJob metadata changed
jobDeletedJob removed
WS /api/v1/ws/control

Device-authoritative shared settings with state broadcasts and acknowledged get/set commands.

Examples

{"type":"set","id":102,"set":{"preamp":64,"threshold":55}}
{"type":"ack","id":102,"ok":true,"rev":43,"state":{"preamp":64,"filterHp":0,"filterLp":500,"armed":true,"threshold":55,"magMode":"both"}}

10. Error Handling

HTTP Status Codes

CodeMeaning
200Success
400Bad request (invalid input, missing fields)
404Resource not found (event, job, file)
409Conflict (e.g., config change during capture)
413Payload too large
500Internal server error

Error Response Format

All errors return a JSON object with an error field:

{
  "error": "Description of what went wrong"
}

Common Errors

ErrorEndpointCause
Event not foundEventsInvalid event ID
Job not foundJobsInvalid client/job path
Config rejected (capturing in progress)Recorder configCannot change config while capturing
Invalid triggerModeRecorder configMust be zeroCrossing, threshold, or combined
Maximum networks reachedWiFiAlready 5 saved networks
No audio dataEvent audioEvent was captured without audio