Billium
Concepts

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

Account and team hierarchyA User can belong to multiple Teams via TeamMember records. Each Team is 1:1 with a Merchant, and every resource (invoices, wallets, API keys, webhooks) is scoped to a Merchant.Useryour login accountTeamMemberrole: OWNERTeamMemberrole: ADMINTeamMemberrole: VIEWERTeam ATeam BTeam CMerchant Ayour shopMerchant Bclient's shopMerchant Cside project
One user, three merchants, three different roles. Every resource belongs to a merchant, not to you directly.

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 OWNER at 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:

RolePermissionsTypical use
OWNER34 (all)The account holder. Can delete the merchant, manage billing, transfer ownership, configure team-wide security enforcement.
ADMIN32Day-to-day operator. Everything except merchant:delete and merchant:billing. Good fit for a trusted employee or co-founder.
MEMBER15Create and edit invoices, products, and customers. Cannot delete resources, cannot manage the team, can only view API keys and webhooks.
VIEWER10Read-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:

FieldRequires the user to have…
enforceTfaTOTP or email OTP enabled
enforcePasskeyAt least one registered WebAuthn/FIDO2 passkey
enforceGoogleLoginA linked Google OAuth identity
enforceGithubLoginA linked GitHub OAuth identity
enforceEmailVerifiedA 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:

  1. The current owner calls POST /api/v1/merchants/merchant/{merchantId}/team/members/{memberId}/transfer-ownership, targeting an existing team member.
  2. Inside a single database transaction, the current owner is demoted to ADMIN and the target member is promoted to OWNER.
  3. 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.

On this page