Skip to content

Developer API

CRUD endpoints for humans and CI pipelines. All endpoints accept either a personal API token (PAT) or a project-scoped token (constrained to that project).

Base path: /api/v1/ (project resources under /api/v1/projects/, personal tokens under /api/v1/me/)

Conventions

  • All requests/responses are JSON unless noted (artifact upload uses multipart/form-data).
  • Timestamps are ISO 8601 UTC.
  • IDs are UUIDs unless explicitly slugged (project.slug).
  • List endpoints return plain JSON arrays (no pagination wrapper).

Resource map

Resource Endpoint Methods
Projects /projects/ GET, POST
/projects/{id}/ GET
Personal tokens /me/tokens/ GET, POST
/me/tokens/{tid}/ DELETE
Project tokens /projects/{id}/tokens/ GET, POST
/projects/{id}/tokens/{tid}/ DELETE
Signing keys /projects/{id}/signing-keys/ GET, POST
/projects/{id}/signing-keys/{key_id}/ DELETE
Devices /projects/{id}/devices/ GET
/projects/{id}/devices/{did}/ GET, PATCH
Groups /projects/{id}/groups/ GET, POST
/projects/{id}/groups/{gid}/members/ POST
/projects/{id}/groups/{gid}/members/{did}/ DELETE
Channels /projects/{id}/channels/ GET, POST
Artifacts /projects/{id}/artifacts/ GET, POST
Deployments /projects/{id}/deployments/ GET, POST
/projects/{id}/deployments/{did}/ GET
/projects/{id}/deployments/{did}/transition/ POST
/projects/{id}/deployments/{did}/states/ GET

Devices are read-only via API. They are created automatically the first time a device calls /api/v1/ota/check/. Use PATCH to update the active flag or metadata. There is no delete endpoint; deactivate instead.

Groups and channels are collection-only. There are no individual GET /groups/{id}/ or GET /channels/{id}/ endpoints; fetch the list and filter client-side.

Selected endpoints

POST /projects/{id}/artifacts/

Uploads a firmware artifact. multipart/form-data only.

curl -X POST https://simpleota.com/api/v1/projects/${PID}/artifacts/ \
  -H "Authorization: Bearer ${TOKEN}" \
  -F "manifest=$(jq -c . manifest.json)" \
  -F "[email protected]"

Form fields:

Field Type Notes
manifest string (JSON) Validated against the manifest schema.
binary file Streamed to object storage; SHA-256 computed.

Response 201:

{
  "id":               "8e1f…",
  "build_number":     24,
  "version_label":    "1.4.2",
  "framework":        "arduino",
  "chip_family":      "esp32",
  "board_id":         "esp32dev",
  "checksum_sha256":  "8f3c…",
  "file_size":        1287456,
  "security_mode":    "basic",
  "archived":         false,
  "created_at":       "2026-04-25T17:21:00Z"
}

Failure modes:

  • 400 manifest_invalid: manifest doesn't validate.
  • 400 monthly artifact upload limit reached for current plan; upgrade your plan to upload more. (plan cap reached)
  • 413: binary exceeds your plan's size cap (default 8 MiB).

POST /projects/{id}/deployments/

{
  "artifact_id":          "8e1f…",
  "target_group_ids":     ["<group-uuid>"],
  "target_channel_ids":   ["<channel-uuid>"],
  "rollout_percentage":   5,
  "rollout_strategy":     "immediate",
  "notes":                ""
}

All fields except artifact_id are optional. Deployments are created in draft state; call /transition/ with {"action": "start"} to activate. Returns the new deployment object.

GET /projects/{id}/deployments/{did}/states/

Returns per-device rollout state records for the deployment; one entry per device that has been offered the update or reported a result:

[
  {
    "device_id":    "d7a2…",
    "status":       "validated",
    "updated_at":   "2026-04-25T18:05:11Z"
  }
]

status values: pending, offered, download_started, downloaded, flashed, validated, reboot, confirmed, failed, rolled_back. Use this to monitor rollout health without polling individual devices. confirmed is the terminal success state; failed and rolled_back are the terminal failure states.

POST /projects/{id}/deployments/{did}/transition/

Transitions a deployment's state. Payload:

{ "action": "start" }

Valid action values: start, pause, resume, cancel, complete, ramp (requires rollout_percentage). Invalid transitions return 400.

~~POST /projects/{id}/deployments/{did}/pause/~~

Removed. Use POST /transition/ with {"action": "pause"} instead.

Errors

All endpoints return errors in a uniform envelope:

{
  "detail":     "monthly artifact upload limit reached…",
  "code":       "over_artifact_limit",
  "request_id": "req_01HX…"
}

See Errors.