Skip to content
LogoLogo

HTTP 402 challenge

An unpaid gateway resource returns status 402. One response may contain both of these carriers:

HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment method="sui.charge", challenge="...", amount="...", ...
PAYMENT-REQUIRED: <base64 of the JSON body below>
Content-Type: application/json
 
{
  "x402Version": 2,
  "error": "payment_required",
  "resource": {
    "url": "https://gateway.example/api/quote",
    "description": "...",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "sui:testnet",
      "amount": "...",
      "asset": "0x2::sui::SUI",
      "payTo": "0x...",
      "maxTimeoutSeconds": 60,
      "extra": {
        "paymentFlow": "upfront",
        "suipay": { "challengeId": "...", "packageId": "0x...", "digest": "...", "...": "..." }
      }
    }
  ]
}

The example is structural. Deployments supply real addresses, amounts, package IDs, revisions, and signed extra fields. Do not fabricate omitted values.

x402 v2 carries the challenge in the PAYMENT-REQUIRED header (base64 JSON); the JSON body is the same object, kept for readability. extra.paymentFlow is always upfront: Sui Agent Payments settles before it serves. extra.suipay carries the signed offer terms a Sui Agent Payments-aware payer needs; a stock payer echoes the whole accept back as accepted and pays with a native Coin<T> transfer (SplitCoins + TransferObjects) or with payment::settle.

The facilitator routes POST /v1/x402/verify and POST /v1/x402/settle are not public. Each requires Authorization: Bearer <SUIPAY_SECRET>, the gateway's challenge secret, and answers 401 without it. They use the same facilitator the gateway settles direct-rail offers with. GET /v1/x402/supported needs no auth. MCP and A2A carriage is in @suipay/seller-runtime.

Failure response

A payment that is refused answers 402 again with a fresh PAYMENT-REQUIRED and a PAYMENT-RESPONSE in the x402 v2 failure form, so a client can tell "pay" from "your payment failed":

{ "success": false, "errorReason": "insufficient_funds", "transaction": "", "network": "sui:testnet", "payer": "0x..." }

A payment header that does not decode answers 400 with errorReason: "invalid_payload" and no PAYMENT-REQUIRED. A broadcast whose outcome is unknown answers 503 with errorReason: "settlement_pending" and the broadcast digest in transaction; reconcile that digest before paying again. The Sui Agent Payments code for every case is in X-Suipay-Error.

Both carriers encode one internal offer (same challenge ID, amount, asset, recipient, network, request binding). A buyer uses one advertised dialect.

Canonical offer fields

FieldMeaning
challengeIdSingle-use ID shared by both dialects
resourceAbsolute paid resource URL
amountGross amount as an atomic-unit decimal string
netAmount, platformFeeExplicit fee split when present
asset, decimals, networkFully qualified coin type, display decimals, Sui network
payTo, packageIdRecipient and settlement package
issuedAt, expiresAtEpoch-millisecond validity window
settlementRailOn-chain engine: spend account for agents (delegate on the wire)
serviceId, serviceRevisionImmutable service snapshot
method, pathBound HTTP request target
bodyHash, contentTypeBinding for a body-bearing request
digestHMAC over the versioned canonical offer

Additional signed fields exist for subscription and refundable offers. Decode the advertised format and validate all present terms rather than dropping unfamiliar signed fields.

Settlement rails on the offer

ValueNotes
delegateSpend-account escrow (agent spends from a funded account under a policy); MPP proof by default
directNon-escrow baseline
recurringRecurring mandate (intent: subscription; Move module subscription)

Credentials and receipts

DialectRetry credentialReceipt
MPPAuthorization: Payment …Payment-Receipt
x402PAYMENT-SIGNATURE: <base64 payload>PAYMENT-RESPONSE

Never send both credentials. Never forward either across an origin-changing redirect. The retry must preserve URL, method, headers relevant to the resource, and body bytes. A POST offer requires the body binding.

Spend-account MPP proof

MPP pay-then-prove credentials for the spend-account rail carry the finalized transaction digest, payer, challenge, and complete offer. They do not carry spendable transaction bytes. Current spend-account resources reject transaction credentials and require an MPP proof.

Binding hashes used on chain:

paymentIdHash = sha256(challengeId)
termsHash     = sha256(offer.digest)

Agent payment sketch

  1. Unpaid request → 402.
  2. Validate offer.
  3. Build and sign spend_account::settle_policy as the registered delegate (or use MCP pay / SDK payer).
  4. Execute; wait for finality.
  5. Retry with MPP proof → resource + receipt.

See Pay with REST and Pay with the SDK.