Getting started
uclogserver stores firmware images and device log files for hardware makers, and tells each of your devices which firmware updates it may install. Everything is available through a REST API; a web console handles day-to-day management.
This page explains the main ideas and takes you through a five-command tour against the public sandbox. You don't need an account for the tour. The API reference has every endpoint, with cURL, HTTPie and Python examples and a Try it button that runs requests against the sandbox from your browser.
What the service does
| You want to… | The service gives you… |
|---|---|
| Publish a firmware build and its release notes | An upload endpoint that reads version, product and type from the build's embedded what string |
| Let devices find out if there's something newer | An unauthenticated update-check URL that returns newer builds with 5-minute download links |
| Roll out a beta to some devices only | Tags on firmware and devices; a device sees a tagged build only if it shares a tag with it |
| Collect log or crash files from devices or test rigs | Upload by a 512-bit hex name; download later through a short-lived link |
| Keep a registry of manufactured devices | Device records keyed by hardware id, with optional Ed25519 public keys and tags |
| Automate all of the above from CI | Scoped, expiring API tokens |
Key concepts in one minute
- Account. Your organisation is identified by a domain name such as
example.com. Your API token belongs to your account, so URLs like/firmwareor/devicesautomatically mean your firmware and devices. Only the public update check names the account in the URL. Each account's data is completely separate. See Concepts. - Firmware. Each build is identified by firmware name, hardware name, hardware
version, version and type —
AFI(application),MFI(manufacturing) orEFI(engineering). The service reads these from the@(#)what string embedded in the binary; you supply any that are missing. - Versions. Semver-like with two extensions: git-describe builds (
1.4.1-10-g30d0049is later than1.4.1) and an optional 4th number (1.5.1.2is later than1.5.1). - General release vs tagged release. A new upload is staged: no device can see it yet. Make it a general release to offer it to everyone, or give it tags to offer it only to devices with a matching tag.
- Devices. A device is identified by its HW id: 1–64 printable ASCII characters
without spaces (e.g.
SN-000123ora1b2c3d4), case-sensitive. It may carry tags and two Ed25519 public keys. Devices are never deleted; they are withdrawn. - Write-once. Firmware builds and log files never change and are never deleted;
re-sending identical content is harmless, different content is a
409. - Log data. Arbitrary binary files, each named by a 128-hex-character (512-bit) identifier.
- Download links. Files are never served from permanent URLs; the service hands out links that expire after 5 minutes (see Download links).
The sandbox
The sandbox is a real account, sandbox.example, with two publicly published API
tokens. The examples in these docs already contain the server address and these tokens, so
you can copy them into a terminal and run them as they are.
| Token | Scopes |
|---|---|
| read-only | fw:read logdata:read devices:read tags:read stats:read |
| write | read-only plus fw:write logdata:write devices:write tags:write |
Read-only token:
eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ
Write token:
eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA
The sandbox is wiped and re-seeded every day at 00:30 UTC. After a reset it contains:
| Kind | Items |
|---|---|
Firmware demo for example-hw@2.0.0 |
1.4.1-10-g30d0049 AFI, v1.5.1.2 AFI, 1.6.0-10-g30d0049 AFI and EFI (all general releases); 1.7.0-rc.1 AFI tagged beta |
| Devices | a1b2c3d4 (tag beta, has an identity key), a1b2c3d4e5f60718 (tag production), 0123456789abcdef01234567 (no tags) |
| Log data | one file named c309d264…4710 |
The tokens are public, so anything they can do is open to everyone (uploading, tagging, withdrawing; the seeded data comes back at the next reset). They can't manage members and tokens, and uploads are limited to 1 MiB per firmware image and 256 KiB per log file. Don't upload anything private. To try member and token management, ask for an evaluation account.
Five-command tour
1. Is the service up?
curl -s https://fw.unitcircle.ca/status | jq .status
"ok"
2. What updates would a device running 1.5.1 get? The update check needs no token.
curl -s https://fw.unitcircle.ca/firmware/sandbox.example/example-hw/2.0.0/demo/1.5.1 | jq '.[] | {version, "fw-type"}'
{ "version": "example-hw/2.0.0/demo/1.5.1.2", "fw-type": "AFI" }
{ "version": "example-hw/2.0.0/demo/1.6.0-10-g30d0049", "fw-type": "AFI" }
3. The same question for the beta-tester device a1b2c3d4. The device's HW id is the
last part of the URL. It also gets the 1.7.0-rc.1 build tagged beta:
curl -s https://fw.unitcircle.ca/firmware/sandbox.example/example-hw/2.0.0/demo/1.6.0-10-g30d0049/a1b2c3d4 | jq '.[].version'
"example-hw/2.0.0/demo/1.7.0-rc.1"
4. Upload a firmware build. Normally you send your build output
(-F firmware=@build/demo.bin); the service reads the embedded what string. For a quick
try, the "binary" can be just a what string, sent inline — no files needed. This one has
no product names, so the path supplies them:
curl -s -X PUT https://fw.unitcircle.ca/firmware/example-hw/2.0.0/demo \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCB3cml0ZXIgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkxNC03MjcxLWE4MGEtMjI3NzYyYWY5NjliIiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCBmdzp3cml0ZSBsb2dkYXRhOndyaXRlIGRldmljZXM6d3JpdGUgdGFnczp3cml0ZSJ9.AdKQR9H96-zqRYvWVf7ALksV1UAht2CzSHdomiepLAfuVjFOPiTowNWssFRgmMuu9z7CHTdug1AcY6jWiMW6DA" \
--form-string 'firmware=@(#)9.9.9, 2026-01-01T00:00:00Z, AFI' \
--form-string 'notes=# 9.9.9
* My first upload.'
OK
Add -H 'Accept: application/json' to get the stored firmware record back as JSON instead
of OK (see Response formats).
5. Download a log file. You get a 302 to a link that stops working after 5 minutes;
-L follows it:
curl -sL -o log.bin -w '%{http_code} after %{num_redirects} redirects\n' \
https://fw.unitcircle.ca/logdata/c309d264c3d86dabc64d34790b50cd9780ab94eb274456ff66e70b0e4cecc6182c008bacf71cdd3201a14a78c1df62cb794f54b6c60e26463f88ee1e01e74710 \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
200 after 2 redirects
Prefer clicking? Every endpoint in the API reference has a Try it button that runs the request against the sandbox.
How the URLs are organised
There is no version prefix.
| URLs | Who calls them | Account comes from |
|---|---|---|
GET /firmware/{domain}/{hw}/{hwver}/{fw}/{fwver}[/{hwid}] |
devices (no token) | the {domain} in the path |
/firmware…, /logdata…, /devices…, /tags…, /tokens…, /members…, /stats/… |
your tools, with a token | your token |
/admin/… |
the service's administrators | ?customer= or the path |
/status, /healthz, /readyz, /health/history, /openapi.json |
monitoring, anyone | – |
Where next
- API reference: every endpoint, with examples and Try it.
- Authentication: console login and API tokens.
- Firmware: uploads, release settings and the update check — the core workflow.
- Errors, limits & CORS: what can go wrong, and why.
- A machine-readable OpenAPI 3.1 description is at
/openapi.json.