StableOps
Guides

Express Integration

Integrate StableOps payment processing into your Express application.

Install

pnpm add @stableops/api-sdk express

Configure

STABLEOPS_API_KEY=sk_sandbox_...
STABLEOPS_WEBHOOK_SECRET=whsec_...
STABLEOPS_API_URL=https://api.stableops.dev

Create 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 }

Create payment orders

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

Verify webhooks

Mount the webhook route before express.json() so the handler receives the raw body used for signing.

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':
        // Fulfill event.data.payment_order_id after deduping X-Event-Id.
        break
      case 'payment.reverted':
      case 'payment.expired':
        break
    }

    res.sendStatus(200)
  },
)

module.exports = router

On this page