Skip to content

Teams & shared budgets ​

A team is a company account with its own prepaid budget. The owner funds a pool, invites colleagues, sets a monthly limit for each of them, and sees who spent what. Members stop using their personal wallet and draw on the pool instead.

Teams are the account-level answer to "who pays for this?" — the choice is stored on your account, so every Nexrall surface follows it: the web app, Nexrall Code for VS Code, the CLI and Nexrall Work.

Teams are separate from your personal wallet

Your personal balance is never touched while a team is paying for you, and the team's pool is never touched while your personal wallet is. Your usage is billed to exactly one target at a time.

The money model ​

A team is deliberately not a set of ring-fenced per-member allocations. It is a shared pool plus optional monthly caps:

ConceptWhere it livesWhat it does
Poolteams.balanceThe prepaid balance every member draws from. Funded by the owner, from their own wallet.
Member limitper member, monthlyA ceiling on one member's spend in the current month. NULL = uncapped.
Team ceilingoptional, monthlyA ceiling on the team's total spend in the current month, independent of how much the pool holds. NULL = no ceiling.

Because nothing is allocated to a member in advance, there is nothing to reclaim, expire or refund when someone leaves. The only money movements are fund (owner wallet → pool) and refund (pool → owner wallet), and both are single-transaction and idempotent.

Nothing is lost when a member uses less than their limit: the unspent headroom simply stays in the pool and remains available to whoever needs it, because each member's cap applies only to that member.

Roles ​

Three roles, with a deliberate split between managing people and moving money:

ActionOwnerAdminMember
Use the team's budget (via "Bill to")✅✅✅
See their own budget and spend✅✅✅
See the pool balance and team-wide spend✅✅❌
Invite / remove members, set their monthly limits✅✅❌
Change team settings (name, default limit, ceiling, overage)✅❌❌
Fund the pool / refund to the owner✅❌❌
Promote or demote another admin✅❌❌
Transfer ownership / delete the team✅❌❌
Leave the team❌ (transfer or delete first)✅✅

Two rules worth calling out:

  • A plain member never sees the pool balance. The API returns a role-aware object, so a member sees "your budget: $80 of $100 left this month" while an admin sees "$4,200 in the pool · 12 members · $1,340 used this month". This is enforced server-side, not just hidden in the UI.
  • A team always has exactly one owner who cannot simply leave or be demoted — ownership has to be transferred first. Deleting the owner's user account is blocked outright rather than silently orphaning a company's prepaid pool.

Create a team and fund it ​

Creating a team is a paid feature: your account must have topped up its wallet at least once (the signup bonus does not count). If it has not, Settings → Teams shows an Add funds to activate Teams notice in place of the create button, and the API refuses the call. Once you have topped up, the steps are:

  1. Open Settings → Teams in the web app and choose Create a team.
  2. Give it a name. You become the owner.
  3. Add funds to the pool — this transfers money from your personal wallet into the team's pool. Top up your wallet first if it is short.
  4. Invite members (below) and optionally set the default monthly limit they get.

The same page has a Refund action (owner only) that moves money from the pool back to the owner's wallet. To delete a team you must refund the pool first — the API refuses to delete a team that still holds funds, so money can never be stranded in a deleted row.

Inviting members ​

Invite by email address:

  • The email already has a Nexrall account → they become an active member immediately. There is no accept-the-invite ceremony, which matches how company provisioning usually works.
  • The email has no account yet → the invite is held, and the membership is created automatically the moment they register with that address.

Each invite carries a role and a monthly limit. If the team has a default member limit set, it is applied to new invites unless you override it.

Removing a member is a soft removal — their usage history stays attributable to a real row, and re-inviting them flips the same record back to active instead of creating a duplicate. When someone is removed (or leaves), their billing target is reset to their personal wallet automatically.

SSO: signing in with the company account ​

A team can sign in through the company's identity provider instead of a Nexrall password. This is OpenID Connect (OIDC), configured by the owner in Settings → Teams → SSO. SAML is not supported.

FieldNotes
Issuer URLDiscovery is derived as ${issuer}/.well-known/openid-configuration. It must match the issuer the provider actually reports.
Client IDRequired.
Client secretOptional — leave it empty for a PKCE public client. Stored encrypted (AES-256-GCM) and never returned by any endpoint; the API reports only whether one exists.
Allowed email domainsComma-separated, up to 20. At least one is required before SSO can be enabled.
EnabledOff until you turn it on.

The issuer is checked with a live discovery request before it is stored, so an issuer that cannot be reached or that reports a different issuer is refused at the door rather than at the first login. The ID token must be signed with RS256, RS384 or RS512.

The page also shows the exact Redirect URI to register at your provider — https://api.nexrall.com/api/auth/sso/callback — with a copy button, because a mismatch there is the most common setup mistake.

How a member signs in ​

SP-initiated only: there is no IdP-initiated (unsolicited) login.

  1. The member opens the sign-in dialog and chooses Sign in with SSO, then enters their work email.
  2. Nexrall resolves that email to a team with SSO enabled and redirects to the provider — authorization code flow with PKCE and a nonce.
  3. The callback verifies the ID token against the provider's JWKS, re-checks the email domain allow-list, and re-checks that the email still belongs to that team.

