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

# POST /x402/access

> The x402 HTTP payment endpoint — handles discovery, challenge issuance, and payment settlement in a single URL.

```
POST /x402/access
Content-Type: application/json
```

The primary x402 HTTP endpoint. It handles four distinct cases depending on the request body and headers:

1. **No `planId`** -- Returns HTTP 400 directing the client to `GET /discover`.
2. **Challenge** -- `planId` in the body, no `PAYMENT-SIGNATURE` header. Returns HTTP 402 with payment requirements and a `challengeId`.
3. **Settlement (subscription)** -- `planId` in the body and a `PAYMENT-SIGNATURE` header. Settles the payment on-chain and returns HTTP 200 with an `AccessGrant` (JWT).
4. **Settlement (route-based standalone)** -- Same as (3) but with a `routeId` and `resource` in the body. Key0 proxies to the backend and returns a `ResourceResponse` (backend data, no token).

***

## Case 1: No planId

Sending a body without `planId` returns HTTP 400 with a pointer to the discovery endpoint.

<Tabs>
  <Tab title="Request">
    ```bash theme={null}
    curl -X POST https://api.example.com/x402/access \
      -H "Content-Type: application/json" \
      -d '{}'
    ```
  </Tab>

  <Tab title="Response">
    **HTTP 400 Bad Request**

    ```json theme={null}
    {
      "error": "Please select a plan from the discovery API response to purchase access. Endpoint: GET /discover"
    }
    ```

    Use `GET /discover` to browse available plans:

    ```bash theme={null}
    curl https://api.example.com/discover
    ```
  </Tab>
</Tabs>

***

## Case 2: Challenge

Send a `planId` in the body without a `PAYMENT-SIGNATURE` header to purchase a subscription. For standalone route calls, send `routeId` plus a `resource` field without a `PAYMENT-SIGNATURE` header. The server creates a PENDING challenge record and returns payment requirements.

<Tabs>
  <Tab title="Request">
    **Subscription plan:**

    ```bash theme={null}
    curl -X POST https://api.example.com/x402/access \
      -H "Content-Type: application/json" \
      -d '{
        "planId": "basic",
        "requestId": "550e8400-e29b-41d4-a716-446655440000",
        "resourceId": "default"
      }'
    ```

    **Route-based standalone call:**

    ```bash theme={null}
    curl -X POST https://api.example.com/x402/access \
      -H "Content-Type: application/json" \
      -d '{
        "routeId": "weather-query",
        "resource": { "method": "GET", "path": "/api/weather/london" }
      }'
    ```

    | Field        | Type               | Required          | Description                                                                             |
    | ------------ | ------------------ | ----------------- | --------------------------------------------------------------------------------------- |
    | `planId`     | string             | Subscription only | Must match a `Plan.planId` from the seller's catalog.                                   |
    | `routeId`    | string             | Route-based only  | Must match a `Route.routeId` from the seller's route catalog.                           |
    | `requestId`  | string             | No                | Client-generated UUID. Auto-generated if omitted. Used as an idempotency key.           |
    | `resourceId` | string             | No                | Defaults to `"default"` for subscription flows.                                         |
    | `resource`   | `{ method, path }` | Route-based only  | Required for standalone route calls. Specifies the backend route to call after payment. |
  </Tab>

  <Tab title="Response">
    **HTTP 402 Payment Required**

    **Headers:**

    | Header             | Value                                                                                    |
    | ------------------ | ---------------------------------------------------------------------------------------- |
    | `payment-required` | Base64-encoded JSON of the payment requirements                                          |
    | `www-authenticate` | `Payment realm="https://api.example.com", accept="exact", challenge="http-a1b2c3d4-..."` |

    **Body:**

    ```json theme={null}
    {
      "x402Version": 2,
      "resource": {
        "url": "https://api.example.com/x402/access",
        "method": "POST",
        "description": "Access to default",
        "mimeType": "application/json"
      },
      "accepts": [
        {
          "scheme": "exact",
          "network": "eip155:84532",
          "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
          "amount": "100000",
          "payTo": "0xSellerWalletAddress",
          "maxTimeoutSeconds": 900,
          "extra": {
            "name": "USDC",
            "version": "2",
            "description": "basic — $0.10 USDC"
          }
        }
      ],
      "extensions": {
        "key0": {
          "inputSchema": {
            "type": "object",
            "properties": {
              "planId": { "type": "string", "description": "Tier to purchase. Must be 'basic'" },
              "requestId": { "type": "string", "description": "Client-generated UUID for idempotency (auto-generated if omitted)" },
              "resourceId": { "type": "string", "description": "Optional: Specific resource identifier (defaults to 'default')" }
            },
            "required": ["planId"]
          },
          "outputSchema": {
            "type": "object",
            "properties": {
              "type": { "type": "string", "enum": ["AccessGrant"] },
              "challengeId": { "type": "string", "description": "Challenge ID that was fulfilled" },
              "requestId": { "type": "string", "description": "Original request ID" },
              "accessToken": { "type": "string", "description": "JWT token for API access" },
              "tokenType": { "type": "string", "description": "Token type (usually 'Bearer')" },
              "resourceEndpoint": { "type": "string", "description": "URL to access the protected resource" },
              "resourceId": { "type": "string", "description": "Resource that was purchased" },
              "planId": { "type": "string", "description": "Plan that was purchased" },
              "txHash": { "type": "string", "description": "On-chain transaction hash" },
              "explorerUrl": { "type": "string", "description": "Blockchain explorer URL" }
            }
          },
          "description": "Access to default via basic tier"
        }
      },
      "challengeId": "http-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "error": "Payment required"
    }
    ```

    The `challengeId` is included in the body for reference. The `accepts[0]` object contains everything the client needs to construct an EIP-3009 `transferWithAuthorization` signature.
  </Tab>
