StableOps
指南

Express

在 Express 后端集成 StableOps:安全创建幂等支付单、保留 Webhook 原始请求体、验证 HMAC 签名,并以可重放且去重的方式处理支付状态事件。示例覆盖环境变量、路由实现、错误响应与订单查询,帮助服务在网络重试和重复事件下仍保持一致履约。

安装

pnpm add @stableops/api-sdk express

配置

STABLEOPS_API_KEY=sk_sandbox_...
STABLEOPS_WEBHOOK_SECRET=whsec_...

创建 lib/stableops.js:

const { StableOps } = require('@stableops/api-sdk')

const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY,
})

module.exports = { stableops }

本指南使用沙盒密钥和 Base Sepolia 测试网。请先在沙盒环境导入 base-sepolia 收款地址,并准备测试 USDC。上线时需同时切换为生产密钥、主网资产和生产地址池,不能只修改密钥。

以下示例省略了业务鉴权。生产环境必须验证当前用户及订单归属,从服务端商品或账单记录读取应付金额,不得直接信任表单或请求体中的金额;查询与取消订单也必须检查权限。

创建支付单

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),
        acceptedAssets: [{ chain: 'base-sepolia', 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

这篇文档怎么样?

最后更新

本页内容