Admin
For the operators of the service. These endpoints need an administrator token (a token
with the admin scope, created by a member of the administrators' account) and are not
available in the sandbox. Administrators can also use every other endpoint on behalf of any
account by adding ?customer=DOMAIN, e.g. GET /devices?customer=example.com.
Administrator actions on an account are recorded in the audit log under the administrator's
identity; the account's own members see them as service administrator. Most of these
functions are also available in the web console under Console → Admin.
curl "https://fw.unitcircle.ca/admin/customers" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/customers"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/customers",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{
"domain": "acme.example",
"name": "Acme Devices",
"status": "active",
"cors_origins": [],
"is_sandbox": false,
"created_at": "2026-09-26T01:06:38.275063Z",
"updated_at": "2026-09-26T01:06:38.275063Z",
"counts": {"firmware": 0, "logdata": 0, "devices": 0, "tags": 0, "active_tokens": 0, "stored_bytes": 0}
}
]
}{"type":"about:blank","title":"Forbidden","status":403,"detail":"admin access required","instance":"/admin/customers","request_id":"…"}
Create an account
POST /admin/customers
Admin token
Creates a customer account identified by its domain. With owner_email, that person is
invited as the account's first owner.
Body parameters (JSON)
domainstring requiredThe customer's domain name, e.g.
acme.example.namestringDisplay name.
owner_emailstringInvite this person as owner.
cors_originsarray of stringsBrowser origins allowed to call the API.
curl -X POST "https://fw.unitcircle.ca/admin/customers" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"acme.example","name":"Acme Devices","owner_email":"owner@acme.example"}'http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/customers" \ 'domain=acme.example' \ 'name=Acme Devices' \ 'owner_email=owner@acme.example'
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/customers",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"domain": "acme.example",
"name": "Acme Devices",
"owner_email": "owner@acme.example"
},
)
print(r.status_code, r.json())
{
"customer": {
"domain": "acme.example",
"name": "Acme Devices",
"status": "active",
"cors_origins": [],
"is_sandbox": false,
"created_at": "2026-09-26T01:06:38.275063Z",
"updated_at": "2026-09-26T01:06:38.275063Z"
}
}{"type":"about:blank","title":"Conflict","status":409,"detail":"customer already exists","instance":"/admin/customers","request_id":"…"}{"type":"about:blank","title":"Bad Request","status":400,"detail":"domain must be a valid DNS name, e.g. example.com","instance":"/admin/customers","request_id":"…"}
Retrieve an account
GET /admin/customers/{domain}
Admin token
The account and its members.
Path parameters
domainstring requiredAccount domain.
curl "https://fw.unitcircle.ca/admin/customers/acme.example" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/customers/acme.example"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/customers/acme.example",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"customer": {
"domain": "acme.example",
"name": "Acme Devices",
"status": "active",
"cors_origins": [],
"is_sandbox": false,
"created_at": "2026-09-26T01:06:38.275063Z",
"updated_at": "2026-09-26T01:06:38.275063Z"
},
"members": [
{"email": "owner@acme.example", "domain": "acme.example", "role": "owner", "disabled": false,
"created_at": "2026-09-26T01:06:38.279233Z", "last_login_at": null, "user_disabled": false}
]
}
Update an account
PATCH /admin/customers/{domain}
Admin token
Rename, suspend (all its tokens stop working and update checks answer 404), reactivate,
or set its CORS origins.
Path parameters
domainstring requiredAccount domain.
Body parameters (JSON)
namestringDisplay name.
statusstringactiveorsuspended.domainstringA new domain for the account.
cors_originsarray of stringsOrigins such as
https://app.acme.example.
curl -X PATCH "https://fw.unitcircle.ca/admin/customers/acme.example" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cors_origins":["https://app.acme.example"]}'http -A bearer -a YOUR_ADMIN_TOKEN PATCH "https://fw.unitcircle.ca/admin/customers/acme.example" \ 'cors_origins:=["https://app.acme.example"]'
import requests
r = requests.patch(
"https://fw.unitcircle.ca/admin/customers/acme.example",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"cors_origins": [
"https://app.acme.example"
]
},
)
print(r.status_code, r.json())
{
"customer": {
"domain": "acme.example",
"name": "Acme Devices",
"status": "active",
"cors_origins": ["https://app.acme.example"],
"is_sandbox": false,
"created_at": "2026-09-26T01:06:38.275063Z",
"updated_at": "2026-09-26T01:06:38.380531Z"
}
}
Delete an account
DELETE /admin/customers/{domain}
Admin token
Deletes the account and all its data. Repeat the domain in confirm.
Members of a single account are managed with the member endpoints
plus ?customer=DOMAIN.
Path parameters
domainstring requiredAccount domain.
Query parameters
confirmstring requiredThe same domain again.
curl -X DELETE "https://fw.unitcircle.ca/admin/customers/acme.example?confirm=acme.example" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN DELETE "https://fw.unitcircle.ca/admin/customers/acme.example?confirm=acme.example"
import requests
r = requests.delete(
"https://fw.unitcircle.ca/admin/customers/acme.example",
params={
"confirm": "acme.example"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.text)
(no body)
{"type":"about:blank","title":"Bad Request","status":400,"detail":"add ?confirm=acme.example to delete this customer and ALL its data","instance":"/admin/customers/acme.example","request_id":"…"}
List users
GET /admin/users
Admin token
Every person with access to any account, one entry per membership.
curl "https://fw.unitcircle.ca/admin/users" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/users"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/users",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{"email": "owner@acme.example", "domain": "acme.example", "role": "owner", "disabled": false,
"created_at": "2026-09-26T01:06:38.279233Z", "last_login_at": null, "user_disabled": false}
]
}
Disable a user
PATCH /admin/users/{email}
Admin token
Disables (or re-enables) a person everywhere. Disabling ends all their sessions and revokes every token they created.
Path parameters
emailstring requiredThe person's email.
Body parameters (JSON)
disabledboolean requiredtrueto disable,falseto re-enable.
curl -X PATCH "https://fw.unitcircle.ca/admin/users/owner@acme.example" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"disabled":true}'http -A bearer -a YOUR_ADMIN_TOKEN PATCH "https://fw.unitcircle.ca/admin/users/owner@acme.example" \ 'disabled:=true'
import requests
r = requests.patch(
"https://fw.unitcircle.ca/admin/users/owner@acme.example",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"disabled": True
},
)
print(r.status_code, r.json())
{"disabled": true, "email": "owner@acme.example"}
Retrieve a usage summary for all accounts
GET /admin/stats/summary
Admin token
Daily totals across the service and totals per account.
(none) counts requests that couldn't be attributed to an account, such as unauthenticated
requests with bad tokens.
Query parameters
fromdateDefault 30 days ago.
todateDefault now.
curl "https://fw.unitcircle.ca/admin/stats/summary?from=2026-09-25" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/stats/summary?from=2026-09-25"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/stats/summary",
params={
"from": "2026-09-25"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"from": "2026-09-25T00:00:00Z",
"to": "2026-09-26T01:07:38.449078Z",
"days": [
{"day": "2026-09-25", "requests": 310, "errors": 0, "firmware_uploads": 3, "logdata_uploads": 3,
"firmware_downloads": 1, "notes_downloads": 0, "logdata_downloads": 3, "update_checks": 22}
],
"customers": [
{"domain": "sandbox.example", "requests": 112, "errors": 0, "update_checks": 25},
{"domain": "example.com", "requests": 92, "errors": 0, "update_checks": 2},
{"domain": "(none)", "requests": 122, "errors": 0, "update_checks": 2}
]
}
List requests for all accounts
GET /admin/stats/events
Admin token
The request log with full IP addresses and user agents.
Query parameters
domainstringOnly this account.
ipstringOnly this client address.
routestringOnly this route pattern.
statusintegerOnly this status.
fromdateDefault 7 days ago.
todateDefault now.
limitinteger1–1000, default 100.
offsetintegerNumber of items to skip; use
next_offsetfrom the previous page.
curl "https://fw.unitcircle.ca/admin/stats/events?domain=sandbox.example&limit=1" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/stats/events?domain=sandbox.example&limit=1"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/stats/events",
params={
"domain": "sandbox.example",
"limit": "1"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{
"ts": "2026-09-26T01:05:57.782977Z",
"domain": "sandbox.example",
"actor": "token:01a0dac8-6da2-77d7-8129-cea999b9a753",
"method": "GET",
"route": "/members",
"path": "/members",
"status": 403,
"ip": "198.51.100.23",
"user_agent": "curl/8.7.1",
"bytes_in": 0,
"bytes_out": 170,
"duration_ms": 1,
"request_id": "6249fb35-1a0b-47ef-93bc-21783f1c16af"
}
],
"next_offset": 1
}
List audit log entries
GET /admin/audit
Admin token
Every change made by people and tokens, with before and after values.
Query parameters
domainstringOnly this account.
limitinteger1–1000, default 100.
curl "https://fw.unitcircle.ca/admin/audit?limit=1" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/audit?limit=1"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/audit",
params={
"limit": "1"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{
"ts": "2026-09-26T01:06:38.38274Z",
"domain": "acme.example",
"actor": "admin:token:01a0db39-6a51-7451-beae-61f769f9a88d",
"action": "customer.update",
"target_type": "customer",
"target_id": "acme.example",
"before": {"cors_origins": [], "…": "…"},
"after": {"cors_origins": ["https://app.acme.example"], "…": "…"}
}
]
}
List security events
GET /admin/security/events
Admin token
Findings of the suspicious-traffic detector (rate limiting, authentication failures, HW id enumeration, log-data scanning, tenant probing, traffic spikes, session-token reuse) and administrator logins. Critical events are also emailed to the operators.
Query parameters
kindstringe.g.
hwid_enumeration.severitystringinfo,warnorcritical.unackedbooleantrue: only unacknowledged events.limitinteger1–1000, default 100.
curl "https://fw.unitcircle.ca/admin/security/events?limit=1&unacked=true" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/security/events?limit=1&unacked=true"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/security/events",
params={
"limit": "1",
"unacked": "true"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{
"id": 9,
"ts": "2026-09-26T01:05:57.416057Z",
"kind": "admin_login",
"severity": "info",
"ip": "198.51.100.23",
"domain": "unitcircle.ca",
"detail": {"email": "admin@unitcircle.ca"},
"notified_at": null,
"acked_at": null,
"acked_by": null
}
]
}
Acknowledge a security event
POST /admin/security/events/{id}/ack
Admin token
Path parameters
idinteger requiredEvent id.
curl -X POST "https://fw.unitcircle.ca/admin/security/events/9/ack" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/security/events/9/ack"
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/security/events/9/ack",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.text)
(no body)
{"type":"about:blank","title":"Not Found","status":404,"detail":"event not found or already acknowledged","instance":"/admin/security/events/9/ack","request_id":"…"}
List blocked addresses
GET /admin/ip-blocks
Admin token
Query parameters
allbooleantrue: include expired blocks.
curl "https://fw.unitcircle.ca/admin/ip-blocks" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/ip-blocks"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/ip-blocks",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{"cidr": "203.0.113.7/32", "reason": "scanning", "created_by": "admin:token:01a0db39-…",
"created_at": "2026-09-26T01:06:38.540085Z", "expires_at": "2026-09-26T02:06:38.538189Z"}
]
}
Block an address
POST /admin/ip-blocks
Admin token
Requests from the address or network get 403 until the block expires or is removed.
Body parameters (JSON)
cidrstring requiredAn IP address or CIDR network.
reasonstringShown in the console.
ttl_secondsintegerExpiry; omit for a permanent block.
curl -X POST "https://fw.unitcircle.ca/admin/ip-blocks" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cidr":"203.0.113.7","reason":"scanning","ttl_seconds":3600}'http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/ip-blocks" \ 'cidr=203.0.113.7' \ 'reason=scanning' \ 'ttl_seconds:=3600'
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/ip-blocks",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"cidr": "203.0.113.7",
"reason": "scanning",
"ttl_seconds": 3600
},
)
print(r.status_code, r.json())
{"cidr": "203.0.113.7/32", "expires_at": "2026-09-26T02:06:38.538189Z", "reason": "scanning"}
Unblock an address
DELETE /admin/ip-blocks
Admin token
Query parameters
cidrstring requiredThe blocked address or network.
curl -X DELETE "https://fw.unitcircle.ca/admin/ip-blocks?cidr=203.0.113.7" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN DELETE "https://fw.unitcircle.ca/admin/ip-blocks?cidr=203.0.113.7"
import requests
r = requests.delete(
"https://fw.unitcircle.ca/admin/ip-blocks",
params={
"cidr": "203.0.113.7"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.text)
(no body)
curl "https://fw.unitcircle.ca/admin/settings/maintenance" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/settings/maintenance"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/settings/maintenance",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{"enabled": false, "message": ""}
Set maintenance mode
PUT /admin/settings/maintenance
Admin token
While enabled, uploads and other changes answer 503 with Retry-After: 300; reads,
update checks and downloads keep working.
Body parameters (JSON)
enabledboolean requiredTurn maintenance mode on or off.
messagestringAdded to the
503message.
curl -X PUT "https://fw.unitcircle.ca/admin/settings/maintenance" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"message":"database upgrade, back at 14:00 UTC"}'http -A bearer -a YOUR_ADMIN_TOKEN PUT "https://fw.unitcircle.ca/admin/settings/maintenance" \ 'enabled:=true' \ 'message=database upgrade, back at 14:00 UTC'
import requests
r = requests.put(
"https://fw.unitcircle.ca/admin/settings/maintenance",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"enabled": True,
"message": "database upgrade, back at 14:00 UTC"
},
)
print(r.status_code, r.json())
{"enabled": true, "message": "database upgrade, back at 14:00 UTC"}
List background jobs
GET /admin/jobs
Admin token
Backups, health roll-ups, retention clean-up and other scheduled work.
Query parameters
limitinteger1–1000, default 100.
curl "https://fw.unitcircle.ca/admin/jobs?limit=1" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/jobs?limit=1"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/jobs",
params={
"limit": "1"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"items": [
{
"id": "01a0db1e-a4e1-77fe-aadf-1493694023cb",
"kind": "sandbox_reset",
"payload": {},
"slot": "sandbox_reset:2026-09-26",
"status": "done",
"run_at": "2026-09-26T00:30:13.729516Z",
"attempts": 1,
"max_attempts": 3,
"last_error": null,
"created_at": "2026-09-26T00:30:13.729172Z",
"finished_at": "2026-09-26T00:30:13.833184Z"
}
]
}
List backups
GET /admin/backups
Admin token
Backup, verification and restore runs, newest first. Backups run automatically every day.
Query parameters
limitinteger1–500, default 50.
curl "https://fw.unitcircle.ca/admin/backups?limit=1" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/backups?limit=1"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/backups",
params={
"limit": "1"
},
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"backend": "tarsnap",
"items": [
{
"id": "01a0db40-1f61-7ed4-afbe-f550cce5d736",
"kind": "backup",
"status": "succeeded",
"archive_name": "uclogserver-20260926T010647Z",
"object_count": 16,
"total_bytes": 29357,
"initiated_by": "admin:token:01a0db39-…",
"created_at": "2026-09-26T01:06:47.778537Z",
"started_at": "2026-09-26T01:06:47.783098Z",
"finished_at": "2026-09-26T01:06:48.044334Z",
"error": null
}
]
}
Start a backup
POST /admin/backups
Admin token
Queues a backup of the database and all files to the off-site backup service. Follow its progress with Retrieve a backup.
curl -X POST "https://fw.unitcircle.ca/admin/backups" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/backups"
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/backups",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{"id": "01a0db40-1f61-7ed4-afbe-f550cce5d736", "kind": "backup", "status": "queued"}{"type":"about:blank","title":"Conflict","status":409,"detail":"backups are disabled (BACKUP_BACKEND=none)","instance":"/admin/backups","request_id":"…"}
Retrieve a backup
GET /admin/backups/{id}
Admin token
Status and log of a backup, verification or restore run.
Path parameters
idstring requiredRun id.
curl "https://fw.unitcircle.ca/admin/backups/01a0db40-1f61-7ed4-afbe-f550cce5d736" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN"
http -A bearer -a YOUR_ADMIN_TOKEN GET "https://fw.unitcircle.ca/admin/backups/01a0db40-1f61-7ed4-afbe-f550cce5d736"
import requests
r = requests.get(
"https://fw.unitcircle.ca/admin/backups/01a0db40-1f61-7ed4-afbe-f550cce5d736",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
)
print(r.status_code, r.json())
{
"id": "01a0db40-1f61-7ed4-afbe-f550cce5d736",
"kind": "backup",
"status": "succeeded",
"archive_name": "uclogserver-20260926T010647Z",
"object_count": 16,
"total_bytes": 29357,
"initiated_by": "admin:token:01a0db39-…",
"created_at": "2026-09-26T01:06:47.778537Z",
"started_at": "2026-09-26T01:06:47.783098Z",
"finished_at": "2026-09-26T01:06:48.044334Z",
"log": "01:06:47 backup uclogserver-20260926T010647Z — dumping database\n01:06:47 database dump: 93912 bytes\n01:06:47 exporting objects\n01:06:48 OBJECTS=16 BYTES=29357\n01:06:48 ARCHIVE=uclogserver-20260926T010647Z\n",
"error": null
}
Verify a backup
POST /admin/backups/verify
Admin token
Queues a verification: the archive (default: the latest) is extracted and every file is checked against its manifest. The server only holds a write-only backup key, so pass a read-capable key (base64 of the key file); it is used for this run only and never stored.
Body parameters (JSON)
archive_namestringDefault: the latest archive.
tarsnap_key_b64stringA read-capable key, base64.
curl -X POST "https://fw.unitcircle.ca/admin/backups/verify" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"archive_name":"uclogserver-20260926T010647Z","tarsnap_key_b64":"\u003cbase64 of the read key\u003e"}'http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/backups/verify" \ 'archive_name=uclogserver-20260926T010647Z' \ 'tarsnap_key_b64=<base64 of the read key>'
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/backups/verify",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"archive_name": "uclogserver-20260926T010647Z",
"tarsnap_key_b64": "\u003cbase64 of the read key\u003e"
},
)
print(r.status_code, r.json())
{"id": "01a0db41-0a2b-7c3d-9e8f-0123456789ab", "kind": "verify", "status": "queued"}
Report an external verification
POST /admin/backups/verify-report
Admin token
Record the result of a verification you ran elsewhere (for example on a machine that holds
the full backup key), so it shows in the console and in /status.
Body parameters (JSON)
archive_namestring requiredThe verified archive.
okboolean requiredWhether it passed.
detailstringFree text (becomes the log / error).
curl -X POST "https://fw.unitcircle.ca/admin/backups/verify-report" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"archive_name":"uclogserver-20260926T010647Z","detail":"restored into scratch DB; row counts match","ok":true}'http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/backups/verify-report" \ 'archive_name=uclogserver-20260926T010647Z' \ 'detail=restored into scratch DB; row counts match' \ 'ok:=true'
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/backups/verify-report",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"archive_name": "uclogserver-20260926T010647Z",
"detail": "restored into scratch DB; row counts match",
"ok": True
},
)
print(r.status_code, r.json())
{
"id": "01a0db40-2b60-7bea-940d-b66cb9239cda",
"kind": "verify",
"status": "succeeded",
"archive_name": "uclogserver-20260926T010647Z",
"object_count": null,
"total_bytes": null,
"initiated_by": "admin:token:01a0db39-… (external)",
"created_at": "2026-09-26T01:06:50.850387Z",
"started_at": "2026-09-26T01:06:50.848779Z",
"finished_at": "2026-09-26T01:06:50.848779Z",
"log": "restored into scratch DB; row counts match",
"error": null
}
Restore a backup
POST /admin/restores
Admin token
Replaces all current data with the archive. The service is in maintenance mode while
the restore runs. Type the confirmation exactly as restore <archive_name>.
Body parameters (JSON)
archive_namestring requiredThe archive to restore.
confirmstring requiredrestore <archive_name>.tarsnap_key_b64stringA read-capable key (base64); required with tarsnap.
curl -X POST "https://fw.unitcircle.ca/admin/restores" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"archive_name":"uclogserver-20260926T010647Z","confirm":"restore uclogserver-20260926T010647Z","tarsnap_key_b64":"\u003cbase64 of the read key\u003e"}'http -A bearer -a YOUR_ADMIN_TOKEN POST "https://fw.unitcircle.ca/admin/restores" \ 'archive_name=uclogserver-20260926T010647Z' \ 'confirm=restore uclogserver-20260926T010647Z' \ 'tarsnap_key_b64=<base64 of the read key>'
import requests
r = requests.post(
"https://fw.unitcircle.ca/admin/restores",
headers={"Authorization": "Bearer YOUR_ADMIN_TOKEN"},
json={
"archive_name": "uclogserver-20260926T010647Z",
"confirm": "restore uclogserver-20260926T010647Z",
"tarsnap_key_b64": "\u003cbase64 of the read key\u003e"
},
)
print(r.status_code, r.json())
{"id": "01a0db42-5d6e-7f80-9a1b-2c3d4e5f6a7b", "kind": "restore", "status": "queued"}{"type":"about:blank","title":"Bad Request","status":400,"detail":"set archive_name and confirm to \"restore <archive_name>\"","instance":"/admin/restores","request_id":"…"}