React integration
Who this is for
React storefronts, SaaS consoles and custom checkout pages that need to show "amount due + QR code / wallet hand-off" in their own UI and refresh the order state once the buyer has paid.
Before you start
- You have an API KEY (see Get started).
- You have configured a collection address and set it active.
- You have a server endpoint to receive the buyer's order request. The API KEY lives only there.
Order creation always happens on your server
In the example below the frontend calls your /api/checkout, and your server calls the platform's Open API with the KEY. Put the KEY in React code (even in a VITE_* environment variable) and it is bundled into the output, where any visitor can read it.
Install
The frontend needs no extra dependency; add a wallet library only if you interact with the chain directly:
npm i viem # EVM chains (ERC20 / BEP20)Minimal working example
1. Order creation on your server
// server/checkout.js — Node/Express
import express from 'express'
const app = express()
app.use(express.json())
app.post('/api/checkout', async (req, res) => {
const r = await fetch(`${process.env.API_BASE}/api/pay/open/createOrder`, {
method: 'POST',
headers: {
'X-Pay-Api-Key': process.env.PAY_API_KEY, // server side only
'Content-Type': 'application/json',
},
body: JSON.stringify({
merchantOrderNo: req.body.orderNo, // idempotency key; reuse on retries
chainId: 2,
tokenId: 1,
amount: String(req.body.amount), // decimal string in token units
subject: `Order ${req.body.orderNo}`,
notifyUrl: 'https://your-shop.com/pay/notify',
returnUrl: 'https://your-shop.com/pay/done',
}),
}).then((x) => x.json())
if (r.code !== 0) return res.status(400).json(r)
// Pick only what the frontend needs; do not hand the whole payload to the browser
const o = r.data.order
res.json({
orderNo: o.orderNo,
payAddress: o.payAddress,
payAmountDisplay: o.payAmountDisplay, // for display; this is the amount to transfer
payAmountMinimal: o.payAmountMinimal,
identify: o.identify,
tokenContract: o.tokenContract,
tokenDecimals: o.tokenDecimals,
chainKey: o.chainKey,
idempotentReplay: r.data.idempotentReplay,
})
})
app.listen(8787)2. React frontend
// Checkout.jsx
import { useEffect, useState } from 'react'
export default function Checkout({ orderNo, amount }) {
const [order, setOrder] = useState(null)
const [err, setErr] = useState('')
useEffect(() => {
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderNo, amount }),
})
.then((r) => r.json())
.then((d) => (d.code !== 0 ? setErr(d.msg || 'create failed') : setOrder(d)))
.catch((e) => setErr(String(e)))
}, [orderNo, amount])
// Poll your own backend; your backend queries the platform
useEffect(() => {
if (!order) return
const t = setInterval(async () => {
const s = await fetch(`/api/order-status?orderNo=${order.orderNo}`).then((r) => r.json())
if (s.status === 3 || s.status === 4) clearInterval(t)
}, 3000)
return () => clearInterval(t)
}, [order])
if (err) return <p>Order failed: {err}</p>
if (!order) return <p>Creating order…</p>
return (
<div className="cg-qr">
<div>
<p>Transfer <b>{order.payAmountDisplay}</b> USDT</p>
<p>Address: <span className="cg-copy">{order.payAddress}</span></p>
<p>Chain: {order.chainKey}</p>
<p>Send exactly this amount; the identify code travels with the transaction.</p>
</div>
</div>
)
}You can also skip building your own checkout UI and send the buyer to the platform's payment counter:
{cashierBaseUrl}/pay/cashier/m/{merchantNo}. Both values come from the merchant-sideGET /api/pay/getMyProfile.
Callback handling
The callback receiver is also your server, independent of React. Key points:
// Verify with the raw body!
import crypto from 'node:crypto'
export function verify(rawBody, ts, sig) {
const want = crypto
.createHmac('sha256', process.env.PAY_API_KEY)
.update(ts).update(rawBody) // timestamp + raw body, string concatenation
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig))
}app.post('/pay/notify', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verify(raw, req.get('X-Pay-Timestamp'), req.get('X-Pay-Signature'))) {
return res.sendStatus(401)
}
const p = JSON.parse(raw)
if (isProcessed(p.merchantOrderNo)) return res.sendStatus(200) // idempotent
markPaid(p.merchantOrderNo, p.txHash)
res.sendStatus(200) // 2xx stops the retries
})- Idempotent: callbacks are retried; deduplicate on
merchantOrderNoso you never ship twice. - Return fast: about a 10-second timeout, so push slow work to a queue.
- Query is authoritative: verify with Query order before critical actions.
FAQ
| Question | Answer |
|---|---|
| Can the KEY go in the frontend? | No. The frontend calls your backend; your backend calls the platform with the KEY |
What goes in amount? | A decimal string in token units ("100.5") on the Open API |
payAmountMinimal or amountMinimal? | Use the display value of payAmountMinimal for what the buyer must transfer |
| Does double-clicking create two orders? | No — the same merchantOrderNo is idempotently reused; a new number creates a new order |
| How do I know payment succeeded? | Two tracks: callbacks plus queries. Poll your own backend, which queries the platform |