Skip to content
API v1 · Checkout SDK v1.4.0

Accept USDT on any website.

Get API keys in the WondrPay Business portal, create a checkout session from your server and send the customer to WondrPay's hosted checkout. They pay with no gas; the money lands in your own wallet and you get a signed webhook.

AAPI keys: how it fits together

  1. Open the WondrPay Business portal, pick (or create) the wallet that should receive the money, and set up your shop name.
  2. Add your website with the domains your checkout runs on. Your wallet approves it once. You get a publishable key wp_pk_test_… and a secret key wp_sk_test_… (shown once).
  3. Your server creates a checkout session with the secret key and sends the customer to session.url.
  4. The customer pays in the WondrPay app, gasless. USDT settles on Whitechain straight to your payout wallet.
  5. You fulfil the order from the signed checkout.session.completed webhook (or by retrieving the session). The customer lands back on your successUrl.

Non-custodial by design: with a website switched on, WondrPay's order key signs orders for your shop, but the payment contract only ever pays your registered payout wallet. A leaked secret key can create checkouts; it can never move or redirect money. Switching off your last website hands order signing back to your wallet.

BAuthentication

wp_sk_test_…Secret key. Server only, in an environment variable. Send as Authorization: Bearer wp_sk_test_…. Rotate any time in the portal (the old key stops at once).
wp_pk_test_…Publishable key. Safe in browser code; identifies your website.
Idempotency-KeyOptional header on POST. Same key from the same website within 24 h returns the first session instead of making a new one.
Test modeAll keys are test keys today: Whitechain Sepolia and test USDT. Live keys come with mainnet.

CCreate a checkout session

POST https://wondrpay.vercel.app/api/v1/checkout/sessions

curl https://wondrpay.vercel.app/api/v1/checkout/sessions \
  -H "Authorization: Bearer $WONDRPAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1042" \
  -d '{
    "amount": "12.40",
    "reference": "order_1042",
    "description": "Kulhad mug ×2",
    "successUrl": "https://shop.example.com/thank-you",
    "cancelUrl": "https://shop.example.com/cart"
  }'
FieldTypeNotes
amountstring, requiredUSDT as a decimal string, "0.01" to "1000", up to 6 decimals. Numbers are accepted only if exact; send strings.
referencestringYour order id, 1-64 of A-Z a-z 0-9 . _ - : # /. Comes back in the webhook.
descriptionstringShown on the checkout (up to 120 characters). Public on Arkiv, so no personal data.
successUrl / cancelUrlURLhttps on one of your website's domains (http://localhost allowed while testing). We add wondrpay_session and wondrpay_status.
expiresInseconds300-1800, default 900 (15 min). The price is locked until then.

Response 201:

{
  "id": "0x9f3c…e1",                  // the session id (= on-chain payment id)
  "object": "checkout.session",
  "status": "open",                   // open | paid | expired
  "amount": "12.40", "currency": "USDT",
  "reference": "order_1042",
  "url": "https://wondrpay.vercel.app/checkout/0x9f3c…e1",   // send the customer here
  "payUrl": "https://wondrpay.vercel.app/pay?req=0x9f3c…e1", // opens straight in the WondrPay app
  "expiresAt": 1793975000,
  "merchant": "0xAB…", "merchantName": "kesar.wondrpay.eth",
  "button": { "entityKey": "0x9f3c…e1", "payUrl": "…", "amount": "12400000", "deadline": "1793975000", "orderId": "0x…" }
}

Retrieve a session

GET https://wondrpay.vercel.app/api/v1/checkout/sessions/:id with the same secret key. Returns the session with status and, once paid, payment: { txHash, payer, explorer }. A key can only read its own website's sessions.

// Node.js / Next.js route: create, then redirect
const r = await fetch("https://wondrpay.vercel.app/api/v1/checkout/sessions", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.WONDRPAY_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": cart.id },
  body: JSON.stringify({ amount: cart.totalUsdt, reference: cart.id, successUrl: "https://shop.example.com/thank-you" }),
})
if (!r.ok) throw new Error((await r.json()).error.message)
const session = await r.json()
return Response.redirect(session.url, 303)

DHosted checkout

/checkout/:id shows your shop name (ENS), the World ID badge, the amount, your description and order reference, and a live countdown. On a computer it shows a QR for the WondrPay app; on a phone, a Pay button. It watches for the payment and then sends the customer to your successUrl after 5 seconds (they can stay). Expired or invalid sessions explain what happened and link back to cancelUrl.

