Skip to content

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 viem

Minimal 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 ​

QuestionAnswer
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)

Web3 payment infrastructure for global merchants