指南
Express
在 Express 后端处理 Webhook 与订单。
安装
pnpm add @stableops/api-sdk express配置
STABLEOPS_API_KEY=sk_sandbox_...
STABLEOPS_WEBHOOK_SECRET=whsec_...
STABLEOPS_API_URL=https://api.stableops.dev创建 lib/stableops.js:
const { StableOps } = require('@stableops/api-sdk')
const stableops = new StableOps({
apiKey: process.env.STABLEOPS_API_KEY,
environment: 'sandbox',
baseUrl: process.env.STABLEOPS_API_URL,
})
module.exports = { stableops }创建支付单
const express = require('express')
const { stableops } = require('../lib/stableops')
const router = express.Router()
router.post('/orders', async (req, res) => {
try {
const { merchantOrderId, amount, metadata } = req.body
if (!merchantOrderId || !amount) {
return res
.status(400)
.json({ error: 'merchantOrderId and amount are required' })
}
const order = await stableops.paymentOrders.create(
{
merchantOrderId,
amount: String(amount),
settlementAsset: 'USDC',
acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
metadata: metadata || {},
},
{ idempotencyKey: merchantOrderId },
)
res.status(201).json({
id: order.id,
merchantOrderId: order.merchantOrderId,
amount: order.amount,
status: order.status,
paymentInstructions: order.paymentInstructions,
})
} catch (error) {
res.status(error.status || 500).json({
error: error.code || 'stableops_error',
message: error.message,
})
}
})
router.get('/orders/:id', async (req, res) => {
const order = await stableops.paymentOrders.retrieve(req.params.id)
res.json(order)
})
router.post('/orders/:id/cancel', async (req, res) => {
const order = await stableops.paymentOrders.cancel(req.params.id)
res.json(order)
})
module.exports = router验证 Webhook
Webhook 路由必须挂在 express.json() 之前,让 handler 拿到原始 body 用于验签。
const express = require('express')
const { SIGNATURE_HEADER, verifySignature } = require('@stableops/api-sdk/webhooks')
const router = express.Router()
const WEBHOOK_SECRET = process.env.STABLEOPS_WEBHOOK_SECRET
router.post(
'/stableops',
express.raw({ type: 'application/json' }),
async (req, res) => {
const rawBody = req.body.toString('utf8')
const result = verifySignature({
secrets: [WEBHOOK_SECRET],
header: req.header(SIGNATURE_HEADER),
rawBody,
})
if (!result.ok) {
return res.status(400).json({ error: result.reason })
}
const event = JSON.parse(rawBody)
switch (event.type) {
case 'payment.finalized':
// 用 X-Event-Id 去重后再履约 event.data.payment_order_id
break
case 'payment.reverted':
case 'payment.expired':
break
}
res.sendStatus(200)
},
)
module.exports = router