Skip to main content
The @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’s engines field)
  • 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-in require(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

NPM

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.auth signs users in, so it needs no session.
  • The API-backed methods on passage.ondo and passage.centrifuge, and requirements.forOffer, also take a wallet session. A wallet session builds only for its own wallet and chain: CoinList answers 403 to any other.
  • passage.tokens is public, so it works with no session and with a read-only server session store.
On 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.