Customers pay with the WondrPay app (QR or /pay), or with MetaMask or any browser wallet right on the page: the wallet switches to Whitechain Sepolia, recomputes the order digest and signs one EIP-3009 authorization. It's a signature, not a transaction, so they pay no gas.

Amounts are always in USDT. Next to them the page shows an estimate in the customer's own currency (for example ≈ ₹1,165, from their browser's locale, at a rate refreshed every 10 minutes). It's for display only; nothing is converted.

The page never signs anything or touches money itself. Wallets re-check the order against the on-chain registry before signing.

EPay button with API keys

Prefer a modal on your own page? Keep the Pay button and have your order endpoint return session.button:

app.post("/api/create-order", async (req, res) => {
  const amount = CATALOG[req.body.payload?.sku]          // price from your server, never the browser
  const r = await fetch("https://wondrpay.vercel.app/api/v1/checkout/sessions", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.WONDRPAY_SECRET_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ amount, reference: req.body.cartId }),
  })
  res.json((await r.json()).button)                      // { entityKey, payUrl, amount, deadline, orderId }
})

FWebhooks for sessions

Set a webhook URL on your website in the portal. When a session is paid we POST checkout.session.completed, signed exactly like order webhooks (same headers, same signer, verify with verifyWondrPayWebhook):

{
  "event": "checkout.session.completed",
  "sessionId": "0x9f3c…e1", "site": "4be1…", "reference": "order_1042",
  "orderId": "0x…", "merchant": "0xAB…", "payer": "0x97…",
  "amount": "12400000",                 // USDT micro-units (6 decimals)
  "txHash": "0x…", "explorer": "https://explorer.testnet.whitechain.io/tx/0x…",
  "receiptKey": "0x…", "paidAt": 1793974321
}

Fulfil once per x-whitepay-event-id. To double-check, retrieve the session or call verifyWondrPayPayment with the tx hash.

Every delivery to your website is logged in the portal for 30 days: event, HTTP status your server answered, tries. Resend re-sends the exact same bytes with the same event id (so your de-duplication still works). Send test webhook posts a signed { "event": "test.ping" } so you can check your endpoint before the first sale. Answer 2xx quickly; anything else is retried 3 times and shown as failed.

HRefunds by API

Refund from your server with your secret key, with no wallet prompt each time. First approve a refund allowance in the portal (Refunds by API): your payout wallet signs one USDT approval (EIP-2612 permit) for a cap, such as 50 USDT, and WondrPay submits it and pays the gas.

curl https://wondrpay.vercel.app/api/v1/refunds \
  -H "Authorization: Bearer $WONDRPAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund_order_1042" \
  -d '{ "paymentId": "0x9f3c…e1", "amount": "2.50", "reason": "damaged item" }'

→ 201 { "object": "refund", "amount": "2500000", "refunded": "2500000", "refundable": "9900000",
        "txHash": "0x…", "explorer": "…", "allowanceLeft": "47500000" }
  • Where the money goes: always back to the wallet that paid, never anywhere else.
  • How much: never more than was paid, counting every refund so far (refund requests, the dashboard and the API all see one total).
  • Exposure: at most the allowance left. Raise it or switch it off in the portal at any time.
  • Default amount: leave out amount to refund everything still refundable.
  • Retries: the same Idempotency-Key returns the first refund, even on another server.
  • Webhook: your website gets a signed refund.succeeded event.

INode helpers

@whitepay/checkout/server wraps the API:

import { createWondrPayCheckoutSession, getWondrPayCheckoutSession } from "@whitepay/checkout/server"

const session = await createWondrPayCheckoutSession({
  secretKey: process.env.WONDRPAY_SECRET_KEY!,
  amount: "12.40", reference: cart.id, description: "Kulhad mug ×2",
  successUrl: "https://shop.example.com/thank-you", idempotencyKey: cart.id,
})
// hosted:  res.redirect(303, session.url)
// button:  res.json(session.button)

const s = await getWondrPayCheckoutSession({ secretKey: process.env.WONDRPAY_SECRET_KEY!, id: session.id })
if (s.status === "paid") fulfil(cart.id, s.payment!.txHash)

JWooCommerce

Download the WondrPay for WooCommerce plugin (v0.1.0, MIT). Then, in WordPress, go to Plugins → Add New → Upload Plugin and activate it.

  1. In the portal, add a website with your shop's domain and set its webhook URL to https://your-shop/?wc-api=wondrpay.
  2. WooCommerce → Settings → Payments → WondrPay (USDT): enable it and paste the secret key.
  3. Optional: approve a refund allowance to refund from the WooCommerce order screen.

