> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cdp.coinbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Accept Agentic Payments with x402

Checkouts created with the Coinbase Business Checkouts API return an `x402_url` — a payment endpoint an AI agent or [x402 client](/x402/buyer/quickstart) can pay programmatically. Paying it **authorizes** a gasless USDC payment on Base ([EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)); Coinbase then captures and settles it server-side, with no hosted page, wallet pop-up, or human required.

You do not implement the paying-agent side to go live. Create a checkout, expose the `x402_url`, and treat the checkout reaching `COMPLETED` as settlement.

<Note>
  Checkouts use an authorize-then-capture flow. Paying the `x402_url` authorizes (escrows) the funds; Coinbase captures and settles them server-side. Treat the checkout reaching `COMPLETED` as the source of truth for settlement — see [Confirm settlement](#2-confirm-settlement).
</Note>

## Prerequisites

* A [Coinbase Business account](https://www.coinbase.com/business) with a CDP API key. See [Authentication](/coinbase-business/authentication-authorization/api-key-authentication).

Paying-agent setup (a CDP account, funded USDC wallet, Node.js) is **not** required to launch. Skip it unless you want to [sanity-check with a test agent](#optional-sanity-check-with-a-test-agent) at the end of this guide.

## 1. Create a checkout

Create a checkout with the [Create Checkout](/api-reference/business-api/rest-api/checkouts/create-checkout) endpoint. Authenticate with a JWT Bearer token signed with your CDP API key secret; the `rat#view` scope is required (see [Authentication](/coinbase-business/authentication-authorization/api-key-authentication)).

```bash theme={null}
curl -X POST https://business.coinbase.com/api/v1/checkouts \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"amount":"1.00","currency":"USDC","description":"x402 agentic payment"}'
```

The response includes both a hosted `url` (for humans) and an `x402_url` (for agents):

```json theme={null}
{
  "id": "68f7a946db0529ea9b6d3a12",
  "status": "ACTIVE",
  "amount": "1.00",
  "currency": "USDC",
  "network": "base",
  "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "url": "https://payments.coinbase.com/payment-sessions/paymentSession_82c879c1-84e1-44ed-a8c2-1ac239cf09ad",
  "x402_url": "https://api.cdp.coinbase.com/platform/v2/payment-sessions/paymentSession_82c879c1-84e1-44ed-a8c2-1ac239cf09ad/authorizations/x402"
}
```

Share `x402_url` with agents (or your x402 client). Only single-checkout responses carry it — [List Checkouts](/api-reference/business-api/rest-api/checkouts/list-checkouts) does not — so keep the value from the create response or re-fetch the checkout by ID.

Humans can pay the hosted `url` instead — open it in a browser, connect a wallet on Base, and approve the (gasless) USDC authorization. Then confirm settlement.

## 2. Confirm settlement

After a payment is authorized, poll the [Get Checkout](/api-reference/business-api/rest-api/checkouts/get-checkout) endpoint until `status` reaches a terminal value:

```bash theme={null}
curl https://business.coinbase.com/api/v1/checkouts/68f7a946db0529ea9b6d3a12 \
  -H "Authorization: Bearer $JWT"
```

In the expected path the checkout moves `ACTIVE → PROCESSING → COMPLETED`. `PROCESSING` is set when Coinbase receives the successful authorization and starts the capture, and `COMPLETED` when the capture settles. Do not require `PROCESSING`: you may never observe it if you poll infrequently. Ordinary payment rejections (bad signature, wrong amount, insufficient payer balance) come back as another `402` and leave the checkout `ACTIVE`, so they are safe to retry. A checkout that has reached `FAILED` is not — create a new one instead.

| Status        | Meaning                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| `COMPLETED`   | Captured and settled. `transactionHash` holds the on-chain capture transaction.                       |
| `FAILED`      | The authorization or the capture did not succeed. Treat it as unpaid — the payment was not settled.   |
| `EXPIRED`     | `expiresAt` passed while the checkout was still unpaid.                                               |
| `DEACTIVATED` | You called [Deactivate Checkout](/api-reference/business-api/rest-api/checkouts/deactivate-checkout). |

You can also subscribe to [webhooks](/coinbase-business/checkout-apis/webhooks) for real-time notifications instead of polling.

## Refunds

A `COMPLETED` or `PARTIALLY_REFUNDED` checkout can be refunded (fully or partially, up to the remaining amount) with the [Refund Checkout](/api-reference/business-api/rest-api/checkouts/refund-checkout) endpoint. Refunds are asynchronous — poll the checkout to watch `refundedAmount` and `status` (`REFUNDED` or `PARTIALLY_REFUNDED`).

## Optional: sanity-check with a test agent

This is not required to go live. Use it only if you want to pay your own `x402_url` once and confirm the checkout reaches `COMPLETED`.

Agents pay `x402_url` with an x402 client (the CDP SDK is optional). Humans pay the hosted `url` in a browser. The sample below uses a CDP-managed account as one convenient test agent — it is not how your customers' agents must pay.

Checkouts advertise the EVM `auth-capture` scheme, which matches the authorize-then-capture flow.
`CdpX402Client` registers this scheme by default. If you use a client that handles only the `exact`
scheme, register `AuthCaptureEvmScheme` from [`@x402/evm`](https://www.npmjs.com/package/@x402/evm)
to pay a checkout.

To run the sample you need:

* A [CDP Secret API Key](https://portal.cdp.coinbase.com/api-keys/secret) (`CDP_API_KEY_ID` and `CDP_API_KEY_SECRET`), a [Wallet Secret](https://portal.cdp.coinbase.com/wallets/non-custodial/security) (`CDP_WALLET_SECRET`, generated separately under Security in the non-custodial wallet dashboard), and USDC on Base to fund the CDP-managed account. The sample signs through `fromCdpEvmAccount`, so no raw private keys are needed.
* [Node.js](https://nodejs.org/en) 22.18 or later, and npm.

Store them in `.env`:

```bash theme={null}
CDP_API_KEY_ID=your-api-key-id
CDP_API_KEY_SECRET=your-api-key-secret
CDP_WALLET_SECRET=your-wallet-secret
X402_URL=your-x402-url-from-step-1
```

Point an x402 client at the `x402_url` from step 1. It is a **POST** endpoint: the first request returns `402 Payment Required` with the payment requirements `base64`-encoded in the `PAYMENT-REQUIRED` response header, and the client retries with the signed payment in the `PAYMENT-SIGNATURE` request header. On success, the authorization result comes back in the `PAYMENT-RESPONSE` response header. With [`@x402/fetch`](https://www.npmjs.com/package/@x402/fetch), `wrapFetchWithPayment` handles that challenge-and-retry automatically.

<Steps>
  <Step title="Install the client packages">
    ```bash theme={null}
    npm install @coinbase/cdp-sdk @x402/fetch @x402/core @x402/evm @x402/svm @x402/extensions dotenv
    ```

    The snippets below are ES modules that use top-level `await`. Add `"type": "module"` to your `package.json` — `npm init -y` writes `"commonjs"`, which fails on the `import` statements. Run each snippet with `node <filename>.ts`; Node.js 22.18 or later runs TypeScript directly.
  </Step>

  <Step title="Create and fund the payer account">
    Run this first. It creates a CDP-managed EVM account and prints its address. Send that address USDC on Base before you pay — the server rejects an underfunded payer with another `402` whose message names insufficient balance.

    ```typescript theme={null}
    import "dotenv/config";
    import { CdpClient } from "@coinbase/cdp-sdk";

    // Reads CDP_API_KEY_ID, CDP_API_KEY_SECRET, and CDP_WALLET_SECRET from env.
    const cdp = new CdpClient();
    const account = await cdp.evm.getOrCreateAccount({ name: "x402-checkouts-payer" });
    console.log("Fund this EVM address with USDC:", account.address);
    ```
  </Step>

  <Step title="Pay the checkout">
    `wrapFetchWithPayment` signs whatever the server asks for, so cap it. `applySpendControls` bounds every payment the client will sign — raise or lower the cap to suit your agent before you point it at mainnet.

    ```typescript theme={null}
    import "dotenv/config";
    import { CdpClient } from "@coinbase/cdp-sdk";
    import { applySpendControls, fromCdpEvmAccount } from "@coinbase/cdp-sdk/x402";
    import { AuthCaptureEvmScheme } from "@x402/evm";
    import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from "@x402/fetch";

    const cdp = new CdpClient();
    const account = await cdp.evm.getOrCreateAccount({ name: "x402-checkouts-payer" });
    console.log("Paying from:", account.address);

    // Register the auth-capture scheme for Base mainnet (checkouts are Base-only).
    const signer = fromCdpEvmAccount(account);
    const client = new x402Client().register(
      "eip155:8453",
      new AuthCaptureEvmScheme(signer),
    );

    // Refuse to sign anything larger than 5 USDC (6 decimals).
    applySpendControls(client, { maxAmountPerPayment: { atomic: 5_000_000n } });

    const fetchWithPayment = wrapFetchWithPayment(globalThis.fetch, client);

    // Set X402_URL to the `x402_url` returned by Create Checkout (step 1).
    const x402Url = process.env.X402_URL;
    if (!x402Url) throw new Error("Set X402_URL to the x402_url returned by Create Checkout");

    // The x402_url is a POST endpoint. Retry: a delayed signed POST can miss
    // the challenge window and come back 402; a fresh attempt issues a new one.
    let response;
    for (let attempt = 0; attempt < 8; attempt++) {
      response = await fetchWithPayment(x402Url, { method: "POST" });
      if (response.ok) break;
    }
    const body = await response.text();
    console.log(response.status, body);
    if (!response.ok) {
      throw new Error(`Payment failed: ${response.status} ${body}`);
    }

    const settlement = response.headers.get("payment-response");
    if (settlement) console.log(decodePaymentResponseHeader(settlement));
    ```
  </Step>
</Steps>

<Note>
  [Sandbox](/coinbase-business/checkout-apis/sandbox) checkouts do not currently return an `x402_url`, so agentic payments cannot be tested there. Use a live checkout with a small amount, and only fund the address with what you intend to pay. Create the checkout after the payer is funded — unpaid checkouts expire at `expiresAt` (24 hours from creation if you omit it).
</Note>

A `200` response means the payment was **authorized**, not settled. The funds are held in escrow until Coinbase captures them. Confirm the checkout reaches `COMPLETED` ([step 2](#2-confirm-settlement)) rather than treating the HTTP response alone as final.

## Next steps

* [x402 overview](/x402/welcome) and [buyer quickstart](/x402/buyer/quickstart)
* [Checkouts API reference](/api-reference/business-api/rest-api/checkouts/introduction)
