StableOps
Guides

Next.js Integration

Integrate StableOps payment processing into your Next.js application.

This guide uses the App Router and the current TypeScript SDK.

Install

pnpm add @stableops/api-sdk

Configure

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

Create lib/stableops.ts:

import { StableOps } from '@stableops/api-sdk'

export const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
  environment: 'sandbox',
  baseUrl: process.env.STABLEOPS_API_URL,
})

Create an order

Create app/actions/payment.ts:

'use server'

import { revalidatePath } from 'next/cache'

import { stableops } from '@/lib/stableops'

export async function createPaymentOrder(formData: FormData) {
  const amount = String(formData.get('amount') ?? '')
  const merchantOrderId = String(formData.get('orderId') ?? '')

  const order = await stableops.paymentOrders.create(
    {
      merchantOrderId,
      amount,
      settlementAsset: 'USDC',
      acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
      expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
      metadata: { source: 'nextjs_checkout' },
    },
    { idempotencyKey: merchantOrderId },
  )

  revalidatePath('/payments')
  return order
}

Show one payment candidate from the returned order:

{
  order.paymentInstructions[0] ? (
    <div>
      Send {order.amount} {order.paymentInstructions[0].asset} on{' '}
      {order.paymentInstructions[0].chain} to:
      <code>{order.paymentInstructions[0].address}</code>
    </div>
  ) : null
}

Verify webhooks

Create app/api/webhooks/stableops/route.ts:

import { SIGNATURE_HEADER, verifySignature } from '@stableops/api-sdk/webhooks'

const WEBHOOK_SECRET = process.env.STABLEOPS_WEBHOOK_SECRET!

export async function POST(req: Request) {
  const rawBody = await req.text()
  const result = verifySignature({
    secrets: [WEBHOOK_SECRET],
    header: req.headers.get(SIGNATURE_HEADER) ?? undefined,
    rawBody,
  })

  if (!result.ok) {
    return new Response(`invalid signature: ${result.reason}`, { status: 400 })
  }

  const event = JSON.parse(rawBody) as {
    type: string
    data: { payment_order_id?: string }
  }

  switch (event.type) {
    case 'payment.detected':
      break
    case 'payment.confirmed':
      break
    case 'payment.finalized':
      // Fulfill after your own idempotency check.
      break
    case 'payment.expired':
    case 'payment.reverted':
      break
  }

  return new Response('ok')
}

Store X-Event-Id in your database before mutating local order state so webhook retries do not duplicate fulfillment.

On this page