01My two-factor authentication status
/v1/account/2faNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorStatusDto |
| Field | Type | What it is |
|---|---|---|
| enabled required | boolean | |
| backupCodesRemaining required | number | Unused backup codes left. Meaningless (0) while enabled is false. |
02Replace the backup codes with ten new ones
/v1/account/2fa/backup-codes/regenerateRequires your password. The new codes are shown once and replace every unused one.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| password required | string |
| Status | Body | When |
|---|---|---|
| 200 | BackupCodesResponseDto |
| Field | Type | What it is |
|---|---|---|
| backupCodes required | string[] | Shown once. Neither this response nor any stored record ever contains these again — only a bcrypt hash of each is kept, exactly like a password. |
03Confirm 2FA enrolment
/v1/account/2fa/enableProves the authenticator app actually has the secret, turns 2FA on, and returns backup codes — shown once.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| code required | string |
| Status | Body | When |
|---|---|---|
| 200 | BackupCodesResponseDto |
| Field | Type | What it is |
|---|---|---|
| backupCodes required | string[] | Shown once. Neither this response nor any stored record ever contains these again — only a bcrypt hash of each is kept, exactly like a password. |
04Start 2FA enrolment
/v1/account/2fa/setupCreates a new secret and its QR code. Not active until POST /account/2fa/enable confirms it.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorSetupResponseDto |
| Field | Type | What it is |
|---|---|---|
| secret required | string | For manual entry, when the authenticator app can't scan a code. |
| otpauthUrl required | string | |
| qrCodeDataUrl required | string | A data:image/png;base64,... QR code of otpauthUrl — draw it straight into an <img>. |
05Set or clear my avatar (data: URL, null to clear)
/v1/account/avatarNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| avatarDataUrl required | string | null |
| Status | Body | When |
|---|---|---|
| 200 | ProfileDto |
| Field | Type | What it is |
|---|---|---|
| name required | string | null | |
| email required | string | |
| jobTitle required | string | null | |
| emailVerified required | boolean | |
| avatarDataUrl required | string | null | The avatar as a data: URL, or null if none is set. |
06Invoices, read live from Stripe
/v1/account/invoicesavailable: false means billing is not configured here — an empty list then means "we could not ask", not "there are none".
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | MyInvoicesDto |
| Field | Type | What it is |
|---|---|---|
| invoices required | MyInvoiceDto[] | |
| invoices[].id required | string | Stripe invoice id. |
| invoices[].number required | string | |
| invoices[].created required | string | ISO-8601. |
| invoices[].status required | string | paid | open | void | uncollectible | draft |
| invoices[].amountDue required | number | Minor units, e.g. cents. |
| invoices[].amountPaid required | number | |
| invoices[].currency required | string | ISO-4217, lowercase, as Stripe reports it. |
| invoices[].hostedInvoiceUrl | string | null | Stripe-hosted invoice page. Short-lived — do not store it. |
| invoices[].invoicePdf | string | null | |
| invoices[].organizationName | string | null | |
| available required | boolean | False when billing is not configured on this deployment. The list is then empty because we cannot ask Stripe, not because there are no invoices. |
07My profile
/v1/account/meNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | ProfileDto |
| Field | Type | What it is |
|---|---|---|
| name required | string | null | |
| email required | string | |
| jobTitle required | string | null | |
| emailVerified required | boolean | |
| avatarDataUrl required | string | null | The avatar as a data: URL, or null if none is set. |
08Update my profile (name, job title, email)
/v1/account/meChanging the email clears its verified state — the address is how an account is recovered, so a new one is not taken on trust.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| name | string | The person's name. Optional, but when given it must not be empty or only spaces. |
| jobTitle | string | Optional, but " " is not a job title either — an empty string is the way to clear it. |
| string (email) |
| Status | Body | When |
|---|---|---|
| 200 | ProfileDto |
| Field | Type | What it is |
|---|---|---|
| name required | string | null | |
| email required | string | |
| jobTitle required | string | null | |
| emailVerified required | boolean | |
| avatarDataUrl required | string | null | The avatar as a data: URL, or null if none is set. |
09My two-factor authentication status
/v1/account/me/2faNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorStatusDto |
| Field | Type | What it is |
|---|---|---|
| enabled required | boolean | |
| backupCodesRemaining required | number | Unused backup codes left. Meaningless (0) while enabled is false. |
10Replace the backup codes with ten new ones
/v1/account/me/2fa/backup-codes/regenerateNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| password required | string |
| Status | Body | When |
|---|---|---|
| 200 | BackupCodesResponseDto |
| Field | Type | What it is |
|---|---|---|
| backupCodes required | string[] | Shown once. Neither this response nor any stored record ever contains these again — only a bcrypt hash of each is kept, exactly like a password. |
11Confirm 2FA enrolment
/v1/account/me/2fa/enableTurns 2FA on and returns backup codes — shown once.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| code required | string |
| Status | Body | When |
|---|---|---|
| 200 | BackupCodesResponseDto |
| Field | Type | What it is |
|---|---|---|
| backupCodes required | string[] | Shown once. Neither this response nor any stored record ever contains these again — only a bcrypt hash of each is kept, exactly like a password. |
12Start 2FA enrolment
/v1/account/me/2fa/setupCreates a new secret and its QR code. Not active until POST me/2fa/enable confirms it.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorSetupResponseDto |
| Field | Type | What it is |
|---|---|---|
| secret required | string | For manual entry, when the authenticator app can't scan a code. |
| otpauthUrl required | string | |
| qrCodeDataUrl required | string | A data:image/png;base64,... QR code of otpauthUrl — draw it straight into an <img>. |
13My notification preferences, with the catalogue that describes them
/v1/account/me/notificationsNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | NotificationPrefsResponseDto |
| Field | Type | What it is |
|---|---|---|
| options required | NotificationOptionDto[] | Every notification this account can control, with its current state. |
| options[].id required | string | Catalogue id. Also the key preferences are stored under. |
| options[].label required | string | |
| options[].description required | string | What will actually arrive, not what the feature is. |
| options[].group required | "account" | "security" | "domains" | "compliance" | "team" | "billing" | "reports" | Section heading in the settings screen. |
| options[].required required | boolean | True when this cannot be switched off — access, money or security, where silence would be worse than an unwanted email. Render it as locked, not as an enabled toggle: a switch that does nothing is worse than no switch. |
| options[].digestible required | boolean | True when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting. |
| options[].marketing required | boolean | True when this is marketing and requires an explicit opt-in. |
| options[].email required | boolean | Whether this recipient currently receives it by email. |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | How often digestible events arrive. immediate sends each one as it happens; off silences them without touching anything required. |
| emailEnabled required | boolean | Master switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this. |
| timeZone | string | null | The zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used. |
14Update my notification preferences
/v1/account/me/notificationsNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| options required | NotificationPrefUpdateDto[] | |
| options[].id required | string | |
| options[].email required | boolean | |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | |
| emailEnabled required | boolean |
| Status | Body | When |
|---|---|---|
| 200 | NotificationPrefsResponseDto |
| Field | Type | What it is |
|---|---|---|
| options required | NotificationOptionDto[] | Every notification this account can control, with its current state. |
| options[].id required | string | Catalogue id. Also the key preferences are stored under. |
| options[].label required | string | |
| options[].description required | string | What will actually arrive, not what the feature is. |
| options[].group required | "account" | "security" | "domains" | "compliance" | "team" | "billing" | "reports" | Section heading in the settings screen. |
| options[].required required | boolean | True when this cannot be switched off — access, money or security, where silence would be worse than an unwanted email. Render it as locked, not as an enabled toggle: a switch that does nothing is worse than no switch. |
| options[].digestible required | boolean | True when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting. |
| options[].marketing required | boolean | True when this is marketing and requires an explicit opt-in. |
| options[].email required | boolean | Whether this recipient currently receives it by email. |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | How often digestible events arrive. immediate sends each one as it happens; off silences them without touching anything required. |
| emailEnabled required | boolean | Master switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this. |
| timeZone | string | null | The zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used. |
15Change my password
/v1/account/me/passwordVerifies the current password, signs out every other session, and emails a security notice. The same operation as the panel’s POST /v1/account/password.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| currentPassword required | string | |
| newPassword required | string | The new password: 8 to 128 characters, the same rule as at sign-up. It must differ from the current one. |
| Status | Body | When |
|---|---|---|
| 204 | — |
16My active sessions
/v1/account/me/sessionsWorkspace sessions — a refresh token is issued per workspace sign-in. The account token this call was made with is not one: it is short-lived and there is nothing to revoke.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | SessionDto[] |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| createdAt required | string | |
| expiresAt required | string | |
| userAgent required | string | null | |
| ip required | string | null | |
| current required | boolean | The session the current access token belongs to (best-effort: most recent). |
17Sign out all other sessions
/v1/account/me/sessions/othersNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 204 | — |
18Sign out one session
/v1/account/me/sessions/{id}Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| id required | path | string |
| Status | Body | When |
|---|---|---|
| 204 | — |
19My notification preferences, with the catalogue that describes them
/v1/account/notificationsEvery notification this account can control, with its current state.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | NotificationPrefsResponseDto |
| Field | Type | What it is |
|---|---|---|
| options required | NotificationOptionDto[] | Every notification this account can control, with its current state. |
| options[].id required | string | Catalogue id. Also the key preferences are stored under. |
| options[].label required | string | |
| options[].description required | string | What will actually arrive, not what the feature is. |
| options[].group required | "account" | "security" | "domains" | "compliance" | "team" | "billing" | "reports" | Section heading in the settings screen. |
| options[].required required | boolean | True when this cannot be switched off — access, money or security, where silence would be worse than an unwanted email. Render it as locked, not as an enabled toggle: a switch that does nothing is worse than no switch. |
| options[].digestible required | boolean | True when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting. |
| options[].marketing required | boolean | True when this is marketing and requires an explicit opt-in. |
| options[].email required | boolean | Whether this recipient currently receives it by email. |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | How often digestible events arrive. immediate sends each one as it happens; off silences them without touching anything required. |
| emailEnabled required | boolean | Master switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this. |
| timeZone | string | null | The zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used. |
20Update my notification preferences
/v1/account/notificationsNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| options required | NotificationPrefUpdateDto[] | |
| options[].id required | string | |
| options[].email required | boolean | |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | |
| emailEnabled required | boolean |
| Status | Body | When |
|---|---|---|
| 200 | NotificationPrefsResponseDto |
| Field | Type | What it is |
|---|---|---|
| options required | NotificationOptionDto[] | Every notification this account can control, with its current state. |
| options[].id required | string | Catalogue id. Also the key preferences are stored under. |
| options[].label required | string | |
| options[].description required | string | What will actually arrive, not what the feature is. |
| options[].group required | "account" | "security" | "domains" | "compliance" | "team" | "billing" | "reports" | Section heading in the settings screen. |
| options[].required required | boolean | True when this cannot be switched off — access, money or security, where silence would be worse than an unwanted email. Render it as locked, not as an enabled toggle: a switch that does nothing is worse than no switch. |
| options[].digestible required | boolean | True when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting. |
| options[].marketing required | boolean | True when this is marketing and requires an explicit opt-in. |
| options[].email required | boolean | Whether this recipient currently receives it by email. |
| digestCadence required | "immediate" | "daily" | "weekly" | "off" | How often digestible events arrive. immediate sends each one as it happens; off silences them without touching anything required. |
| emailEnabled required | boolean | Master switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this. |
| timeZone | string | null | The zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used. |
21Change my password (signs out other sessions)
/v1/account/passwordNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| currentPassword required | string | |
| newPassword required | string | The new password: 8 to 128 characters, the same rule as at sign-up. It must differ from the current one. |
| Status | Body | When |
|---|---|---|
| 204 | — |
22Get my UI preferences
/v1/account/preferencesNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | PreferencesDto |
| Field | Type | What it is |
|---|---|---|
| language | string | |
| timezone | string | |
| defaultPage | string | |
| density | "compact" | "comfortable" | "cozy" | |
| themeMode | "light" | "dark" | "system" | |
| accent | string | The panel's accent colour, as a hex colour such as #e85520. |
23Update my UI preferences
/v1/account/preferencesNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| language | string | |
| timezone | string | |
| defaultPage | string | |
| density | "compact" | "comfortable" | "cozy" | |
| themeMode | "light" | "dark" | "system" | |
| accent | string | The panel's accent colour, as a hex colour such as #e85520. |
| Status | Body | When |
|---|---|---|
| 200 | PreferencesDto |
| Field | Type | What it is |
|---|---|---|
| language | string | |
| timezone | string | |
| defaultPage | string | |
| density | "compact" | "comfortable" | "cozy" | |
| themeMode | "light" | "dark" | "system" | |
| accent | string | The panel's accent colour, as a hex colour such as #e85520. |
24Get my profile
/v1/account/profileNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | ProfileDto |
| Field | Type | What it is |
|---|---|---|
| name required | string | null | |
| email required | string | |
| jobTitle required | string | null | |
| emailVerified required | boolean | |
| avatarDataUrl required | string | null | The avatar as a data: URL, or null if none is set. |
25Update my profile (name, job title, email)
/v1/account/profileNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| name | string | The person's name. Optional, but when given it must not be empty or only spaces. |
| jobTitle | string | Optional, but " " is not a job title either — an empty string is the way to clear it. |
| string (email) |
| Status | Body | When |
|---|---|---|
| 200 | ProfileDto |
| Field | Type | What it is |
|---|---|---|
| name required | string | null | |
| email required | string | |
| jobTitle required | string | null | |
| emailVerified required | boolean | |
| avatarDataUrl required | string | null | The avatar as a data: URL, or null if none is set. |
26List my active sessions
/v1/account/sessionsNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | SessionDto[] |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| createdAt required | string | |
| expiresAt required | string | |
| userAgent required | string | null | |
| ip required | string | null | |
| current required | boolean | The session the current access token belongs to (best-effort: most recent). |
27Sign out all other sessions
/v1/account/sessions/othersNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 204 | — |
28Sign out one session
/v1/account/sessions/{id}Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| id required | path | string |
| Status | Body | When |
|---|---|---|
| 204 | — |
29What each workspace’s bill is for, divided by client
/v1/account/statementsAn agency pays one subscription and rebills its clients out of it. This divides the charge by billed domains per domain group — the same unit the plan allowance counts — and reports anything division left over as its own line, so the statement adds up to the invoice exactly. Owners only.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | MyStatementsDto |
| Field | Type | What it is |
|---|---|---|
| statements required | WorkspaceStatementDto[] | |
| statements[].organizationId required | string | |
| statements[].organizationName required | string | |
| statements[].plan required | string | |
| statements[].currency required | string | ISO-4217, lowercase. |
| statements[].total required | number | What the workspace is charged for the period, in minor units. |
| statements[].units required | number | Billed domains across the workspace. |
| statements[].clients required | ClientShareDto[] | |
| statements[].rounding required | number | Whatever division left over, in minor units. Its own line rather than hidden in a client’s share, so the statement adds up to the invoice exactly. |
30One workspace’s client statement, as a CSV
/v1/account/statements/{organizationId}.csvThe file an agency pastes into their own billing. Owners only; 404 when the workspace has no charge to divide (a demo, or an enterprise agreement).
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| organizationId required | path | string |
| Status | Body | When |
|---|---|---|
| 200 | — |
31What this account pays for
/v1/account/subscriptionsWorkspaces this account OWNS. Being a member is not enough.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | MySubscriptionsDto |
| Field | Type | What it is |
|---|---|---|
| subscriptions required | MySubscriptionDto[] | |
| subscriptions[].organizationId required | string | |
| subscriptions[].organizationName required | string | |
| subscriptions[].slug required | string | |
| subscriptions[].plan required | string | |
| subscriptions[].status | string | null | Stripe’s own status (active, past_due, canceled, …). Null for a demo, which has no subscription behind it. |
| subscriptions[].currentPeriodEnd | string | null | |
| subscriptions[].cancelAt | string | null | When the subscription is set to END instead of renewing — a cancellation scheduled in the billing portal. Null while it renews. Until this moment it can be withdrawn with POST /v1/billing/subscription/keep. |
| subscriptions[].contractEndedAt | string | null | When a paid subscription ended. Null while paying, and for a demo. |
| subscriptions[].dataDeletedAt | string | null | When the workspace and its data will be deleted, for one whose subscription has ended. Null when nothing is scheduled. |
| subscriptions[].vatNumber | string | null | The EU or UK VAT number on this workspace’s invoices, as Stripe holds it. Null when none has been given. Example: "DE123456789" |
| subscriptions[].vatNumberStatus | "pending" | "verified" | "unverified" | "unavailable" | null | Whether the tax authority’s register confirmed the number. pending while the check is running; unavailable when the register could not be reached. |
| subscriptions[].trialEndsAt | string | null | |
| subscriptions[].currency | "usd" | "gbp" | "eur" | null | The currency this workspace is billed in. Null when there is no subscription yet, which is the only time the currency can still be chosen — Stripe cannot re-denominate a live subscription. |
| subscriptions[].isDemo required | boolean | True when this is a demo rather than a paid plan. |
32Who is in each of my workspaces
/v1/account/teamMembers are listed only for workspaces where the caller is an owner or admin; elsewhere the workspace still appears with members: null and a count. Seeing a colleague’s email address is a permission, and holding an account token is not one.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | MyTeamDto |
| Field | Type | What it is |
|---|---|---|
| workspaces required | MyTeamWorkspaceDto[] | |
| workspaces[].organizationId required | string | |
| workspaces[].name required | string | |
| workspaces[].slug required | string | |
| workspaces[].role required | string | This user’s role here. |
| workspaces[].memberCount required | number | Members in this workspace, whether or not they are listed. |
| workspaces[].members | TeamMemberDto[] | null | Null when the caller’s role here does not permit seeing who the members are — the count is still true. An empty array would claim the workspace is empty. |
| workspaces[].manageUrl required | string | Where to manage members — the workspace’s own panel. |
33Every workspace this account belongs to
/v1/account/workspacesNeeds: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | MyWorkspacesDto |
| Field | Type | What it is |
|---|---|---|
| workspaces required | MyWorkspaceDto[] | |
| workspaces[].id required | string | |
| workspaces[].name required | string | |
| workspaces[].slug required | string | |
| workspaces[].url required | string | Where to sign in to this workspace. |
| workspaces[].role required | string | This user’s role here. |
| workspaces[].plan required | string | demo | starter | pro | agency | enterprise |
| workspaces[].status required | string | active | suspended |
| workspaces[].trialEndsAt | string | null | When the demo stops working. Null on paid plans. |
| workspaces[].members required | number | Members in this workspace. |
| workspaces[].purgeAfter | string | null | When this expired demo's data is destroyed. Null unless it is a demo with an end date. Surfaced because a deletion nobody can see coming is not a promise anyone can act on — this is the date by which paying still restores everything. |