All bodies are JSON unless noted otherwise, timestamps are Unix seconds,
and every error is { "error": "message" } with a matching HTTP status.
User requests authenticate with the bmcloud_session cookie set by
POST /login or with Authorization: Bearer <token>
(a login session or the personal API token from POST /api-token).
Sessions live for 7 days, the API token does not expire.
Device requests authenticate with the token shown once at creation:
Authorization: Bearer <device_token>.
POST/register
Create an account. The password needs 8+ ASCII characters with a digit, a lowercase letter, an uppercase letter and a punctuation mark.
{ "email": "anna@mail.ru", "password": "SuperSecret1!" }
201 → { "id": 1, "email": "anna@mail.ru" }
400 invalid email or weak password 409 email is already taken
POST/login
Exchange email and password for a session token; also sets the HttpOnly session cookie.
{ "email": "anna@mail.ru", "password": "SuperSecret1!" }
200 → { "user": { "id": 1, "email": "anna@mail.ru" }, "token": "5f1c…" }
+ Set-Cookie: bmcloud_session=5f1c…; HttpOnly; Max-Age=604800
401 invalid email or password
POST/logout user
Delete the current session and expire the cookie.
204 401 missing or invalid token
GET/me user
200 → { "id": 1, "email": "anna@mail.ru" }
POST/api-token user
Generate the personal API token for scripts and CI - it never expires. Shown only this once; generating again replaces the old token, which stops working immediately.
201 → { "token": "bm_9f3a…" }
GET /me with "Authorization: Bearer bm_9f3a…" → 200
GET/devices user
All your devices with their last reported state (offline, online, error, busy). The job poll doubles as a presence ping - a device not heard from for 30 seconds is shown offline.
200 → [ { "id": 1, "name": "blackmagic-c5", "state": "online", "updated_at": 1760000000 } ]
POST/devices user
Create a device. The token is shown only this once.
{ "name": "blackmagic-c5" }
201 → { "device": { "id": 1, "name": "blackmagic-c5", "state": "offline", "updated_at": 1760000000 },
"token": "7b0e…" }
400 invalid device name
PATCH/device/{id} user
Rename your device.
{ "name": "bench-probe" }
204 400 invalid device name 404 unknown or foreign device
DELETE/device/{id} user
Delete the device together with its state and jobs (foreign keys cascade).
204 404 unknown or foreign device
GET/device/{id}/state user
200 → { "id": 1, "name": "blackmagic-c5", "state": "busy", "updated_at": 1760000000 }
404 unknown or foreign device
GET/jobs user
Your flash jobs, newest first. Status moves pending → flashing → done | failed; a job stuck in flashing for 10 minutes without a report is failed on listing.
200 → [ { "job_id": 3, "device_id": 1, "fw_name": "app.bin", "status": "done", "error": null } ]
POST/device/{id}/flash?name=app.bin user
Upload a firmware and queue a flash job for the device in one request.
The body is the raw file (Content-Type: application/octet-stream),
the name goes in the query string. Limit: 8 MB (10 MB behind nginx).
POST /device/1/flash?name=app.bin
Content-Type: application/octet-stream
<raw firmware bytes>
201 → { "job_id": 1, "firmware_id": 1, "name": "app.bin", "size": 1024 }
400 invalid firmware name 404 unknown or foreign device
Mind the naming: /device/{id}/… above is the owner addressing a device by id,
while the endpoints below identify themselves by token and take no id.
POST/device/state device
Report the device state: offline, online, error or busy.
{ "state": "busy" }
204 400 invalid device state
GET/device/job device
Claim the oldest pending job — it atomically moves to flashing, so a second poll will not see it. Poll every few seconds; 404 simply means no work. Each poll also marks the device online (a reported 'error' stays sticky) - keep polling during long flashes, or the device will show offline after 30 seconds. A claimed job that gets no report within 10 minutes is assumed dead and marked failed — there are no automatic retries, re-flash manually if needed.
200 → { "job_id": 1, "firmware_id": 1 }
404 no pending jobs
GET/device/firmware/{firmware_id} device
Download the firmware bytes (octet-stream), only if the firmware belongs to the device's owner. Separate from the claim so a dropped download can be retried.
200 → raw bytes 404 unknown or foreign firmware
POST/device/job/{job_id} device
Report the result of a claimed job. Only a job currently in flashing accepts a report.
{ "status": "done" }
{ "status": "failed", "error": "flash write timeout" }
204 400 invalid job status 404 foreign, unknown or already reported job
owner POST /device/{id}/flash?name=app.bin job → pending
device GET /device/job pending → flashing, {job_id, firmware_id}
device GET /device/firmware/{firmware_id} raw bytes, retried on network failure
device POST /device/job/{job_id} flashing → done | failed