Deleg8

API

Deleg8 speaks JSON over HTTPS. Everything the app does is in the API. The full description is an OpenAPI 3.1 document at /api/v1/openapi.json, built from the routes the server really serves; the table below is the published contract (deleg8/packages/contract).

API keys

An owner or admin makes a key in Admin → API keys. It is shown once. Send it as Authorization: Bearer d8k_… on /api/v1; no cookie, no safety code. A key acts as one team role (owner or admin; member keys are not offered yet), never higher than the person who made it, and its scopes narrow it further: read:<object>, write:<object> (objects: clients, users, boards, tickets, requests, files, and each custom object as obj_<key>, e.g. read:obj_locations), read:*, write:* and admin. Each operation in the OpenAPI document names the scope it needs. A key gets 600 calls a minute and 20 a second, told in the RateLimit-* headers; a POST may carry Idempotency-Key. Every call made with a key is logged. Revoke a key and it stops at once.

Webhooks

In Admin → Webhooks, an owner or admin points Deleg8 at an https address and picks events: <object>.created, .updated, .deleted, ticket.status_changed, request.created, request.reply, update.published, user.invited, user.deactivated; a custom object's records as obj_<key>.created (and so on), or record.created for all of them. Each delivery is a JSON POST signed with the endpoint's secret: Deleg8-Signature: t=<seconds>,v1=<hex HMAC-SHA256 of "t.body">. Check it, and refuse a t more than five minutes old. A delivery that does not get a 2xx within 10 seconds is tried again over about 24 hours; an endpoint that keeps failing is turned off. Only public https addresses are allowed, checked at every send.

Doors

DoorWhoHow
/api/v1You, in a browserThe sign-in cookie, plus x-deleg8-csrf on every change
/api/v1Your toolsAn API key: Bearer d8k_…
/tickets/v1A computer or script with a work token (Settings → Work tokens)Bearer stt_… and x-ticket-session

A list you have no role on answers 404, never 403, so nothing reveals that it exists. Errors are { "error": "<a sentence>", "code": "<code>" }. Live updates come from GET <door>/events as server-sent events, carrying ticket and list ids but never ticket text.

Routes