Three gates have to pass, and they are enforced server-side:

  • The email must be an active member of the team, or hold a pending invitation for it. Anything else gets one uniform "not found", which is deliberate: it does not reveal whether an address has an account.
  • The domain must be on the team's allowed list.
  • The ID token signature must verify against the provider's keys.

SSO is invite-gated — there is no auto-join by domain

A correct domain is necessary but not sufficient. Someone whose address ends in your domain still has to be an invited member. Provisioning whole domains automatically needs DNS-based domain verification, which is not built.

On a successful first sign-in the account is created (its auth provider is recorded as sso) and any pending invitation for that address is resolved. A member who signed up with a password keeps that option; SSO is not enforced org-wide, and a team member does not lose password sign-in by using it.

SCIM: provisioning members from your directory ​

SCIM 2.0 replaces manual inviting. It is mounted at /api/scim/v2 and authenticates with a per-team bearer token (nxscim_…).

The owner enables it in Settings → Teams → SCIM. The token is shown exactly once — only its SHA-256 hash is stored, so it cannot be displayed again. It can be rotated (the previous token stops working immediately) or deleted, which disables SCIM and kills every token for the team.

What it doesBehaviour
ProvisionAn address that already has an account becomes an active member; an address with no account yet becomes a pending invitation. No email is sent to the employee — the directory already told you they exist.
DeprovisionMembership is soft-removed, any pending invitation is deleted, and the person's refresh tokens are revoked, so an already-signed-in session cannot be renewed.
RoleProvisioned members always get the member role. Roles are not mapped from directory groups.

Two rules worth knowing:

  • The owner cannot be deprovisioned through SCIM. The call fails until ownership is transferred — the same protection that stops a company's prepaid pool from being orphaned.
  • Deprovisioning is a membership action, not a billing action. It does not touch Brix schedules or settle anything outstanding.

What SCIM does not do ​

Being explicit, because directory integrations are where assumptions cause trouble:

  • Groups are not provisioned. GET /Groups returns an empty list and group writes return 501. Configure your identity provider for users-only sync.
  • Filtering is narrow: only userName eq "..." is accepted; any other filter returns 400. Pagination uses startIndex/count, capped at 200.
  • PATCH understands the Okta and Entra shapes for the fields it models. Attributes it does not model (job title, phone numbers) are ignored rather than rejected, so a full directory sync is not derailed by them.
  • No bulk operations, sorting, ETags or password changes — the service provider configuration reports these as unsupported.

SSO and SCIM endpoints ​

Both require the team's owner (the read endpoints also accept an admin), and use the same bearer token as the rest of the API — see Authentication.

MethodEndpointPurpose
GET/api/teams/:id/ssoCurrent SSO configuration, including whether a client secret is stored.
PUT/api/teams/:id/ssoSet issuer, client id, secret, allowed domains and enabled state (owner).
DELETE/api/teams/:id/ssoRemove the configuration (owner).
GET/api/teams/:id/scimSCIM status and the token hint (never the token).
PUT/api/teams/:id/scimEnable SCIM and mint a token — returned once (owner).
POST/api/teams/:id/scim/rotateMint a new token, invalidating the old one (owner).
DELETE/api/teams/:id/scimDisable SCIM and delete every token (owner).
POST/api/auth/sso/startBegin an SSO sign-in for an email address.
GET/api/auth/sso/callbackThe provider's redirect target.
POST/api/auth/sso/exchangeExchange the short-lived hand-off code for a session.

Member limits and overage ​

Each member has an optional monthly limit in USD. What happens when they reach it is a team-level policy set by the owner:

  • Hard stop (default) — the member's turns are refused once they hit their limit, even if the pool is full and money is available. This is what makes a budget a budget: the "remaining" number a member sees is a promise.
  • Allow overage — the member's limit becomes advisory. They keep drawing on the pool. The pool balance and the team's monthly ceiling still apply, because those are team-level money rather than a personal cap.

The team monthly ceiling ​

Independently of how much money the pool holds, a team can set an optional monthly ceiling on total team spend. This protects against the failure mode a pool alone cannot: a large prepaid balance being consumed faster than expected in one month.

When the ceiling is reached, further team-funded turns are refused until the next calendar month. A turn already running may finish, at a one-round overshoot — the same semantics as the personal wallet's floor-at-zero debit.

What "remaining budget" means ​

Everywhere a budget number appears — the switcher, the app, the API — it is the same single computation: the smallest of

  1. the pool balance,
  2. your remaining member limit for this month (skipped under allow overage),
  3. the team's remaining monthly ceiling.

The number is computed server-side and never re-derived by a client, so what you see and what the server enforces can't disagree. A plain member's number is their own; an admin's is the same number for their own usage.

Monthly periods are calendar months in UTC and reset on their own — there is no reset job to wait for. Rollover is computed on every read and write, and spend from a stale period is simply not counted.

Choosing who pays: "Bill to" ​

