StableOps
SDKs

Python API SDK

Install, configure, and call from a Python project.

Install

pip install stableops

Requires Python 3.8+ and depends on httpx and pydantic v2. It ships both a synchronous StableOps and an asynchronous AsyncStableOps client with an identical API shape.

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 os

from stableops import StableOps

client = StableOps(
    api_key=os.environ["STABLEOPS_API_KEY"],
    # Optional:
    # base_url="https://api.stableops.dev",
    # timeout=30.0,
    # max_retries=2,
    # checkout_base_url="https://pay.stableops.dev",  # only affects checkout url building
)

The environment (sandbox / live) is determined by the API key prefix (sk_sandbox_… / sk_live_…) — no extra parameter needed.

Async usage:

import asyncio

from stableops import AsyncStableOps


async def main() -> None:
    async with AsyncStableOps(api_key="sk_sandbox_...") as client:
        order = await client.payment_orders.create(
            merchant_order_id="sub_89231_2026_06",
            amount="49.00",
            accepted_assets=[{"chain": "base", "asset": "USDC"}],
            expires_at="2026-06-20T12:30:00Z",
        )
        print(order.id)


asyncio.run(main())

Payment orders

from datetime import datetime, timedelta, timezone

order = client.payment_orders.create(
    merchant_order_id="sub_89231_2026_06",
    amount="49.00",
    accepted_assets=[
        {"chain": "base", "asset": "USDC"},
        {"chain": "tron", "asset": "USDT"},
    ],
    # Auto-expire after 30 minutes; the order moves to `expired` and the address is released.
    expires_at=(datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
    # Optional: 'auto' nudges the amount to a unique value (no manual offset on SHARED addresses).
    amount_mode="auto",
)

client.payment_orders.retrieve(order.id)
client.payment_orders.list(status="detected", limit=50)
client.payment_orders.cancel(order.id)

merchant_order_id doubles as the idempotency key, so retries from your worker land on the same record instead of creating duplicates.

Amounts come back as string in smallest units — don't reach for float(amount). requested_amount is the base amount you submitted (for an auto order, the pre-nudge amount, used for reconciliation).

Checkout sessions (hosted)

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

session = client.checkout_sessions.create(
    merchant_order_id="sub_89231_2026_06",
    amount="49.00",
    accepted_assets=[{"chain": "base", "asset": "USDC"}],
    expires_at="2026-06-20T12:30:00Z",
    title="Pro plan",
    success_url="https://your-app.example.com/pay/success",
    cancel_url="https://your-app.example.com/pay/cancel",
)

print(session.url)  # send the user here to pay

Webhook endpoints

endpoint = client.webhooks.create_endpoint(
    url="https://your-app.example.com/hooks/stableops",
    enabled_events=["payment.detected", "payment.confirmed", "payment.finalized"],
    # Optional: strip order metadata from the delivered payload.
    redact_metadata=True,
)

endpoints = client.webhooks.list_endpoints()
client.webhooks.update_endpoint(endpoint.id, description="Production")

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

Deliveries and replay

# List deliveries (filter by status / endpoint / order).
deliveries = client.webhooks.list_deliveries(status="failed", limit=20)

# Re-deliver a specific event to a specific endpoint.
client.webhooks.replay(endpoint.id, event_id)

# Replay a single delivery.
client.webhooks.replay_delivery(delivery_id)

# Batch-replay dead letters.
result = client.webhooks.replay_dead_letters(endpoint_id=endpoint.id, limit=100)
print(result.replayed)

Webhook verification

Verify the signature inside your callback handler (Flask example):

from stableops import SIGNATURE_HEADER, verify_webhook_signature


@app.route("/hooks/stableops", methods=["POST"])
def handle_webhook():
    body = request.get_data(as_text=True)
    result = verify_webhook_signature(
        body=body,
        header=request.headers.get(SIGNATURE_HEADER),
        secret=os.environ["STABLEOPS_WEBHOOK_SECRET"],
    )
    if not result.valid:
        return {"error": result.reason}, 401
    # Verify against the raw body (don't re-serialize the JSON first).
    ...
    return "", 204

During key rotation, pass secrets=[old, new] to accept multiple secrets at once.

Errors

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

from stableops import StableOpsError

try:
    client.payment_orders.create(
        merchant_order_id="sub_89231_2026_06",
        amount="49.00",
        accepted_assets=[{"chain": "base", "asset": "USDC"}],
        expires_at="2026-06-20T12:30:00Z",
    )
except StableOpsError as err:
    if err.status == 409:
        # Idempotency-key was reused with a different body, etc.
        ...
    raise

Network errors (timeouts / connection failures) are also wrapped as StableOpsError, with status == 0.

How is this guide?

Last updated

On this page