StableOps
SDKs

Python API SDK

Install the StableOps Python SDK to create payment orders and checkout sessions, manage Webhook endpoints, verify signatures, and handle API errors and replay.

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 datetime import datetime, timedelta, timezone

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=(datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
        )
        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 human-readable decimal strings (for example, "49.00"). Keep them as strings or use Decimal; 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.

from datetime import datetime, timedelta, timezone

session = client.checkout_sessions.create(
    merchant_order_id="sub_89231_2026_06",
    amount="49.00",
    accepted_assets=[{"chain": "base", "asset": "USDC"}],
    expires_at=(datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
    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 datetime import datetime, timedelta, timezone

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=(datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat(),
    )
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