bmcloud

REST API

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>.

Accounts and sessions

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

Devices - owner side

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

Firmware and flash jobs - owner side

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

Programmer side - device token

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

The flash loop

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