Skip to content

HTTP API

Everything the server copy of the page does is one of these calls. Interactive documentation (OpenAPI) is served at /api/docs.

Authentication is a session cookie (sufs_session, HttpOnly, SameSite=Strict, Secure over https) set by login or sign-up, or an API token in Authorization: Bearer <token> (see API tokens). Requests without either get 401. Bodies and responses are JSON unless noted.

Errors are {"detail": "message"} or, for validation, {"detail": ["field: problem", …]}. Error responses never echo the submitted input.

Sign-in

Method and path Body Notes
POST /api/auth/login {username, password} 200 {username}. 401 on a wrong pair; 429 after ten failures from one address for one username within five minutes.
POST /api/auth/signup {username, password, code} Creates a login with a one-time join code and joins that account as the user's only one. 404 for a bad or used code, 409 if the username exists, 422 for an invalid username or short password.
POST /api/auth/logout — Ends the session.
GET /api/me — {username, defaultAccount, accounts: [{id, name, members: [username…], invited: [username…]}], invites: [{id, account, from}], tokens: [{id, name, created, lastUsed}]}
GET /api/health — {ok: true} when the database answers. No session needed.

API tokens

A token is a long-lived credential for another program acting as the user (a browser extension, a script). It carries the same rights as the user's login in every account they belong to. Tokens start with sufs_ and are shown once; only a hash is stored.

Method and path Body Notes
POST /api/tokens {name} 200 {token}. The name is a label for the list in /api/me.
DELETE /api/tokens/{id} — Revoke. 404 for another user's token.

Send it as Authorization: Bearer sufs_…. A token can create and revoke tokens too, so treat it like a password.

Accounts and membership

All …/{aid}/… routes require the caller to be a member of account aid; otherwise 404.

Method and path Body Notes
POST /api/accounts {name} Create an account with the caller as its member. 200 {id}.
PUT /api/accounts/{aid}/name {name} Rename.
POST /api/accounts/{aid}/default — Make this the caller's default account.
POST /api/accounts/{aid}/invites {username} Invite an existing login; they accept via /api/invites. 404 unknown user, 409 already a member. Re-inviting renews the invitation.
POST /api/accounts/{aid}/codes — 200 {code}: a one-time join code valid for seven days, returned once.
DELETE /api/accounts/{aid}/members/{username} — Remove a member or leave. 409 if they are the only member.
DELETE /api/accounts/{aid} — Delete the account and all its data. 409 unless the caller is its sole member.
POST /api/invites/{id}/accept — Join the inviting account. 200 {account}.
DELETE /api/invites/{id} — Decline.
POST /api/join {code} Redeem a join code as a signed-in user. 200 {account}.

Invitations and codes expire after seven days.

Tracker data

Documents have the shapes in the data model. {collection} is status, log, meta or file; {id} is 1–64 characters from letters, digits, _ and - (for file, 32 lowercase hex digits). The only meta document is settings.

Method and path Notes
GET /api/accounts/{aid}/data {status: {key: doc}, log: {id: doc}, meta: {settings: doc}, file: {id: doc}, sufs: {id: doc}}. Sends an ETag; with a matching If-None-Match the answer is 304, which is how the page polls cheaply.
GET /api/accounts/{aid}/{collection} One collection as {id: doc}.
GET /api/accounts/{aid}/{collection}/{id} One document, or 404.
PUT /api/accounts/{aid}/{collection}/{id} Create or replace. The body is validated against the document's shape (422 otherwise). 200 {revision}. A file record needs its contents uploaded first (409 otherwise).
DELETE /api/accounts/{aid}/{collection}/{id} Delete; deleting a missing document is fine. 200 {revision}.
PUT /api/accounts/{aid}/sufs {requests: {id: doc}, full: bool}: the browser extension's reading of the SUFS portal in one write (at most 2000 requests). Upserts every request; with full: true (a complete list) also deletes the account's requests missing from it. 200 {revision}.
GET /api/accounts/{aid}/search?q= Full-text search. {log: [id…], status: [key…]}; log ids newest first. Matches any substring of three or more characters across the log's vendor, description, note and form line text, and across line notes and paths. Shorter queries use a plain substring scan.

revision is a per-account counter that increases on every write; the ETag is derived from it.

Attached files

The contents of a file document. Upload the bytes first, then save the record with the same id.

Method and path Notes
PUT /api/accounts/{aid}/files/{id} Body: the raw bytes; Content-Type: the file's type. {id} must be the first 32 hex digits of the body's SHA-256 (422 otherwise). 415 for a type not in the allowed list, 413 over the size limit, 507 when the account's storage is full, 422 for an empty body. Uploading contents that already exist is a no-op. 200 {ok, size}.
GET /api/accounts/{aid}/files/{id} The bytes, with the record's type, X-Content-Type-Options: nosniff, and Content-Disposition: inline for PDFs and images, attachment for everything else. 404 until the record exists.

There is no DELETE for the bytes: deleting the file document removes them. Limits are in Configuration.

Pages

GET / serves the server copy of the tracker; GET /standalone/ serves the standalone copy (with its PWA files alongside). Both are generated when the server starts.