API reference

Base URL https://api.swtchpay.com. Looking for setup steps instead? See the developer guide and the WooCommerce plugin.

Overview

Switch Pay lets shoppers pay from Cash App, Venmo, PayPal, Coinbase, Robinhood or any crypto wallet. You create a checkout session from your server, send the shopper to the hosted checkout, and we POST a signed webhook to your server when the payment confirms. Funds settle to your balance in USD and are paid out once a day in USDC.

  • Requests and responses are JSON. Send Content-Type: application/json; bodies are limited to 10 KB.
  • Money and crypto amounts are returned as strings (for example "49.99"). Timestamps are ISO-8601 in UTC.
  • IDs: checkout sessions start with cps_, payments with cpay_, refunds with cpay_rfd_, merchants with sp_merch_.
  • Call the API from your server only. Never put an API key in browser or app code.

Authentication

Send your API key as a Bearer token on every authenticated request:

Authorization: Bearer YOUR_API_KEY

Keys are issued per merchant, together with an HMAC secret that signs your webhooks. You get both from your Switch Pay dashboard (Integrations) or from support; the secret is shown once, so store it with the key. A key can only create and read payments for the merchant it belongs to. Revoke or rotate a key from the dashboard at any time.

StatuserrorWhen
401unauthorizedMissing header, not Bearer <key>, or an unknown or revoked key
403merchant_scope_requiredThe endpoint needs a merchant key (checkout sessions)
403merchant_mismatchThe body names a different merchant than the key

Quick start

  1. Create a checkout session for the order.
  2. Send the shopper to checkout_url (redirect, new tab or an iframe).
  3. Receive the confirmed webhook, verify its signature, mark the order paid.
curl https://api.swtchpay.com/api/v1/checkout-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "order_10422",
    "amount_usd": 49.99,
    "callback_url": "https://shop.example.com/switchpay/webhook"
  }'

Checkout sessions

A checkout session is the recommended integration: the shopper picks an app on our hosted page and we create the payment for them. The WooCommerce plugin uses the same API.

Create a session

POST/api/v1/checkout-sessionsMerchant API key
FieldTypeNotes
transaction_idstring, requiredYour order id, 1 to 255 characters. Unique per merchant; repeating it returns the same session (see Idempotency).
amount_usdnumber, requiredThe order amount in USD, greater than 0 and at most 1,000,000, with up to 2 decimals. Send a JSON number, not a string. The shopper pays exactly this amount.
callback_urlstring, optionalPublic HTTPS URL that receives the webhooks for this session's payments. It must resolve in DNS to a public address. Without it, no webhooks are sent.
enabled_methodsarray, optionalLimit the app tiles shown: cashapp, venmo, paypal, coinbase, robinhood, wallet (bitcoin is coming soon). Omit to use the methods enabled on your account.
enabled_currenciesarray, optionalLimit the currencies offered (see currencies). Codes that are not accepted yet are dropped.
metadataobject, optionalString keys and string values stored with the session for your records. It is not included in API responses or webhooks.

Unknown fields are ignored. Sessions last 30 minutes, or one year when every offered currency is a stablecoin (the usual case); an expired session restarts cleanly if the shopper comes back.

Response

201 Created for a new session, 200 OK with "idempotent": true when the transaction_id already has one.

{
  "session_id": "cps_9b1e04c7a2f34d6e8c55",
  "checkout_url": "https://api.swtchpay.com/checkout/cps_9b1e04c7a2f34d6e8c55",
  "status": "awaiting_currency",
  "amount_usd": "49.99",
  "enabled_currencies": ["USDC_SOL", "USDC_ETH", "USDC_BASE", "PYUSD_SOL"],
  "enabled_methods": ["cashapp", "venmo"],
  "expires_at": "2027-09-28T17:00:00.000Z",
  "payment_id": null,
  "idempotent": false
}

Errors: 400 validation_error, 403 merchant_scope_required, 422 currency_not_accepted (nothing you asked for is accepted), 429, 500.

Retrieve a session

GET/api/v1/checkout-sessions/{session_id}No key; the session id is the credential
{
  "session_id": "cps_9b1e04c7a2f34d6e8c55",
  "status": "currency_selected",
  "amount_usd": "49.99",
  "enabled_currencies": ["USDC_SOL", "USDC_ETH", "USDC_BASE", "PYUSD_SOL"],
  "enabled_methods": ["cashapp", "venmo"],
  "expires_at": "2027-09-28T17:00:00.000Z",
  "payment_id": "cpay_5c0d2e91aa7b4f13b6e0"
}
Session statusMeaning
awaiting_currencyThe shopper has not picked an app yet (payment_id is null)
currency_selectedA payment exists; follow it with payment_id or your webhook

