# Webhooks (/reference/webhooks)

## Your endpoints

`GET /v1/webhooks`

At most 16 per project, so this list is never paginated. `event_catalog` is every event type that exists — the values `event_types` may contain.

### Responses

- `200` — Endpoints, newest first.
  - `object` (required, string)
  - `data` (required, array)
  - `has_more` (required, boolean)
  - `next_cursor` (optional, string | null) — Opaque. Pass as `cursor` for the next page. Do not construct or parse it.
  - `data` (optional, array)
    - `object` (required, string)
    - `id` (required, string (uuid))
    - `tenant_id` (optional, string (uuid))
    - `url` (required, string (uri)) — https only, port 443 or 8443, public host — see QUICKSTART §9.5.
    - `description` (optional, string | null)
    - `event_types` (required, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test) — Which events this endpoint receives. EMPTY means every event type, including ones added later.
    - `status` (required, string, one of: active, paused, disabled) — `paused` is yours (nothing is sent, deliveries are recorded as `skipped`); `disabled` is ours — auto-disabled after 5 consecutive exhausted deliveries or on a 410. PATCH `status: active` re-enables either.
    - `delivery_mode` (required, string, one of: unordered, ordered_per_resource)
    - `consecutive_failures` (required, integer) — Exhausted deliveries in a row. Reset by any success and by re-enabling.
    - `disabled_reason` (optional, string | null)
    - `disabled_at` (optional, string | null (date-time))
    - `secret_overlap_until` (optional, string | null (date-time)) — While set, deliveries carry two `v1` signatures — the old secret still verifies until this instant.
    - `last_success_at` (optional, string | null (date-time))
    - `last_failure_at` (optional, string | null (date-time))
    - `created_at` (required, string (date-time))
  - `event_catalog` (optional, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test)
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Register an endpoint

`POST /v1/webhooks`

The response carries the endpoint's signing `secret` — **the only time it is ever shown**. Store it before doing anything else; no endpoint can return it afterwards, and a lost secret is answered by `rotate_secret`.

The URL is checked before the endpoint exists (QUICKSTART §9.5): https only, port 443 or 8443, no credentials, resolving to a public address. It is checked again on every send.

An empty `event_types` subscribes to every event, including types added later — always keep a default branch in your handler.

### Parameters

- `Idempotency-Key` (header, optional, string) — Recommended. Replays the stored response for a repeated key.

### Request body (required)

- `url` (required, string (uri))
- `description` (optional, string)
- `event_types` (optional, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test)
- `delivery_mode` (optional, string, one of: unordered, ordered_per_resource)

### Responses

- `201` — Registered. `secret` appears here and nowhere else.
  - `object` (required, string)
  - `id` (required, string (uuid))
  - `tenant_id` (optional, string (uuid))
  - `url` (required, string (uri)) — https only, port 443 or 8443, public host — see QUICKSTART §9.5.
  - `description` (optional, string | null)
  - `event_types` (required, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test) — Which events this endpoint receives. EMPTY means every event type, including ones added later.
  - `status` (required, string, one of: active, paused, disabled) — `paused` is yours (nothing is sent, deliveries are recorded as `skipped`); `disabled` is ours — auto-disabled after 5 consecutive exhausted deliveries or on a 410. PATCH `status: active` re-enables either.
  - `delivery_mode` (required, string, one of: unordered, ordered_per_resource)
  - `consecutive_failures` (required, integer) — Exhausted deliveries in a row. Reset by any success and by re-enabling.
  - `disabled_reason` (optional, string | null)
  - `disabled_at` (optional, string | null (date-time))
  - `secret_overlap_until` (optional, string | null (date-time)) — While set, deliveries carry two `v1` signatures — the old secret still verifies until this instant.
  - `last_success_at` (optional, string | null (date-time))
  - `last_failure_at` (optional, string | null (date-time))
  - `created_at` (required, string (date-time))
  - `secret` (required, string) — `whsec_` + 43 base64url characters. Returned ONLY here and in the rotate response; no endpoint can read it back. Store it now.
  - `overlap_until` (optional, string (date-time)) — Rotate only: until when the previous secret also signs.
