uclogserver
Log in

Firmware

Upload firmware builds with their release notes, decide which devices are offered them, and let devices ask for newer builds. Each build is identified by its firmware name, hardware name and version, version and type (AFI, MFI or EFI). Most of these come from the what string embedded in the binary: a NUL-terminated string starting with @(#), such as

@(#)1.8.0, demo, example-hw@2.0.0, 2026-02-01T10:00:00Z, AFI

Shorter what strings are accepted too (1.4.1-10-g30d0049, 2025-07-14T22:25:07Z, AFI); you then supply the missing fields in the upload URL or as form fields. See Concepts for the formats and version ordering.

Builds are write-once. A build's binary and release notes never change and builds are never deleted. Uploading the identical binary and notes again is harmless (200); a different binary or notes for the same identity is refused (409) — release a new version instead. X.Y.Z and X.Y.Z-0-gHASH are the same version, so they are the same identity: uploading 0.3.2 when 0.3.2-0-gfac16cc exists is a 409 naming the existing build. What you can change are its release settings: its tags, whether it is a general release, and its status. Withdrawing a build (status: withdrawn) stops it being offered and its download links stop working; its identity stays reserved, so the same version can never be uploaded again (409), even with identical bytes.

A new build is staged: no device is offered it until you tag it for a group of devices or mark it as a general release. Devices find builds with the update check.

The firmware object

Field Type Description
id string UUID
fw_name, hw_name, hw_version string product and hardware the build is for
fw_version string version exactly as in the what string, e.g. v1.5.1.2
version string canonical version used for ordering and identity, e.g. 1.5.1.2
fw_type string AFI (application), MFI (manufacturing) or EFI (engineering)
build_date timestamp or null from the what string
what_string, what_format string the what string used and its detected format
fields_from_request array fields that came from the request instead of the what string
size_bytes, sha256 integer, string the binary
notes_size, notes_sha256, notes_filename the release notes
tag_filter_disabled boolean true = general release: offered to every device
tags array devices sharing one of these tags are offered the build
status string active or withdrawn (never offered; download links stop working)
uploaded_at, uploaded_by, updated_at audit fields
download_count, notes_download_count, last_accessed_at usage

Check for updates

GET /firmware/{domain}/{hw_name}/{hw_version}/{fw_name}/{fw_version}[/{hwid}]

No authentication

Devices ask "is there anything newer than what I run?". No authentication is needed: the account is named by its domain in the path. The answer lists newer builds, oldest first, each with download links for the binary and the release notes that are valid for 5 minutes. Append the device's HW id as the optional last path segment to also get builds targeted at the device's tags (percent-encode reserved characters, e.g. lab/7 → lab%2F7). In the sandbox, a1b2c3d4 is tagged beta, so it also gets the beta build 1.7.0-rc.1; leave the HW id empty in Try it to see general releases only.

How builds are chosen

  1. Take the account's active firmware with the same firmware name, hardware name and hardware version, and an allowed type (default AFI).
  2. Keep builds whose version is strictly later than the device's version (see version ordering): 1.4.1-10-g30d0049 is later than 1.4.1, and 1.5.1.2 is later than 1.5.1.
  3. Keep a build if it is a general release, or if the device's HW id (the optional last path segment) belongs to a registered device that shares at least one tag with it. The result is the union of the two.
  4. Sort by version ascending, then build date, then type (AFI, MFI, EFI).

A HW id that isn't registered, or belongs to a withdrawn device, is treated exactly like no HW id, so the check can't be used to discover device ids.

Response format. The answer is always a JSON array. Errors are one line of plain text by default, or application/problem+json if the request has Accept: application/json. Responses carry Cache-Control: no-store, and cross-origin browser requests are allowed.

The update object — each array item:

Key Description
id firmware id
version {hw_name}/{hw_version}/{fw_name}/{version}, with the full canonical version
fw-version the version exactly as in the what string (e.g. v1.5.1.2)
fw-type AFI, MFI or EFI
build-date RFC 3339 build date, or null
size binary size in bytes
sha256 hex SHA-256 of the binary — verify it after downloading
firmware-url 5-minute link to the binary
release-notes-url 5-minute link to the release notes
expires when the two links stop working
general-release true if offered to every device, false if offered through a tag

Device checklist: send the full running version (git describe output, not just X.Y.Z); append the HW id if the device should receive tag-targeted builds; download promptly; verify sha256 before flashing; poll at most hourly or once per boot and honour Retry-After on 429.

An unknown firmware or hardware name is not an error; it just returns [].

Path parameters

domain string required

Your account's domain.

hw_name string required

Hardware name (case-insensitive).

hw_version string required

Hardware version (a leading v is ignored).

fw_name string required

Firmware name (case-insensitive).

fw_version string required

The version the device runs now, in full, e.g. 1.6.0-10-g30d0049. A v prefix and four-part versions are fine.

hwid string

Optional: the device's HW id (1–64 printable characters, case-sensitive, percent-encoded). Adds builds that share one of the device's tags.

Query parameters

type string

AFI (default), all, or a list such as AFI,MFI.

latest boolean

true: only the newest matching build.

Headers

Accept string

application/json for application/problem+json errors; otherwise errors are plain text.

GET /firmware/{domain}/{hw_name}/{hw_version}/{fw_name}/{fw_version}[/{hwid}]
curl "https://fw.unitcircle.ca/firmware/sandbox.example/example-hw/2.0.0/demo/1.6.0-10-g30d0049/a1b2c3d4"
http GET "https://fw.unitcircle.ca/firmware/sandbox.example/example-hw/2.0.0/demo/1.6.0-10-g30d0049/a1b2c3d4"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware/sandbox.example/example-hw/2.0.0/demo/1.6.0-10-g30d0049/a1b2c3d4",
)
print(r.status_code, r.json())
Response
[
  {
    "build-date": "2026-01-10T09:00:00Z",
    "expires": "2026-09-26T01:06:49Z",
    "firmware-url": "https://fw.unitcircle.ca/dl/AaDZvRgieMa7-iOfg8tIIwEBoNs4tA5zEZyOsJKJRQe4AAAAAGq3GqlIDKRVL0AHiw.7eh3rKhfdaJTLCCKMhJuR3m9EjQ",
    "fw-type": "AFI",
    "fw-version": "1.7.0-rc.1",
    "general-release": false,
    "id": "01a0db38-b40e-7311-9c8e-b092894507b8",
    "release-notes-url": "https://fw.unitcircle.ca/dl/AaDZvRgieMa7-iOfg8tIIwIBoNs4tA5zEZyOsJKJRQe4AAAAAGq3GqkpcjZQolgmPw.Gg99hBIx9LPo1t2GheHclynE4b8",
    "sha256": "053099e9651d6efc083f595967ee0ff684c23c4b6831f8f9481ba42e6f36d9a4",
    "size": 4096,
    "version": "example-hw/2.0.0/demo/1.7.0-rc.1"
  }
]

Upload firmware

PUT /firmware[/{hw_name}/{hw_version}/{fw_name}]

Bearer token · scope fw:write

Upload a binary and its release notes as multipart/form-data. The three path segments are optional: use PUT /firmware when the what string contains every field (version, firmware name, hw-name@hw-version, date, type), or PUT /firmware/{hw_name}/{hw_version}/{fw_name} for binaries whose what string lacks the product fields (e.g. 1.9.0, 2026-03-01T10:00:00Z, AFI). Fields present in both must match. Missing fields can also be given as form fields or query parameters.

The response format follows the Accept header: without it the answer is 200 with the body OK and errors are one line of plain text; with Accept: application/json you get the stored firmware object (201 for a new build, 200 for an identical re-upload) and application/problem+json errors. Builds are write-once: the same identity with a different binary or notes, or the identity of a withdrawn build, is a 409.

The example needs no files: its "binary" is just a what string, which is enough for the service to read the version, names and type (a what string may end at a NUL byte or at the end of the file). A real upload sends your build output, e.g. -F firmware=@build/demo.bin with cURL. Running the example again returns 200 — the same build, unchanged. Try it lets you edit the what string (bump the version to upload a new build) or fill in the three optional path segments for a what string without product names, such as @(#)1.9.4, 2026-09-26T12:00:00Z, AFI.

Other errors: several different what strings without what_index → 422; no what string at all → 422; release notes not UTF-8 → 422; missing firmware or notes part → 400; parts too large → 413; maintenance mode → 503 with Retry-After.

Path parameters

hw_name string

Optional: hardware product name, for what strings without it.

hw_version string

Optional: hardware version (a leading v is ignored).

fw_name string

Optional: firmware product name.

Query parameters

what_index integer

Which what string to use when the binary has several different ones (see Parse a binary).

Headers

Accept string

application/json for a JSON response; otherwise plain text.

Form parts (multipart/form-data)

firmware file required

The binary (max 16 MiB; 1 MiB in the sandbox). Must contain a what string.

notes file required

Release notes, UTF-8 (max 1 MiB). A .md file name marks them as Markdown.

fw_name, hw_name, hw_version, fw_type, fw_version, build_date string

Fields missing from the what string (also accepted as query parameters).

general_release boolean

true: publish immediately to every device.

PUT /firmware[/{hw_name}/{hw_version}/{fw_name}]
curl -X PUT "https://fw.unitcircle.ca/firmware" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA" \
  -H "Accept: application/json" \
  --form-string 'firmware=@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI' \
  --form-string 'notes=# demo 1.9.3

- Faster start-up.
'
http --multipart -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA PUT "https://fw.unitcircle.ca/firmware" \
  'Accept:application/json' \
  'firmware=\@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI' \
  'notes=# demo 1.9.3

- Faster start-up.
'
import requests

r = requests.put(
    "https://fw.unitcircle.ca/firmware",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA", "Accept": "application/json"},
    files={"firmware": ("demo-1.9.3.bin", "@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI"), "notes": ("README.md", "# demo 1.9.3\n\n- Faster start-up.\n")},
)
print(r.status_code, r.json())
Response
{
  "id": "01a0ded0-f69d-77e7-a321-002fef69a4cf",
  "fw_name": "demo",
  "hw_name": "example-hw",
  "hw_version": "2.0.0",
  "fw_version": "1.9.3",
  "version": "1.9.3",
  "fw_type": "AFI",
  "build_date": "2026-09-26T12:00:00Z",
  "what_string": "1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI",
  "what_format": "v2-5field",
  "fields_from_request": [],
  "size_bytes": 60,
  "sha256": "394cccafdb7042d1e58a52aa76a392adaf8a41d2a1463783c26e14504499fa50",
  "notes_size": 33,
  "notes_sha256": "45045e4f10dcc2e2399359afc4be5fbb8330444b3d31ac6f4bbbde7303c696b0",
  "notes_filename": null,
  "tag_filter_disabled": false,
  "status": "active",
  "uploaded_at": "2026-09-26T17:43:51.70613Z",
  "uploaded_by": "token:01a0dac8-6da3-7697-a806-fac3085296d0",
  "download_count": 0,
  "notes_download_count": 0,
  "last_accessed_at": null,
  "updated_at": "2026-09-26T17:43:51.714744Z",
  "tags": []
}

How upload fields are resolved

Each field is taken from the what string when it's there. Otherwise it comes from the request, in this order: path, then form field, then query parameter; the firmware object's fields_from_request lists which fields came from the request.

  • If a field appears in both places and the values differ, the upload is rejected (422). Comparisons ignore case, and a v prefix on versions.
  • fw_version, fw_name, hw_name, hw_version and fw_type must all end up known; build_date is optional.
  • If the binary contains several different what strings, choose one with what_index (see Parse a binary).
  • A binary without any what string is rejected (422).

Parse a binary

POST /firmware/parse

Bearer token · scope fw:read

A dry run: shows the what strings found in a binary and how they are read. Nothing is stored. A what string that can't be parsed is listed with an error field.

Form parts (multipart/form-data)

firmware file required

The binary to inspect.

POST /firmware/parse
curl -X POST "https://fw.unitcircle.ca/firmware/parse" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ" \
  --form-string 'firmware=@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI'
http --multipart -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ POST "https://fw.unitcircle.ca/firmware/parse" \
  'firmware=\@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI'
import requests

r = requests.post(
    "https://fw.unitcircle.ca/firmware/parse",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
    files={"firmware": ("demo.bin", "@(#)1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI")},
)
print(r.status_code, r.json())
Response
{
  "sha256": "394cccafdb7042d1e58a52aa76a392adaf8a41d2a1463783c26e14504499fa50",
  "size_bytes": 60,
  "what_strings": [
    {
      "build_date": "2026-09-26T12:00:00Z",
      "format": "v2-5field",
      "fw_name": "demo",
      "fw_type": "AFI",
      "fw_version": "1.9.3",
      "hw_name": "example-hw",
      "hw_version": "2.0.0",
      "index": 0,
      "version": "1.9.3",
      "what_string": "1.9.3, demo, example-hw@2.0.0, 2026-09-26T12:00:00Z, AFI"
    }
  ]
}

List firmware

GET /firmware

Bearer token · scope fw:read

Newest version first (use order=asc for oldest first), 100 per page.

Query parameters

fw_name string

Exact firmware name.

hw_name string

Exact hardware name.

hw_version string

Exact hardware version.

fw_type string

AFI, MFI or EFI.

tag string

Only builds with this tag.

status string

active or withdrawn.

general boolean

true: general releases only; false: tagged or staged only.

order string

asc for oldest version first.

limit integer

1–500, default 100.

offset integer

Number of items to skip; use next_offset from the previous page.

GET /firmware
curl "https://fw.unitcircle.ca/firmware?fw_name=demo&limit=2" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/firmware?fw_name=demo&limit=2"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware",
    params={
        "fw_name": "demo",
        "limit": "2"
    },
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
print(r.status_code, r.json())
Response
{
  "items": [
    {
      "id": "01a0db3a-18c0-7e7b-ae23-05cc81dd68e3",
      "fw_name": "demo",
      "hw_name": "example-hw",
      "hw_version": "2.0.0",
      "fw_version": "1.9.0",
      "version": "1.9.0",
      "fw_type": "AFI",
      "tag_filter_disabled": false,
      "status": "active",
      "tags": [],
      "…": "all other firmware fields"
    },
    {
      "id": "01a0db3a-1883-73e4-b695-e6f21b5ade15",
      "fw_name": "demo",
      "hw_name": "example-hw",
      "hw_version": "2.0.0",
      "fw_version": "1.8.0",
      "version": "1.8.0",
      "fw_type": "AFI",
      "tag_filter_disabled": false,
      "status": "active",
      "tags": [],
      "…": "all other firmware fields"
    }
  ],
  "next_offset": 2
}

Retrieve firmware

GET /firmware/{id}

Bearer token · scope fw:read

Returns the firmware object with a weak ETag, which you can send back as If-Match when updating.

Path parameters

id string required

Firmware id.

GET /firmware/{id}
curl "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
print(r.status_code, r.json())
Response
{
  "id": "01a0db38-b3fb-7af4-b46b-638aec828af5",
  "fw_name": "demo",
  "hw_name": "example-hw",
  "hw_version": "2.0.0",
  "fw_version": "1.6.0-10-g30d0049",
  "version": "1.6.0-10-g30d0049",
  "fw_type": "AFI",
  "build_date": "2025-12-05T20:25:18Z",
  "what_string": "1.6.0-10-g30d0049, demo, example-hw@2.0.0, 2025-12-05T15:25:18-05:00, AFI",
  "what_format": "v2-5field",
  "fields_from_request": [],
  "size_bytes": 4096,
  "sha256": "04be2a18a2f8e1832bd08e9e631c8bd7db95c6dc09a4c3927a6938f907659562",
  "notes_size": 110,
  "notes_sha256": "5b3e…",
  "notes_filename": "README.md",
  "tag_filter_disabled": true,
  "status": "active",
  "uploaded_at": "2026-09-26T00:58:47.102Z",
  "uploaded_by": "sandbox:seed",
  "download_count": 3,
  "notes_download_count": 1,
  "last_accessed_at": "2026-09-26T00:59:30Z",
  "updated_at": "2026-09-26T00:58:47.102Z",
  "tags": []
}

Update release settings

PATCH /firmware/{id}

Bearer token · scope fw:write

Changes who a build is offered to — never its content. Set its tags (replaces the whole set; devices sharing a tag are offered the build), make it a general release (tag_filter_disabled: true, offered to every device) or take it back to tag-only delivery, and withdraw or reactivate it (status). A withdrawn build is never offered and its download links stop working; reactivating it makes it available again.

Path parameters

id string required

Firmware id.

Headers

If-Match string

The ETag you retrieved; 412 if the firmware changed since.

Body parameters (JSON)

tags array of strings

Replaces the build's tags. Tags are created as needed.

tag_filter_disabled boolean

true = general release (offered to every device); false = only devices sharing a tag.

status string

active or withdrawn.

PATCH /firmware/{id}
curl -X PATCH "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA" \
  -H "Content-Type: application/json" \
  -d '{"tag_filter_disabled":true,"tags":["beta"]}'
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA PATCH "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689" \
  'tag_filter_disabled:=true' \
  'tags:=["beta"]'
import requests

r = requests.patch(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"},
    json={
        "tag_filter_disabled": True,
        "tags": [
            "beta"
        ]
    },
)
print(r.status_code, r.json())
Response
{
  "id": "01a0db38-b3fb-7af4-b46b-638aec828af5",
  "fw_name": "demo",
  "version": "1.6.0-10-g30d0049",
  "fw_type": "AFI",
  "tag_filter_disabled": true,
  "status": "active",
  "tags": ["beta"],
  "updated_at": "2026-09-26T01:00:28.204287Z",
  "…": "all other firmware fields"
}

Download the binary

GET /firmware/{id}/binary

Bearer token · scope fw:read

Redirects (302) to a download link that is valid for 5 minutes. Following it downloads a file named like demo-example-hw-2.0.0-1.6.0-10-g30d0049-AFI.bin.

Path parameters

id string required

Firmware id.

GET /firmware/{id}/binary
curl -L -o download.bin "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/binary" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http --follow --download -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/binary"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/binary",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
r.raise_for_status()  # requests follows the redirect to the file
open("download.bin", "wb").write(r.content)
Response
HTTP/1.1 302 Found
Location: https://fw.unitcircle.ca/dl/AaDZvRgieMa7-iOfg8tIIwEBoNs6GMB-e64jBcyB3WjjAAAAAGq3GliVRhyw2lQ36w.Ek5wjL5qLh0sgeMoOeWl9ASyU1s
Cache-Control: no-store

<!doctype html> … Redirecting... …

Download the release notes

GET /firmware/{id}/notes

Bearer token · scope fw:read

Like the binary, but for the notes (…-notes.md or …-notes.txt).

Path parameters

id string required

Firmware id.

GET /firmware/{id}/notes
curl -L -o download.bin "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/notes" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http --follow --download -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/notes"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/notes",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
r.raise_for_status()  # requests follows the redirect to the file
open("download.bin", "wb").write(r.content)
Response
HTTP/1.1 302 Found
Location: https://fw.unitcircle.ca/dl/AaDZvRgieMa7-iOfg8tIIwIBoNs6GMB-e64jBcyB3WjjAAAAAGq3Glg3cJ7…
Cache-Control: no-store

List a build's tags

GET /firmware/{id}/tags

Bearer token · scope fw:read

Path parameters

id string required

Firmware id.

GET /firmware/{id}/tags
curl "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
print(r.status_code, r.json())
Response
{"tags": ["beta"]}

Replace a build's tags

PUT /firmware/{id}/tags

Bearer token · scope tags:write

Devices that share one of these tags are offered the build (in addition to everyone, if it is a general release). Tags are created on first use.

Path parameters

id string required

Firmware id.

Body parameters (JSON)

tags array of strings required

The complete new tag set.

PUT /firmware/{id}/tags
curl -X PUT "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA" \
  -H "Content-Type: application/json" \
  -d '{"tags":["beta","qa"]}'
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA PUT "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags" \
  'tags:=["beta","qa"]'
import requests

r = requests.put(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"},
    json={
        "tags": [
            "beta",
            "qa"
        ]
    },
)
print(r.status_code, r.json())
Response
{"tags": ["beta", "qa"]}

Add a tag to a build

PUT /firmware/{id}/tags/{tag}

Bearer token · scope tags:write

Idempotent.

Path parameters

id string required

Firmware id.

tag string required

Letters, digits, . _ : -; up to 64 characters.

PUT /firmware/{id}/tags/{tag}
curl -X PUT "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA PUT "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc"
import requests

r = requests.put(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"},
)
print(r.status_code, r.text)
Response
(no body)

Remove a tag from a build

DELETE /firmware/{id}/tags/{tag}

Bearer token · scope tags:write

Idempotent.

Path parameters

id string required

Firmware id.

tag string required

Tag name.

DELETE /firmware/{id}/tags/{tag}
curl -X DELETE "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA DELETE "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc"
import requests

r = requests.delete(
    "https://fw.unitcircle.ca/firmware/01a0f8ad-87f3-74b1-8172-257e904c8689/tags/rc",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA"},
)
print(r.status_code, r.text)
Response
(no body)