# StablePay: full API reference StablePay is a self-custody USDC checkout for apps. Create a checkout over HTTP, send the customer to the hosted page, and receive a signed webhook when the USDC payment lands on Base or Solana. Receive-only: no private keys, funds go straight to the merchant's own wallet. API base URL: https://pay.shipfast.sh Checkout URLs returned by the API always use: https://pay.growfast.sh/c/ (Every host listed in this service's PUBLIC_HOSTS serves the same API and the same hosted pages.) Terms: not available to residents of Vietnam. Requests classified as coming from a blocked country (BLOCKED_COUNTRIES, default VN) get HTTP 451 {"error":"StablePay is not available in your region"} on POST /v1/checkouts, GET /c/:id and /x402/*. ## How it works - Merchants are created by the operator (no self-serve signup). Each merchant has an API key, a webhook URL, a webhook secret and its own receiving wallets (Base address, Solana address). - Base: each checkout gets a unique USDC amount (your price plus 0.000001 to 0.009999 USDC, unique per receiver among active checkouts). A worker reads USDC Transfer logs to the receiver, waits for 3 confirmations and matches the exact amount. The payer must send exactly the displayed amount. - Solana: Solana Pay transfer request with a per-checkout reference key, validated at "finalized" commitment. The amount is the price. - USDC only. Receive-only: StablePay holds no private keys and never custodies funds. No chargebacks. - Fees: the service code does not deduct any fee from payments (funds go directly to the merchant wallet). Payers pay normal network fees. No pricing plans are published. - Checkouts expire after 30 minutes by default (non-subscription). Payments arriving up to 24 hours after expiry are still credited. Anything that cannot be matched is recorded as unmatched for the operator. ## Authentication Merchant endpoints (/v1/*) need: Authorization: Bearer Missing or invalid key: 401 {"error":"missing bearer api key"} or {"error":"invalid api key"}. ## POST /v1/checkouts Request (JSON): - amountUsd (string, required): decimal with at most 2 decimals, greater than 0, at most 1000000. e.g. "19.00" - productName (string, required): 1-200 chars - customerRef (string, required): 1-200 chars, your own user/order id (echoed in webhooks) - successUrl (string, required): http(s) URL. Customer is redirected here after payment - cancelUrl (string, optional): http(s) URL - metadata (object, optional): arbitrary JSON, echoed back and in webhooks - periodDays (integer, optional): 1-3650. Makes it a subscription period (see Subscriptions) Example: curl -X POST https://pay.shipfast.sh/v1/checkouts \ -H "Authorization: Bearer $STABLEPAY_API_KEY" -H "Content-Type: application/json" \ -d '{"amountUsd":"19.00","productName":"Pro monthly","customerRef":"user_123","periodDays":30,"successUrl":"https://app.example/billing?ok=1"}' Response 201: { "id": "co_...", "url": "https://pay.growfast.sh/c/co_...", "expiresAt": "2026-01-01T00:30:00.000Z" } Redirect the customer to "url". ## GET /v1/checkouts/{id} Bearer auth. Only your own checkouts. 404 {"error":"not found"} otherwise. Response 200: { id, url, status, amountUsd, baseAmount, productName, customerRef, metadata, periodDays, expiresAt, paidAt, chain, tx, accessUntil, renewalOf } - amountUsd: the price. baseAmount: the exact USDC amount to send on Base (price plus unique offset). - chain: "base" or "solana" once paid, else null. tx: transaction hash/signature once paid. - accessUntil: for subscriptions, end of the paid period. renewalOf: id of the checkout this renews. ## Statuses open -> paid | expired. An expired checkout can still become paid within 24 hours (late payment). ## Hosted page and helpers (public) - GET /c/{id}: hosted payment page (Base and Solana tabs, QR codes, live status). Redirects (302) to successUrl when paid. 404 if unknown. noindex. - GET /c/{id}/status: {"status":"open|expired"} or {"status":"paid","successUrl":"..."} - GET /health: {"ok":true} ## Webhooks Sent as POST application/json to the merchant's webhook URL. Events (field "type"): - checkout.paid - checkout.expired - subscription.renewal_due (includes data.url, the renewal checkout URL) Body: { "type": "checkout.paid", "createdAt": ISO8601, "data": { checkoutId, customerRef, productName, metadata, amountUsd, amountUnits, chain, tx, paidAmountUnits, paidAt, accessUntil, periodDays, renewalOf, expiresAt } } Amounts in *Units are USDC base units (6 decimals). Signature header: StablePay-Signature: t=,v1= v1 = hex(HMAC_SHA256(webhookSecret, t + "." + rawBody)) Verify against the RAW request body, compare in constant time, and reject if |now - t| > 300 seconds. Node example (node:crypto only): const [t, v1] = req.headers["stablepay-signature"].split(",").map((p) => p.split("=")[1]); const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const ok = v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - Number(t)) <= 300; Delivery: any non-2xx or timeout (10 s) is retried with exponential backoff (30 s, doubling, capped at 6 h) for up to 24 hours. Handlers MUST be idempotent (dedupe on data.checkoutId + type). One delivery is queued per (checkout, event). ## Subscriptions Create a checkout with periodDays. When paid, accessUntil = payment time extended by periodDays (or from the current accessUntil when renewing). 3 days before accessUntil a renewal checkout (valid 7 days) is created and subscription.renewal_due is sent with its URL; your app emails the customer. One renewal per period. ## Errors JSON body {"error": string}. Statuses: - 400 invalid body ({"error":"invalid body","issues":[...zod issues]}) or invalid amountUsd (message explains) - 401 missing bearer api key / invalid api key - 404 not found - 451 StablePay is not available in your region - 503 "Could not allocate a unique Base amount; try again" (too many concurrent open checkouts at that price) - 500 internal error ## Limits - amountUsd: > 0 and <= 1000000, max 2 decimals - productName, customerRef: <= 200 chars; periodDays: 1-3650 - Checkout TTL: 30 minutes default (operator setting); late payment grace 24 h - Webhook retry window: 24 h ## x402 facilitator (operator-gated) StablePay can also act as an x402 facilitator for USDC on Base ("exact" scheme, EIP-3009): POST /x402/verify, POST /x402/settle, GET /x402/supported. Access is gated by operator-issued bearer tokens; settlement is disabled unless configured. Not part of the merchant API above; no self-serve access. ## Machine-readable - https://pay.shipfast.sh/openapi.json - https://pay.shipfast.sh/.well-known/api-catalog - https://pay.shipfast.sh/llms.txt