API reference

Account

A person's own account across workspaces: profile, security, subscriptions.

  1. GET/v1/account/2fa
  2. POST/v1/account/2fa/backup-codes/regenerate
  3. POST/v1/account/2fa/enable
  4. POST/v1/account/2fa/setup
  5. PUT/v1/account/avatar
  6. GET/v1/account/invoices
  7. GET/v1/account/me
  8. PATCH/v1/account/me
  9. GET/v1/account/me/2fa
  10. POST/v1/account/me/2fa/backup-codes/regenerate
  11. POST/v1/account/me/2fa/enable
  12. POST/v1/account/me/2fa/setup
  13. GET/v1/account/me/notifications
  14. PUT/v1/account/me/notifications
  15. POST/v1/account/me/password
  16. GET/v1/account/me/sessions
  17. DELETE/v1/account/me/sessions/others
  18. DELETE/v1/account/me/sessions/{id}
  19. GET/v1/account/notifications
  20. PUT/v1/account/notifications
  21. POST/v1/account/password
  22. GET/v1/account/preferences
  23. PUT/v1/account/preferences
  24. GET/v1/account/profile
  25. PATCH/v1/account/profile
  26. GET/v1/account/sessions
  27. DELETE/v1/account/sessions/others
  28. DELETE/v1/account/sessions/{id}
  29. GET/v1/account/statements
  30. GET/v1/account/statements/{organizationId}.csv
  31. GET/v1/account/subscriptions
  32. GET/v1/account/team
  33. GET/v1/account/workspaces
GET/v1/account/2fa

My two-factor authentication status

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200TwoFactorStatusDto
200 body · TwoFactorStatusDto
FieldTypeWhat it is
enabled requiredboolean
backupCodesRemaining requirednumberUnused backup codes left. Meaningless (0) while enabled is false.
POST/v1/account/2fa/backup-codes/regenerate

Replace 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>.

Request body · RegenerateBackupCodesDto
FieldTypeWhat it is
password requiredstring
Responses
StatusBodyWhen
200BackupCodesResponseDto
200 body · BackupCodesResponseDto
FieldTypeWhat it is
backupCodes requiredstring[]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.
POST/v1/account/2fa/enable

Confirm 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>.

Request body · TwoFactorEnableDto
FieldTypeWhat it is
code requiredstring
Responses
StatusBodyWhen
200BackupCodesResponseDto
200 body · BackupCodesResponseDto
FieldTypeWhat it is
backupCodes requiredstring[]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.
POST/v1/account/2fa/setup

Start 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>.

Responses
StatusBodyWhen
200TwoFactorSetupResponseDto
200 body · TwoFactorSetupResponseDto
FieldTypeWhat it is
secret requiredstringFor manual entry, when the authenticator app can't scan a code.
otpauthUrl requiredstring
qrCodeDataUrl requiredstringA data:image/png;base64,... QR code of otpauthUrl — draw it straight into an <img>.
PUT/v1/account/avatar

Set or clear my avatar (data: URL, null to clear)

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · UpdateAvatarDto
FieldTypeWhat it is
avatarDataUrl requiredstring | null
Responses
StatusBodyWhen
200ProfileDto
200 body · ProfileDto
FieldTypeWhat it is
name requiredstring | null
email requiredstring
jobTitle requiredstring | null
emailVerified requiredboolean
avatarDataUrl requiredstring | nullThe avatar as a data: URL, or null if none is set.
GET/v1/account/invoices

Invoices, 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>.

Responses
StatusBodyWhen
200MyInvoicesDto
200 body · MyInvoicesDto
FieldTypeWhat it is
invoices requiredMyInvoiceDto[]
invoices[].id requiredstringStripe invoice id.
invoices[].number requiredstring
invoices[].created requiredstringISO-8601.
invoices[].status requiredstringpaid | open | void | uncollectible | draft
invoices[].amountDue requirednumberMinor units, e.g. cents.
invoices[].amountPaid requirednumber
invoices[].currency requiredstringISO-4217, lowercase, as Stripe reports it.
invoices[].hostedInvoiceUrlstring | nullStripe-hosted invoice page. Short-lived — do not store it.
invoices[].invoicePdfstring | null
invoices[].organizationNamestring | null
available requiredbooleanFalse when billing is not configured on this deployment. The list is then empty because we cannot ask Stripe, not because there are no invoices.
GET/v1/account/me

