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

# Troubleshooting: webhook lifecycle error codes

> Map the webhook lifecycle codes — WEBHOOK_DELIVERY_FAILED, WEBHOOK_PROCESSING_FAILED, and WEBHOOK_SIGNATURE_INVALID — to the delivery state they fire in, plus the creation-time and dedup sibling codes, with a fix for each.

# Troubleshooting: webhook lifecycle error codes

When a webhook flow breaks, the error code tells you **where in the endpoint
lifecycle** it broke — before the endpoint exists, while a delivery is being
attempted, or as the receiver processes (or rejects) the event. This page maps
each webhook code to its lifecycle state and gives you the detection query and
the fix for each one. For the deep-dive runbook behind any single code, follow
the cross-links at the bottom.

## 1. The webhook endpoint lifecycle

An endpoint moves through four states, and each code fires in exactly one of
them:

1. **created** — `POST /api/v1/webhooks` accepts the URL, validates it, and
   persists the endpoint.
2. **delivery-attempt** — Orbit POSTs the event to your URL and waits for a
   `2xx` ack.
3. **delivery-failed** — the attempt got a non-2xx response, a timeout, or a
   receiver-side exception; the retry cadence starts.
4. **retry / DLQ** — up to 9 retries over \~4.3 hours, then the row moves to
   the dead-letter queue until you requeue or replay it.

Match the code to the state first — it decides which fix applies:

| Lifecycle state  | Codes it can return                                                                                                                                |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| created          | `WEBHOOK_ENDPOINT_CAP_REACHED`, `WEBHOOK_DNS_INVALID`, `INVALID_WEBHOOK_URL`                                                                       |
| delivery-attempt | `WEBHOOK_DELIVERY_FAILED` (receiver answered non-2xx or timed out), `WEBHOOK_SIGNATURE_INVALID` (receiver acked 401/4xx after a failed HMAC check) |
| delivery-failed  | `WEBHOOK_PROCESSING_FAILED` (receiver threw while handling an otherwise-valid request)                                                             |
| retry / DLQ      | no new codes — deliveries in this state are requeued or replayed with the same event id, which is a dedup concern, not a new error                 |

## 2. The codes

### `WEBHOOK_DELIVERY_FAILED` — receiver answered non-2xx (HTTP 502)

Orbit POSTed the event and got back a `404`, `5xx`, or a timeout instead of a
`2xx`. Anything outside the 2xx range counts as a failed attempt, so the
retry cadence starts and the row heads for the DLQ if it keeps failing.

### `WEBHOOK_PROCESSING_FAILED` — receiver threw (HTTP 500)

Your handler accepted the request but raised an exception — an unhandled
throw, a crashed downstream call — while processing it. From Orbit's side
this is a `5xx` ack, identical in shape to `WEBHOOK_DELIVERY_FAILED`; the
registry records it separately because the fix is in your handler's code, not
in routing or uptime.

### `WEBHOOK_SIGNATURE_INVALID` — HMAC mismatch (HTTP 401)

Your verifier rejected the delivery: the computed HMAC matched none of the
`v1` candidates in the signature header. This is the same failure class as
`INVALID_SIGNATURE`, reported from the delivery-log side. Until verification
passes, every delivery fails in the **delivery-attempt** state.

### Creation-time siblings

The three **created**-state rejections are expanded in
[Troubleshooting: webhook endpoint creation errors](/troubleshooting/webhook-endpoint-creation):
`WEBHOOK_ENDPOINT_CAP_REACHED` (tenant already at 10 endpoints),
`WEBHOOK_DNS_INVALID` (hostname does not resolve), and
`INVALID_WEBHOOK_URL` (non-HTTPS scheme, or a private/loopback/internal
address blocked by safety validation). All three answer `422` before any
endpoint is persisted.

### The dedup sibling

Retry and DLQ states re-deliver the **same event id** — that is at-least-once
semantics, not a new error code. If your consumer treats the re-delivery as a
new event, the failure is a consumer-side dedup gap; see
[Troubleshooting: duplicate webhook events and consumer-side dedup](/troubleshooting/webhook-event-dedup)
for the event-id pattern that makes retries and replays safe.

## 3. Detecting each code

List the deliveries for the endpoint and filter to the failures:

```bash theme={null}
curl -G "https://api.orbit.devotel.io/api/v1/webhooks/{id}/deliveries" \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "status=failed"
```