If the shopper switches to another app, the session gets a new payment_id, and that payment's transaction_id carries a suffix: order_10422_v2, order_10422_v3 and so on. Strip _v<n> to match your order.

Hosted page and embedding

GET/checkout/{session_id}Public page

Send the shopper to checkout_url, open it in a new tab, or load it in an iframe on your checkout page (framing is allowed from any site). Add ?method=cashapp (or any enabled method) to skip the app picker. The page shows the amount, the address, a QR code, step-by-step instructions for the chosen app, and watches the network live.

When the page is embedded, it tells your page the outcome with postMessage:

window.addEventListener('message', (e) => {
  if (e.origin !== 'https://api.swtchpay.com') return;
  if (e.data?.type === 'switchpay:confirmed') showThankYou(e.data.payment_id);
  if (e.data?.type === 'switchpay:expired' || e.data?.type === 'switchpay:failed') offerRetry();
});

Treat these events as a user-interface signal only. Mark orders paid from the signed webhook.

Payments

Create a payment directly when you already know the currency, for example in your own checkout UI. Most integrations use checkout sessions instead.

Create a payment

POST/api/v1/paymentsAPI key
FieldTypeNotes
merchant_idstring, requiredYour merchant id (sp_merch_...). It must match your key.
transaction_idstring, requiredYour order id. Unique per merchant (see Idempotency).
amount_usdnumber, requiredGreater than 0, at most 1,000,000, up to 2 decimals.
currencystring, requiredAn accepted currency code, for example USDC_SOL.
callback_urlstring, optionalPublic HTTPS webhook URL. Without it, no webhooks are sent.
customer_idstring, optionalStored for your records.

Response 201 Created

{
  "payment_id": "cpay_3f9c2a7e1b4d4c0e9a12",
  "status": "awaiting_payment",
  "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "crypto_amount": "49.990000",
  "currency": "USDC_SOL",
  "exchange_rate": "0.999812",
  "qr_code": "data:image/png;base64,iVBORw0KGgo...",
  "qr_page_url": "https://api.swtchpay.com/api/v1/payments/cpay_3f9c2a7e1b4d4c0e9a12/qr",
  "qr_image_url": "https://api.swtchpay.com/api/v1/payments/cpay_3f9c2a7e1b4d4c0e9a12/qr?raw=1",
  "expires_at": "2027-09-28T17:00:00.000Z",
  "amount_usd": "49.99"
}
  • crypto_amount is the exact amount the shopper must send to wallet_address. Send the shopper to qr_page_url, or show the address, the amount and qr_code yourself.
  • exchange_rate is the live USD price of the coin when the payment was created.
  • A repeated transaction_id returns 200 OK with the existing payment and "idempotent": true.

Errors: 400 validation_error, 403 merchant_mismatch, 422 currency_not_accepted, 503 price_unavailable (retry in a minute), 429, 500.

Retrieve a payment

GET/api/v1/payments/{payment_id}API key
{
  "payment_id": "cpay_3f9c2a7e1b4d4c0e9a12",
  "status": "confirmed",
  "merchant_id": "sp_merch_01002",
  "transaction_id": "order_10422",
  "amount_usd": "49.99",
  "currency": "USDC_SOL",
  "crypto_amount": "49.990000000000000000",
  "exchange_rate": "0.99981200",
  "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "failure_code": null,
  "expires_at": "2027-09-28T17:00:00.000Z",
  "confirmed_at": "2026-09-28T17:03:10.512Z",
  "created_at": "2026-09-28T17:00:00.101Z"
}

Payment status

GET/api/v1/payments/{payment_id}/statusAPI key
{ "payment_id": "cpay_3f9c2a7e1b4d4c0e9a12", "status": "awaiting_payment", "failure_code": null, "confirmed_at": null }

Both return 404 not_found for a payment that does not exist or belongs to another merchant. They read our records; they do not check the network themselves. Webhooks are the fastest way to learn about a confirmation.

Payment page and live status

GET/api/v1/payments/{payment_id}/qrPublic page

The hosted payment page for one payment: amount, QR code, address, copy buttons, instructions and a live tracker. Add ?embed=1 for an iframe version without page chrome, or ?raw=1 for just the QR code as a PNG image.

GET/api/v1/payments/{payment_id}/public-statusPublic
{ "payment_id": "cpay_5c0d2e91aa7b4f13b6e0", "status": "awaiting_payment", "expires_at": "2027-09-28T17:00:00.000Z", "checked_at": "2026-09-28T17:01:02.000Z", "seen": null }

