Vue 3 integration
Who this is for
Vue 3 + Vite storefronts, admin-built checkout pages or campaign pages that display the amount due and collection details, and refresh the 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 your own server endpoint to receive order requests — the API KEY lives only there.
Order creation always happens on your server
The frontend below calls your own /api/checkout. Frontend environment variables prefixed with VITE_ are bundled into the build output, which is equivalent to publishing the KEY.
Install
bash
npm create vite@latest my-shop -- --template vue
# add chain interaction only if needed:
npm i viemMinimal working example
1. Order creation on your server
js
// server/checkout.mjs
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,
'Content-Type': 'application/json',
},
body: JSON.stringify({
merchantOrderNo: req.body.orderNo,
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',
}),
}).then((x) => x.json())
if (r.code !== 0) return res.status(400).json(r)
const o = r.data.order
res.json({
orderNo: o.orderNo,
payAddress: o.payAddress,
payAmountDisplay: o.payAmountDisplay,
payAmountMinimal: o.payAmountMinimal,
identify: o.identify,
chainKey: o.chainKey,
})
})
app.listen(8787)2. Vue 3 frontend (<script setup>)
vue
<!-- Checkout.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
orderNo: { type: String, required: true },
amount: { type: [String, Number], required: true },
})
const order = ref(null)
const status = ref(1)
const err = ref('')
let timer = null
async function createOrder() {
const r = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderNo: props.orderNo, amount: props.amount }),
})
const d = await r.json()
if (d.code !== 0) {
err.value = d.msg || 'create failed'
return
}
order.value = d
}
async function pollStatus() {
const s = await fetch(`/api/order-status?orderNo=${order.value.orderNo}`).then((r) => r.json())
status.value = s.status
if (s.status === 3 || s.status === 4) {
clearInterval(timer) // confirmed / finished: stop polling
}
}
onMounted(async () => {
await createOrder()
if (order.value) timer = setInterval(pollStatus, 3000)
})
onUnmounted(() => timer && clearInterval(timer))
</script>
<template>
<div class="cg-qr">
<p v-if="err" class="cg-err">Order failed: {{ err }}</p>
<template v-else-if="order">
<div>
<p>Transfer <b>{{ order.payAmountDisplay }}</b> USDT (chain: {{ order.chainKey }})</p>
<p>Address: <span class="cg-copy">{{ order.payAddress }}</span></p>
<p v-if="status === 1">Waiting for payment…</p>
<p v-else-if="status === 2">Transaction seen, waiting for confirmations…</p>
<p v-else-if="status === 3 || status === 4" class="cg-ok">Payment confirmed</p>
</div>
</template>
<p v-else>Creating order…</p>
</div>
</template>Callback handling
The callback is received by your server, independent of Vue:
js
import crypto from 'node:crypto'
function verify(rawBody, ts, sig) {
const want = crypto
.createHmac('sha256', process.env.PAY_API_KEY)
.update(ts).update(rawBody)
.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 retries
})- Idempotent: callbacks are retried; deduplicate on
merchantOrderNo. - Return fast: about a 10-second timeout; push slow work to async.
- Query is authoritative: verify with Query order first.
FAQ
| Question | Answer |
|---|---|
Can the KEY go in import.meta.env? | No — it is bundled into the output |
| Must my wallet address match the collection address? | No. Buyers pay to the order's payAddress, which you configure in the merchant console |
| Polling interval? | 3–5 seconds. You may rely on callbacks alone, but keep the query check |
| How do I decide "paid"? | Only states 3 / 4 — never compare numeric values (expired is 5, manual is 6) |