At checkout the plugin creates a session and sends the customer to WondrPay's hosted checkout. When the webhook arrives, or the customer comes back to the thank-you page, the plugin asks the WondrPay API whether that order's session is paid, and only then completes the order. USD stores charge 1:1; other currencies are converted at WondrPay's display rate.

KShopify

Shopify only lists new payment providers through its Payments Partner program, so for now use payment links. No app is needed.

  • Pay button on product pages. Make a payment link per product in the portal, then in Online Store → Themes → Edit code add the SDK to theme.liquid and a button to your product template:
<!-- theme.liquid, before </head> -->
<script src="https://wondrpay.vercel.app/sdk/v1.4.0/checkout.js" integrity="sha384-3jD3JLkSDhHfnJk8Wf6YqdYoTLe0wNrIVShXyoF9IyImk0QErP0Sg7rhD9BcaR/s" crossorigin="anonymous" defer></script>

<!-- sections/main-product.liquid: product metafield custom.wondrpay_link = "pl_…" -->
{% if product.metafields.custom.wondrpay_link %}
  <wondrpay-button link="{{ product.metafields.custom.wondrpay_link }}"
                   label="Pay {{ product.price | money_without_currency }} USDT"></wondrpay-button>
{% endif %}
  • Manual payment method. In Settings → Payments → Manual payment methods, add "Pay with WondrPay (USDT)". When an order comes in, send the customer a payment link for the total (or the product's link) and mark the order paid when the webhook arrives.
  • Confirming payments: the link's webhook carries link and reference: "link:pl_…", and each link's stats show up in the portal.

LTeam access

In the portal's Team section the shop owner can add a teammate's wallet as a developer, for example a web agency. The developer opens /portal with their own wallet and manages the shop's websites, with no shared passwords and no access to the owner's wallet.

ActionOwnerDeveloper
Add or switch off a website✓-
New secret key✓✓
Create or switch off payment links✓✓
Send test webhook, resend webhooks✓✓
Add or remove teammates✓-
Receive payouts, approve refundsowner's wallet only-

Each action is signed by the wallet that took it, and the server checks that wallet's role at the moment of the request. Removing a teammate takes effect within a few seconds.

MErrors and limits

Errors are { "error": { "type", "message" } }; messages are written to show to a developer as is.

StatustypeWhen
400bad_requestBad amount, reference, URL or expiresIn
401unauthorizedMissing, malformed, wrong or rotated secret key
403forbiddenWebsite switched off, or a return URL outside your domains
404not_foundSession doesn't exist or belongs to another website
429-More than 60 session creates a minute per IP (respect retry-after)
500api_errorOur side. Retry with the same Idempotency-Key; nothing was charged

Up to 5 websites and 20 active payment links per shop. A website's domains also limit where customers can be sent back to.

Or: sign orders yourself (SDK)

The original integration, for shops that keep their own order-signing key on their server. Everything below still works, including for shops that also use API keys.

1Load the SDK (browser)

Pin an exact version with Subresource Integrity: versioned files (/sdk/v1.4.0/) never change, and if the bytes ever differ the browser refuses to run them. /sdk/v1/checkout.js always serves the latest 1.x for quick prototypes (no SRI).

<script src="https://wondrpay.vercel.app/sdk/v1.4.0/checkout.js"
        integrity="sha384-3jD3JLkSDhHfnJk8Wf6YqdYoTLe0wNrIVShXyoF9IyImk0QErP0Sg7rhD9BcaR/s"
        crossorigin="anonymous" defer></script>

Using a Content-Security-Policy? Allow https://wondrpay.vercel.app in script-src (or put your nonce on the tag), connect-src and img-src.

2Add the button

<wondrpay-button order-endpoint="/api/create-order"
                 payload='{"sku":"chai-kit"}'
                 label="Pay 2.50 USDT"></wondrpay-button>

<script>
  document.addEventListener("wondrpay:paid", (e) => showThanks(e.detail))
</script>
order-endpointYour same-origin route that signs the order (step 3). Cross-origin endpoints are refused.
payloadJSON sent to your endpoint (product id, cart id). Never the price.
labelButton text.
wondrpay:created{ entityKey, payUrl, deadline } the order is live on Arkiv, QR shown
wondrpay:paid{ entityKey, orderId, txHash, explorer, amount } UI hint only: fulfil from the webhook
wondrpay:expired / wondrpay:errorQR expired unpaid / something failed ({ message })

It is a standard Web Component, so it works in React, Next.js, Vue (isCustomElement), Svelte, Angular (CUSTOM_ELEMENTS_SCHEMA), WordPress, Shopify themes and plain HTML.

3Sign the order (your server)

import { createWondrPayOrder } from "@whitepay/checkout/server"

const CATALOG = { "chai-kit": "2.50" }            // prices live on the server

app.post("/api/create-order", async (req, res) => {
  const amount = CATALOG[req.body.payload?.sku]
  if (!amount) return res.status(400).json({ error: "unknown product" })
  res.json(await createWondrPayOrder({
    merchantKey: process.env.SHOP_PK,               // your registered shop key, never in the browser
    amount,
    webhook: "https://shop.example/api/webhook",
  }))
})

Your shop key signs merchant, amount, orderId, deadline (EIP-712). The WondrPay router contract only settles that exact amount, to your payout address, once.

4Fulfil from the webhook

import { verifyWondrPayWebhook, verifyWondrPayPayment } from "@whitepay/checkout/server"

app.post("/api/webhook", express.text({ type: "*/*" }), async (req, res) => {
  const auth = await verifyWondrPayWebhook({ rawBody: req.body, headers: new Headers(req.headers) })
  if (!auth.ok) return res.status(401).end()          // forged, tampered or replayed
  if (await db.seen(auth.eventId)) return res.end()   // idempotent
  const { txHash, orderId, amount } = auth.event
  const v = await verifyWondrPayPayment({ txHash, merchant: SHOP, orderId, minAmount: BigInt(amount) })
  if (v.ok) await fulfil(orderId)                      // money confirmed on Whitechain
  res.end()
})
x-whitepay-signatureEIP-191 signature of whitepay-webhook:v1:<timestamp>:<keccak256(body)>
x-whitepay-timestampUnix seconds; rejected if older than 5 minutes
x-whitepay-event-idUnique per payment: de-duplicate on it
Signer0x27d2F6770F18E1C0Eefc129124747407Ef44aBe1 (also at /api/webhook-key)

5Subscriptions (Autopay)

Same button, mode="subscribe". The shopper approves once with their fingerprint; WondrPay charges every period with zero gas and sends you a signed webhook per payment.

<wondrpay-button mode="subscribe" order-endpoint="/api/create-subscription"
                 label="Subscribe · 4.99 USDT / month"></wondrpay-button>

// server
import { createWondrPaySubscription } from "@whitepay/checkout/server"
app.post("/api/create-subscription", async (_req, res) => res.json(await createWondrPaySubscription({
  merchantKey: process.env.SHOP_PK, title: "Pro plan", amount: "4.99", period: "monthly", cycles: 12,
  webhook: "https://shop.example/api/webhook",   // subscription.started | charged | completed | cancelled | failed
})))
  • One signature: the shopper signs a single EIP-2612 permit to a tiny contract made only for their subscription. Its address commits to every term, so one wallet prompt approves exactly that shop, amount, period and number of payments.
  • How it runs: each upcoming payment is an Arkiv entity whose expiry is its countdown. At 0 the keeper calls the contract's charge(), deletes the entity and creates the next one.
  • Hard limits on-chain: never early, never late (missed windows are skipped), never more than amount × cycles, never to anyone but you.
  • Cancel: one signature switches the subscription's contract off; you can also cancel from your side.
  • Verify charges: the contract emits Charged(cycle, amount); the webhook carries the tx hash.

Security model

  • Price tampering: impossible from the browser. The amount is in the shop-signed order and enforced by the contract.
  • Fake payments: the browser event is only a hint; fulfilment needs a signed webhook and a PaymentSettled event on-chain.
  • Replay: EIP-3009 nonces are single-use on-chain; webhooks expire after 5 minutes and carry an event id.
  • Supply chain: versioned SDK path + SRI hash. The SDK never touches keys or money.
  • XSS in the SDK: every network value is regex-validated and written with textContent; links use noopener noreferrer.
  • SSRF: webhooks only go to https:443 hostnames resolving to public IPs, re-checked before every send, redirects not followed.
  • Abuse: per-IP rate limits, JSON-only writes, body caps, cross-site POST blocking and per-payer relayer limits on WondrPay.
  • Refunds: only from your payout wallet, only to the original payer, capped at the amount paid (on-chain).
  • API keys: secret keys are 32 random bytes, stored only as SHA-256 and compared in constant time; every website change is approved by the shop wallet's signature. Keys can create checkouts but never change where money goes.
Whitechain Sepolia testnet · official USDT 0xcedf…55a7 · receipts on Arkiv · names on ENS