Configure webhooks
Stables webhooks allow you to receive real-time notifications when events occur in your Stables account. There are two ways to manage webhook subscriptions:
- Developer Dashboard (recommended) — manage webhook endpoints from the dashboard, with a built-in event catalog, test events, delivery history and manual replay.
- Webhook API — create and manage subscriptions programmatically. This applies to accounts that haven't yet been migrated to dashboard-managed webhooks.
The event payload structure is identical on both — switching between them doesn't require any changes to how you process events.
Managing webhooks in the Dashboard
Open Settings → Webhooks in the Developer Dashboard to manage your endpoints.
- Add an endpoint. Enter the HTTPS URL that should receive events and select the event types you want delivered to it.
- Copy the signing secret. Each endpoint has its own signing secret (starting with
whsec_), available on the endpoint's detail page. Use it to verify deliveries — see Verifying signatures below. - Send a test event. Use the Testing tab to send example payloads for any event type to your endpoint before going live.
- Inspect and replay deliveries. Each endpoint shows a full delivery log with the response your server returned. Failed deliveries can be replayed manually at any time.
Deliveries to dashboard-managed endpoints are retried automatically on an exponential back-off schedule for over a day after the first failure. If every retry fails, the delivery is marked failed and can still be replayed from the dashboard.
Subscribing via the Webhook API
For accounts not yet on dashboard-managed webhooks, create a webhook subscription with the Stables API. The subscription is active as soon as it's created.
Request
curl -X POST https://api.stables.money/api/v1/webhooks \
-H "X-Api-Key: <api-key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <idempotency-key>" \
-d '{
"name": "Payment Status Notifications",
"url": "https://your-domain.com/webhooks/stables",
"eventTypes": [
"customer.created",
"kyc_link.updated.status_transitioned",
"transfer.updated.status_transitioned"
],
"secret": "your_optional_signing_secret"
}'Response
{
"subscription": {
"subscriptionId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Payment Status Notifications",
"url": "https://your-domain.com/webhooks/stables",
"eventTypes": [
"customer.created",
"kyc_link.updated.status_transitioned",
"transfer.updated.status_transitioned"
],
"active": true,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}Receiving Events
When an event occurs, Stables sends a POST to your endpoint URL. The webhook payload follows the same structure regardless of how the subscription was created.
| Field | Type | Description |
|---|---|---|
api_version | string | API version used when the event was generated. Currently "v1". |
event_id | string | Unique UUID for this event. Use as an idempotency key to handle duplicate deliveries. |
event_developer_id | string | Your tenant ID. |
event_category | string | Top-level resource category (e.g. "customer", "virtual_account.activity"). |
event_type | string | Full event type string, e.g. "kyc_link.updated.status_transitioned". |
event_object_id | string | ID of the primary resource. |
event_object_status | string | (optional) The new status that triggered the event. Present on status-transition events. |
event_object | object | Full resource snapshot. Shape varies by event_type. |
event_created_at | string | ISO 8601 UTC timestamp when the event was created. |
For the full event type catalog and per-event payloads, see the Webhooks documentation in the Developer Platform.
Verifying signatures
Dashboard-managed endpoints
Every delivery includes three headers:
| Header | Description |
|---|---|
svix-id | Unique ID for the message. Constant across retries of the same event. |
svix-timestamp | Unix timestamp (seconds) when the delivery was sent. |
svix-signature | Base64-encoded HMAC-SHA256 signature(s) of the delivery. |
The signature is an HMAC-SHA256 of {svix-id}.{svix-timestamp}.{raw request body} using your endpoint's signing secret. We recommend verifying with the open-source Svix libraries rather than implementing this by hand:
import { Webhook } from "svix";
const wh = new Webhook(process.env.STABLES_WEBHOOK_SECRET); // whsec_...
app.post("/webhooks/stables", (req, res) => {
try {
// Verify against the raw request body, not the parsed JSON.
const event = wh.verify(req.rawBody, req.headers);
// Process asynchronously after responding.
res.status(200).send();
} catch (err) {
res.status(400).send();
}
});Verification also rejects stale timestamps, which protects against replay attacks.
API-managed subscriptions
If you set a secret on the subscription, each delivery includes an X-Webhook-Signature header containing the HMAC-SHA256 of the raw request body with that secret. Reject requests whose signature doesn't match.
Security best practices
- Verify webhook signatures on every delivery, as described above.
- Use HTTPS for your webhook URL and ensure the certificate is valid so traffic and signatures can't be tampered with in transit.
- Store webhook secrets securely (e.g. environment variables or a secrets manager). Don't hardcode them in source.
- Return 200 quickly so Stables doesn't treat the delivery as failed and retry. Process the event asynchronously after responding.
- Implement idempotency using
event_idso duplicate deliveries or retries don't cause duplicate side effects. - Log webhook events (event type, event_id, and outcome) for debugging and monitoring
IP Address
Deliveries to API-managed subscriptions originate from a fixed egress IP address per environment. We recommend adding these addresses to your endpoint's allowlist as an extra layer of security, ensuring that incoming webhooks genuinely originate from Stables:
| Environment | Egress IP |
|---|---|
| Production | 54.253.39.1 |
| Sandbox | 13.237.39.75 |
These addresses are stable. In the unlikely event they change, we will notify you in advance.
Dashboard-managed endpoints are delivered from a different, also static, set of IP addresses. Contact us if you need the current allowlist for your environment.
Inspecting Delivery History
Dashboard-managed endpoints show their full delivery history — including your server's response and manual replay — directly in the Settings → Webhooks page.
For API-managed subscriptions, use the delivery history endpoint to inspect past delivery attempts — useful for debugging missed events or understanding retry behaviour. Filter by subscription using the subscriptionId query parameter.
GET /api/v1/webhooks/deliveriesOptional query parameters: subscriptionId, status, eventType, pageSize, pageToken.
The response returns a deliveries array of WebhookDeliveryRecord objects:
| Field | Type | Description |
|---|---|---|
deliveryId | string | Unique ID for this delivery attempt |
status | string | PENDING, PROCESSING, RETRYING, SUCCESS, or FAILED |
attemptCount | number | Number of delivery attempts made so far |
responseCode | number | HTTP status code returned by your endpoint (if any) |
nextRetryAt | string | ISO 8601 timestamp of the next scheduled retry (if retrying) |
createdAt | string | When the delivery was first attempted |
Stables retries failed deliveries on an exponential back-off schedule. Once status reaches FAILED, no further retries are made.
Next Steps
- Review the Webhooks documentation for the full event type catalog and payload examples
- Implement proper error handling and retry logic
- Set up monitoring and alerting for webhook failures
This completes your webhook integration with Stables. Your application will now receive real-time notifications for customer and payment events and can respond accordingly.