Account
A person's own account across workspaces: profile, security, subscriptions.
- GET/v1/account/2fa
- POST/v1/account/2fa/backup-codes/regenerate
- POST/v1/account/2fa/enable
- POST/v1/account/2fa/setup
- PUT/v1/account/avatar
- GET/v1/account/invoices
- GET/v1/account/me
- PATCH/v1/account/me
- GET/v1/account/me/2fa
- POST/v1/account/me/2fa/backup-codes/regenerate
- POST/v1/account/me/2fa/enable
- POST/v1/account/me/2fa/setup
- GET/v1/account/me/notifications
- PUT/v1/account/me/notifications
- POST/v1/account/me/password
- GET/v1/account/me/sessions
- DELETE/v1/account/me/sessions/others
- DELETE/v1/account/me/sessions/{id}
- GET/v1/account/notifications
- PUT/v1/account/notifications
- POST/v1/account/password
- GET/v1/account/preferences
- PUT/v1/account/preferences
- GET/v1/account/profile
- PATCH/v1/account/profile
- GET/v1/account/sessions
- DELETE/v1/account/sessions/others
- DELETE/v1/account/sessions/{id}
- GET/v1/account/statements
- GET/v1/account/statements/{organizationId}.csv
- GET/v1/account/subscriptions
- GET/v1/account/team
- GET/v1/account/workspaces
/v1/account/2faMy two-factor authentication status
Needs: 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. |
/v1/account/2fa/backup-codes/regenerateReplace the backup codes with ten new ones
Requires 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. |
/v1/account/2fa/enableConfirm 2FA enrolment
Proves 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. |
/v1/account/2fa/setupStart 2FA enrolment
Creates 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>. |
/v1/account/avatarSet or clear my avatar (data: URL, null to clear)
Needs: 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. |
/v1/account/invoicesInvoices, read live from Stripe
available: 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. |
/v1/account/meMy profile
Needs: 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. |
/v1/account/meUpdate my profile (name, job title, email)
Changing 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. |
/v1/account/me/2faMy two-factor authentication status
Needs: 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. |
/v1/account/me/2fa/backup-codes/regenerateReplace the backup codes with ten new ones
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. |
/v1/account/me/2fa/enableConfirm 2FA enrolment
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. |
/v1/account/me/2fa/setupStart 2FA enrolment
Creates 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>. |
/v1/account/me/notificationsMy notification preferences, with the catalogue that describes them
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. |
/v1/account/me/notificationsUpdate my notification preferences
Needs: 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. |
/v1/account/me/passwordChange my password
Verifies 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 | — |
/v1/account/me/sessionsMy active sessions
Workspace 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). |
/v1/account/me/sessions/othersSign out all other sessions
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 204 | — |
/v1/account/me/sessions/{id}Sign out one session
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 | — |
/v1/account/notificationsMy notification preferences, with the catalogue that describes them
Every 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. |
/v1/account/notificationsUpdate my notification preferences
Needs: 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. |
/v1/account/passwordChange my password (signs out other sessions)
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 | — |
/v1/account/preferencesGet my UI preferences
Needs: 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. |
/v1/account/preferencesUpdate my UI preferences
Needs: 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. |
/v1/account/profileGet my profile
Needs: 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. |
/v1/account/profileUpdate my profile (name, job title, email)
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. |
/v1/account/sessionsList my active sessions
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). |
/v1/account/sessions/othersSign out all other sessions
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 204 | — |
/v1/account/sessions/{id}Sign out one session
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 | — |
/v1/account/statementsWhat each workspace’s bill is for, divided by client
An 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. |
/v1/account/statements/{organizationId}.csvOne workspace’s client statement, as a CSV
The 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 | — |
/v1/account/subscriptionsWhat this account pays for
Workspaces 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. |
/v1/account/teamWho is in each of my workspaces
Members 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. |
/v1/account/workspacesEvery workspace this account belongs to
Needs: 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. |
Next groupContact