- `400` — `webhook_url_insecure`, `webhook_url_port`, `webhook_url_credentials`, `webhook_url_forbidden`, `webhook_url_unresolvable`, or an unknown event type.
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `422` — `webhook_endpoint_limit` — 16 endpoints already exist. `url_not_on_verified_domain` — on a live project with a verified domain, the endpoint must be on it.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## GET /v1/webhooks/{id}

`GET /v1/webhooks/{id}`

### Parameters

- `id` (path, required, string (uuid))

### Responses

- `200` — The endpoint. Never includes the secret.
  - `object` (required, string)
  - `id` (required, string (uuid))
  - `tenant_id` (optional, string (uuid))
  - `url` (required, string (uri)) — https only, port 443 or 8443, public host — see QUICKSTART §9.5.
  - `description` (optional, string | null)
  - `event_types` (required, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test) — Which events this endpoint receives. EMPTY means every event type, including ones added later.
  - `status` (required, string, one of: active, paused, disabled) — `paused` is yours (nothing is sent, deliveries are recorded as `skipped`); `disabled` is ours — auto-disabled after 5 consecutive exhausted deliveries or on a 410. PATCH `status: active` re-enables either.
  - `delivery_mode` (required, string, one of: unordered, ordered_per_resource)
  - `consecutive_failures` (required, integer) — Exhausted deliveries in a row. Reset by any success and by re-enabling.
  - `disabled_reason` (optional, string | null)
  - `disabled_at` (optional, string | null (date-time))
  - `secret_overlap_until` (optional, string | null (date-time)) — While set, deliveries carry two `v1` signatures — the old secret still verifies until this instant.
  - `last_success_at` (optional, string | null (date-time))
  - `last_failure_at` (optional, string | null (date-time))
  - `created_at` (required, string (date-time))
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found — also for another project's endpoint.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Edit, pause, resume or re-enable

`PATCH /v1/webhooks/{id}`

`status: paused` stops sends while recording every event as a `skipped` delivery, so nothing is lost. `status: active` on a `disabled` endpoint (auto-disabled, or a 410 from your side) re-enables it and resets the strike count — then replay the gap with `POST /v1/webhook_deliveries/replay`. A changed URL is re-checked like a new one.

### Parameters

- `id` (path, required, string (uuid))

### Request body (required)

- `url` (optional, string (uri))
- `description` (optional, string | null)
- `event_types` (optional, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test)
- `status` (optional, string, one of: active, paused)
- `delivery_mode` (optional, string, one of: unordered, ordered_per_resource)

### Responses

- `200` — Updated.
  - `object` (required, string)
  - `id` (required, string (uuid))
  - `tenant_id` (optional, string (uuid))
  - `url` (required, string (uri)) — https only, port 443 or 8443, public host — see QUICKSTART §9.5.
  - `description` (optional, string | null)
  - `event_types` (required, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test) — Which events this endpoint receives. EMPTY means every event type, including ones added later.
  - `status` (required, string, one of: active, paused, disabled) — `paused` is yours (nothing is sent, deliveries are recorded as `skipped`); `disabled` is ours — auto-disabled after 5 consecutive exhausted deliveries or on a 410. PATCH `status: active` re-enables either.
  - `delivery_mode` (required, string, one of: unordered, ordered_per_resource)
  - `consecutive_failures` (required, integer) — Exhausted deliveries in a row. Reset by any success and by re-enabling.
  - `disabled_reason` (optional, string | null)
  - `disabled_at` (optional, string | null (date-time))
  - `secret_overlap_until` (optional, string | null (date-time)) — While set, deliveries carry two `v1` signatures — the old secret still verifies until this instant.
  - `last_success_at` (optional, string | null (date-time))
  - `last_failure_at` (optional, string | null (date-time))
  - `created_at` (required, string (date-time))
- `400` — No fields to update, a bad URL, or an unknown event type.
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Delete an endpoint and its delivery history

`DELETE /v1/webhooks/{id}`

### Parameters

- `id` (path, required, string (uuid))

### Responses

- `200` — Deleted.
  - `deleted` (optional, boolean)
  - `id` (optional, string (uuid))
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Issue a new signing secret

`POST /v1/webhooks/{id}/rotate_secret`

Both secrets sign every delivery until `overlap_until` (24 h by default, up to 168): the `MolnPay-Signature` header carries two `v1` values, and one match passes. Switch your receiver whenever you like inside the window. The new secret is shown only in this response.

### Parameters

- `id` (path, required, string (uuid))