My profile

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200ProfileDto
200 body · ProfileDto
FieldTypeWhat it is
name requiredstring | null
email requiredstring
jobTitle requiredstring | null
emailVerified requiredboolean
avatarDataUrl requiredstring | nullThe avatar as a data: URL, or null if none is set.
PATCH/v1/account/me

Update 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>.

Request body · UpdateProfileDto
FieldTypeWhat it is
namestringThe person's name. Optional, but when given it must not be empty or only spaces.
jobTitlestringOptional, but " " is not a job title either — an empty string is the way to clear it.
emailstring (email)
Responses
StatusBodyWhen
200ProfileDto
200 body · ProfileDto
FieldTypeWhat it is
name requiredstring | null
email requiredstring
jobTitle requiredstring | null
emailVerified requiredboolean
avatarDataUrl requiredstring | nullThe avatar as a data: URL, or null if none is set.
GET/v1/account/me/2fa

My two-factor authentication status

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200TwoFactorStatusDto
200 body · TwoFactorStatusDto
FieldTypeWhat it is
enabled requiredboolean
backupCodesRemaining requirednumberUnused backup codes left. Meaningless (0) while enabled is false.
POST/v1/account/me/2fa/backup-codes/regenerate

Replace the backup codes with ten new ones

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · RegenerateBackupCodesDto
FieldTypeWhat it is
password requiredstring
Responses
StatusBodyWhen
200BackupCodesResponseDto
200 body · BackupCodesResponseDto
FieldTypeWhat it is
backupCodes requiredstring[]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.
POST/v1/account/me/2fa/enable

Confirm 2FA enrolment

Turns 2FA on and returns backup codes — shown once.

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · TwoFactorEnableDto
FieldTypeWhat it is
code requiredstring
Responses
StatusBodyWhen
200BackupCodesResponseDto
200 body · BackupCodesResponseDto
FieldTypeWhat it is
backupCodes requiredstring[]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.
POST/v1/account/me/2fa/setup

Start 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>.

Responses
StatusBodyWhen
200TwoFactorSetupResponseDto
200 body · TwoFactorSetupResponseDto
FieldTypeWhat it is
secret requiredstringFor manual entry, when the authenticator app can't scan a code.
otpauthUrl requiredstring
qrCodeDataUrl requiredstringA data:image/png;base64,... QR code of otpauthUrl — draw it straight into an <img>.
GET/v1/account/me/notifications

My notification preferences, with the catalogue that describes them

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200NotificationPrefsResponseDto
200 body · NotificationPrefsResponseDto
FieldTypeWhat it is
options requiredNotificationOptionDto[]Every notification this account can control, with its current state.
options[].id requiredstringCatalogue id. Also the key preferences are stored under.
options[].label requiredstring
options[].description requiredstringWhat 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 requiredbooleanTrue 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 requiredbooleanTrue when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting.
options[].marketing requiredbooleanTrue when this is marketing and requires an explicit opt-in.
options[].email requiredbooleanWhether 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 requiredbooleanMaster switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this.
timeZonestring | nullThe zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used.
PUT/v1/account/me/notifications

Update my notification preferences

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · UpdateNotificationPrefsDto
FieldTypeWhat it is
options requiredNotificationPrefUpdateDto[]
options[].id requiredstring
options[].email requiredboolean
digestCadence required"immediate" | "daily" | "weekly" | "off"
emailEnabled requiredboolean
Responses
StatusBodyWhen
200NotificationPrefsResponseDto
200 body · NotificationPrefsResponseDto
FieldTypeWhat it is
options requiredNotificationOptionDto[]Every notification this account can control, with its current state.
options[].id requiredstringCatalogue id. Also the key preferences are stored under.
options[].label requiredstring
options[].description requiredstringWhat 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 requiredbooleanTrue 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 requiredbooleanTrue when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting.
options[].marketing requiredbooleanTrue when this is marketing and requires an explicit opt-in.
options[].email requiredbooleanWhether 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 requiredbooleanMaster switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this.
timeZonestring | nullThe zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used.
POST/v1/account/me/password

Change 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>.

Request body · ChangePasswordDto
FieldTypeWhat it is
currentPassword requiredstring
newPassword requiredstringThe new password: 8 to 128 characters, the same rule as at sign-up. It must differ from the current one.
Responses
StatusBodyWhen
204—
GET/v1/account/me/sessions