Then pull one row (`GET /api/v1/webhooks/{id}/deliveries/{deliveryId}`) and
read the HTTP status your endpoint returned:

* **401 → signature path.** Your verifier rejected the HMAC; work the
  `WEBHOOK_SIGNATURE_INVALID` fix below.
* **404 / 5xx / timeout → delivery or processing path.** A `404` means the
  route moved; a `500` from your handler means it threw; a timeout means it
  ran past your endpoint's `timeout_seconds`.

For the signature path specifically, wrap your verifier so a rejection logs
the header shape before you return `401`:

```javascript theme={null}
import crypto from 'node:crypto';

export function verifySignature(rawBody, header, secret) {
  try {
    const parts = (header ?? '').split(',').map((p) => p.trim());
    let t = null;
    const v1s = [];
    for (const part of parts) {
      const eq = part.indexOf('=');
      if (eq < 0) continue;
      if (part.slice(0, eq) === 't') t = part.slice(eq + 1);
      else if (part.slice(0, eq) === 'v1') v1s.push(part.slice(eq + 1));
    }
    if (!t || v1s.length === 0) return false;

    const expected = Buffer.from(
      crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf-8').digest('hex'),
      'hex',
    );
    return v1s.some((candidate) => {
      const buf = Buffer.from(candidate, 'hex');
      return buf.length === expected.length && crypto.timingSafeEqual(buf, expected);
    });
  } catch {
    return false;
  }
}
```

The reject path must return `false`, not throw — a throwing verifier turns
every HMAC mismatch into a `WEBHOOK_PROCESSING_FAILED` (500) instead of the
`WEBHOOK_SIGNATURE_INVALID` (401) you can actually diagnose.

## 4. Fix per code

### Non-2xx path — `WEBHOOK_DELIVERY_FAILED` / `WEBHOOK_PROCESSING_FAILED`

* **Ack within the delivery window.** Your endpoint must return a `2xx`
  within the window documented on
  [Webhook security](/webhooks/security) (30 seconds by default, or your
  endpoint's `timeout_seconds` override). Move heavy work to a queue and ack
  first — a slow handler reads as a timeout.
* **Return 2xx on duplicates.** A re-delivery of a known event id still needs
  a `2xx`: a fast duplicate ack stops the retry cadence.
* **Handle receiver exceptions.** A `500` from your handler is the same retry
  trigger as a routing failure. Catch and log the exception, run the side
  effect on your worker queue, and still ack `2xx` once the event is
  persisted — the
  [durable consumer pattern](/guides/webhook-consumer) covers the shape.

### Signature path — `WEBHOOK_SIGNATURE_INVALID`

* **Wrong secret.** Test and live secrets do not cross-verify; a masked
  preview (`whsec_********...<last4>`) is for identification, not for pasting
  into your verifier. If the cleartext is lost, rotate with
  `POST /api/v1/webhooks/{id}/rotate-secret` and store the new value — it is
  returned once.
* **Raw-body mismatch.** The signed string is `<t>.<raw_body>`. If a body
  parser (Express `express.json()`, a global Fastify parser, Next.js
  `bodyParser`) consumed the request first, the re-serialized bytes never
  match — verify against the raw bytes (`express.raw()`, `request.text()`)
  before any parsing.
* **Rotation window.** During the grace window, check every `v1` candidate in
  `X-Orbit-Signature` (fall back to `X-Devotel-Signature`) and also
  `X-Orbit-Signature-Next` against the previous secret.

Every signature failure class — rotation headers, clock skew, mutated bodies,
unsafe comparison — is expanded in
[Troubleshooting: signature verification failures](/webhooks/troubleshooting-signature-failures).

## See also

* [Troubleshooting: signature verification failures](/webhooks/troubleshooting-signature-failures) — every HMAC failure class, with copy-paste verifiers.
* [Troubleshooting: failed webhook deliveries, retries, DLQ, and replay](/troubleshooting/webhook-deliveries) — the retry/DLQ/replay recovery loop.
* [Troubleshooting: webhook endpoint creation errors](/troubleshooting/webhook-endpoint-creation) — the three created-state 422s.
* [Troubleshooting: duplicate webhook events and consumer-side dedup](/troubleshooting/webhook-event-dedup) — at-least-once semantics and the event-id anchor.
* [Error code reference](/reference/error-codes) — the raw registry table this page routes.
