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
- Open the WondrPay Business portal, pick (or create) the wallet that should receive the money, and set up your shop name.
- 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 keywp_sk_test_…(shown once). - Your server creates a checkout session with the secret key and sends the customer to
session.url. - The customer pays in the WondrPay app, gasless. USDT settles on Whitechain straight to your payout wallet.
- You fulfil the order from the signed
checkout.session.completedwebhook (or by retrieving the session). The customer lands back on yoursuccessUrl.
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-Key | Optional header on POST. Same key from the same website within 24 h returns the first session instead of making a new one. |
| Test mode | All 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"
}'| Field | Type | Notes |
|---|---|---|
amount | string, required | USDT as a decimal string, "0.01" to "1000", up to 6 decimals. Numbers are accepted only if exact; send strings. |
reference | string | Your order id, 1-64 of A-Z a-z 0-9 . _ - : # /. Comes back in the webhook. |
description | string | Shown on the checkout (up to 120 characters). Public on Arkiv, so no personal data. |
successUrl / cancelUrl | URL | https on one of your website's domains (http://localhost allowed while testing). We add wondrpay_session and wondrpay_status. |
expiresIn | seconds | 300-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.
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.
GPayment links (no code)
Make a link with a fixed title and price in the portal (Payment links). Share https://wondrpay.vercel.app/l/pl_… or its QR anywhere. Each visit starts a fresh 15-minute checkout on that website, and the payment shows up in the link's stats and in your webhooks with link and reference: "link:pl_…". Switch a link off at any time.
Put a link's button on any page, even a static one with no server:
<script src="https://wondrpay.vercel.app/sdk/v1.4.0/checkout.js" integrity="sha384-3jD3JLkSDhHfnJk8Wf6YqdYoTLe0wNrIVShXyoF9IyImk0QErP0Sg7rhD9BcaR/s" crossorigin="anonymous" defer></script>
<wondrpay-button link="pl_3c4bfa1a852b395c983e" label="Pay 12.40 USDT"></wondrpay-button>GET /api/v1/links/:id | Public facts: title, amount (micro-USDT), shop name, verified, active |
POST /api/v1/links/:id/session | Send {} as JSON. Creates a session for the link (no key needed: price and shop are fixed by the link). 10 a minute per IP. |
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
amountto refund everything still refundable. - Retries: the same
Idempotency-Keyreturns the first refund, even on another server. - Webhook: your website gets a signed
refund.succeededevent.
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.
- In the portal, add a website with your shop's domain and set its webhook URL to
https://your-shop/?wc-api=wondrpay. - WooCommerce → Settings → Payments → WondrPay (USDT): enable it and paste the secret key.
- 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.liquidand 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
linkandreference: "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.
| Action | Owner | Developer |
|---|---|---|
| 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 refunds | owner'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.
| Status | type | When |
|---|---|---|
| 400 | bad_request | Bad amount, reference, URL or expiresIn |
| 401 | unauthorized | Missing, malformed, wrong or rotated secret key |
| 403 | forbidden | Website switched off, or a return URL outside your domains |
| 404 | not_found | Session doesn't exist or belongs to another website |
| 429 | - | More than 60 session creates a minute per IP (respect retry-after) |
| 500 | api_error | Our 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-endpoint | Your same-origin route that signs the order (step 3). Cross-origin endpoints are refused. |
payload | JSON sent to your endpoint (product id, cart id). Never the price. |
label | Button 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:error | QR 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-signature | EIP-191 signature of whitepay-webhook:v1:<timestamp>:<keccak256(body)> |
x-whitepay-timestamp | Unix seconds; rejected if older than 5 minutes |
x-whitepay-event-id | Unique per payment: de-duplicate on it |
| Signer | 0x27d2F6770F18E1C0Eefc129124747407Ef44aBe1 (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
permitto 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
PaymentSettledevent 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 usenoopener 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.