StableOps
Guides

Express Integration

Integrate StableOps into Express with sandbox payment orders, server-side keys, raw-body Webhook verification, and idempotent processing for safe fulfillment.

Install

pnpm add @stableops/api-sdk express

Configure

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

Create lib/stableops.js:

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

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

module.exports = { stableops }

This guide uses a sandbox key and Base Sepolia. Import a base-sepolia receiving address in Sandbox and prepare test USDC first. For production, switch the API key, accepted mainnet assets, and Live address pool together; changing only the key is not enough.

These snippets omit business authentication. In production, authenticate the user, check order ownership, and load the payable amount from trusted server-side product or invoice records rather than accepting a client-supplied price. Authorize order retrieval and cancellation as well.

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

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

How is this guide?

Last updated

On this page