Work with organizations
List the orgs a user belongs to, create one, switch the acting entity, read treasury/members/roles, and invite a member with a role.
This guide covers the organization surface end to end: list the organizations the signed-in user belongs to, create a new one, switch which entity the UI is acting as, read an org's treasury, members, and roles, and invite a member by email with a role.
Before you start
- You have a working auth flow (Build the auth flow) and a signed-in, onboarded user.
- Read Identity & orgs if you want the model behind entities, members, and roles — this guide only uses it.
Everything below is entity-scoped and explicit: the SDK holds no global
"acting as" state. Every org read and write names its orgId, either as a
hook argument or through client.org(orgId).
Render org screens behind your auth gate (bootstrap ready, session present).
Most org hooks expect a bootstrapped provider and throw if rendered while the
SDK is still starting up — the gate from
Build the auth flow guarantees they never are.
List organizations
useCapxulOrgs lists the orgs
the user belongs to. It is a session-scoped authenticated read — gate it on
auth readiness so it does not fire before the session settles and surface a
spurious NOT_AUTHENTICATED:
"use client";
import { useCapxulOrgs, useCapxulSession } from "@capxul/sdk-react";
export function OrgList() {
const session = useCapxulSession();
const orgs = useCapxulOrgs({ enabled: session.data != null });
if (orgs.isLoading) return <p>Loading organizations…</p>;
if (orgs.isError) return <p>{orgs.error.message}</p>;
return (
<ul>
{(orgs.data ?? []).map((org) => (
<li key={org.id}>
<a href={`/orgs/${org.id}`}>{org.name}</a> · @{org.handle} · your role: {org.role}
</li>
))}
</ul>
);
}Each OrgView carries the org's id, name, handle, the viewing member's
own role label within that org, and the org treasury snapshot.
Create an organization
useCapxulCreateOrg binds
to client.createOrg(input). The input takes:
name— display name.handle— globally unique, normalized slug (^[a-z0-9-]{3,32}$).template—"Solo","Startup", or"Custom"; seeds the initial role set. For"Custom", pass your authoredrolesdefinitions.country— optional.
"use client";
import { useCapxulCreateOrg } from "@capxul/sdk-react";
const createOrg = useCapxulCreateOrg();
const handleCreate = async () => {
const org = await createOrg.mutateAsync({
name: "Acme Studio",
handle: "acme-studio",
template: "Startup",
});
navigate(`/orgs/${org.id}`);
};On success the hook invalidates the org list, so any mounted useCapxulOrgs
refetches automatically.
If the user is onboarding as an organization founder for the first time, use
useCapxulCompleteOrganizationOnboarding
instead — it saves the founder's identity profile and creates the org in one
step. useCapxulCreateOrg is for adding further orgs to an existing identity.
Switch the acting entity
"Acting as" (personal account vs. a specific org) is your app's state, not
the SDK's. The idiomatic pattern — used by the reference app — is to derive
the active entity from the route (/dashboard = personal, /orgs/:orgId =
that org) and pass the derived orgId into the org hooks on those screens.
useCapxulSwitchActingEntity
is the stable mutation seam for the switch interaction. It carries no SDK side
effect — the actual context switch is whatever your app does afterwards
(usually navigation):
"use client";
import { useNavigate } from "react-router-dom";
import { useCapxulOrgs, useCapxulSwitchActingEntity } from "@capxul/sdk-react";
export function EntitySwitcher({ activeOrgId }: { activeOrgId?: string }) {
const navigate = useNavigate();
const orgs = useCapxulOrgs();
const switchEntity = useCapxulSwitchActingEntity();
const handleChange = (value: string) => {
if (value === "personal") {
void switchEntity.mutateAsync(undefined).then(() => navigate("/dashboard"));
return;
}
const org = orgs.data?.find((candidate) => candidate.id === value);
if (org === undefined) return;
void switchEntity.mutateAsync({ orgId: org.id }).then(() => navigate(`/orgs/${org.id}`));
};
return (
<select value={activeOrgId ?? "personal"} onChange={(e) => handleChange(e.target.value)}>
<option value="personal">Personal account</option>
{orgs.data?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
);
}Because scoping is explicit per call, two screens can act as two different
entities at the same time without stepping on each other's cache — each org's
data lives under its own ["capxul", "org", orgId, …] keys (see
TanStack Query integration).
Read treasury, members, and roles
All three reads are parameterized by orgId and disabled until it is defined.
OrgId is an opaque type you get from SDK data — OrgView.id from the org
list, or org.id from a create/onboarding result. To turn a route param back
into an OrgId, match it against the org list
(orgs.data?.find((org) => org.id === param)?.id) rather than casting the raw
string.
"use client";
import {
useCapxulOrg,
useCapxulOrgMembers,
useCapxulOrgRoles,
useCapxulOrgTreasury,
} from "@capxul/sdk-react";
import type { OrgId } from "@capxul/sdk";
export function OrgDetail({ orgId }: { orgId: OrgId }) {
const org = useCapxulOrg(orgId);
const treasury = useCapxulOrgTreasury(orgId);
const members = useCapxulOrgMembers(orgId);
const roles = useCapxulOrgRoles(orgId);
if (org.isLoading || treasury.isLoading || members.isLoading) {
return <p>Loading organization…</p>;
}
if (org.isError) return <p>{org.error.message}</p>;
if (org.data == null) return <p>Organization not found.</p>;
return (
<>
<h2>{org.data.name}</h2>
{treasury.data ? (
<p>
Treasury: {treasury.data.balance.value} {treasury.data.balance.currency} (available:{" "}
{treasury.data.available.value})
</p>
) : null}
<ul>
{members.data?.map((member) => (
<li key={`${member.orgId}:${member.email}`}>
{member.email} · {member.role} · {member.status}
</li>
))}
</ul>
<ul>
{roles.data?.map((role) => (
<li key={role.roleKey}>{role.label}</li>
))}
</ul>
</>
);
}What each returns:
useCapxulOrgTreasury— the org's single treasuryAccount: a pot of money withbalanceandavailable. Nothing else.useCapxulOrgMembers—MemberView[]:email,name,rolelabel, and a lifecyclestatusrunningpending(including the intermediatepending_safe/pending_grantsteps) →active, withrevoked/expiredoff-ramps. Apendingmember has been invited but has not finished joining yet.useCapxulOrgRoles—RoleView[]: each role'slabeland itsdefinition, including optional spend caps (perTx,perDay, allowed recipients) and management permissions. Role labels are what you pass when inviting or assigning.
Invite a member with a role
useCapxulInviteMember
is scoped to an org and takes an email plus a role label (one of the
labels from useCapxulOrgRoles):
"use client";
import { useCapxulInviteMember } from "@capxul/sdk-react";
import type { OrgId } from "@capxul/sdk";
export function InviteForm({ orgId }: { orgId: OrgId }) {
const invite = useCapxulInviteMember(orgId);
const handleInvite = async (email: string, role: string) => {
const member = await invite.mutateAsync({ email, role });
// member.status is "pending" until the invitee joins.
};
// …form UI…
}Email is the universal entry point: inviting an address that is not yet a
Capxul user still works — it creates a pending membership that resolves when
that person signs in for the first time. On success the hook invalidates the
org's member list.
To offboard or change someone's role later, use
useCapxulRemoveMember
and useCapxulAssignRole;
both are scoped the same way and also refresh the member list.
What you end up with
- An org list gated on auth, a create form that lands on the new org's page, and an entity switcher whose state lives in your router.
- An org detail screen reading treasury, members, and roles for one explicit
orgId. - An invite form that adds members by email with a role, including people who are not Capxul users yet.
Org spending (treasury payments, payroll) is a separate surface — see Capability status for what is buildable today.
Build the auth flow
The complete email OTP journey — send the code, verify, route on session/profile/lifecycle state, complete onboarding, and sign out.
Send money
Pay a typed recipient, understand Commitment escrow for unvalidated recipients, list and read payments, cancel, withdraw to an external wallet, and pay out to a saved destination.