### Request body

- `overlap_hours` (optional, integer)

### Responses

- `201` — Rotated. `secret` is the new one; `overlap_until` says how long the old one still verifies.
  - `object` (required, string)
  - `id` (required, string (uuid))
  - `tenant_id` (optional, string (uuid))
  - `url` (required, string (uri)) — https only, port 443 or 8443, public host — see QUICKSTART §9.5.
  - `description` (optional, string | null)
  - `event_types` (required, array, of string, one of: customer.created, wallet.created, deposit.detected, deposit.confirmed, deposit.failed, deposit.orphaned, payout.created, payout.approved, payout.rejected, payout.submitted, payout.confirmed, payout.failed, payout.canceled, balance.transferred, balance.transfer_reversed, own_wallet.transferred, conversion.completed, conversion.failed, invoice.created, invoice.pending, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired, invoice.canceled, invoice.refund_requested, invoice.refunded, invoice.refund_failed, billing.balance_low, billing.balance_exhausted, billing.auto_topup, relay.rent_created, relay.credit_detected, relay.credit_forwarded, relay.credit_fallback, relay.rent_expiring, endpoint.test) — Which events this endpoint receives. EMPTY means every event type, including ones added later.
  - `status` (required, string, one of: active, paused, disabled) — `paused` is yours (nothing is sent, deliveries are recorded as `skipped`); `disabled` is ours — auto-disabled after 5 consecutive exhausted deliveries or on a 410. PATCH `status: active` re-enables either.
  - `delivery_mode` (required, string, one of: unordered, ordered_per_resource)
  - `consecutive_failures` (required, integer) — Exhausted deliveries in a row. Reset by any success and by re-enabling.
  - `disabled_reason` (optional, string | null)
  - `disabled_at` (optional, string | null (date-time))
  - `secret_overlap_until` (optional, string | null (date-time)) — While set, deliveries carry two `v1` signatures — the old secret still verifies until this instant.
  - `last_success_at` (optional, string | null (date-time))
  - `last_failure_at` (optional, string | null (date-time))
  - `created_at` (required, string (date-time))
  - `secret` (required, string) — `whsec_` + 43 base64url characters. Returned ONLY here and in the rotate response; no endpoint can read it back. Store it now.
  - `overlap_until` (optional, string (date-time)) — Rotate only: until when the previous secret also signs.
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Send an `endpoint.test` event to this endpoint only

`POST /v1/webhooks/{id}/test`

Goes through the real pipeline — signed, SSRF-checked, retried — so a green delivery here proves the receiver. Returns 202: watch the delivery at `GET /v1/webhook_deliveries/{delivery_id}`. Five per minute.

### Parameters

- `id` (path, required, string (uuid))

### Responses

- `202` — Queued.
  - `event_id` (required, string)
  - `delivery_id` (required, string)
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `409` — `endpoint_not_active` — a paused or disabled endpoint would only record a `skipped` delivery.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## The delivery log

`GET /v1/webhook_deliveries`

One row per attempt series: a delivery is retried in place (`attempt` climbs) and a replay is a NEW row with `replay_of`. Newest first. Filter by endpoint, event or status; `status=skipped` for one endpoint is exactly the gap an outage left.

### Parameters

- `limit` (query, optional, integer)
- `cursor` (query, optional, string) — Opaque cursor from the previous page's `next_cursor`.
- `endpoint_id` (query, optional, string (uuid))
- `event_id` (query, optional, string)
- `status` (query, optional, string, one of: pending, succeeded, failed, exhausted, skipped, canceled)

### Responses

