@passage-cl/react package is the recommended way to integrate Passage in React and Next.js apps with a backend. It gives you OAuth with PKCE on the client, wallet sign-in for users with no CoinList account, token exchange and sessions on the server, and ready-made UI for common flows.
For a linear walkthrough, start with SDK quickstart.
Requirements
- React and React DOM 18 or 19 (
^18.0.0 || ^19.0.0), and viem 2 (^2.0.0), as peer dependencies - Node.js
^20.19.0 || >=22.12.0(the package’senginesfield) - An ES module toolchain. The package is ESM-only. Bundlers such as Next.js, Vite and webpack consume it natively, and
require()works only through Node’s built-inrequire(esm)support in the Node versions above - A backend you control to store
client_secret, exchange authorization codes, and keep refresh tokens out of the browser
Install
Entry points
The package exposes three canonical entry points:
Every name has exactly one entry point, the one whose environment declares it. The client entry point does not re-export universal names, so import domain types, the
EvmWallet and Logger ports and the error classes from @passage-cl/react/universal even in a React app. Reach for /server only inside server-side code. The package is ESM-only, so all three entry points share one copy of each class, and instanceof HttpError holds whichever entry point an error came from.
The public surface is root containers, the viewmodels and data hooks behind them, and the namespaces. Views, step components and step viewmodels are internal. To build your own UI, use a product’s checkout viewmodel, such as useOndoBuyCheckoutViewModel, and render your own markup.
Sub-path imports like /client/components and /client/hooks were removed in v0.5.0, and /shared was renamed to /universal in v0.12.0 - see the React SDK changelog. v0.13.0 renamed the package from @coinlist-co/react to @passage-cl/react, with no deprecated aliases.
Namespaces
The whole public surface is namespaces. On the client,init() is the only top-level member, because it is lifecycle rather than domain; PassageServer has none at all.
CoinList’s own namespaces are named after what they do. A third-party provider’s namespace is named after the provider, because a second provider ships its own swap and passage.swap could only ever mean one of them.
Methods follow one vocabulary, with the namespace’s own noun dropped:
list() (every item, auto-paginating), listPage(params), get(id), and create* / submit* / remove* for writes, plus a real domain verb where one exists (connectExternal, allowWallet, execute). Any method taking more than one argument takes a single params object, apart from the optional app-level token on PassageServer’s offers.listPage, offers.get, and requirements.forOffer. SDK structure explains why the namespaces are named as they are, and the patterns the hooks and components above follow.
Unless a namespace says otherwise, its API-backed methods require a CoinList user and throw NotAuthenticatedError otherwise, for a wallet sign-in session too. Three things say otherwise:
passage.authsigns users in, so it needs no session.- The API-backed methods on
passage.ondoandpassage.centrifuge, andrequirements.forOffer, also take a wallet session. A wallet session builds only for its own wallet and chain: CoinList answers403to any other. passage.tokensis public, so it works with no session and with a read-only server session store.
PassageServer, passage.offers and requirements.forOffer also accept an app-level token from auth.clientCredentials(), which needs no session and is what reads offers for a wallet session.
On-chain reads - passage.erc20, the passage.superstate reads and the Centrifuge position reads - call the contract directly. By default they go through CoinList’s authenticated JSON-RPC proxy, so a logged-out read fails with the proxy’s 401 as a viem error rather than NotAuthenticatedError. A chain you point at your own node with Config.rpc needs no CoinList session.
v0.11.0 moved the last flat methods into namespaces and renamed swap to superstate. v0.12.0 forked ondo’s order builders by direction. v0.13.0 added centrifuge, wallets.connect and superstate.getQuote, and usePassage() returns the client as passage where useCoinList() returned coinlist. v0.14.0 added the Centrifuge redemption (centrifuge.buildRedeemTransaction, prepareSell and sell), and removed centrifuge.createClaim and createClaimProceeds. v0.15.0 added centrifuge.buildClaimTransaction, which centrifuge.claim now uses to build the claim with CoinList before the wallet sends it, and made offerOptionId required on ondo.buildBuyTransaction, ondo.buildSellTransaction, ondo.prepareBuy, ondo.prepareSell, centrifuge.buildBuyTransaction, centrifuge.prepareBuy and centrifuge.buy. v0.17.0 added wallet sign-in (auth.signInWithWallet, auth.walletChallenge, and auth.completeWalletSignIn), and auth.getState() now returns an object rather than a string. See the changelog for the full migration tables.
Next steps
SDK structure
The ladder, namespaces, hook state, and flows that return instead of throwing.
Set up OAuth authentication
End-to-end sign-in with backend session.
Building the Checkout flow
One container that renders whichever provider an offer belongs to.
Errors and edge cases
Every error shape the flows return, and how to handle them.