Skip to main content

POST /x402/access

POST /x402/access
Content-Type: application/json
The primary x402 HTTP endpoint. It handles three distinct cases depending on the request body and headers:
  1. Discovery — No planId in the body. Returns HTTP 402 with all available plans.
  2. ChallengeplanId in the body, no PAYMENT-SIGNATURE header. Returns HTTP 402 with payment requirements and a challengeId.
  3. SettlementplanId in the body and a PAYMENT-SIGNATURE header. Settles the payment on-chain and returns HTTP 200 with an AccessGrant.

Case 1: Discovery

Send an empty body or a body without planId to discover all available plans and pricing.
curl -X POST https://api.example.com/x402/access \
  -H "Content-Type: application/json" \
  -d '{}'

Case 2: Challenge

Send a planId in the body without a PAYMENT-SIGNATURE header. The server creates a PENDING challenge record and returns payment requirements.
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"
  }'
FieldTypeRequiredDescription
planIdstringYesMust match a Plan.planId from the seller’s catalog.
requestIdstringNoClient-generated UUID. Auto-generated if omitted. Used as an idempotency key.
resourceIdstringNoDefaults to "default".

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 an AccessGrant.
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:
{
  "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.
If planId is missing from the request body but present in the PAYMENT-SIGNATURE payload’s accepted.extra.planId, the server extracts it automatically. This supports standard x402 clients that replay the exact same request with only the header added.

Settlement Strategies

Key0 supports two settlement strategies, configured via SellerConfig:
StrategyConfigDescription
Facilitator (default)facilitatorUrl or network defaultSends the EIP-3009 authorization to the Coinbase facilitator for verification and on-chain settlement. The facilitator pays gas.
Gas WalletgasWalletPrivateKeyVerifies and settles the EIP-3009 authorization directly using your own gas wallet. You pay gas. Supports distributed locking via Redis for multi-instance deployments.

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 StatusError CodeWhen
400TIER_NOT_FOUNDplanId does not match any plan in the catalog.
400INVALID_REQUESTMalformed PAYMENT-SIGNATURE header or request body.
402PAYMENT_FAILEDEIP-3009 verification or on-chain settlement failed.
409TX_ALREADY_REDEEMEDThe transaction hash was already used for a different challenge.
410CHALLENGE_EXPIREDThe challenge expired or was cancelled before payment arrived.
500INTERNAL_ERRORConcurrent state transition conflict or unexpected error.