</Tabs>

***

## Case 3: Settlement

Resend the same request with a `PAYMENT-SIGNATURE` header containing a base64url-encoded `X402PaymentPayload`. The server settles the payment on-chain and returns either an `AccessGrant` (subscription) or a `ResourceResponse` (route-based standalone).

<Tabs>
  <Tab title="Request">
    ```bash theme={null}
    curl -X POST https://api.example.com/x402/access \
      -H "Content-Type: application/json" \
      -H "PAYMENT-SIGNATURE: eyJ4NDAyVmVyc2lvbiI6Miwi..." \
      -d '{
        "planId": "basic",
        "requestId": "550e8400-e29b-41d4-a716-446655440000",
        "resourceId": "default"
      }'
    ```

    The `PAYMENT-SIGNATURE` header is a base64url-encoded JSON object with the following structure:

    ```json theme={null}
    {
      "x402Version": 2,
      "network": "eip155:84532",
      "payload": {
        "signature": "0xabc...",
        "authorization": {
          "from": "0xClientAddress",
          "to": "0xSellerAddress",
          "value": "100000",
          "validAfter": "0",
          "validBefore": "9999999999",
          "nonce": "0x123"
        }
      },
      "accepted": {
        "scheme": "exact",
        "network": "eip155:84532",
        "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "amount": "100000",
        "payTo": "0xSellerAddress",
        "maxTimeoutSeconds": 900
      }
    }
    ```

    The `accepted` field echoes back the payment requirements from the 402 response. The `payload.signature` is the EIP-3009 `transferWithAuthorization` signature. The `payload.authorization` contains the EIP-3009 parameters.

    <Note>
      If `planId` or `routeId` is missing from the request body but present in the `PAYMENT-SIGNATURE` payload's `accepted.extra`, the server extracts it automatically. This supports standard x402 clients that replay the exact same request with only the header added.
    </Note>
  </Tab>

  <Tab title="Response">
    **HTTP 200 OK**

    **Headers:**

    | Header             | Value                                    |
    | ------------------ | ---------------------------------------- |
    | `payment-response` | Base64-encoded `X402SettleResponse` JSON |

    **Body:**

    ```json theme={null}
    {
      "type": "AccessGrant",
      "challengeId": "http-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "requestId": "550e8400-e29b-41d4-a716-446655440000",
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZXF1ZXN0SWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJyZXNvdXJjZUlkIjoiZGVmYXVsdCIsInBsYW5JZCI6ImJhc2ljIiwiaWF0IjoxNzA1MzE1MjAwfQ.abc123",
      "tokenType": "Bearer",
      "resourceEndpoint": "https://api.example.com/a2a/resources/default",
      "resourceId": "default",
      "planId": "basic",
      "txHash": "0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385",
      "explorerUrl": "https://sepolia.basescan.org/tx/0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385"
    }
    ```

    The `payment-response` header contains:

    ```json theme={null}
    {
      "success": true,
      "transaction": "0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385",
      "network": "eip155:84532",
      "payer": "0xClientAddress"
    }
    ```
  </Tab>
</Tabs>

***

## Settlement Strategies

Key0 supports two settlement strategies, configured via `SellerConfig`:

