## Documentation Index

Fetch the complete documentation index at: [/llms.txt](https://docs.walletconnect.com/llms.txt)

Use this file to discover all available pages before exploring further.

This page walks through building a complete checkout on the Headless SDK, with **React / Next.js** and **JavaScript** examples side by side. New to the SDK? Read [How it works](https://docs.walletconnect.com/payments/psps/headless-sdk/how-it-works) first for the architecture and the role of each seam. Wallet connection is now **zero-config**: the SDK owns the entire Reown AppKit setup. You install only the `@walletconnect/pay-*` packages and never touch `@reown/*`, `wagmi`, or `viem` directly. You build three things:

1. A **server proxy** — routes that forward to the Engine with your secret key.
2. A **browser transport** — points the runtime at those routes.
3. The **AppKit provider** — one component (`<PayAppKitProvider>` in React) or one factory call (`createPayAppKit` in JavaScript).

The wallet seam, the signer, and the clock all come from the SDK. Then `usePaymentSession` (React) or `createPaymentController` (JavaScript) ties everything together and gives you a snapshot to render.

The browser never holds the Engine API key. It talks to _your_ server, and your server talks to the WalletConnect Pay Engine — see [The Engine API key never reaches the browser](https://docs.walletconnect.com/payments/psps/headless-sdk/how-it-works#the-engine-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](https://dashboard.reown.com/). Enable the **headless** feature on the project.
- A **WalletConnect Pay Gateway API key** for the Engine (server-side). [Talk to us](https://share.hsforms.com/1XsMCkUxFT2Cte8SCeAh89wnxw6s) 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.

```
npm install @walletconnect/pay-core @walletconnect/pay-state \
            @walletconnect/pay-appkit @walletconnect/pay-react
```

`@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 Engine 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/engine.ts

```
import 'server-only'
import { createEngineClient } from '@walletconnect/pay-core/server'

const client = createEngineClient({
  apiUrl: process.env.WCP_API_URL ?? 'https://staging.api.pay.walletconnect.org',
  apiKey: process.env.WCP_WALLET_API_KEY ?? '' // secret — server-side only
})

/** Forward a browser call to the Engine and return an EngineResponse-shaped Response. */
export async function callEngine(
  path: string,
  init: { method: 'GET' | 'POST'; body?: unknown }
): Promise<Response> {
  const paymentId = path.split('/')[4]! // /v1/gateway/payment/:id/...
  let result

if (path.endsWith('/options')) {
    result = await client.getPaymentOptions(paymentId, init.body as never)
  } else if (path.endsWith('/fetch')) {
    result = await client.fetchOptionActions(paymentId, init.body as never)
  } else if (path.endsWith('/confirm')) {
    result = await client.confirmPayment(paymentId, init.body as never)
  } else if (path.endsWith('/status')) {
    result = await client.getPaymentStatus(paymentId)
  } else {
    result = await client.getPayment(paymentId)
  }

return Response.json(result)
}
```

Then expose one route per Engine call under `/api/wcp/payment/[id]`. The browser transport (Step 2) calls exactly these paths:

app/api/wcp/payment/[id]/options/route.ts

```
import { callEngine } from '@/lib/server/engine'

export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const body = await req.json()
  return callEngine(`/v1/gateway/payment/${id}/options`, { method: 'POST', body })
}
```

app/api/wcp/payment/[id]/status/route.ts

```
import { callEngine } from '@/lib/server/engine'

export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  return callEngine(`/v1/gateway/payment/${id}/status`, { method: 'GET' })
}
```

Create the same handler for each route the transport uses:

| Route Handler                          | Method | Engine call             |
|----------------------------------------|--------|-------------------------|
| `app/api/wcp/payment/[id]/route.ts`   | `GET`  | `getPayment`            |
| `app/api/wcp/payment/[id]/options/route.ts` | `POST` | `getPaymentOptions`     |
| `app/api/wcp/payment/[id]/fetch/route.ts`   | `POST` | `fetchOptionActions`    |
| `app/api/wcp/payment/[id]/confirm/route.ts` | `POST` | `confirmPayment`        |
| `app/api/wcp/payment/[id]/status/route.ts`   | `GET`  | `getPaymentStatus`      |

These proxy routes are a **starting point**, not production-ready — add your own origin allowlist, rate limiting, and auth before shipping. Their only job here is to keep the Engine key off the browser.

## Step 2 — Browser transport

On the client, point the runtime at your proxy. `createHttpTransport` issues requests to `${baseUrl}/payment/:id/...`, matching the routes above.

```
import { createHttpTransport } from '@walletconnect/pay-core'

const transport = createHttpTransport({ baseUrl: '/api/wcp' })
```

That’s the entire `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 your `projectId` 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.

React — components/providers.tsx

```
'use client'

import { PayAppKitProvider } from '@walletconnect/pay-appkit/react'

const projectId = process.env.NEXT_PUBLIC_APPKIT_PROJECT_ID ?? ''

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PayAppKitProvider
      projectId={projectId}
      metadata={{
        name: 'Acme Pay',
        description: 'Headless checkout',
        url: 'https://example.com',
        icons: []
      }}
    >
      {children}
    </PayAppKitProvider>
  )
}
```

JavaScript — appkit.ts

```
import { createPayAppKit } from '@walletconnect/pay-appkit'

const payAppKit = createPayAppKit({
  projectId: import.meta.env.VITE_APPKIT_PROJECT_ID ?? '',
  metadata: {
    name: 'Acme Pay',
    description: 'Headless checkout',
    url: window.location.origin,
    icons: []
  }
})

// Construction is client-only and async — await it before reading the instance.
await payAppKit.whenReady()
export const appKit = payAppKit.getInstance()
```

`<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.

React — components/checkout.tsx

```
'use client'

import { createHttpTransport } from '@walletconnect/pay-core'
import { createAppKitSigner } from '@walletconnect/pay-appkit'
import {
  getPayAppKitInstance,
  useAppKitWalletProvider,
  usePayAppKit
} from '@walletconnect/pay-appkit/react'
import { browserClock } from '@walletconnect/pay-state'
import { usePaymentSession } from '@walletconnect/pay-react'
import { useMemo } from 'react'

export function Checkout({ paymentId }: { paymentId: string }) {
  // The provider constructs AppKit asynchronously; read the instance once it's ready.
  const { isReady } = usePayAppKit()
  const appKit = isReady ? getPayAppKitInstance() : undefined

// The wallet seam + a ready-made picker (list, search, pagination, QR URI).
  const { wallet, wallets, wcUri, getWcUri } = useAppKitWalletProvider(appKit, {
    wcPayUrl: typeof window !== 'undefined' ? window.location.href : undefined
  })

// Assemble the runtime seams. The signer is one built-in call.
  const seams = useMemo(
    () => ({
      transport: createHttpTransport({ baseUrl: '/api/wcp' }),
      clock: browserClock,
      signer: createAppKitSigner(wallet)
    }),
    [wallet]
  )

const {
    snapshot,
    connectWallet,
    disconnectWallet,
    selectOption,
    confirmSelection,
    submitInfoCapture
  } = usePaymentSession({ paymentId, seams, wallet })

return <div>{/* render per snapshot.state — see Step 5 */}</div>
}
```

JavaScript — main.ts

```
import { createHttpTransport } from '@walletconnect/pay-core'
import { createAppKitSigner, createAppKitWalletList } from '@walletconnect/pay-appkit'
import { browserClock, createPaymentController } from '@walletconnect/pay-state'

import { appKit } from './appkit'

// The framework-neutral wallet-list controller: list / search / paginate / QR URI /
// connect — and `walletList.wallet`, the seam the runtime drives.
const walletList = createAppKitWalletList(appKit, {
  wcPayUrl: window.location.href
})
const wallet = walletList.wallet

const controller = createPaymentController({
  paymentId,
  wallet,
  seams: {
    transport: createHttpTransport({ baseUrl: '/api/wcp' }),
    clock: browserClock,
    signer: createAppKitSigner(wallet)
  }
})

controller.subscribe(() => render(controller.getSnapshot()))
controller.start()
```

In React, render the checkout from a route wrapped in your providers:

app/[paymentId]/page.tsx

```
import { Checkout } from '@/components/checkout'
import { Providers } from '@/components/providers'

export default async function PaymentPage({ params }: { params: Promise<{ paymentId: string }> }) {
  const { paymentId } = await params
  return <Providers><Checkout paymentId={paymentId} /></Providers>
}
```

## 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()`).

```
switch (snapshot.state) {
  case 'ReadyForWallet':
    // Show the QR (from getWcUri/wcUri) and a wallet picker. On pick:
    return <WalletPicker wallets={wallets} onPick={(w) => connectWallet(w, w.namespaces[0])} />

case 'ConnectingWallet':
    return <Spinner label="Connecting…" />

case 'LoadingOptions':
    return <Spinner label="Finding payment options…" />

case 'OptionsReady':
    return (
      <OptionList
        options={snapshot.options}
        onSelect={(opt: PaymentOptionExtended, rank: number) => selectOption(opt, rank)}
      />
    )

case 'NoOptions':
    return <Empty label="No payment options for this wallet." />

case 'InformationCapture':
    // Render snapshot.collectData.fields, then:
    return <KycForm fields={snapshot.collectData?.fields} onSubmit={submitInfoCapture} />

case 'OptionSelected':
  case 'RequiresApproval':
    return (
      <button onClick={() => confirmSelection()}>
        {snapshot.requiresApproval ? 'Approve & pay' : 'Confirm'}
      </button>
    )

case 'AwaitingWalletApproval':
    return <Spinner label="Approve in your wallet…" />

case 'WaitingForConfirmation':
    return <Spinner label="Submitting payment…" />

case 'Succeeded':
    return <Success payment={snapshot.payment} />

case 'Failed':
  case 'PaymentExpired':
  case 'PaymentCancelled':
  case 'InvalidPayment':
  case 'SanctionedUser':
    return <Failure state={snapshot.state} error={snapshot.signingError} />
}
```

That’s a full gateway. Connect → options → (optional KYC) → confirm → sign → settle, all driven by the runtime; you only render and call actions. Once a wallet is connected, `disconnectWallet(namespace?)` drops one namespace or all of them.

## Environment variables

.env.local

```
# Reown AppKit project ID — required for wallet connection / QR pairing (public)
NEXT_PUBLIC_APPKIT_PROJECT_ID=

# WalletConnect Pay Engine — server-side only, NEVER exposed to the browser
WCP_API_URL=https://staging.api.pay.walletconnect.org
WCP_WALLET_API_KEY=
```

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.](https://github.com/WalletConnect/walletconnect-pay-examples/tree/main/gateway/headless-checkout)

[**headless-checkout-vanilla** \
\
The same checkout with no framework — `createPaymentController` + manual subscribe + imperative render.](https://github.com/WalletConnect/walletconnect-pay-examples/tree/main/gateway/headless-checkout-vanilla)

## Next steps

[**Packages Reference** \
\
The full public API of pay-core, pay-state, pay-react, and pay-appkit.](https://docs.walletconnect.com/payments/psps/headless-sdk/packages-reference)

[**API Reference** \
\
The Gateway and Payments endpoints behind the SDK.](https://docs.walletconnect.com/api-reference/index)

[How it works\
\
Previous](https://docs.walletconnect.com/payments/psps/headless-sdk/how-it-works) [Packages Reference\
\
Next](https://docs.walletconnect.com/payments/psps/headless-sdk/packages-reference)