Your billing target is a property of your account, so it applies to every product at once:

  • Settings → Teams in the web app has a "Bill my usage to this team" switch on each team you belong to. The page always states, in one line, which pot is currently paying for you.
  • Nexrall Code for VS Code shows the same choice in its "Bill to" row in the Session Manager, read from the same account setting — so the panel and the web app can never disagree about who is paying.
  • The Visual Code view in the web app carries the same row in its settings gear: it appears only once you belong to at least one team, and the balance above it reads "Team budget" whenever a team is the one paying. A team that cannot pay is listed with the reason instead of vanishing, and a paused team says so while your wallet carries the usage.
  • The Canvas (Docs) view chat header carries the same row in its settings gear: one balance line that reads your wallet while the wallet pays and switches to "Team budget" the moment a team does — the same single-row model as the Visual Code gear, so a second figure can never look like money the next turn could spend.
  • The CLI and headless runs can override it per request, which is how a CI job opts out of a company budget and bills a personal wallet instead.

Resolution order ​

When a request arrives, the billed target is resolved in this order:

RequestWho pays
No team specifiedYour saved choice — a team, or your personal wallet
teamId: nullYour personal wallet, explicitly
teamId: <id>That team, explicitly (you must be an active member)

Only one target ever pays a given turn; the request either names it or inherits the saved one. A malformed explicit team id is refused, never quietly reinterpreted as "no team" — a client bug must not silently bill the wrong wallet.

Suspensions and fallbacks ​

Two things can pause team billing: the team being suspended, or a single membership being suspended.

In both cases your saved choice is kept, and billing falls back to your personal wallet for the duration. The UI says so explicitly ("this team is suspended, so your usage falls back to your personal wallet until it is restored") rather than quietly showing Personal. When the suspension is lifted, team billing resumes on its own with no action needed.

The only case where the saved choice is cleared is when it has become permanently unusable — the team was deleted, or you are no longer a member. Then it is dropped and your personal wallet is used, logged as such.

If a team's funds run out, the member's turn is refused with a message telling them to ask a team admin to add funds, or to switch back to their personal wallet — so nobody is stuck without an explanation. The refusal notice carries no call to action at all on purpose (2026-10-03): the chat states the fact it was refused for, and nothing else — while a team is the billing target, topping up a personal wallet the team's turns never charge changes nothing, and the remedies live in the Settings gear ("Bill to", Billing). The VS Code extension and the CLI make the same split.

Usage, transactions and audit ​

Each team page has tabs that answer three different questions:

  • Members — who is in the team, their role, their monthly limit, and what they have spent this month.
  • Usage — a per-day, per-member, per-model breakdown of team spend, including which client the usage came from (VS Code, CLI, desktop, or a fixed-price action).
  • Billing — the team's money ledger: every fund, refund and usage charge, newest first. A usage row records which member's turn spent the money, along with the underlying provider cost and platform markup.
  • Audit (owner only) — an append-only log of every action that moved money or changed access, with the before/after values for each change, so a dispute can be reconstructed without diffing other tables.
  • Settings — name, default member limit, ceiling and overage policy.

Team spend is a separate ledger

A team-funded charge is recorded in the team's ledger, not in the member's personal transaction history. That is deliberate: the personal ledger is your own spend, and a company paying for your work is not your personal spend.

Teams and the API ​

If you are integrating directly, the team endpoints live under /api/teams and the account-level billing target under /api/user/billing-target. Both use the same bearer token as the rest of the API — see Authentication.

MethodEndpointPurpose
GET/api/teamsEvery team you belong to, with your role and remaining budget.
POST/api/teamsCreate a team (you become owner). Requires a wallet top-up on record — otherwise 402 topup_required.
GET/api/teams/:idOne team's view for you.
PATCH/api/teams/:idUpdate name, default member limit, ceiling, overage policy (owner).
DELETE/api/teams/:idDelete the team — the pool must be empty first (owner).
POST/api/teams/:id/fundMove money from your wallet into the pool (owner).
POST/api/teams/:id/refundMove money from the pool back to your wallet (owner).
GET/api/teams/:id/membersList members with roles, limits and spend.
POST/api/teams/:id/membersInvite a member by email, with role and limit.
PATCH/api/teams/:id/members/:memberIdChange a member's role, limit or status.
DELETE/api/teams/:id/members/:memberIdRemove a member.
POST/api/teams/:id/leaveLeave a team yourself.
POST/api/teams/:id/transferTransfer ownership to another member (owner).
GET/api/teams/:id/usagePer-day usage report for the team.
GET/api/teams/:id/transactionsThe team's money ledger.
GET/api/teams/:id/auditThe team's audit log (owner only).
GET/api/user/billing-targetYour saved choice and the effective one.
PUT/api/user/billing-targetSave the choice — a team id, or null for personal.
GET/api/code/orgsOne call returning your personal balance, every team with its remaining budget, and your saved choice — what the VS Code "Bill to" switcher renders.

GET /api/user/billing-target returns the saved choice and the effective target separately, because the two differ while a saved team is suspended — the saved value is intentionally kept so billing resumes by itself.

Unauthorized actions return 403; a suspended team returns 409; and a team whose state cannot be read safely returns 503 rather than guessing with your money.

Built by Maxrall, Inc.