Webhooks
Real-time notifications for payment events and system alerts.
Webhooks allow your application to receive real-time notifications when events occur in StableOps, such as when a payment is detected, confirmed, or finalized.
Overview
Instead of polling the API to check payment status, StableOps sends HTTP POST requests to your server when events happen. This enables:
- Real-time updates: Know immediately when payments arrive
- Reduced API calls: No need to poll for status changes
- Reliable delivery: Automatic retries with exponential backoff
- Event history: All webhook deliveries are logged and can be replayed
How Webhooks Work
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Blockchain │────────▶│ StableOps │────────▶│ Your App │
└─────────────┘ └─────────────┘ └─────────────┘
Payment sent Event detected Webhook sent- An event occurs (e.g., payment detected on blockchain)
- StableOps creates a webhook delivery
- HTTP POST request is sent to your endpoint
- Your server processes the event and returns 200 OK
- If delivery fails, StableOps retries with exponential backoff
Webhook Events
Payment Events
payment_order.created
Sent when a new payment order is created.
payment.detected
Sent when a payment transaction is first seen by the scanner. This can lag the chain head, so don't assume exactly 0 confirmations at this point.
Action: Update UI to show "Payment received, confirming..."
Warning: Do not fulfill the order yet - the transaction could still be reverted.
payment.confirmed
Sent when the payment has received sufficient on-chain confirmations. The transaction is likely final but has not yet reached the chain's finality guarantee.
Action: Safe to update internal systems and notify the user, but consider waiting for payment.finalized before releasing goods.
payment.finalized
Sent when the payment has reached finality and cannot be reverted.
Action: Safe to fulfill the order - payment is guaranteed.
This is the recommended event to trigger order fulfillment.
payment.expired
Sent when an order passes its expiration time without matching any transfer.
Action: Mark the order as expired and prompt the user to start over.
payment.reverted
Sent when a previously detected payment is rolled back. The receipt failed, or after a reorg the block hash no longer matches the stored event.
Action: Undo any optimistic handling done on payment.detected /
payment.confirmed. This is why we recommend fulfilling on payment.finalized.
payment_order.canceled
Sent when an order still in created (before any deposit is detected) is canceled
via the cancel endpoint.
Action: Mark the order as canceled and release any resources reserved for it.
Note: Even though cancellation is caller-initiated and the HTTP response already returns the result, this event is still emitted, so integrators that treat the webhook stream as the source of truth (event sourcing) never miss the terminal transition. Endpoints that don't care can simply ignore it.
See the Payment Events API reference for the full payload schemas.
Merchant Subscription Events
StableOps also emits events for merchant-managed end-user subscriptions and their invoices. Use these events to activate accounts, track renewals, suspend overdue users, and reconcile invoice payment outcomes.
| Event | Trigger |
|---|---|
end_user_subscription.created | A merchant subscription was created |
end_user_subscription.activated | The first invoice was paid and the subscription became active |
end_user_subscription.renewed | A renewal invoice was paid and the billing period advanced |
end_user_subscription.canceled | The subscription was canceled immediately |
end_user_subscription.expired | The subscription expired after cancellation or unpaid invoices |
end_user_subscription.past_due | The subscription entered the past-due state |
end_user_invoice.open | A new subscription invoice was issued |
end_user_invoice.paid | A subscription invoice was paid |
end_user_invoice.payment_failed | A subscription invoice payment failed |
end_user_invoice.payment_late | A payment arrived after the invoice was already uncollectible |
Subscription invoice checkout still creates payment orders, so you may also
receive the normal payment events for the underlying order. Treat the
end_user_invoice.* and end_user_subscription.* events as the source of truth
for subscription state.
See the Subscription Events API reference for the full payload schemas.
Operational & Agent Events
Beyond payment events, StableOps also pushes the following. In the dashboard these form the Operational events subscription group. A single toggle covers all of them, separate from the payment-events group:
| Event | Trigger |
|---|---|
address.pool.low | The available receiving-address pool for a chain is low |
agent.action.requested | An agent requested a write action that needs approval |
agent.action.approved | A pending agent action was approved |
agent.action.executed | An approved agent action finished executing |
See the Operational Events API reference for the full payload schemas.
Setting Up Webhooks
1. Create an Endpoint
In your StableOps dashboard:
- Go to Webhooks in the sidebar
- Click Add Endpoint
- Enter your webhook URL (use HTTPS in production for security)
- Select which events to receive
- Save the webhook secret
The secret field is returned only on create and rotate. Store it
somewhere durable; the dashboard will not show it again. After you call
rotate-secret, the previous secret stays valid alongside the new one for
24 hours, so you can roll out the new value without dropping deliveries.
2. Verify Signatures
Always verify webhook signatures to ensure requests are from StableOps.
import { SIGNATURE_HEADER, verifySignature } from '@stableops/api-sdk/webhooks'
const result = verifySignature({
secrets: [webhookSecret],
header: request.headers.get(SIGNATURE_HEADER) ?? undefined,
rawBody,
})
if (!result.ok) {
console.error('Invalid signature:', result.reason)
// Reject the request
}Delivery, retries, and the dead letter queue
Success criteria
Any 2xx response is treated as success. Any other status code, a network
error, or no response within 10 seconds counts as failure.
Retry schedule
Failed deliveries are rescheduled on this curve before moving to the dead letter queue on attempt 6:
| Attempt | Delay |
|---|---|
| 1 | 30 s |
| 2 | 60 s |
| 3 | 5 min |
| 4 | 30 min |
| 5 | 2 h |
| 6+ | DLQ |
Delivery log fields
Key fields you'll see on webhook-deliveries:
| Field | Description |
|---|---|
attempts | Number of HTTP attempts performed |
response_status | Downstream status code on the most recent attempt |
response_duration_ms | How long the last attempt took |
error_message | Short, log-friendly reason for the most recent failure |
next_retry_at | When the worker will retry (null once delivery is terminal) |
status | pending, succeeded, failed, dead_letter |
Replay
The three replay endpoints (endpoint replay, single-delivery replay, and replay-dead-letters) all enqueue a brand-new delivery row. The original audit log is preserved. The dashboard's "replay" button is just these endpoints under the hood.
Best Practices
1. Always Verify Signatures
Reject any request that doesn't carry a valid X-Product-Signature. Without
verification, anyone who discovers your endpoint URL can forge events.
2. Return 200 Quickly
Return 200 as soon as you've verified the signature and recorded the event id.
Heavy processing (fulfillment, notifications, ledger updates) should happen
asynchronously so delivery doesn't time out and trigger a retry.
3. Process Idempotently
Store X-Event-Id before mutating your own state. Network retries and platform
replays can deliver the same event more than once; applying it twice must be
safe.
4. Use Raw Body for Verification
Compute the signature over the exact bytes you received, before any JSON parsing or re-serialization. Frameworks that parse and re-stringify the body change it and break the signature.
5. Handle All Event Types
Subscribe to and handle every event you might receive, including
payment.reverted, payment.expired, and subscription events such as
end_user_invoice.paid / end_user_subscription.renewed. Unhandled events are
still counted as successful deliveries but your order and subscription tracking
will drift.
6. Log Everything
Record each delivery's X-Event-Id, X-Delivery-Id, event type, and your
processing result. Good logs are the fastest way to debug missed or duplicate
fulfillment.
7. Monitor Delivery Health
Check the delivery log for endpoints with a rising failed or dead_letter
count. Set up alerts for endpoints that haven't returned 2xx in the last
hour, and use the replay endpoint to drain the DLQ after fixing the issue.
Next Steps
- Payment Orders - Understanding payment orders
- Integration Guides - Framework-specific examples
How is this guide?
Last updated