My 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>.

Responses
StatusBodyWhen
200SessionDto[]
200 body · SessionDto[]
FieldTypeWhat it is
id requiredstring
createdAt requiredstring
expiresAt requiredstring
userAgent requiredstring | null
ip requiredstring | null
current requiredbooleanThe session the current access token belongs to (best-effort: most recent).
DELETE/v1/account/me/sessions/others

Sign out all other sessions

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
204—
DELETE/v1/account/me/sessions/{id}

Sign out one session

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Parameters
NameWhereTypeWhat it is
id requiredpathstring
Responses
StatusBodyWhen
204—
GET/v1/account/notifications

My 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>.

Responses
StatusBodyWhen
200NotificationPrefsResponseDto
200 body · NotificationPrefsResponseDto
FieldTypeWhat it is
options requiredNotificationOptionDto[]Every notification this account can control, with its current state.
options[].id requiredstringCatalogue id. Also the key preferences are stored under.
options[].label requiredstring
options[].description requiredstringWhat 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 requiredbooleanTrue 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 requiredbooleanTrue when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting.
options[].marketing requiredbooleanTrue when this is marketing and requires an explicit opt-in.
options[].email requiredbooleanWhether 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 requiredbooleanMaster switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this.
timeZonestring | nullThe zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used.
PUT/v1/account/notifications

Update my notification preferences

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · UpdateNotificationPrefsDto
FieldTypeWhat it is
options requiredNotificationPrefUpdateDto[]
options[].id requiredstring
options[].email requiredboolean
digestCadence required"immediate" | "daily" | "weekly" | "off"
emailEnabled requiredboolean
Responses
StatusBodyWhen
200NotificationPrefsResponseDto
200 body · NotificationPrefsResponseDto
FieldTypeWhat it is
options requiredNotificationOptionDto[]Every notification this account can control, with its current state.
options[].id requiredstringCatalogue id. Also the key preferences are stored under.
options[].label requiredstring
options[].description requiredstringWhat 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 requiredbooleanTrue 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 requiredbooleanTrue when this event can be rolled into a digest instead of sent one at a time, and therefore obeys the cadence setting.
options[].marketing requiredbooleanTrue when this is marketing and requires an explicit opt-in.
options[].email requiredbooleanWhether 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 requiredbooleanMaster switch. False stops every optional email regardless of the rows above. Required notifications still send — they are not covered by this.
timeZonestring | nullThe zone scheduled mail is timed against, read from the account preferences. Null means UTC is being used.
POST/v1/account/password

Change my password (signs out other sessions)

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · ChangePasswordDto
FieldTypeWhat it is
currentPassword requiredstring
newPassword requiredstringThe new password: 8 to 128 characters, the same rule as at sign-up. It must differ from the current one.
Responses
StatusBodyWhen
204—
GET/v1/account/preferences

Get my UI preferences

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200PreferencesDto
200 body · PreferencesDto
FieldTypeWhat it is
languagestring
timezonestring
defaultPagestring
density"compact" | "comfortable" | "cozy"
themeMode"light" | "dark" | "system"
accentstringThe panel's accent colour, as a hex colour such as #e85520.
PUT/v1/account/preferences

Update my UI preferences

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · PreferencesDto
FieldTypeWhat it is
languagestring
timezonestring
defaultPagestring
density"compact" | "comfortable" | "cozy"
themeMode"light" | "dark" | "system"
accentstringThe panel's accent colour, as a hex colour such as #e85520.
Responses
StatusBodyWhen
200PreferencesDto
200 body · PreferencesDto
FieldTypeWhat it is
languagestring
timezonestring
defaultPagestring
density"compact" | "comfortable" | "cozy"
themeMode"light" | "dark" | "system"
accentstringThe panel's accent colour, as a hex colour such as #e85520.
GET/v1/account/profile

Get my profile

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200ProfileDto
200 body · ProfileDto
FieldTypeWhat it is
name requiredstring | null
email requiredstring
jobTitle requiredstring | null
emailVerified requiredboolean
avatarDataUrl requiredstring | nullThe avatar as a data: URL, or null if none is set.
PATCH/v1/account/profile

