# Webhooks

> Connect Paymos to your backend with signed, environment-isolated webhook endpoints and a complete delivery workflow.

Paymos sends an HTTPS `POST` when an invoice, a withdrawal, or a payment-channel deposit changes state. Webhooks are the authoritative asynchronous signal for your backend: API responses start an operation, while webhook events tell you what happened after blockchain processing.

## Integration flow

1. Create a webhook endpoint in **Dashboard → Developer → Webhooks**.
2. Choose one or more event categories: `Invoice`, `Withdrawal`, `Payment channel`.
3. Optionally restrict invoice and payment-channel events to selected projects — one filter covers both. Withdrawal events are merchant-wide and ignore it.
4. Store the displayed `whsec_…` secret on your server.
5. Verify every delivery against the unchanged raw request body.
6. Persist `event_id`, acknowledge with `2xx`, and process the event asynchronously.

Production and Sandbox have separate endpoints, secrets, and events. A sandbox event is never delivered to a production endpoint.

## Endpoint rules

- The URL must use HTTPS.
- The URL may name port 443 — what a URL without a port uses — or any port from 1024 up. Other ports are refused when you save, and an endpoint that already has one gets no deliveries.
- Up to 10 endpoints can be configured per merchant and environment.
- The same active URL cannot be registered twice in one environment.
- An endpoint must subscribe to at least one category.
- No project filter means every project is included.
- Suspending or deleting an endpoint stops new deliveries to it.

## Delivery headers

| Header | Contract |
|--------|----------|
| `Content-Type` | `application/json; charset=utf-8` |
| `X-Webhook-Id` | Same `evt_…` value as `event_id` in the body; not covered by the signature |
| `X-Webhook-Timestamp` | Unix seconds for this delivery attempt |
| `X-Webhook-Signature` | `t={timestamp},v1={hmac_hex}`; may contain two `v1` values during rotation |

Delivery is at-least-once and ordering is not guaranteed. Deduplicate on `event_id` from the verified body, then apply the resource state from `data` instead of counting events or assuming arrival order.

`X-Webhook-Id` is there for your logs, not for deduplication. The signature covers only the timestamp and the body, so a captured delivery can be resent with a different `X-Webhook-Id` and still verify while its timestamp passes your check — and a handler keyed on the header processes that copy a second time.

## Webhook documentation

- [Event catalog](/docs/webhooks/events.md) — every invoice, withdrawal, and payment-channel event type
- [Payload contract](/docs/webhooks/payloads.md) — envelope and resource snapshots
- [Verify signatures](/docs/webhooks/verify.md) — raw-body HMAC verification and secret rotation
- [Delivery and retries](/docs/webhooks/retry.md) — timeout, backoff, idempotency, and replay
- [Test webhooks](/docs/webhooks/testing.md) — sandbox prerequisites and the interactive playground
