Skip to content

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:

bash
npm i viem            # EVM chains (ERC20 / BEP20)

Minimal working example ​

1. Order creation on your server ​

js
// 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 ​

jsx
// 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-side GET /api/pay/getMyProfile.

Callback handling ​

The callback receiver is also your server, independent of React. Key points:

js
// 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))
}
js
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 merchantOrderNo so 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 ​

QuestionAnswer
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

Web3 payment infrastructure for global merchants