Update my profile (name, job title, email)

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · UpdateProfileDto
FieldTypeWhat it is
namestringThe person's name. Optional, but when given it must not be empty or only spaces.
jobTitlestringOptional, but " " is not a job title either — an empty string is the way to clear it.
emailstring (email)
Responses
StatusBodyWhen
200ProfileDto
200 body · ProfileDto
FieldTypeWhat it is
name requiredstring | null
email requiredstring
jobTitle requiredstring | null
emailVerified requiredboolean
avatarDataUrl requiredstring | nullThe avatar as a data: URL, or null if none is set.
GET/v1/account/sessions

List my active sessions

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200SessionDto[]
200 body · SessionDto[]
FieldTypeWhat it is
id requiredstring
createdAt requiredstring
expiresAt requiredstring
userAgent requiredstring | null
ip requiredstring | null
current requiredbooleanThe session the current access token belongs to (best-effort: most recent).
DELETE/v1/account/sessions/others

Sign out all other sessions

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
204—
DELETE/v1/account/sessions/{id}

Sign out one session

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Parameters
NameWhereTypeWhat it is
id requiredpathstring
Responses
StatusBodyWhen
204—
GET/v1/account/statements

What 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>.

Responses
StatusBodyWhen
200MyStatementsDto
200 body · MyStatementsDto
FieldTypeWhat it is
statements requiredWorkspaceStatementDto[]
statements[].organizationId requiredstring
statements[].organizationName requiredstring
statements[].plan requiredstring
statements[].currency requiredstringISO-4217, lowercase.
statements[].total requirednumberWhat the workspace is charged for the period, in minor units.
statements[].units requirednumberBilled domains across the workspace.
statements[].clients requiredClientShareDto[]
statements[].rounding requirednumberWhatever 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.
GET/v1/account/statements/{organizationId}.csv

One 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>.

Parameters
NameWhereTypeWhat it is
organizationId requiredpathstring
Responses
StatusBodyWhen
200—
GET/v1/account/subscriptions

What 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>.

Responses
StatusBodyWhen
200MySubscriptionsDto
200 body · MySubscriptionsDto
FieldTypeWhat it is
subscriptions requiredMySubscriptionDto[]
subscriptions[].organizationId requiredstring
subscriptions[].organizationName requiredstring
subscriptions[].slug requiredstring
subscriptions[].plan requiredstring
subscriptions[].statusstring | nullStripe’s own status (active, past_due, canceled, …). Null for a demo, which has no subscription behind it.
subscriptions[].currentPeriodEndstring | null
subscriptions[].cancelAtstring | nullWhen 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[].contractEndedAtstring | nullWhen a paid subscription ended. Null while paying, and for a demo.
subscriptions[].dataDeletedAtstring | nullWhen the workspace and its data will be deleted, for one whose subscription has ended. Null when nothing is scheduled.
subscriptions[].vatNumberstring | nullThe 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" | nullWhether the tax authority’s register confirmed the number. pending while the check is running; unavailable when the register could not be reached.
subscriptions[].trialEndsAtstring | null
subscriptions[].currency"usd" | "gbp" | "eur" | nullThe 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 requiredbooleanTrue when this is a demo rather than a paid plan.
GET/v1/account/team

Who 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>.

Responses
StatusBodyWhen
200MyTeamDto
200 body · MyTeamDto
FieldTypeWhat it is
workspaces requiredMyTeamWorkspaceDto[]
workspaces[].organizationId requiredstring
workspaces[].name requiredstring
workspaces[].slug requiredstring
workspaces[].role requiredstringThis user’s role here.
workspaces[].memberCount requirednumberMembers in this workspace, whether or not they are listed.
workspaces[].membersTeamMemberDto[] | nullNull 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 requiredstringWhere to manage members — the workspace’s own panel.
GET/v1/account/workspaces

Every workspace this account belongs to

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200MyWorkspacesDto
200 body · MyWorkspacesDto
FieldTypeWhat it is
workspaces requiredMyWorkspaceDto[]
workspaces[].id requiredstring
workspaces[].name requiredstring
workspaces[].slug requiredstring
workspaces[].url requiredstringWhere to sign in to this workspace.
workspaces[].role requiredstringThis user’s role here.
workspaces[].plan requiredstringdemo | starter | pro | agency | enterprise
workspaces[].status requiredstringactive | suspended
workspaces[].trialEndsAtstring | nullWhen the demo stops working. Null on paid plans.
workspaces[].members requirednumberMembers in this workspace.
workspaces[].purgeAfterstring | nullWhen 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