Prefer to let an AI agent build this? Install the WalletConnect Pay Headless skill — with it loaded, Claude Code, Cursor, Codex, and other agents can scaffold the steps below directly in your codebase:
@walletconnect/pay-* packages and never touch @reown/*, wagmi, or viem directly.
You build three things:
- A server proxy — routes that forward to WCP with your secret key.
- A browser transport — points the runtime at those routes.
- The AppKit provider — one component (
<PayAppKitProvider>in React) or one factory call (createPayAppKitin JavaScript).
usePaymentSession (React) or createPaymentController (JavaScript) ties everything together and gives you a snapshot to render.
The browser never holds the WCP API key. It talks to your server, and your server talks to WCP — see The WCP API key never reaches the browser.
Prerequisites
- Node 18+. The React example uses Next.js (App Router); the JavaScript example is framework-neutral.
- A Reown Project ID — create one at dashboard.reown.com. Enable the headless feature on the project.
- A WalletConnect Pay Gateway API key for the WCP API (server-side). Talk to us to get onboarded.
Install
Install only the Headless SDK. Wallet connectivity (@reown/appkit, wagmi, viem, @solana/web3.js, @tanstack/react-query) comes transitively through @walletconnect/pay-appkit — you don’t add or configure any of it.
@walletconnect/pay-react is the React hook binding — omit it if you’re not using React.
Step 1 — Server proxy (keep the API key server-side)
Create a server-only module that constructs the WCP client once and forwards calls. The key comes from server env and never ships to the browser. This is framework-agnostic — any server works; the example uses Next.js Route Handlers.lib/server/wcp.ts
/api/wcp/payment/[id]. The browser transport (Step 2) calls exactly these paths:
app/api/wcp/payment/[id]/options/route.ts
app/api/wcp/payment/[id]/status/route.ts
Step 2 — Browser transport
On the client, point the runtime at your proxy.createHttpTransport issues requests to ${baseUrl}/payment/:id/..., matching the routes above.
Transport seam. It speaks the same five methods as the server client, but routes through your origin — no key, no CORS.
Step 3 — Set up AppKit (zero-config)
The SDK constructs the AppKit instance, the Wagmi/Solana adapters, and the WalletConnect-owned network set for you, in headless mode (no built-in modal — you render your own wallet picker). You supply only yourprojectId and metadata.
In React, render <PayAppKitProvider> once near the root. It owns AppKit’s client-only construction, the WagmiProvider + QueryClientProvider tree, and an SSR-safe context. In JavaScript, call createPayAppKit and await its async construction.
<PayAppKitProvider> accepts an optional queryClient (a host with its own passes it to share one cache; omit it for a fully internal one) and optional themeVariables (e.g. a host font). Both createPayAppKit and the provider load the Reown modules through a client-only dynamic import, so AppKit’s UI never enters your SSR bundle.
Step 4 — Build the checkout
Assemble the seams and drive the session. The wallet seam comes from the SDK’s wallet-list hook/controller, and the signer is a single built-in call —createAppKitSigner(wallet) — so you no longer wire up signing strategies by hand. clock is browserClock.
In React, useAppKitWalletProvider turns the AppKit instance into the WalletProvider seam and a ready-made picker controller (list, search, pagination, the pairing QR URI). Read the instance from getPayAppKitInstance() once usePayAppKit().isReady is true. In JavaScript, createAppKitWalletList is the framework-neutral equivalent.
app/[paymentId]/page.tsx
Step 5 — Render the snapshot
snapshot.state is a single string you switch on. Each state maps to one piece of UI; the named actions advance the flow. The logic is the same for React and JavaScript — the only difference is where the snapshot comes from (usePaymentSession vs controller.getSnapshot()).
disconnectWallet(namespace?) drops one namespace or all of them.
Information capture
Some payments require identity details (name, date of birth, country, terms acceptance) before they can settle — usually for regulatory reasons, often only on a buyer’s first payment. When the selected option needs it, the runtime enters theInformationCapture state and describes what to collect on snapshot.collectData.
Render the form from snapshot.collectData.schema — a JSON Schema (properties, required, anyOf for proof-of-birth / proof-of-residence, and a tosConfirmed checkbox) — and submit the collected values with submitInfoCapture(data). The runtime sends them with the payment confirmation; the flow advances on its own.
collectData.fields (name + date of birth only) is deprecated — it under-collects. Drive your form from collectData.schema.Environment variables
.env.local
In a Vite / non-Next.js host, expose the project ID under that toolchain’s client env convention (e.g.
VITE_APPKIT_PROJECT_ID) and keep WCP_WALLET_API_KEY on the server only.Reference apps
headless-checkout (Next.js)
The full React checkout —
<PayAppKitProvider> + usePaymentSession, no @reown/* in the app.headless-checkout-vanilla
The same checkout with no framework —
createPaymentController + manual subscribe + imperative render.Next steps
Packages Reference
The full public API of pay-core, pay-state, pay-react, and pay-appkit.
API Reference
The Gateway and Payments endpoints behind the SDK.