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.

Want to use StableOps hosted checkout to accept USDC in Next.js? Read the blog tutorial and check out the complete example project that deploys to Vercel with one click.

Install

pnpm add @stableops/api-sdk

Configure

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

Create lib/stableops.ts:

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

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

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

How is this guide?

Last updated

On this page