RouteDoorsWhoBodyAnswer
GET /admin/api-keys/api/v1person{ keys: [{ id, name, last4, role, scopes, clients, createdBy, createdAt, expiresAt, lastUsedAt, lastUsedFrom, revokedAt }], objects: [{ key, name, plural }] } (team owner or admin; a key needs `admin`; objects: custom objects a scope may name)
POST /admin/api-keys/api/v1person{ name, role: owner | admin, scopes: [read:<object> | write:<object> | read:* | write:* | admin], days?: 1..730 } (team owner or admin; role no higher than yours: 403 role_too_high; a key makes only keys no stronger than itself){ id, name, last4, role, scopes, ..., token: "d8k_..." (shown once) } (201). 400 bad_name | bad_role | bad_scopes | bad_days | member_keys_later (no member keys yet); 409 too_many (50 live)
DELETE /admin/api-keys/:id/api/v1person{ ok } (revoked at once; team owner or admin; an owner key only by an owner: 403 role_too_high; 404 not_found)
GET /admin/api-keys/:id/calls/api/v1person{ calls: [{ at, method, path, status, ip }], next: cursor | null } (?limit=1..200 (50)&cursor=<next>; newest first, kept 90 days; team owner or admin)
GET /admin/setup/api/v1person{ warnings: string[] } (what the operator must fix, the start log's WARNING lines, e.g. DELEG8_OWNER_EMAILS unset on a public install; [] when nothing; team owner or admin)
GET /admin/webhooks/api/v1person{ webhooks: [{ id, url, events, description, enabled, disabledWhy, createdAt, createdBy, lastDelivery: { state, at } | null }], events: string[] } (events: the names an endpoint may subscribe to; team owner or admin)
POST /admin/webhooks/api/v1person{ url, events: [<event> | "*"], description? } (team owner or admin; url: public https only)Webhook with secret: "whsec_..." (shown once) (201). 400 bad_events | bad_url | private_address | dns; 409 too_many (20)
PATCH /admin/webhooks/:id/api/v1person{ url?, events?, description?, enabled? } (team owner or admin; turning on one whose maker is gone makes you its maker)Webhook (enabled: true clears its failure count; 400 bad_events | bad_url; 404 not_found)
DELETE /admin/webhooks/:id/api/v1person{ ok } (the endpoint and its delivery log go; team owner or admin; 404 not_found)
GET /admin/webhooks/:id/deliveries/api/v1person{ deliveries: [{ id, event, type, state, tries: [{ at, status, ms, error, answer }], nextAt, createdAt }], next: cursor | null } (?limit=1..200 (50)&cursor=<next>; newest first; team owner or admin; 404 not_found)
POST /admin/webhooks/:id/deliveries/:dlv/redeliver/api/v1person{ delivery: "dlv_..." } (201) (a new delivery of the same event and body, queued now; team owner or admin; 404 not_found)
POST /admin/webhooks/:id/roll-secret/api/v1person{ secret: "whsec_..." (shown once), oldUntil } (the old secret signs too for 24 hours; team owner or admin; 404 not_found)
POST /admin/webhooks/:id/test/api/v1person{ delivery, ok, try: { at, status, ms, error, answer } } (sends a ping now and waits for the one try; team owner or admin; 404 not_found)
GET /auth/pending-link/api/v1anyone{ email, provider: "st8n" } (signed out is fine: this browser's pending st8n link, 10 minutes; 404 no_pending; 404 not_found while st8n sign-in is off (DELEG8_ST8N_SIGNIN))
GET /clients/api/v1person{ clients: Client[] } (Client = { id, name, owner: { id, name }, portalHost, ownRequestsOnly, boards: [{ id, name, portal }], people (active users) }; Clients you own, and the team's by role: owner/admin all, a member those assigned)
POST /clients/api/v1person{ name } (team owner or admin, or an account with no team role, which owns it; a member: 403 forbidden)Client (201) (400 no_name; 409 too_many (500 per owner))
GET /clients/:id/api/v1personClient with people: [{ id, name, email, role, status, at }] (active or not), invites: [{ email, at, expiresAt }] (clients.view: its owner, team owner/admin, a member assigned; else 404 no_client)
PATCH /clients/:id/api/v1person{ name?, portalHost?: one of DELEG8_CUSTOMER_HOSTS | null, ownRequestsOnly?: boolean } (clients.edit: team owner/admin or its own owner; portalHost only a team owner/admin on a team Client)Client (400 no_name | bad_host | bad_own_only | empty; 409 host_taken; 403 forbidden)
DELETE /clients/:id/api/v1person{ ok } (clients.edit; only with no boards: 409 has_boards; its client users and invites go, their accounts and requests stay)
GET /clients/:id/audit/api/v1person{ entries: AuditEntry[] } (?limit= (100, at most 500); that Client's log; team owner or admin only (audit.view), a key needs `admin`; its own owner with no team role: 403)
POST /clients/:id/invite/api/v1person{ email, role?: admin | requester | viewer } (clients.users.manage: team owner/admin or its own owner; role default requester){ invited, mail: "sent" } | { invited, mail: "console", link } (no mail set up) | { invited, mail: "failed", link } (the send failed) (201) (link works once, 14 days; 400 bad_email | bad_role; 409 owner | team_invite | already; 429 rate_limited)
DELETE /clients/:id/invites/:email/api/v1person{ ok } (withdraws the invite, audit client.invite.withdrawn; ok: false when there was none; clients.users.manage)
POST /clients/:id/invites/:email/resend/api/v1person{ invited, mail: "sent" | "console" | "failed", link? } (the same invite and role with a new link; the old link stops working; 404 no_invite; 429 rate_limited; clients.users.manage)
DELETE /clients/:id/people/:person/api/v1person{ ok } (old name of DELETE /clients/:id/users/:person: removed from the Client, their requests stay; 404 no_user; clients.users.manage)
GET /clients/:id/requests/api/v1person{ requests: [{ ticket, id, number, title, status, statusLabel, createdBy: { name }, lastCustomerAt, drafts, waiting }] } (every board of the Client, newest first, at most 500; clients.view)
GET /clients/:id/users/api/v1person{ users: ClientUser[], invites: [{ email, role, at, expiresAt }] } (ClientUser = { id, name, email, role, status, lastActiveAt, at }; clients.view)
PATCH /clients/:id/users/:person/api/v1person{ role: admin | requester | viewer } (clients.users.manage)ClientUser (400 bad_role; 404 no_user)
DELETE /clients/:id/users/:person/api/v1person{ ok } (removed from the Client; their requests stay; no other active Client and no team role: their sessions end; 404 no_user; clients.users.manage)
POST /clients/:id/users/:person/deactivate/api/v1personClientUser (status deactivated) (no other active Client and no team role: their sessions end at once; 404 no_user; clients.users.manage)
POST /clients/:id/users/:person/reactivate/api/v1personClientUser (status active) (the team may turn on anyone, also one a client admin turned off; 404 no_user; clients.users.manage)
POST /clients/:id/users/:person/reset-link/api/v1person{ mail: "sent" } | { mail: "console", link } | { mail: "none" } (no password here) (to the Client's own address, 24 hours, once; only a Client-only account: 409 not_client_account; 404 no_user; 429; 502 mail_failed)
GET /events/api/v1person, sessiontext/event-stream of ChangeEvent (event: tickets); a st8n link as its person also gets `event: command`
POST /files/api/v1personthe file's raw bytes (any content-type), ?name=log.pdf (team user; PNG, JPEG, GIF, WebP, HEIC or PDF, type from the bytes, 10 MB)File (201) (waiting: attach it within 24 h with files: [id]; 400 no_file, 413 too_big, 415 bad_type, 429 too_many_uploads at 20 waiting, 429 rate_limited at 60 a minute)
GET /files/:id/api/v1personthe bytes (images inline, HEIC and PDF as a download; nosniff, sandbox CSP, no-store) (anyone on the ticket's list, or the uploader of a waiting file; else 404 no_file)
GET /invites/api/v1person{ invites: [{ list, name, role, from }] } (waiting for this person's email)
DELETE /invites/:list/api/v1person{ ok } (the invited person declines)
POST /invites/:list/accept/api/v1person{ ok } (the person joins the list; their sessions take its tickets only after PUT /lists/:id/work-on)
GET /links/api/v1person{ links: St8nLink[] }
POST /links/api/v1person{ name, days?, kind?: "st8n" }{ id, token: "d8l_..." (shown once), name, kind, ... } (201)
DELETE /links/:id/api/v1person{ ok } (that st8n's sessions' claims go back to open)
GET /lists/api/v1person, session{ lists: List[] } (?archived=1 adds archived)
POST /lists/api/v1person{ name, about? }List
GET /lists/:id/api/v1person, sessionList (with its fields)
PATCH /lists/:id/api/v1person{ name?, about?, archived? } (list owner)List
DELETE /lists/:id/api/v1person{ confirm: <the list's name> } (list owner; not the Inbox; every ticket on it goes too){ ok, id, tickets: <how many went> }
GET /lists/:id/fields/api/v1person, session{ fields: Field[] }
POST /lists/:id/fields/api/v1person{ name, kind: FieldKind, options?: [{ label, color? }] } (list owner)Field (201)
PATCH /lists/:id/fields/:field/api/v1person{ name?, hidden? } (list owner)Field
DELETE /lists/:id/fields/:field/api/v1person{ ok, cleared: <tickets that lost a value> } (list owner)
POST /lists/:id/fields/:field/options/api/v1person{ label, color? } (list owner; a dropdown)Field (201)
PATCH /lists/:id/fields/:field/options/:option/api/v1person{ label?, color?, retired? } (list owner; a rename shows on every ticket)Field
PUT /lists/:id/fields/:field/options/order/api/v1person{ ids: [every value id, in order] } (list owner)Field
PUT /lists/:id/fields/order/api/v1person{ ids: [every field id, in order] } (list owner){ fields: Field[] }
POST /lists/:id/invite/api/v1person{ email, role: manager | work | view } (owner; a manager may invite as work or view){ ok, invited?: email, person?: id, people, invites, emailed, note? } (everyone invited by email, known or not, waits until they accept)
DELETE /lists/:id/invites/:email/api/v1person{ people, invites } (the owner withdraws an invite)
GET /lists/:id/people/api/v1person, session{ people: ListPerson[], invites: Invite[] } (emails and invites: the list's owner only; others see names)
PUT /lists/:id/people/:person/api/v1person{ role: manager | work | view } (owner; a manager may set work or view){ people, invites }
DELETE /lists/:id/people/:person/api/v1person{ people, invites } (their claims there end, the list leaves their tokens)
GET /lists/:id/requests/api/v1person{ requests: [{ ticket, id, number, title, status, statusLabel, createdBy: { name }, lastCustomerAt, drafts, waiting }] } (anyone on the list; newest first, at most 500; for badges)
PUT /lists/:id/work-on/api/v1person{ on: boolean }{ ok, workOn } (a shared person's sessions take (or stop taking) tickets from this list)
GET /me/api/v1person, sessionMe
GET /objects/api/v1person{ objects: Object[] } (any team user; built-ins first: clients, users, boards, tickets)
POST /objects/api/v1person{ name, plural?, icon?, portal? } (objects.manage: team owner or admin; icon one short word, default box)Object (201) (key made once from the plural, obj_<plural>, never changes; 409 too_many at 50 custom objects)
PATCH /objects/:id/api/v1person{ name?, plural?, icon?, portal?, archived? } (objects.manage; a custom object only, 409 builtin; another key is 400 key_fixed)Object
DELETE /objects/:id/api/v1person{ ok } (objects.manage; a custom object with no records: 409 has_records, archive it instead; 409 builtin)
GET /objects/:id/fields/api/v1person{ fields: Field[] } (any team user; ?board=L-n on tickets adds that board's own fields)
POST /objects/:id/fields/api/v1person{ name, type: FieldKind, board?, options?: [{ label, color? }], required?, default?, help?, portal?: hidden | read | edit, target?, currency? } (objects.manage; target for record, currency for money)Field (201) (409 taken: one name per ticket or object; board fields: text, number, date, yesno, dropdown only)
PATCH /objects/:id/fields/:field/api/v1person{ name?, required?, default?, help?, portal?, hidden?, archived? } (objects.manage; the type never changes: 400 type_fixed)Field
POST /objects/:id/fields/:field/options/api/v1person{ label, color? } (objects.manage; a dropdown or multi field, else 400 not_dropdown)Field (201) (409 taken; at most 50 values)
PATCH /objects/:id/fields/:field/options/:option/api/v1person{ label?, color?, retired? } (objects.manage; a rename shows on every record)Field
PUT /objects/:id/fields/:field/options/order/api/v1person{ ids: [every value id, in order] } (objects.manage)Field
PUT /objects/:id/fields/order/api/v1person{ ids: [every one of the object's own field ids, in order] } (objects.manage; board fields keep theirs){ fields: Field[] }
GET /objects/:id/records/api/v1person{ records: Record[], next: <skip> | null } (records.read; a custom object, else 409 builtin; ?q=&client=&f.<F-n>=<value|none|any>&sort=f.<F-n>|-f.<F-n>&limit=&skip=&archived=1)
POST /objects/:id/records/api/v1person{ client?, fields?: { <name or id>: value } } (records.write: a member only for assigned Clients or none)Record (201) (defaults filled; 400 required, 400 bad_field_value; 409 archived object)
GET /objects/:id/records/:rid/api/v1personRecord (records.read, else 404 no_record); on clients, users or boards: { id, object, title, fields }
PATCH /objects/:id/records/:rid/api/v1person{ client?, fields?: { <name or id>: value | null }, archived? } (records.write on the old and the new Client; on clients/users/boards only fields, by clients.edit / team.users.role / board owner)Record; on a built-in { id, object, title, fields } (tickets: 409 builtin, use PATCH /tickets/:id)
DELETE /objects/:id/records/:rid/api/v1person{ ok } (records.write; a custom object's record only)
GET /openapi.json/api/v1anyoneOpenAPI 3.1 document of every /api/v1 route (public, no sign-in; someone on the team, or a key, also gets this team's custom objects it may read)
GET /people/api/v1person, session{ people: [{ id, name, email }] } (people you already share with; never a directory)
POST /portal/files/api/v1clientraw bytes, any content-type (?name=photo.jpg); PNG, JPEG, GIF, WebP, HEIC or PDF by the bytes, 10 MBFile (201): { id, name, type, size, image }, waiting 24 h to be sent with a request or reply (any client user; 413 too_big, 415 bad_type, 429 too_many_uploads at 20 waiting)
GET /portal/files/:id/api/v1clientthe bytes (nosniff, sandbox CSP; images inline, HEIC/PDF as downloads): your own waiting upload, or a file on a request you may read, sent with it, a reply or a PUBLISHED update; else 404 no_file
GET /portal/invite/api/v1anyone{ email, client: { name }, from: { name }, account: "new" | "existing", role: admin | requester | viewer } (public, ?token=d8c_...; 404 no_invite when used, expired, withdrawn or another Client's address; 429 per IP)
POST /portal/join/api/v1anyone{ token, name?, password } (new account) | { token } (signed in as the proven holder of the invited email){ person: { id, name, email }, csrfToken } (public; joins the Client with the invite's role, signs a new account in; 404 no_invite, 400 bad_password, 409 account_exists | other_account; JSON only: 415 json_only; from another site: 403 cross_site)
GET /portal/me/api/v1client{ person: { id, name, email }, csrfToken, clients: [{ id, name, owner: { name }, role, ownRequestsOnly, boards: [{ id, name }] }] } (on a Client's own address: that Client only; 403 not_client)
GET /portal/objects/api/v1client{ objects: [{ id, name, plural, icon, fields: [{ id, name, type, access: read | edit, required, help, options? }] }] } (?client=; portal objects only, never a hidden field; 404 no_client)
GET /portal/objects/:id/records/api/v1client{ records: PortalRecord[] } (?client=; that Client's own, newest first, up to 500; an object not in the portal is 404 no_object)
POST /portal/objects/:id/records/api/v1client{ client, fields?: { <name or id>: value } } (client admin or requester; edit fields only, another is 400 not_editable)PortalRecord (201) (viewer: 403 forbidden)
GET /portal/objects/:id/records/:rid/api/v1clientPortalRecord = { id, title, fields: { <name>: { type, value, option?, options?, access } }, createdAt, updatedAt } (?client=; another Client's: 404 no_record)
PATCH /portal/objects/:id/records/:rid/api/v1client{ client, fields: { <name or id>: value } } (client admin or requester; edit fields only, else 400 not_editable)PortalRecord
GET /portal/profile/api/v1clientProfile = { id, name, email, jobTitle, phone, timeZone, photo: url | null, hasPassword, canChangeEmail, events: [{ id: update_published, label, email, push }] }
PATCH /portal/profile/api/v1client{ name?, jobTitle?, phone?, timeZone?, photo?: <your uploaded image id> | null } (photo: PNG/JPEG/WebP/GIF up to 3 MB from POST /portal/files)Profile (400 bad_name, bad_phone, bad_time_zone, bad_photo)
GET /portal/profile/devices/api/v1client{ devices: [{ id, current, label: "Safari on iPhone", where: "203.0.113.x", signedInAt, lastSeenAt }] } (your own signed-in browsers)
DELETE /portal/profile/devices/:id/api/v1client{ ok, current } (signs that browser out; the current one clears its cookie; 404 no_device)
POST /portal/profile/devices/sign-out-others/api/v1client{ ok, count } (every browser but this one)
POST /portal/profile/email/api/v1client{ email, password }{ ok, mail: sent | console } (a 24 h link goes to the NEW address; nothing changes yet; 400 bad_email, bad_credentials, same_email, st8n_email; 429 after 3 an hour)
POST /portal/profile/email/confirm/api/v1client{ token } (the account that asked, signed in){ ok, email } (every other browser is signed out; the old address is told; 400 bad_token, 409 email_taken)
PUT /portal/profile/notify/api/v1client{ update_published?: { email?: boolean, push?: boolean } } (unknown events ignored; default on)Profile
GET /portal/profile/photo/:person/api/v1clientthe photo bytes (nosniff, sandbox CSP) (your own, or of someone you share a Client with: its owner, its active users, the team on it; else 404 no_photo, as for no photo)
GET /portal/push/key/api/v1client{ publicKey: string | null } (null: push is not set up here)
POST /portal/push/subscribe/api/v1client{ endpoint, keys: { p256dh, auth } } (endpoint: https on a known push service){ ok } (at most 10 devices, the oldest go; 404 push_off, 400 bad_endpoint | bad_keys, 409 endpoint_taken: another account's browser)
POST /portal/push/unsubscribe/api/v1client{ endpoint }{ ok } (your own subscription for that endpoint goes; none is still ok)
GET /portal/requests/api/v1client{ requests: Request[] } (?client=&board=&status=open | closed | all, default open; newest first; requesters see only their own when the Client's own-requests switch is on)
POST /portal/requests/api/v1client{ board?, title, details?, urgency?: low | normal | high | urgent, files?: [id], fields?: { <name>: value } } (board: needed when you see more than one; fields: edit ones only)Request (201) (client admin or requester; viewer 403 forbidden; 400 pick_board | no_title | bad_urgency | not_editable, 404 no_list; mails and pushes the team)
GET /portal/requests/:id/api/v1client{ request: Request, thread: Message[] } (published updates and client replies only, oldest first; never drafts, notes or internal files; one you may not read is 404 no_request)
POST /portal/requests/:id/close/api/v1clientRequest (status closed) (client admin any, requester their own; viewer 403 forbidden; the ticket itself is left as it is; already closed: the same answer)
POST /portal/requests/:id/replies/api/v1client{ text?, files?: [id] } (text up to 10,000; one of the two needed, else 400 no_text)Message (201) (client admin or requester; viewer 403 forbidden; 409 closed; clears Waiting on you; mails and pushes the team)
GET /portal/users/api/v1client{ users: ClientUser[], invites: [{ email, role, at, expiresAt }] } (?client=; client admin only (portal.users.view), else 403 forbidden; not one of yours: 404 no_client)
PATCH /portal/users/:person/api/v1client{ client, role: admin | requester | viewer } (client admin)ClientUser (400 bad_role, 404 no_user, 409 last_admin: the Client keeps one active client admin)
DELETE /portal/users/:person/api/v1client{ ok } (?client=; client admin; removed from that Client, signed out when they have no other access; 409 last_admin, 404 no_user)
POST /portal/users/:person/deactivate/api/v1client{ client } (client admin)ClientUser (status deactivated; signed out when they have no other access; 409 last_admin)
POST /portal/users/:person/reactivate/api/v1client{ client } (client admin)ClientUser (403 team_deactivated: the team turned them off, only the team turns them on)
POST /portal/users/:person/reset-link/api/v1client{ client } (client admin){ mail: sent } | { mail: console, link } (no mail set up) | { mail: withheld } (no mail set up and the user is also in another Client: a client admin is not handed that link; it is only logged) | { mail: none } (to the Client's address; 409 not_client_account: they reset their own; 429; 502 mail_failed)
GET /portal/users/audit/api/v1client{ entries: [{ id, at, by: { name }, action, target: { kind, name }, client: { id, name }, detail }] } (?client=&limit=, default 100, at most 500; client admin (portal.users.view); no ids of the team)
POST /portal/users/invite/api/v1client{ client, email, role?: admin | requester | viewer } (client admin; default requester){ invited, mail: sent } | { invited, mail: console | failed, link } (201) (14-day link; 400 bad_email | bad_role, 409 owner | team_invite | already, 429)
DELETE /portal/users/invites/:email/api/v1client{ ok } (?client=; client admin withdraws; ok false when there was none)
POST /portal/users/invites/:email/resend/api/v1client{ client } (client admin){ invited, mail: sent | console | failed, link? } (the same invite and role, a new link; the old one stops working; 404 no_invite)
GET /profile/api/v1personProfile = { id, name, email, jobTitle, phone, timeZone, photo: url | null, hasPassword, canChangeEmail, events: [{ id: new_request | customer_reply, label, email, push }] }
PATCH /profile/api/v1person{ name?, jobTitle?, phone?, timeZone?, photo?: <your uploaded image id> | null } (photo: PNG/JPEG/WebP/GIF up to 3 MB from POST /files)Profile (400 bad_name, bad_phone, bad_time_zone, bad_photo)
GET /profile/devices/api/v1person{ devices: [{ id, current, label: "Safari on iPhone", where: "203.0.113.x", signedInAt, lastSeenAt }] } (your own signed-in browsers)
DELETE /profile/devices/:id/api/v1person{ ok, current } (signs that browser out; the current one clears its cookie; 404 no_device)
POST /profile/devices/sign-out-others/api/v1person{ ok, count } (every browser but this one)
POST /profile/email/api/v1person{ email, password }{ ok, mail: sent | console } (a 24 h link goes to the NEW address; nothing changes yet; 400 bad_email, bad_credentials, same_email, st8n_email; 429 after 3 an hour)
POST /profile/email/confirm/api/v1person{ token } (the account that asked, signed in){ ok, email } (every other browser is signed out; the old address is told; 400 bad_token, 409 email_taken)
PUT /profile/notify/api/v1person{ new_request?: { email?, push? }, customer_reply?: { email?, push? } } (booleans; unknown events ignored; default on)Profile
GET /profile/photo/:person/api/v1personthe photo bytes (nosniff, sandbox CSP) (a team user: anyone's; an account of its own: its own and those it shares a Client with; else 404 no_photo, as for no photo)
GET /push/key/api/v1person{ publicKey: string | null } (null: push is not set up here)
POST /push/subscribe/api/v1person{ endpoint, keys: { p256dh, auth } } (endpoint: https on a known push service){ ok } (at most 10 devices, the oldest go; 404 push_off, 400 bad_endpoint | bad_keys, 409 endpoint_taken: another account's browser)
POST /push/unsubscribe/api/v1person{ endpoint }{ ok } (your own subscription for that endpoint goes; none is still ok)
GET /sessions/api/v1person, session{ sessions: Session[], st8ns: St8nPresence[] } (your own connected st8n computers' sessions; empty when none is on; with st8n off always { sessions: [], st8ns: [] })
GET /team/audit/api/v1person{ entries: AuditEntry[] } (?user=&client=&limit= (100, at most 500); newest first; team owner or admin, a key needs `admin`)
POST /team/invite/api/v1person{ email, role?: admin | member, clients?: [Client id] } (team owner or admin; role default member; clients: the team's Clients, for a member){ invited, mail: "sent" } | { invited, mail: "console" | "failed", link } (201) (applied when that address signs in; 400 bad_email | bad_role | bad_clients; 409 client_account | already)
DELETE /team/invites/:email/api/v1person{ ok } (withdraws the invite; ok: false when there was none; team owner or admin)
POST /team/invites/:email/resend/api/v1person{ invited, mail: "sent" | "console" | "failed", link? } (the same invite, role and Clients, mailed again; 404 no_invite; 429 rate_limited (20 an hour); team owner or admin)
GET /team/users/api/v1person{ users: TeamUser[], invites: [{ email, role, clients, at }] } (?q= matches name or email; TeamUser = { id, name, email, role, status, clients: [{ id, name }], lastActiveAt, createdAt }; team owner or admin)
PATCH /team/users/:id/api/v1person{ role?: owner | admin | member, clients?: [Client id] } (team owner; an admin only on a non-owner and never to owner; clients: a member's assigned Clients)TeamUser (an owner or admin made a member hands their Clients and boards to you; 400 bad_role | bad_clients; 404 no_user; 409 last_owner)
DELETE /team/users/:id/api/v1person{ ok } (off the team: no role, no assigned Clients, no shares on the team's boards; an owner's or admin's Clients and boards move to you, their sessions and keys end; 409 last_owner; 404 no_user)
POST /team/users/:id/deactivate/api/v1personTeamUser (status deactivated) (sign-in blocked, every session ends at once, their webhooks turn off; an admin only on a non-owner; 409 last_owner; 404 no_user)
POST /team/users/:id/reactivate/api/v1personTeamUser (status active) (team owner; an admin only on a non-owner; 404 no_user)
POST /team/users/:id/reset-link/api/v1person{ mail: "sent" } | { mail: "console", link } | { mail: "none" } (st8n sign-in, no password here) (24 hours, once; an admin never for an owner; 404 no_user; 429 rate_limited; 502 mail_failed)
GET /tickets/api/v1person, session{ tickets: Ticket[], next: n | null } (?list=&status=open,working&assignee=&label=&q=&batch=&machine=&mine=1&before=<n>&limit=&sort=queue | f.<F-n> | -f.<F-n>&f.<F-n>=<filter>) filter: dropdown O-1,O-2 | yes/no yes|no | text: contains | number/date a..b, a.., ..b | none | any
POST /tickets/api/v1person{ list?, title, details?, acceptance?, where?, assignee?, priority?, labels?, fields?: { <name or id>: value }, parent? } (list default: your Inbox; parent: a new sub-ticket on the parent's list, docs/SUBTICKETS.md)Ticket (201)
GET /tickets/:id/api/v1person, sessionTicket with timeline, can, parent, children and related (docs/SUBTICKETS.md)
PATCH /tickets/:id/api/v1person{ title?, details?, acceptance?, where?, priority?, labels?, list?, fields?: { <name or id>: value | null } } (fields merge; null clears)Ticket
DELETE /tickets/:id/api/v1person{ confirm: <the ticket id> } (list owner or manager, or whoever added it; not while a session holds it){ ok, id }
POST /tickets/:id/assign/api/v1person{ assignee, st8n? } (st8n: the link id a session: assignee runs behind)Ticket
POST /tickets/:id/block/api/v1session, person{ why }Ticket
POST /tickets/:id/cancel/api/v1person{ why? }Ticket
POST /tickets/:id/claim/api/v1session{ folder? }Ticket
GET /tickets/:id/customer/api/v1person{ request: Request | null, thread: TeamMessage[] } (anyone on the list; null: not a client request; the thread includes drafts)
POST /tickets/:id/customer/updates/api/v1person{ text, waiting?, files?: [id] } (list owner or manager; waiting: the request waits on the Client once published)TeamMessage (201) (a draft; 409 not_a_request, 400 no_text)
PATCH /tickets/:id/customer/updates/:uid/api/v1person{ text?, waiting?, files? } (list owner or manager; files replaces the draft's files)TeamMessage (409 published: write a new one)
DELETE /tickets/:id/customer/updates/:uid/api/v1person{ ok } (list owner or manager; a draft only, its files go too; 409 published)
POST /tickets/:id/customer/updates/:uid/publish/api/v1personTeamMessage (list owner or manager; the client users who may read the request see it; while its board is shown, mail and push go to those who chose them; again: no-op)
POST /tickets/:id/done/api/v1session, personEvidenceTicket
GET /tickets/:id/files/api/v1person{ files: (File & { on: request | reply | update | ticket, message, published, by: { name }, at })[] } (anyone on the list; 404 not_found)
POST /tickets/:id/files/api/v1person{ files: [id], note? } (owner, manager or work on the list; at most 5, each your own waiting upload){ files: File[] } (201) (internal: never shown to the Client; a timeline note says so; 403 read_only; 404 not_found)
DELETE /tickets/:id/files/:fid/api/v1person{ ok } (whoever added it, or the list owner or a manager; 403 not_editor; 404 not_found | no_file; 409 sent: the Client already has it)
POST /tickets/:id/note/api/v1session, person{ text }Ticket
PUT /tickets/:id/parent/api/v1person{ parent: "T-n" }Ticket with parent, children, related (one level, same list; docs/SUBTICKETS.md; 404 not_found)
DELETE /tickets/:id/parent/api/v1personTicket with parent, children, related (no longer a sub-ticket)
POST /tickets/:id/related/api/v1person{ ticket: "T-n" }Ticket with parent, children, related (both ways; at most 20; 404 not_found)
DELETE /tickets/:id/related/:other/api/v1personTicket with parent, children, related
POST /tickets/:id/release/api/v1session, person{ why? } (person: list owner)Ticket
POST /tickets/:id/renew/api/v1session{ leaseUntil }
POST /tickets/:id/reopen/api/v1person{ why? }Ticket
POST /tickets/:id/session/api/v1person{} (an open ticket you may work; your st8n connected and on){ ticket, session: { id, name, machine } } (201): your st8n started a session told to work only this ticket; it claims it. 403 person_only (a session, a link or an API key: a person at the page only), 403 no_work; 404 st8n_off with st8n off; 409 no_st8n | not_open, or what your st8n said
POST /tickets/:id/start/api/v1sessionTicket
GET /tickets/:id/track/api/v1person, session{ track: TicketTrack, timeline: Change[] } (anyone who sees the ticket; track: opened, claimed, done, review, merged, verified, times...; timeline oldest first)
POST /tickets/:id/track/api/v1person, session{ type: merged | reviewed | verified, key, at?, text? } (work rights on the list; key: one mark per key){ ok, added } (added false: that key was already written; verified only by the list owner or a manager: 403 not_manager; 403 read_only; at held between the ticket's making and now)
POST /tickets/ask/api/v1person, session{ question, list? } (question under 1,000 chars; list: one board, else every list you see){ say, cites, looked, refused?, model? } (read only; 503 helper_off without a model; 429 rate_limited at 30 an hour per person, every door, link and session together, helper_cap, list_rate_limited)
POST /tickets/bulk/api/v1person{ list?, text, defaults?: { where, assignee, priority, labels } }{ batch, tickets } (201)
GET /tickets/changes/api/v1person, session{ since, changes: [{ ticket, title, at, type, from, to, text, who }] } (newest first; ?since=<ms or date, default 24 h ago>&list=&limit= up to 5000, default 500; 10 a minute per person across reports, changes and workers: 429 rate_limited)
GET /tickets/imported/:site/:id/api/v1person, sessionTicket with can and importedFrom: { site, id } (the ticket st8n imported under that old id; else 404)
POST /tickets/next/api/v1session{ list?, labels?, folder?, ties? }{ ticket: Ticket | null } (claimed for this session); with ties: true it claims nothing: { ticket: null, ties: Ticket[] } (the tied first candidates, at most 20)
GET /tickets/proposals/api/v1person{ proposals: Proposal[] } (?status=waiting|approved|declined; the lists you own)
POST /tickets/proposals/api/v1session{ kind: create|edit|close|cancel|reopen|move|assign, ticket?, list?, changes: { title?, details?, acceptance?, where?, priority?, labels?, fields?, summary? (close), list? (move), assignee? (assign) }, why? } (10 an hour per session)Proposal (201) (P-n, waiting: nothing changes until the list owner approves). The ticket's own assignee editing or closing it: { applied: true, kind, ticket } at once, on the timeline (T-330)
GET /tickets/proposals/:id/api/v1personProposal
POST /tickets/proposals/:id/approve/api/v1person{ edits?: { title?, details?, acceptance?, where?, priority?, labels?, list?, fields? } } (list owner){ proposal, ticket }
POST /tickets/proposals/:id/decline/api/v1person{ why? } (list owner)Proposal
POST /tickets/proposals/approve/api/v1person{ ids: [P-n, ...] } (list owner; more than 3 needs a fresh sign-in){ approved: { proposal, ticket }[] }
POST /tickets/propose/api/v1session{ list?, title, details?, acceptance?, where?, priority?, labels?, fields? } (at most 10 an hour per session)Proposal (201) (P-n, status waiting: not a ticket until approved)
GET /tickets/reports/api/v1person, session{ at, from, to, filters, totals, perDay, times, workers, teams, stuck, tracks? } (?list=&days=&from=&to=&tz=&worker=&team=&label=&status=&tracks=1; &format=csv&part=perDay|workers|teams|stuck|tickets: text/csv; 10 a minute per person across reports, changes and workers: 429 rate_limited)
GET /tickets/workers/api/v1person, session{ workers: [{ worker, name, machine, team, tickets, done, sentBack, blocked, released, holding, busyMs, idleMs, idleNowMs, avgClaimToDoneMs, firstSeen, lastSeen }] } (?list=&days=&from=&to=; 10 a minute per person across reports, changes and workers: 429 rate_limited)
GET /tokens/api/v1person{ tokens: WorkToken[] }
POST /tokens/api/v1person{ computer, lists, days? }{ id, token: "stt_..." (shown once), ... } (201)
DELETE /tokens/:id/api/v1person{ ok }
POST /block/:id/tickets/v1sessionas /tickets/:id/block
POST /claim/:id/tickets/v1sessionas /tickets/:id/claim
POST /done/:id/tickets/v1sessionas /tickets/:id/done
GET /get/:id/tickets/v1sessionTicket
GET /lists/tickets/v1session{ lists: List[] } (the token's)
POST /next/tickets/v1session{ list?, labels?, folder? }{ ticket }
POST /note/:id/tickets/v1sessionas /tickets/:id/note
POST /release/:id/tickets/v1sessionas /tickets/:id/release
POST /renew/:id/tickets/v1sessionas /tickets/:id/renew
POST /start/:id/tickets/v1sessionas /tickets/:id/start