TanStack Query integration
How the SDK rides TanStack Query — the provider-owned QueryClient, the key namespace, automatic invalidation, manual invalidation, and what clears on sign-out and re-bootstrap.
Every @capxul/sdk-react hook is a thin binding over TanStack Query: query
hooks return UseQueryResult, mutation hooks return UseMutationResult, and
all reactivity — caching, refetching, invalidation — is TanStack's. This guide
explains the pieces you interact with when your app shares the cache with the
SDK.
Before you start
@tanstack/react-queryis installed (it is a peer dependency — the quickstart installs it).CapxulProvideris mounted; you do not mount aQueryClientProvideryourself unless you bring your own client (below).
The provider owns the QueryClient
CapxulProvider creates a QueryClient and renders its own
QueryClientProvider, so the hooks work with zero TanStack setup. The
defaults it creates:
new QueryClient({
defaultOptions: {
queries: { retry: 2, staleTime: 30_000 },
mutations: { retry: 0 },
},
});- Queries retry twice and are considered fresh for 30 seconds.
- Mutations never retry — money movements must not be silently re-fired.
Because the provider renders a real QueryClientProvider, your own
useQuery/useMutation calls inside the tree resolve the same client and
share its cache.
Bring your own QueryClient
Pass queryClient to the provider to share a client you configure yourself
(your own defaults, devtools, persistence):
"use client";
import { QueryClient } from "@tanstack/react-query";
import { CapxulProvider } from "@capxul/sdk-react";
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 1 } },
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<CapxulProvider publishableKey={publishableKey} queryClient={queryClient}>
{children}
</CapxulProvider>
);
}The client is pinned at mount: swapping the queryClient prop later is
ignored. Create it once (module scope or useState initializer) and leave it.
Ownership also changes cleanup behavior — see what clears when.
The key namespace
Every SDK query key is an array whose first element is the literal
"capxul". The catalog is hierarchical; representative shapes:
| Data | Key |
|---|---|
| Session | ["capxul", "session"] |
| Profile | ["capxul", "profile"] |
| Account lifecycle | ["capxul", "accountLifecycle"] |
| Personal balance | ["capxul", "accountBalance"] |
| Org list | ["capxul", "orgs"] |
| One org | ["capxul", "org", orgId] |
| Org members / roles / treasury | ["capxul", "org", orgId, "members" | "roles" | "treasury"] |
| Payments list | ["capxul", "payments"] |
| One payment | ["capxul", "payments", paymentId] |
| Actor-scoped reads (address book, inbox, insights) | ["capxul", "actor", "account" | "org", …] |
| Destinations | ["capxul", "destinations", …] |
Parameterized keys use the placeholder "pending" while their parameter is
still undefined (the query is disabled in that state, so nothing fetches
under a placeholder key).
The catalog object itself is internal — the stable contract is the "capxul"
prefix plus the exact key documented on each hook's reference page (under
"Query key"). If you drop to the raw client, put
your custom keys under the same prefix.
Automatic invalidation
You should rarely invalidate SDK data yourself. Two families of mutations do it for you:
Money mutations — useCapxulPay, useCapxulPayout, useCapxulWithdraw
(and useCapxulCancelPayment when it lands) invalidate, on success:
- the payments list and the affected payment's detail key,
- the acting entity's insights summary and history,
- the personal balance — or, for an org actor, that org's account and treasury keys,
- the actor's address book (for
pay, which can create a new counterparty).
Auth boundary changes:
useCapxulVerifyOtp(and the account-lifecycleretry) invalidates session, profile, account lifecycle, and balance — a background refetch, so mounted screens update without hard resets.useCapxulSignOutresets those same keys after cancelling in-flight balance fetches — cached authenticated rows are dropped immediately, not refetched.- The onboarding completion hooks invalidate profile and account lifecycle (the organization variant also invalidates the org list).
Org mutations invalidate their own slices: useCapxulCreateOrg → the org
list; useCapxulInviteMember / useCapxulRemoveMember /
useCapxulAssignRole → that org's member list. Each hook's reference page
documents its exact cache behavior.
Manual invalidation
For anything the automatic rules do not cover (for example a server-driven
change you learn about out of band), use useQueryClient from
@tanstack/react-query. To target every Capxul query, predicate on the first
key element:
"use client";
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
// Refetch everything the SDK has cached:
await queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === "capxul",
});
// Or one slice, using the documented key shape:
await queryClient.invalidateQueries({ queryKey: ["capxul", "payments"] });invalidateQueries with a key prefix matches hierarchically —
["capxul", "payments"] invalidates the list and every
["capxul", "payments", paymentId] detail under it.
What clears on sign-out and re-bootstrap
Two different events, two different scopes:
Sign-out resets exactly the auth boundary: session, profile, account
lifecycle, and balance. Other capxul keys (org lists, payments, address
book) stay cached; their next fetch runs against the signed-out session and
fails with NOT_AUTHENTICATED. If your UX needs a hard wipe of all Capxul
data at sign-out, remove by predicate after the mutation succeeds:
const signOut = useCapxulSignOut();
const queryClient = useQueryClient();
await signOut.mutateAsync();
queryClient.removeQueries({
predicate: (query) => query.queryKey[0] === "capxul",
});Re-bootstrap — the provider replacing its client (a retry after a
bootstrap error, a publishableKey change, unmount) — clears client-scoped
data so nothing from the previous client leaks into the next one:
- Provider-owned QueryClient: the provider calls
queryClient.clear()— everything goes, including your app's own queries on that client. - Your own QueryClient: the provider removes only queries whose
queryKey[0] === "capxul"— your app's queries survive.
That asymmetry is the main reason to bring your own QueryClient once your app caches non-Capxul data in the same tree.
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.
Handle errors
CapxulError is the single error type across hooks and the raw client — the error shape, where it surfaces, bootstrap retry, and the public error-code catalog.