Add ?check=1 to have us look at the network right away (rate-limited per payment). This is what our own pages poll; use it for a waiting screen, not to mark orders paid.

Account

GET/api/v1/accountAPI key

Who a key belongs to and which currencies you can accept right now. Useful to validate a key when someone pastes it into your settings.

{
  "provider_id": null,
  "provider_name": "Switch Pay",
  "merchant_id": "sp_merch_01002",
  "merchant_name": "Acme Coffee",
  "key_scope": "merchant",
  "supported_currencies": ["USDC_SOL", "PYUSD_SOL", "USDT_SOL", "SOL", "USDC_BASE", "ETH_BASE", "USDC_ETH", "USDT_ETH", "ETH"]
}

Audit log

GET/api/v1/auditAPI key

The API requests made with this key: payment and checkout-session calls, newest first, with 24-hour totals. Query parameters: limit (default 50, at most 500), since (ISO-8601 timestamp), path (text the URL contains), status (4xx, 5xx or an exact code). GET /api/v1/audit/live returns the last 10 requests.

{
  "stats_24h": { "total_requests": "42", "unique_ips": "2", "avg_latency_ms": 38, "max_latency_ms": 412, "error_count": "3",
                 "first_request": "2026-09-27T18:02:11.000Z", "last_request": "2026-09-28T16:59:40.000Z" },
  "entries": [
    { "id": "10231", "timestamp": "2026-09-28T16:59:40.000Z", "method": "POST", "path": "/api/v1/checkout-sessions",
      "status_code": 201, "latency_ms": 41, "ip_address": "203.0.113.7", "user_agent": "WordPress/6.6",
      "request_body": { "transaction_id": "order_10422", "amount_usd": 49.99 }, "error": null, "payment_id": null, "idempotent": false }
  ],
  "count": 1,
  "filters": { "limit": 50 }
}

Payment methods and currencies

Methods are the app tiles shoppers see. Each method pays in a currency on a specific network; the checkout picks the best one you have enabled.

MethodShopper sends
cashappUSDC (Solana, falls back to Ethereum or Base)
venmoPYUSD on Solana
paypalPYUSD on Solana
coinbaseUSDC on Base (falls back to Solana or Ethereum)
robinhoodUSDC on Solana (falls back to Base or Ethereum)
walletAny accepted currency; the shopper picks the network
bitcoinComing soon
CurrencyNetworkTypePayment expires
USDC_SOLSolanaStablecoin1 year
PYUSD_SOLSolanaStablecoin1 year
USDT_SOLSolanaStablecoin1 year
SOLSolanaNative30 minutes
USDC_BASEBaseStablecoin1 year
ETH_BASEBaseNative30 minutes
USDC_ETHEthereumStablecoin1 year
USDT_ETHEthereumStablecoin1 year
ETHEthereumNative30 minutes
BTC, PYUSD_ETHBitcoin, EthereumComing soon
  • Amounts. Stablecoins are 1:1 with USD, so a $49.99 order asks for exactly 49.99. Native coins (SOL, ETH) are quoted from the live price with a small allowance for price movement during the payment window, rounded up.
  • Tolerance. A payment confirms when at least 98% of crypto_amount arrives. Less than that is an underpayment (failed). More is accepted.
  • What you can accept today is always in supported_currencies from GET /api/v1/account.

Statuses

Payment statusfailure_codeMeaning
awaiting_paymentWaiting for the shopper's transfer
confirmedThe amount arrived on the network. Fulfil the order.
failedUNDERPAYMENTLess than the required amount arrived. Final; contact the shopper or refund from the dashboard.
expiredEXPIREDNothing arrived before expires_at. The shopper can start a new checkout.

Refunds do not change a payment's status; they are reported by refund webhooks.

Webhooks

We POST JSON to the callback_url of the payment (or of its checkout session) when a payment reaches a final status and when a refund completes. Webhooks are sent at least once: handle duplicates by payment_id and status (or refund_id).

Payment events

POST https://shop.example.com/switchpay/webhook
Content-Type: application/json
X-SwitchPay-Signature: 3b1f0c...e9
X-SwitchPay-Payment-Id: cpay_5c0d2e91aa7b4f13b6e0
X-SwitchPay-Attempt: 1

