uclogserver
Log in

Tokens & members

Manage the API tokens and the people (members) of your account. Creating tokens and managing members is done by a person, so these endpoints are normally used from the web console. They also work from a script that has logged in with a console session: the examples use a cookie jar (cookies.txt) plus the session's CSRF token (replace YOUR_CSRF_TOKEN with the csrf_token from /auth/me), as described in Authentication. An API token can never create another token.

Members have one of three roles: viewer (read everything), editor (also upload and edit) and owner (also manage members and revoke anyone's tokens). A token can only have scopes its creator's role allows.

The token object

Field Type Description
jti string token id
name string your label
scopes array what the token may do
created_by string email of the member who created it
created_at, expires_at timestamp lifetime (at most 365 days)
last_used_at, use_count usage
revoked_at timestamp or null set when revoked

The token value itself is only returned once, when the token is created.

List tokens

GET /tokens

Bearer token · scope fw:read

Also available to any API token of the account (fw:read).

GET /tokens
curl "https://fw.unitcircle.ca/tokens" \
  -H "Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"
http -A bearer -a eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ GET "https://fw.unitcircle.ca/tokens"
import requests

r = requests.get(
    "https://fw.unitcircle.ca/tokens",
    headers={"Authorization": "Bearer eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjYtMTAtMDEtYjItaiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2Z3LnVuaXRjaXJjbGUuY2EiLCJzdWIiOiJzdmM6c2FuZGJveCByZWFkLW9ubHkgKHB1YmxpYykiLCJhdWQiOlsidWNsb2dzZXJ2ZXIiXSwiZXhwIjoxODIyNDE0NTE3LCJuYmYiOjE3OTA4Nzg1MTIsImlhdCI6MTc5MDg3ODUxNywianRpIjoiMDFhMGY4YWQtODkwZi03ODA5LWI3YTgtMDE0MTZlOWM0YTI3IiwidHlwIjoiYXBpIiwiY3VzdCI6InNhbmRib3guZXhhbXBsZSIsImNpZCI6IjAxYTBmOGExLTEzYWMtN2QzNC05NDEyLTMwYjAwOGFkYzk4NyIsInNjcCI6ImZ3OnJlYWQgbG9nZGF0YTpyZWFkIGRldmljZXM6cmVhZCB0YWdzOnJlYWQgc3RhdHM6cmVhZCJ9.IaQSEW8c2zmJWEioQ8PBaezftMriGUzGQhIof_T7HGG4Usjt9dRmhj-xiw_tMuEYcsR7EzLzXyELG0DrdJGADQ"},
)
print(r.status_code, r.json())
Response
{
  "items": [
    {
      "jti": "01a0db3f-5adc-7b3e-a76a-671533a68674",
      "name": "ci uploader",
      "scopes": ["devices:read", "fw:read", "fw:write", "logdata:read", "logdata:write", "stats:read", "tags:read"],
      "created_by": "alice@example.com",
      "created_at": "2026-09-26T01:05:57.469919Z",
      "expires_at": "2026-12-25T01:05:57Z",
      "last_used_at": null,
      "use_count": 0,
      "revoked_at": null
    }
  ]
}

Create a token

POST /tokens

Console session (browser)

Choose a scope bundle — read-only, uploader or full — and/or explicit scopes. Store the returned token immediately; it is not shown again.

Other errors: asking for a scope your role doesn't have → 403 you cannot grant scope fw:write (exceeds your role); ttl_seconds out of range or no name → 400.

Body parameters (JSON)

name string required

1–100 characters.

bundle string

read-only, uploader or full.

scopes array of strings

Explicit scopes, added to the bundle.

ttl_seconds integer

60 to 31536000 (365 days); default 90 days.

POST /tokens
curl -X POST "https://fw.unitcircle.ca/tokens" \
  -b cookies.txt \
  -H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bundle":"uploader","name":"ci uploader","ttl_seconds":7776000}'
http --session=./console.json POST "https://fw.unitcircle.ca/tokens" \
  X-CSRF-Token:YOUR_CSRF_TOKEN \
  'bundle=uploader' \
  'name=ci uploader' \
  'ttl_seconds:=7776000'
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.post(
    "https://fw.unitcircle.ca/tokens",
    headers={"X-CSRF-Token": csrf},
    json={
        "bundle": "uploader",
        "name": "ci uploader",
        "ttl_seconds": 7776000
    },
)
print(r.status_code, r.json())
Response
{
  "customer": "example.com",
  "expires_at": "2026-12-25T01:05:57Z",
  "jti": "01a0db3f-5adc-7b3e-a76a-671533a68674",
  "name": "ci uploader",
  "note": "Store this token now; it is not shown again.",
  "scopes": ["devices:read", "fw:read", "fw:write", "logdata:read", "logdata:write", "stats:read", "tags:read"],
  "token": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRldi0xIiwi…"
}

Revoke a token

DELETE /tokens/{jti}

Console session (browser)

The token stops working within 30 seconds. Members can revoke their own tokens; owners can revoke any token of the account. Revoking an already revoked token is not an error.

Path parameters

jti string required

Token id.

DELETE /tokens/{jti}
curl -X DELETE "https://fw.unitcircle.ca/tokens/01a0db3f-5adc-7b3e-a76a-671533a68674" \
  -b cookies.txt \
  -H "X-CSRF-Token: YOUR_CSRF_TOKEN"
http --session=./console.json DELETE "https://fw.unitcircle.ca/tokens/01a0db3f-5adc-7b3e-a76a-671533a68674" \
  X-CSRF-Token:YOUR_CSRF_TOKEN
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.delete(
    "https://fw.unitcircle.ca/tokens/01a0db3f-5adc-7b3e-a76a-671533a68674",
    headers={"X-CSRF-Token": csrf},
)
print(r.status_code, r.text)
Response
(no body)

The member object

Field Type Description
email string the person's address — any domain
domain string your account
role string viewer, editor or owner
disabled boolean membership disabled
user_disabled boolean the person is disabled service-wide
created_at timestamp when invited
last_login_at timestamp or null null: invited but never logged in

The member endpoints need the members:manage permission, which owners have.

List members

GET /members

Console session (browser) · scope members:manage

GET /members
curl "https://fw.unitcircle.ca/members" \
  -b cookies.txt
http --session=./console.json GET "https://fw.unitcircle.ca/members"
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.get(
    "https://fw.unitcircle.ca/members",
)
print(r.status_code, r.json())
Response
{
  "items": [
    {
      "email": "alice@example.com",
      "domain": "example.com",
      "role": "owner",
      "disabled": false,
      "created_at": "2026-09-25T18:03:49.631373Z",
      "last_login_at": "2026-09-26T01:05:57.41496Z",
      "user_disabled": false
    }
  ]
}

Invite a member

POST /members

Console session (browser) · scope members:manage

Access is by invitation only. The address can be at any domain; the person gets an email with a link to the login page. Inviting someone who is already a member updates their role (200).

Body parameters (JSON)

email string required

The person's email address.

role string

viewer (default), editor or owner.

POST /members
curl -X POST "https://fw.unitcircle.ca/members" \
  -b cookies.txt \
  -H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"bob@contractor.example","role":"editor"}'
