Accounts and teams
How users, teams, and merchants relate in Billium — and who can do what.
Billium is organized around merchants, not users. A user account is just the credential you log in with; every invoice, wallet, API key, and webhook lives under a merchant. The same person can belong to several merchants at once — for example a freelance developer who runs their own shop, works as an admin on a client's merchant, and is a viewer on a friend's side project. Each relationship is mediated by a team, which holds the role and security policy for that user on that merchant.
The hierarchy
Rules:
- Every merchant has exactly one team. You can't have two teams on the same merchant, and you can't share a team across merchants.
- Every merchant has exactly one
OWNERat any given time. The user who creates the merchant is the initial owner and can later transfer ownership. - A user can belong to any number of teams — there is no hard limit defined in the backend. If you run multiple Billium accounts under one login, each merchant has its own team and its own role for you.
- All resources are scoped to
merchantId. Every API call (whether from the dashboard or a server-side SDK) must specify which merchant it's acting on.
Roles
Four roles, with strictly decreasing authority:
| Role | Permissions | Typical use |
|---|---|---|
OWNER | 34 (all) | The account holder. Can delete the merchant, manage billing, transfer ownership, configure team-wide security enforcement. |
ADMIN | 32 | Day-to-day operator. Everything except merchant:delete and merchant:billing. Good fit for a trusted employee or co-founder. |
MEMBER | 15 | Create and edit invoices, products, and customers. Cannot delete resources, cannot manage the team, can only view API keys and webhooks. |
VIEWER | 10 | Read-only — one *:view permission per domain. Good fit for accountants, auditors, or customer-support agents who should see the dashboard but not change anything. |
Rank order for operations that compare two users (e.g. "can this admin remove that member?"): OWNER > ADMIN > MEMBER > VIEWER. A user can only invite, remove, or change the role of someone strictly below them.
See Authentication → Permissions for the full 34-permission matrix grouped by domain.
Team security enforcement
An OWNER can enforce team-wide authentication requirements. These apply to every dashboard user on the team — API-key requests are not subject to them. The fields live on the Team model:
| Field | Requires the user to have… |
|---|---|
enforceTfa | TOTP or email OTP enabled |
enforcePasskey | At least one registered WebAuthn/FIDO2 passkey |
enforceGoogleLogin | A linked Google OAuth identity |
enforceGithubLogin | A linked GitHub OAuth identity |
enforceEmailVerified | A verified email address |
When any of these are set, every request from a team member is validated against their current auth state. Missing factors return 403 with error code SECURITY_REQUIREMENT_NOT_MET and a missing[] array naming the gaps — so your frontend can show "enable 2FA to continue" instead of a generic permission error.
Enforcement is team-level, not user-level. If you enable enforceTfa and one of your admins hasn't set up 2FA yet, that admin is locked out of the dashboard until they add a TOTP or email-OTP factor. Warn your team before flipping these switches.
Inviting team members
The invitation flow is token-based and email-verified:
An existing member with the team:invite permission calls POST /api/v1/merchants/merchant/{merchantId}/team/invitations with the invitee's email and the role to assign. The caller's role must be strictly higher than the role they are assigning — a member can't invite an admin.
Billium creates a TeamInvitation row with a 7-day expiry, stores a SHA-256 hash of the invitation token (the raw token is never persisted), and sends the invitee an email with a link containing the unhashed token.
The invitee either clicks the link (which hits POST /team/invitations/accept with the token and their authenticated email must match the invitation's email) or, if they're already logged in and the email matches, they can call POST /team/invitations/{invitationId}/accept-by-id directly from the dashboard.
On acceptance, Billium creates a TeamMember record atomically and the invitee immediately has the role that was assigned.
Invitations that aren't accepted within 7 days expire and need to be re-sent. Revoking a pending invitation is a simple delete on the invitation row.
Ownership transfer
A merchant has exactly one OWNER. To hand it off:
- The current owner calls
POST /api/v1/merchants/merchant/{merchantId}/team/members/{memberId}/transfer-ownership, targeting an existing team member. - Inside a single database transaction, the current owner is demoted to
ADMINand the target member is promoted toOWNER. - Both role changes emit audit events.
Preconditions: the caller must currently be OWNER, and the target must already be a team member (you cannot transfer ownership to someone who hasn't accepted an invitation yet).
Ownership transfer is one-way within a single request. The new owner can always transfer it back to you — but until they do, you no longer have the merchant:delete or merchant:billing permissions.
What happens when you leave
Removing yourself (or being removed) from a team deletes your TeamMember record. The resources that team owns — invoices, wallets, webhooks, API keys — are unaffected; they belong to the merchant, not to individual members. Any API keys you personally created remain valid until they're explicitly deleted or rotated, because API keys are scoped to the merchant, not the user. Revoke your keys before leaving if you want to cut all access cleanly.
Hard limits
None of these are enforced in the backend today:
- Maximum members per team — unlimited
- Maximum merchants per user — unlimited
- Maximum invitations pending per team — unlimited
If you hit a practical ceiling (e.g. hundreds of members on one team), get in touch — we'll likely want to understand the use case before adding hard caps.