{
  "payment_id": "cpay_5c0d2e91aa7b4f13b6e0",
  "status": "confirmed",
  "merchant_id": "sp_merch_01002",
  "transaction_id": "order_10422",
  "amount_usd": "49.99",
  "currency": "PYUSD_SOL",
  "crypto_amount": "49.990000000000000000",
  "wallet_address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
  "tx_hash": "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn...",
  "received_amount": "49.99",
  "confirmed_at": "2026-09-28T17:03:10.512Z"
}
FieldSent forNotes
statusallconfirmed, failed or expired
transaction_idallMay end in _v2, _v3 after an app switch
amount_usd, currency, crypto_amount, wallet_addressallAs created
tx_hashconfirmed, failedThe network transaction when known; otherwise our internal reference
received_amountconfirmed, failedIn the payment currency
confirmed_atconfirmed
failure_codefailed, expiredUNDERPAYMENT or EXPIRED

Refund events

X-SwitchPay-Event-Type: refund.partially_refunded
X-SwitchPay-Refund-Id: cpay_rfd_8e3b1c0a7d2f4e19

{
  "event_type": "refund.partially_refunded",
  "refund_id": "cpay_rfd_8e3b1c0a7d2f4e19",
  "payment_id": "cpay_5c0d2e91aa7b4f13b6e0",
  "merchant_id": "sp_merch_01002",
  "transaction_id": "order_10422",
  "refund_amount_usd": "10.00",
  "total_refunded_usd": "10.00",
  "original_amount_usd": "49.99",
  "destination_wallet": "0x9f2c...",
  "tx_hash": "0xab12...",
  "reason": "Damaged item",
  "confirmed_at": "2026-09-28T18:10:00.000Z"
}

event_type is refund.completed once the payment is fully refunded, otherwise refund.partially_refunded.

Delivery and retries

  • We wait up to 10 seconds and do not follow redirects. Reply with a 2xx status quickly and do the work afterwards.
  • Any status below 500 counts as delivered. A 5xx, a timeout or a connection error is retried 30 seconds, 2 minutes, 10 minutes, 1 hour, 4 hours and 24 hours later (7 attempts in total).
  • The URL must be public HTTPS; it is checked again on every attempt.

Verifying signatures

X-SwitchPay-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the HMAC secret issued with your API key. Compute it over the exact bytes you received (before parsing JSON) and compare in constant time. After a key rotation, webhooks are signed with the newest key's secret.

// Node (Express: use express.raw({ type: 'application/json' }) on this route)
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', HMAC_SECRET).update(req.body).digest('hex');
const got = String(req.get('X-SwitchPay-Signature') || '');
const ok = got.length === 64 && crypto.timingSafeEqual(Buffer.from(got, 'hex'), Buffer.from(expected, 'hex'));

// PHP
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $hmacSecret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_SWITCHPAY_SIGNATURE'] ?? '');

# Python
expected = hmac.new(HMAC_SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers.get('X-SwitchPay-Signature', ''))

Refunds

Refunds are issued from your Switch Pay dashboard, not the API. A confirmed payment can be refunded in full or in parts, up to its amount. The refund goes back to the address the shopper paid from, and a refund webhook is sent when it completes. See the refund policy.

Idempotency

Your transaction_id is the idempotency key, unique per merchant. Creating a session or payment again with the same transaction_id returns the existing one with 200 OK and "idempotent": true, so retrying after a timeout is always safe. The other fields of the repeated request are ignored: to charge a different amount, use a new transaction_id.

Rate limits

Each API key has a per-minute limit (100 to 120 requests) shared across its endpoints, counted in fixed one-minute windows. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit you get:

HTTP/1.1 429 Too Many Requests

{ "error": "rate_limit_exceeded", "message": "Rate limit of 120 requests per 60s exceeded", "retry_after": 60 }

Wait for the next minute and retry. Need more? Ask support.

Errors

Errors return a JSON body with a machine-readable error and usually a human-readable message. Validation errors add details listing each problem.

{ "error": "validation_error", "message": "Invalid request body", "details": [ { "path": ["amount_usd"], "message": "Invalid input: expected number, received string" } ] }
StatuserrorMeaning
400validation_errorA field is missing or invalid, or callback_url is not public HTTPS
401unauthorizedMissing or invalid API key
403merchant_mismatch, merchant_scope_requiredThe key cannot act for that merchant, or the endpoint needs a merchant key
404not_foundUnknown id, or it belongs to another merchant
409session_unusableThe checkout session can no longer be used
422currency_not_acceptedThe currency (or every requested one) is not accepted yet
429rate_limit_exceededSlow down; see Rate limits
500internal_errorSomething went wrong on our side; retry, it is safe with the same transaction_id
503price_unavailableNo live price for that coin right now; retry in a minute

Health

GET/healthPublic
{ "status": "ok", "timestamp": "2026-09-28T17:00:00.000Z" }

Questions or an integration this page does not cover: [email protected].