HTTP API

Everything the web app does — submitting build/test jobs and driving your BenchPod devices (power, capture, signal generation, flashing, the waveform library) — is a plain HTTP API you can call from scripts or CI. All paths below are under your server's /api base (e.g. https://www.embeddedci.com/api).

Authentication

Create a personal API key and send it as an Authorization: ApiKey header. A key authenticates as you, scoped to your organization — it is not limited to job submission; it works across the whole API, including the BenchPod endpoints.

# Create a key on the API keys page, then send it on every request:
Authorization: ApiKey eci_<kid>_<secret>

Keys carry scopes, enforced per endpoint: jobs:submit for job submission, benchpod:control for the BenchPod endpoints (devices, command, capture, waveforms, DAC, and the /auth/token exchange), and emi:analyze for the EMI Analyzer (board upload and analysis runs — see EMI analysis in CI). Pick scopes when you create a key, or edit them later, on the API keys page. A key missing the required scope gets a 403. (Scopes constrain API keys only — interactive web sessions are unrestricted.)

Direct device access (REST)

These accept your API key directly — no exchange step. This is the same surface the BenchPod page uses. You address a device by its id (from GET /benchpod/devices).

MethodPathPurpose
GET/benchpod/devicesList your devices + online/capability state
PATCH/benchpod/devices/{id}Rename a device
DELETE/benchpod/devices/{id}Remove a device
POST/benchpod/devices/{id}/commandSingle command: ping, status, target_power, la, dac_set, generate, dac_stop, …
GET/benchpod/devices/{id}/flashable-artifactsList artifacts that can be flashed
POST/benchpod/devices/{id}/flashFlash firmware over the pod's CMSIS-DAP probe
GET/benchpod/devices/{id}/firmwareThe pod's own running firmware version
POST/benchpod/devices/{id}/otaStart a pod firmware update (returns 202; the update runs in the background)
GET/benchpod/devices/{id}/otaPoll the running firmware update: phase, percent, bytes
POST/benchpod/devices/{id}/ota/abortDrop a staged image and release the pod
POST/scope/captures/startStart an ADC (scope) capture
POST/scope/measure/startStimulus + capture (DAC drive while sampling ADC)
POST/analyzer/captures/startStart an I²C logic-analyzer capture
POST/la/captures/startStart a raw multi-channel LA capture
POST/capture/startStart a simultaneous ADC + LA capture (one trigger)
GET/capture/{id}Fetch a capture's raw frames for replay (ADC + LA + I²C)
GET / POST/benchpod/waveformsList / save DAC waveforms (the library)
GET / DELETE/benchpod/waveforms/{id}Fetch / delete a saved waveform
POST/dac/replay/startReplay a saved/inline waveform out the DAC (AC-DAC boards)
WS/ws/capture · /benchpod/devices/{id}/uart/wsLive streams (unified capture: ADC + LA + I²C); auth via ?token=eci_… (your key) or a JWT
# List your BenchPod devices
curl -H "Authorization: ApiKey $ECI_KEY" \
     https://www.embeddedci.com/api/benchpod/devices

# Power the target on (efuse 1 = internal 5V) via the generic command channel
curl -H "Authorization: ApiKey $ECI_KEY" -H 'Content-Type: application/json' \
     -d '{"command":{"cmd":"target_power","efuse":1,"state":1}}' \
     https://www.embeddedci.com/api/benchpod/devices/<device_id>/command

# Start an ADC (scope) capture, then poll the result
curl -H "Authorization: ApiKey $ECI_KEY" -H 'Content-Type: application/json' \
     -d '{"device_id":"<id>","samples":1024,"sample_rate_mhz":1}' \
     https://www.embeddedci.com/api/scope/captures/start
curl -H "Authorization: ApiKey $ECI_KEY" \
     https://www.embeddedci.com/api/capture/<capture_id>

For the live WebSocket streams, pass the key in the ?token= query parameter (an eci_ token is recognised as an API key):

# Live capture/terminal streams take the key as a ?token= query param
# (eci_ tokens are recognised as API keys):
wss://www.embeddedci.com/api/ws/capture?device_id=<id>&token=$ECI_KEY

Capabilities gate some features per board: e.g. generate/replay require an AC waveform DAC and are rejected on DC-only boards.

Drive devices by name (cloud) — token exchange

The cloud endpoints address a device by name and add a one-consumer-at-a-time lease and a raw byte-tunnel (for flashing/captures from CI). They take a short-lived session token, not your static key — so a long-lived secret never ends up in a WebSocket URL or a CI log. Mint one from your API key with POST /auth/token (the API-key analogue of the GitHub Actions OIDC exchange).

# 1) Exchange your API key for a short-lived cloud-session token.
#    Omit the body for any device in your org, or scope it down:
curl -H "Authorization: ApiKey $ECI_KEY" -H 'Content-Type: application/json' \
     -d '{"devices":["pod-a"],"ttl_seconds":3600}' \
     https://www.embeddedci.com/api/auth/token
# -> { "token": "<jwt>", "token_type": "bearer", "expires_at": "...", "expires_in": 3600 }

# 2) Drive a device BY NAME with the minted token (Bearer):
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
     -H 'X-Benchpod-Device: pod-a' \
     -d '{"cmd":"status"}' \
     https://www.embeddedci.com/api/cloud/devices/command

# WebSocket byte-tunnel (flash / capture) — token in the query string is a
# short-lived JWT, never your static key:
wss://www.embeddedci.com/api/cloud/devices/ws?device=pod-a&token=$TOKEN
MethodPathPurpose
POST/auth/tokenAPI key → short-lived cloud_session JWT (optional devices[] + ttl_seconds)
POST/cloud/devices/commandOne command on a device by name (X-Benchpod-Device)
WS/cloud/devices/wsRaw byte-tunnel for flashing / captures
POST/cloud/devices/lease · /lease/renew · /lease/releaseAcquire / renew / release the device lease

Higher-level clients

Most users don't call these endpoints by hand. The pytest SDK, CLI, and MCP server wrap them (auth, leasing, tunnels, chunk reassembly) — reach for those first; drop to the raw API for custom integrations. The Python SDK performs the /auth/token exchange for you: set BENCHPOD_API_KEY (or pass api_key=) and connect to embeddedci:<device-name>.