docs
Get startedTroubleshootingPaymentsWebhooksAgent paymentsSwop MCPOviIdentity (swop.id)SwivyRPC (preview)API reference

Headless checkout

Your products, your cart, your payment UI. Your server sends Swop the cart and Swop returns a payment for the buyer to complete on your page.

Headless checkout gives you full control of the buying experience. It's how swopme.co/shop works.

The flow

  1. The buyer builds a cart on your site.
  2. Your server sends the cart to Swop: POST /checkout-intents. Swop prices everything from your catalogue and returns a checkout.
  3. Your page shows the amount and a way to pay: a USDC payment link/QR code, and a card form if card is offered.
  4. The buyer pays. Swop detects the payment, creates the order and emails the receipt.
  5. Your page polls GET /checkout-intents/{intentId} until it's paid, and your back office gets a checkout.paid webhook.

Key scopes: commerce.checkout (create) and commerce.read (poll). commerce.checkout is the only merchant scope that can create something payable, so give it only to the server that needs it.

1. Create the checkout

POST https://apps.apiswop.co/api/v5/merchant/checkout-intents

FieldTypeNotes
itemsarray, required[{ productId, quantity, selectedOptions }]. Up to 60 lines. quantity from 1 to 999 (default 1). selectedOptions looks like { "Color": "Black" }; send one value for every option group the product has.
customerInfoobject{ name, email, phone, address: { line1, line2, city, state, postalCode, country } }. Required for physical items: name, email, address.line1, address.city, address.postalCode, address.country.
paymentRailstringLeave it out for crypto only. Send "buyer_choice" to offer card too, when your account and cart qualify. Any other value is a 400.
descriptionstringOptional. Defaults to one built from the items.
expiresAtISO 8601Optional. Default 24 hours, maximum 30 days.

Never send prices

Swop prices every line, including shipping, from your catalogue. A request with price, amount or unitPrice on any item is rejected, not ignored. That way a tampered cart in the browser can never change what the buyer pays.

curl -X POST https://apps.apiswop.co/api/v5/merchant/checkout-intents \
  -H "Authorization: Bearer $SWOP_MERCHANT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "productId": "66a1b2c3d4e5f6a7b8c9d0e1", "quantity": 1, "selectedOptions": { "Color": "Black" } }
    ],
    "customerInfo": {
      "name": "Ada Buyer",
      "email": "ada@example.com",
      "address": { "line1": "1 Main St", "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US" }
    },
    "paymentRail": "buyer_choice"
  }'

The 201 response is the full checkout. See the checkout-intents reference for every field; these are the ones your page uses:

FieldUse it to
intentIdStore with your order; poll with it.
fees.totalDueAmountShow the buyer what they pay (USDC, shipping included).
fees.merchantReceivesAmountShow yourself what you'll receive after the 0.5% fee.
paymentRequestPresent the USDC payment (see below). null while the buyer still has to choose card or crypto.
cardOfferedWhether to show a card option.
checkoutUrlSwop's hosted page for this checkout, if you'd rather redirect or frame it.
expiresAtWhen it stops being payable.

Retries create duplicates

POST /checkout-intents has no idempotency key today. If your request times out and you retry, you may create a second checkout. Only one will ever be paid, but keep the intentId you showed the buyer and ignore the other.

2. Take the payment

Crypto (USDC)

When paymentRequest is set, it's a Solana Pay request for USDC on Solana:

"paymentRequest": {
  "rail": "solana_pay",
  "amount": 30,
  "currency": "USDC",
  "tokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "url": "solana:…",
  "status": "awaiting_payment",
  "expiresAt": "2026-10-28T18:00:00.000Z"
}

Show paymentRequest.url as an Open your wallet link (mobile) and as a QR code (desktop). The buyer's wallet pays it; Swop sees the transfer on-chain and completes the order. You don't call anything to confirm.

Card

When cardOffered is true, paymentRequest stays null until the buyer picks a method. Show two buttons and call one of these public endpoints. They need no API key: the unguessable intentId authorises them, so you can call them from the browser or your server.

Buyer picksCallYou get
USDCPOST https://apps.apiswop.co/api/v5/checkout-intents/{intentId}/select-crypto (no body)Confirmation. Read the checkout again to get paymentRequest.
CardPOST https://apps.apiswop.co/api/v5/checkout-intents/{intentId}/card-payment-intents (no body)clientSecret and publishableKey. Mount Stripe's Payment Element with them.
Back to USDC after choosing cardPOST …/{intentId}/card-payment/release, then select-crypto

Card numbers go from the buyer's browser straight to Stripe and never touch Swop or your server. These public endpoints are rate-limited (20 requests per 15 minutes per client).

3. Show "paid"

Poll GET /checkout-intents/{intentId} every few seconds while the buyer is on your payment step:

const res = await fetch(`https://apps.apiswop.co/api/v5/merchant/checkout-intents/${intentId}`, {
  headers: { Authorization: `Bearer ${process.env.SWOP_MERCHANT_KEY}` },
});
const { data } = await res.json();
statusMeaningShow the buyer
active, pending_paymentWaiting for paymentKeep waiting
paid, settledPaid; the order exists"Payment received"
conversion_failed, settlement_failedSwop has the money, but a step after payment failed"We have your payment. Support will complete your order or refund you." Never say "you weren't charged".
refunding, refundedBeing refunded / refundedSay so
expired, cancelledNo longer payableOffer to start again

For card, the checkout can stay active while Stripe processes the charge. Also read GET https://apps.apiswop.co/api/v5/checkout-intents/{intentId}/card-payment (public) and treat a state of transfer_pending, transfer_in_progress or transferred as paid.

Polling is for the buyer's screen. For your order system, use webhooks.

Go-live checklist

  • Key in a server environment variable, never in the browser.
  • Key has only commerce.checkout and commerce.read (add commerce.embed only if you frame).
  • No prices in the checkout request.
  • Physical items send name, email and a full address.
  • Your webhook endpoint verifies Swop-Signature and de-duplicates deliveries.
  • You ship on checkout.paid and book revenue on payout.released.
  • Error states never tell a buyer they weren't charged.
  • One real low-value purchase completed end to end.

On this page