http --session=./console.json POST "https://fw.unitcircle.ca/members" \
  X-CSRF-Token:YOUR_CSRF_TOKEN \
  'email=bob@contractor.example' \
  'role=editor'
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.post(
    "https://fw.unitcircle.ca/members",
    headers={"X-CSRF-Token": csrf},
    json={
        "email": "bob@contractor.example",
        "role": "editor"
    },
)
print(r.status_code, r.json())
Response
{"created": true, "domain": "example.com", "email": "bob@contractor.example", "role": "editor"}

Update a member

PATCH /members/{email}

Console session (browser) · scope members:manage

Change the role or disable the membership. Either change ends the person's sessions on your account and revokes the API tokens they created for it.

Path parameters

email string required

The member's email.

Body parameters (JSON)

role string

viewer, editor or owner.

disabled boolean

true to disable.

PATCH /members/{email}
curl -X PATCH "https://fw.unitcircle.ca/members/bob@contractor.example" \
  -b cookies.txt \
  -H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role":"viewer"}'
http --session=./console.json PATCH "https://fw.unitcircle.ca/members/bob@contractor.example" \
  X-CSRF-Token:YOUR_CSRF_TOKEN \
  'role=viewer'
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.patch(
    "https://fw.unitcircle.ca/members/bob@contractor.example",
    headers={"X-CSRF-Token": csrf},
    json={
        "role": "viewer"
    },
)
print(r.status_code, r.json())
Response
{
  "email": "bob@contractor.example",
  "domain": "example.com",
  "role": "viewer",
  "disabled": false,
  "created_at": "2026-09-26T01:05:57.670992Z",
  "last_login_at": null,
  "user_disabled": false
}

Remove a member

DELETE /members/{email}

Console session (browser) · scope members:manage

Removes the membership and revokes the person's sessions and tokens for your account.

Path parameters

email string required

The member's email.

DELETE /members/{email}
curl -X DELETE "https://fw.unitcircle.ca/members/bob@contractor.example" \
  -b cookies.txt \
  -H "X-CSRF-Token: YOUR_CSRF_TOKEN"
http --session=./console.json DELETE "https://fw.unitcircle.ca/members/bob@contractor.example" \
  X-CSRF-Token:YOUR_CSRF_TOKEN
import requests

# Log in (POST /auth/login emails you a code): see /docs/authentication#log-in-from-a-script
session = requests.Session()
session.post("https://fw.unitcircle.ca/auth/verify", json={"email": "you@example.com", "code": "123456"}).raise_for_status()
csrf = session.get("https://fw.unitcircle.ca/auth/me").json()["csrf_token"]

r = session.delete(
    "https://fw.unitcircle.ca/members/bob@contractor.example",
    headers={"X-CSRF-Token": csrf},
)
print(r.status_code, r.text)
Response
(no body)