StableOps
SDKs

TypeScript API SDK

Install, configure, and call from a TS project.

Install

pnpm add @stableops/api-sdk

The default API client targets Node 18+ and edge runtimes that provide global fetch, AbortController, and crypto.randomUUID. Webhook verification and the mock server use Node.js-only subpath exports.

Want a runnable end-to-end example?

The Playground walks through create → wallet payment → confirm → finalized in the browser, with source you can read.

Configure

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

const client = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
  // Optional. Inject your own fetch (e.g. msw, undici, edge fetch).
  fetch: globalThis.fetch,
})

Payment orders

const order = await client.paymentOrders.create(
  {
    merchantOrderId: 'sub_89231_2026_06',
    amount: '49.00',
    acceptedAssets: [
      { chain: 'base', asset: 'USDC' },
      { chain: 'tron', asset: 'USDT' },
    ],
    // Auto-expire after 30 minutes; the order moves to `expired` and the address is released.
    expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
  },
  { idempotencyKey: crypto.randomUUID() },
)

await client.paymentOrders.retrieve(order.id)
await client.paymentOrders.list({ status: 'detected', limit: 50 })
await client.paymentOrders.cancel(order.id)

paymentOrders.create always requires idempotencyKey. Use a UUID derived from your order id so retries from your worker land on the same record. Amounts come back as human-readable decimal strings (for example, "49.00"). Keep them as strings or use a decimal library instead of Number(amount).

Optional amountMode: 'auto' nudges the amount to a unique value (no manual offset on SHARED addresses). settlementAsset is derived server-side from acceptedAssets and isn't passed on create.

Checkout sessions (hosted)

checkoutSessions.create returns a hosted (WalletConnect) payment page — redirect the user to session.url.

const session = await client.checkoutSessions.create(
  {
    merchantOrderId: 'sub_89231_2026_06',
    amount: '49.00',
    acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
    expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
    title: 'Pro plan',
    successUrl: 'https://your-app.example.com/pay/success',
    cancelUrl: 'https://your-app.example.com/pay/cancel',
  },
  { idempotencyKey: crypto.randomUUID() },
)

console.log(session.url) // send the user here to pay

Webhook endpoints

const endpoint = await client.webhooks.createEndpoint({
  url: 'https://your-app.example.com/hooks/stableops',
  enabledEvents: ['payment.detected', 'payment.confirmed', 'payment.finalized'],
})

// endpoint.secret is only present on create/rotate. Store it somewhere durable.
await client.webhooks.rotateSecret(endpoint.id)

Deliveries and replay: client.webhooks.listDeliveries(...), replay(endpointId, eventId), replayDelivery(deliveryId), replayDeadLetters({ endpointId, limit }).

Errors

Every non-2xx response is thrown as StableOpsError with .status, .code, .message, and a raw .details body.

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

try {
  await client.paymentOrders.create(input, { idempotencyKey: key })
} catch (err) {
  if (err instanceof StableOpsError && err.status === 409) {
    // Idempotency-key was reused with a different body, etc.
  }
  throw err
}

Local mock server

The SDK ships an in-process mock for tests and demos:

import { StableOps } from '@stableops/api-sdk'
import { MockServer } from '@stableops/api-sdk/mock'
import { verifySignature } from '@stableops/api-sdk/webhooks'

const mock = new MockServer()
const { url } = await mock.listen()

const client = new StableOps({ baseUrl: url })
const order = await client.paymentOrders.create(
  {
    merchantOrderId: 'mock-order-1',
    amount: '49.00',
    acceptedAssets: [{ chain: 'base', asset: 'USDC' }],
    expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
  },
  { idempotencyKey: 'mock-order-1' },
)

const endpoint = await client.webhooks.createEndpoint({
  url: 'https://example.com/webhooks/stableops',
  enabledEvents: ['payment.detected'],
})

const fixture = mock.buildSignedFixture(endpoint.id, 'payment.detected', {
  id: order.id,
})
verifySignature({
  secret: fixture.secret,
  header: fixture.header,
  rawBody: fixture.rawBody,
})

await mock.close()

The mock implements only the surface area needed for SDK contract tests: payment orders, webhook endpoints, and a signature fixture builder.

How is this guide?

Last updated

On this page