Browse docs
Funding
Panes is prepaid. Signup provides no credit. There are no automatic top-ups or usage overages. Each top-up is $10 to $10,000, expressed as an integer number of USD cents. Add as many settled top-ups as you need; total account credit has no purchase cap. The full purchase amount becomes credit.
GET /v1/payment-methods before choosing a rail. It lists only currently enabled methods. stripe_mpp uses a Stripe Shared Payment Token; usdc_x402 uses USDC on Base; checkout opens a browser checkout. A card wallet may require its owner to approve a payment. A funded wallet with delegated spending authority can pay without that step.
Create one immutable quote
curl --fail-with-body -sS "$PANES_API/v1/topups" \
-H "Authorization: Bearer $PANES_API_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $TOPUP_REQUEST_KEY" \
-d '{"amount_cents":1000,"method":"usdc_x402"}' -o topup.json
export TOPUP_ID="$(jq -er .id topup.json)"
export PAY_URL="$(jq -er .pay_url topup.json)"
export STATUS_URL="$(jq -er .status_url topup.json)"Keep the same request key, amount and method on retry. Quotes expire after 30 minutes. A new quote does not charge anything. POST pay_url without a credential returns HTTP 402 and the protocol challenge.
USDC x402: HTTP wallet signing
The quote's payment.accepts[0] contains the exact recipient, token, amount and network. Mainnet is Base (eip155:8453), using native USDC with 6 decimals. Do not send another token or use another chain.
The requests below use an authorized EVM wallet exposing eth_signTypedData_v4 over JSON-RPC. Set WALLET_RPC_URL and WALLET_ADDRESS from that wallet. It must hold at least the quoted amount of USDC. The facilitator submits the authorization and pays transaction gas. Never expose an unlocked signer publicly.
export NONCE="0x$(openssl rand -hex 32)"
export VALID_BEFORE="$(($(date +%s) + 240))"
jq --arg from "$WALLET_ADDRESS" --arg nonce "$NONCE" --arg end "$VALID_BEFORE" '
.payment.accepts[0] as $r |
{domain:{name:$r.extra.name,version:$r.extra.version,
chainId:($r.network|split(":")[1]|tonumber),verifyingContract:$r.asset},
types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"},
{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}],
TransferWithAuthorization:[{name:"from",type:"address"},{name:"to",type:"address"},
{name:"value",type:"uint256"},{name:"validAfter",type:"uint256"},
{name:"validBefore",type:"uint256"},{name:"nonce",type:"bytes32"}]},
primaryType:"TransferWithAuthorization",
message:{from:$from,to:$r.payTo,value:$r.amount,validAfter:"0",validBefore:$end,nonce:$nonce}}
' topup.json > typed-data.json
jq -n --arg from "$WALLET_ADDRESS" --arg typed "$(jq -c . typed-data.json)" \
'{jsonrpc:"2.0",id:1,method:"eth_signTypedData_v4",params:[$from,$typed]}' > sign-request.json
curl --fail-with-body -sS "$WALLET_RPC_URL" -H 'Content-Type: application/json' \
--data-binary @sign-request.json -o signature.json
export SIGNATURE="$(jq -er '.result | select(startswith("0x"))' signature.json)"
jq --arg signature "$SIGNATURE" --slurpfile typed typed-data.json '
{x402Version:2,resource:.payment.resource,accepted:.payment.accepts[0],
payload:{signature:$signature,authorization:$typed[0].message}}
' topup.json > payment-payload.json
export PAYMENT_SIGNATURE="$(jq -c . payment-payload.json | tr -d '\n' | base64 | tr -d '\n')"
curl --fail-with-body -sS -X POST "$PAY_URL" \
-H "Authorization: Bearer $PANES_API_KEY" \
-H "PAYMENT-SIGNATURE: $PAYMENT_SIGNATURE" -o payment.jsonFor Coinbase CDP Server Wallet, use EIP-712 signing with the same typed data. An x402-capable wallet can also handle the 402 exchange directly. Keep the original quote and payment payload until the status is settled.
Stripe MPP: scoped card authorization
Create a quote with "method":"stripe_mpp". Its payment is an MPP Stripe charge challenge. Authorize its amount, currency, merchant networkId and expiry through your wallet's SPT API. The resulting spt_… is a payment credential; it is not your Panes API key.
Use an MPP wallet to construct the Payment-Authorization header. The server advertises that header so Authorization remains available for the Panes key:
curl --fail-with-body -sS -X POST "$PAY_URL" \
-H "Authorization: Bearer $PANES_API_KEY" \
-H "Payment-Authorization: $PAYMENT_AUTHORIZATION" -o payment.jsonThe credential payload is {"spt":"spt_…","externalId":"<topup.id>"}; echo the issued challenge unchanged. MPP wallet documentation.
Confirm settlement and recover
curl --fail-with-body -sS "$STATUS_URL" \
-H "Authorization: Bearer $PANES_API_KEY" -o payment-status.json
jq '{id,status,balance_usd,next_action}' payment-status.json| Status | What to do |
|---|---|
quoted | Authorize this quote before its expiry. |
pending | Poll the same status URL after 3 seconds. Do not pay again. |
paid | Credit is available. Save the receipt and continue. |
failed | This attempt did not settle. Use a new quote and an authorized payment method. |
expired | No payment was submitted. Create a new quote with a new request key. |
reconcile | Do not pay again. Contact support@usepanes.com with the top-up ID. |
refunded | The credit was reversed. Check your balance before starting work. |
A payment can settle even when its HTTP reply is lost. GET status resumes the stored attempt. Panes stops automatic Stripe creation retries before its idempotency guarantee expires. An ambiguous on-chain settlement is held for reconciliation instead of asking for another transfer.
Browser fallback
Create a checkout quote, then POST its pay_url. Open the returned checkout_url in a browser and complete Stripe Checkout. Poll status_url afterward. This rail requires a browser and does not satisfy unattended funding.
Refunds and disputes affect the same prepaid balance regardless of rail. A dispute blocks new compute immediately and gives running computers five minutes to save work. The refund policy governs refunds.