StableOps
指南

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

本页内容