> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stableyard.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Signed event notifications that tell your backend a resource changed, and how to receive them.

Webhooks tell your backend that a resource may have changed. They are a delivery channel, not a ledger, and they are the cheapest way to avoid polling everything.

<Info>
  Products counterpart: Exceptions and interventions covers what each money-edge event means for your operations team and who resolves it.
</Info>

## How it works

1. **Create an endpoint** for an app environment. Stableyard returns its signing secret once.
2. **A resource changes**, such as a payment succeeding or a deposit settling, and Stableyard records an event.
3. **Stableyard sends the event** as a signed `POST` to every active endpoint in that app environment subscribed to the event name.
4. **Your handler verifies and records it**, returns `2xx`, then reads the resource for its current state.

## An event is a prompt to read

An event says something changed. The resource says what is true now. Events are additive, may be delivered more than once and may arrive out of order, so never reconstruct state from delivery order: fetch the resource, such as `GET /v2/payments/{paymentId}`, and act on what it returns. See [Reconciliation](/payments/reconciliation).

New partner events are added over time. Your consumer must ignore an unknown event name safely rather than throw.

## Creating an endpoint

Create endpoints in the Partner Dashboard, or from your backend with a credential that carries the `v2:webhooks` and `v2:console` permissions:

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/console/apps/app_123/environments/env_123/webhooks \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/webhooks/stableyard",
    "description": "Production ledger consumer",
    "subscribedEvents": ["payment.accepted", "payment.succeeded", "deposit.settled"]
  }'
```

```json theme={null}
{
  "webhookEndpoint": {
    "id": "webhook_123",
    "appId": "app_123",
    "environmentId": "env_123",
    "url": "https://api.example.com/webhooks/stableyard",
    "description": "Production ledger consumer",
    "subscribedEvents": ["payment.accepted", "payment.succeeded", "deposit.settled"],
    "status": "active",
    "createdAt": "2026-10-01T12:00:00Z",
    "updatedAt": "2026-10-01T12:00:00Z"
  },
  "signingSecret": "whsec_…"
}
```

| Field | Required | Rules |
| - | - | - |
| `url` | Yes | Public HTTPS, up to 2,048 characters, no embedded credentials. Unique within the app environment |
| `description` | No | Up to 512 characters |
| `subscribedEvents` | No | Up to 100 partner event names from the [Event catalog](/webhooks/event-catalog). Empty or omitted: every public partner event for the environment |

Stableyard sends partner-facing lifecycle events for every account owned by that app environment. A second endpoint with the same URL in the same app environment is refused with `409`.

<Warning>
  Store the signing secret immediately. Stableyard returns it once at creation and never again. If you lose it, rotate the endpoint's secret with `POST /v2/console/webhooks/{webhookId}/rotate-secret`, which invalidates the previous one at once.
</Warning>

## Managing an endpoint

| Call | What it does |
| - | - |
| `GET /v2/console/webhooks` | Lists the app environment's endpoints, with the event catalog and delivery policy |
| `PATCH /v2/console/webhooks/{webhookId}` | Changes `description`, `subscribedEvents` or `status`: `active`, `disabled` or `archived` |
| `POST /v2/console/webhooks/{webhookId}/rotate-secret` | Returns a new signing secret once. See [Verifying signatures](/webhooks/verifying-signatures#rotating-the-signing-secret) |
| `GET /v2/console/webhooks/{webhookId}/deliveries` | Lists deliveries and their outcomes. See [Delivery and retries](/webhooks/delivery-and-retries#inspect-deliveries) |

An archived endpoint cannot be changed or reactivated: create a new one. Disabling, archiving or unsubscribing changes what happens to deliveries still waiting. See [Delivery and retries](/webhooks/delivery-and-retries#endpoint-changes-and-pending-deliveries).

## Delivery guarantees

Delivery is **at-least-once**. A network failure can leave an HTTP outcome ambiguous on Stableyard's side, so the same event may arrive again. Dedupe on two levels:

* By `x-stableyard-delivery` for delivery-level retries, which reuse the same delivery attempt.
* By `x-stableyard-event-id` plus the resource ID in the payload for resource-level processing.

If one endpoint succeeds while another subscribed to the same event fails, the successful endpoint is not sent the event again. An app environment may have only one active endpoint per URL; historical duplicate rows are collapsed by URL at delivery time.

Failed deliveries are retried with backoff, then abandoned. See [Delivery and retries](/webhooks/delivery-and-retries).

## What a delivery contains

Every delivery is a `POST` with a JSON body and these headers:

| Header | Contents |
| - | - |
| `x-stableyard-event` | Event name |
| `x-stableyard-event-id` | Stable outbox event ID |
| `x-stableyard-delivery` | Delivery ID |
| `x-stableyard-account-id` | The account ID for account-bound events. Omitted for public wallet Payments |
| `x-stableyard-external-user-id` | Percent-encoded partner user ID, when available |
| `x-stableyard-timestamp` | Unix timestamp |
| `x-stableyard-signature` | `t=<timestamp>,v1=<hmac_sha256>` |

```json theme={null}
{
  "id": "outbox_123",
  "name": "deposit.settled",
  "apiVersion": "2026-09-09",
  "createdAt": "2026-10-01T12:05:00Z",
  "payload": { "accountId": "acct_123", "depositId": "deposit_123" }
}
```

Every envelope contains `id`, `name`, `apiVersion`, `createdAt` and `payload`. `id` equals `x-stableyard-event-id`. `apiVersion` is the immutable date-version of the resource and event contract; use it to select your decoder rather than inferring the version from delivery time. What each `payload` carries is in the [Event catalog](/webhooks/event-catalog).

Public wallet Payments use an internal accounting principal for ledger ownership and webhook routing. Stableyard never exposes that principal as an account: its ID is removed from the delivery payload and headers.

## A minimal handler

1. Read the raw request bytes. Do not parse JSON first.
2. Verify the signature. See [Verifying signatures](/webhooks/verifying-signatures).
3. In one database transaction, record `x-stableyard-event-id` under a unique constraint and apply the event. On a conflict, skip the business effect.
4. Return `2xx` after the transaction commits.
5. Read the resource before acting on anything user-visible.

```js theme={null}
import express from "express";
import { verifyStableyardWebhook } from "./verify-stableyard-webhook.js";

const app = express();

app.post("/webhooks/stableyard", express.raw({ type: "application/json" }), async (req, res) => {
  let verified;
  try {
    verified = verifyStableyardWebhook(req.body, req.headers, process.env.STABLEYARD_WEBHOOK_SECRET);
  } catch {
    return res.status(400).end();
  }

  const event = JSON.parse(req.body.toString("utf8"));
  await recordAndApply(verified.eventId, event); // your code: unique on eventId, one transaction
  return res.status(200).end();
});
```

A `5xx` from your handler is retried; a `400` is abandoned at once. Return `5xx` when your own database is down, so the event comes back.

## Related

<CardGroup cols={2}>
  <Card title="Event catalog" icon="list" href="/webhooks/event-catalog">
    Every partner event, what it means and which to subscribe to.
  </Card>

  <Card title="Verifying signatures" icon="shield-check" href="/webhooks/verifying-signatures">
    Check each delivery's HMAC-SHA256 signature before you trust it.
  </Card>

  <Card title="Delivery and retries" icon="rotate" href="/webhooks/delivery-and-retries">
    Retries, backoff, abandonment and requeueing a delivery.
  </Card>

  <Card title="Reconciliation" icon="scale-balanced" href="/payments/reconciliation">
    Why an event is a prompt to read the resource, and what to persist alongside the event ID.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.