- `200` — Deliveries.
  - `object` (required, string)
  - `data` (required, array)
  - `has_more` (required, boolean)
  - `next_cursor` (optional, string | null) — Opaque. Pass as `cursor` for the next page. Do not construct or parse it.
  - `data` (optional, array)
    - `object` (required, string)
    - `id` (required, string)
    - `tenant_id` (optional, string (uuid))
    - `event_id` (required, string)
    - `endpoint_id` (required, string (uuid))
    - `event_type` (optional, string | null)
    - `endpoint_url` (optional, string | null)
    - `status` (required, string, one of: pending, succeeded, failed, exhausted, skipped, canceled) — `pending` is queued or between retries; `exhausted` ran out of attempts; `skipped` was recorded while the endpoint was paused or disabled and is the gap you replay; `failed` is terminal without retry (a 410, or the URL failed the send-time check).
    - `attempt` (required, integer) — Attempts made so far.
    - `max_attempts` (required, integer)
    - `next_attempt_at` (optional, string | null (date-time))
    - `response_status` (optional, integer | null) — The receiver's HTTP status on the last attempt.
    - `resolved_ip` (optional, string | null) — The address the last attempt connected to.
    - `duration_ms` (optional, integer | null)
    - `error_detail` (optional, string | null) — Why the last attempt did not succeed, in our words.
    - `replay_of` (optional, string | null) — Set on a replay: the delivery it re-sends. The EVENT id is unchanged.
    - `delivered_at` (optional, string | null (date-time))
    - `created_at` (required, string (date-time))
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## One delivery, with the receiver's response body

`GET /v1/webhook_deliveries/{id}`

### Parameters

- `id` (path, required, string) — `whd_…`

### Responses

- `200` — The delivery. `response_body` is what your endpoint answered, up to 2 KB.
  - `object` (required, string)
  - `id` (required, string)
  - `tenant_id` (optional, string (uuid))
  - `event_id` (required, string)
  - `endpoint_id` (required, string (uuid))
  - `event_type` (optional, string | null)
  - `endpoint_url` (optional, string | null)
  - `status` (required, string, one of: pending, succeeded, failed, exhausted, skipped, canceled) — `pending` is queued or between retries; `exhausted` ran out of attempts; `skipped` was recorded while the endpoint was paused or disabled and is the gap you replay; `failed` is terminal without retry (a 410, or the URL failed the send-time check).
  - `attempt` (required, integer) — Attempts made so far.
  - `max_attempts` (required, integer)
  - `next_attempt_at` (optional, string | null (date-time))
  - `response_status` (optional, integer | null) — The receiver's HTTP status on the last attempt.
  - `resolved_ip` (optional, string | null) — The address the last attempt connected to.
  - `duration_ms` (optional, integer | null)
  - `error_detail` (optional, string | null) — Why the last attempt did not succeed, in our words.
  - `replay_of` (optional, string | null) — Set on a replay: the delivery it re-sends. The EVENT id is unchanged.
  - `delivered_at` (optional, string | null (date-time))
  - `created_at` (required, string (date-time))
  - `response_body` (optional, string | null)
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Re-send one delivery

`POST /v1/webhook_deliveries/{id}/replay`

Mints a new delivery (`replay_of` points back) for the SAME event — the receiver sees the same `id`, so an idempotent handler is unaffected. Only `failed`, `exhausted` and `skipped` deliveries can be replayed. Ten per minute.

### Parameters

- `id` (path, required, string)

### Responses

- `202` — Queued.
  - `replays` (required, array)
    - `replay_of` (required, string) — The delivery that was re-sent.
    - `id` (required, string) — The new delivery. Watch it at `GET /v1/webhook_deliveries/{id}`.
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — Not found.
- `409` — `delivery_not_replayable` — the delivery is pending or already succeeded.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.

## Replay everything an endpoint missed

`POST /v1/webhook_deliveries/replay`

The recovery after an outage or an auto-disable: fix the receiver, PATCH the endpoint back to `active`, then call this. Replays up to 100 `failed`/`exhausted`/`skipped` deliveries for the endpoint, oldest first; narrow with `status` and `since`, and call again for a larger gap. Two per minute.

### Request body (required)

- `endpoint_id` (required, string (uuid))
- `status` (optional, string, one of: failed, exhausted, skipped)
- `since` (optional, string (date-time)) — Only deliveries created at or after this instant.
- `limit` (optional, integer)

### Responses

- `202` — Queued. Empty `replays` means nothing matched.
  - `replays` (required, array)
    - `replay_of` (required, string) — The delivery that was re-sent.
    - `id` (required, string) — The new delivery. Watch it at `GET /v1/webhook_deliveries/{id}`.
- `401` — Missing, malformed, expired or revoked API key.
- `403` — The key lacks the scope, its IP allowlist rejected the source, or signing is disabled.
- `404` — The endpoint is not yours.
- `429` — Rate limit or quota exceeded. Honour `Retry-After`.
- `500` — Unexpected error.
- `503` — A dependency is unavailable. Retryable with backoff.