| Strategy                  | Config                              | Description                                                                                                                                                             |
| ------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Facilitator** (default) | `facilitatorUrl` or network default | Sends the EIP-3009 authorization to the Coinbase facilitator for verification and on-chain settlement. The facilitator pays gas.                                        |
| **Gas Wallet**            | `gasWalletPrivateKey`               | Verifies and settles the EIP-3009 authorization directly using your own gas wallet. You pay gas. Supports distributed locking via Redis for multi-instance deployments. |

## Case 4: Route-Based Standalone Response

When settlement succeeds for a route-based call (with `proxyTo` or `fetchResource` configured), the response is a `ResourceResponse` containing the backend's data — **no JWT is issued**:

```json theme={null}
{
  "type": "ResourceResponse",
  "challengeId": "http-a1b2c3d4-...",
  "requestId": "550e8400-...",
  "routeId": "weather-query",
  "txHash": "0xSettledTx...",
  "explorerUrl": "https://sepolia.basescan.org/tx/0xSettledTx...",
  "resource": {
    "status": 200,
    "body": { "city": "london", "tempF": 65, "condition": "Cloudy" }
  }
}
```

If the backend returns a non-2xx status, the challenge remains `PAID` (awaiting refund) and the response includes the backend's error body.

***

## Pre-Settlement Check

Before settling a payment, the server checks for existing challenge records:

* **Already DELIVERED**: Returns the cached `AccessGrant` immediately (HTTP 200). No on-chain transaction.
* **EXPIRED or CANCELLED**: Returns an error (HTTP 410). The client must start a new flow.
* **PENDING or PAID**: Proceeds with settlement.

This prevents burning USDC on duplicate settlements.

## Error Responses

| HTTP Status | Error Code            | When                                                             |
| ----------- | --------------------- | ---------------------------------------------------------------- |
| 400         | `TIER_NOT_FOUND`      | `planId` does not match any plan in the catalog.                 |
| 400         | `INVALID_REQUEST`     | Malformed `PAYMENT-SIGNATURE` header or request body.            |
| 400         | `RESOURCE_REQUIRED`   | Route-based standalone request missing the `resource` field.     |
| 402         | `PAYMENT_FAILED`      | EIP-3009 verification or on-chain settlement failed.             |
| 409         | `TX_ALREADY_REDEEMED` | The transaction hash was already used for a different challenge. |
| 410         | `CHALLENGE_EXPIRED`   | The challenge expired or was cancelled before payment arrived.   |
| 500         | `INTERNAL_ERROR`      | Concurrent state transition conflict or unexpected error.        |

***

## A2A-Native Clients (Express only)

When using the **Express integration**, the same `POST /x402/access` endpoint also handles A2A JSON-RPC requests. A2A-native clients (e.g. the Google ADK `A2AClient`) signal their intent by including the `X-A2A-Extensions` header in the request:

```http theme={null}
POST /x402/access
Content-Type: application/json
X-A2A-Extensions: a2a/1.0
```

When this header is detected, the Express middleware skips the x402 HTTP flow and delegates the request directly to the A2A JSON-RPC handler (`Key0Executor`). The response conforms to the A2A protocol (task lifecycle with `AccessRequest` and payment metadata).

<Note>
  Header-based A2A routing is **Express only**. The Hono and Fastify integrations serve the standard x402 HTTP flow on `POST /x402/access` only. A2A-native agents should use the Express integration or deploy the Key0 standalone Docker image.
</Note>

| Header             | Value     | Effect                                                       |
| ------------------ | --------- | ------------------------------------------------------------ |
| *(absent)*         | —         | Standard x402 HTTP flow (discovery / challenge / settlement) |
| `X-A2A-Extensions` | any value | Delegates to A2A JSON-RPC handler (Express only)             |

***

## Related

<CardGroup cols={2}>
  <Card title="x402 HTTP Flow" icon="money-bill-transfer" href="/protocol/x402-http-flow">
    End-to-end walkthrough of the x402 payment protocol over HTTP.
  </Card>

  <Card title="A2A Flow" icon="robot" href="/protocol/a2a-flow">
    How A2A-native agents discover and pay for access via Key0Executor.
  </Card>

  <Card title="Data Models" icon="database" href="/api-reference/data-models">
    TypeScript types for X402PaymentPayload, X402SettleResponse, and more.
  </Card>

  <Card title="ChallengeEngine" icon="gears" href="/sdk-reference/challenge-engine">
    The state machine that processes challenges created by this endpoint.
  </Card>
</CardGroup>
