StableOps API SDK
Install, configure, and call from a TS project.
Install
pnpm add @stableops/api-sdkThe 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.
Configure
import { StableOps } from '@stableops/api-sdk'
const client = new StableOps({
apiKey: process.env.STABLEOPS_API_KEY!,
organizationSlug: 'demo',
environment: 'sandbox',
// Optional. Defaults to the hosted API. Override for self-hosted or mock testing.
baseUrl: process.env.STABLEOPS_API_URL,
// 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',
settlementAsset: 'USDC',
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.
Events
const events = await client.events.list({
chain: 'base',
asset: 'USDC',
paymentOrderId: order.id,
})Amounts come back as string in smallest units. Don't reach for Number(amount).
Webhook endpoints
const endpoint = await client.webhookEndpoints.create({
url: 'https://your-app.example.com/hooks/stableops',
enabledEvents: ['payment.detected', 'payment.confirmed', 'payment.finalized'],
})
// endpoint.secret is only present here. Store it somewhere durable.
await client.webhookEndpoints.rotateSecret(endpoint.id)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, environment: 'sandbox' })
await client.paymentOrders.create(
{
/* … */
},
{ idempotencyKey: 'a' },
)
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.