# When a webhook never arrives

Source: https://help.didit.me/integration/webhook-troubleshooting
Updated: 2026-10-12

Check in order: the delivery log, the event name, the firewall, the signature. The Deliveries list shows whether Didit sent it, halving the problem at once.

Start at the endpoint's **Deliveries** section. If Didit never attempted the
delivery, it's a subscription or endpoint problem. If it attempted and failed,
the response code tells you which: **404** means your route wasn't reachable at
that URL, a signature mismatch means you hashed the wrong bytes.

## Step 1: did Didit try to send it?

Go to **Developers → Webhooks**. The list shows each endpoint's status, its success rate over 30 days and its last delivery; clicking a row shows its recent deliveries. Press **Open** for the endpoint's full **Deliveries** list, where every attempt is logged individually with the request and the response. **Resend** on a delivery sends it again once you've fixed your side. A failing endpoint is flagged at the top of the Webhooks page, and where available, a **Retry failed** button there resends every failed delivery at once.

![The Webhooks page in the Didit console showing delivery status per endpoint](https://help.didit.me/screenshots/webhooks-light.webp)

*Everything needed to tell a missing event from a failing endpoint is on this page.*

That single check splits the problem in half:

- **No attempt logged** → the event was never generated for that endpoint. Go to step 2.
- **Attempt logged, failed** → Didit sent it and your side rejected or didn't receive it. Go to step 3.
- **Attempt logged, 2xx** → it was delivered successfully. The problem is inside your handler, not in delivery.

## Step 2: no attempt was logged

In order of likelihood:

1. **The event isn't subscribed.** There's no wildcard - every event family has to be listed explicitly. And a name that doesn't exist (`session.status.updated`, `kyc.completed`) silently receives nothing. Check against [the event list](https://help.didit.me/integration/webhooks-basics).
2. **Wrong application or environment.** Endpoints belong to an application, and Live and Sandbox have separate endpoints: the list shows each endpoint's environment. If your sessions run under a different application than the endpoint, no event will ever reach it. This is the most common cause when everything looks correctly configured.
3. **Nothing actually changed.** Webhooks fire on change. A session that hasn't moved, or an AML monitoring re-screen that found nothing above threshold, correctly produces no event.
4. **The endpoint is disabled.** A disabled endpoint shows **Disabled** in the list and receives nothing until you enable it again.
5. **The status you're waiting for hasn't happened.** A session at In progress isn't finished. See [when a session never finishes](https://help.didit.me/verification-sessions/session-not-finished).

## Step 3: the delivery was attempted and failed

**A 404** means the request reached something that didn't have your route. Check:

- The exact path, including a trailing slash. A framework that redirects `/hook` to `/hook/` can turn a working endpoint into a 404 or a lost body.
- Whether the URL is public. A local or staging host that isn't reachable from the internet fails this way.
- Whether a proxy, load balancer, or path-based router in front of your app is sending that path somewhere else.

**A 5xx** means your handler threw. Log the raw body before parsing so you can see what it actually received.

**A timeout** means you didn't respond fast enough. Return 2xx first, process afterwards.

**Nothing at all / connection refused** means your edge blocked it. Didit delivers from the static IP **18.203.201.92** with a `DiditWebhook/2.0` user agent. If you're behind Cloudflare or a WAF with a default-deny posture, allow that IP for the receiving hostname.

## The classic: automatic delivery 404s but Resend works

This one comes up often enough to name. The automated delivery fails with a 404, then clicking **Resend** on the same event succeeds.

That combination means the payload and your endpoint are both fine - so the difference is timing or path, not content. Check:

- **A deploy or restart** at the moment of the original delivery. Resend succeeds later because the app is up again.
- **A cold start** that exceeded your platform's timeout - common on serverless with a slow first invocation.
- **Path-based routing that changed** between the two attempts, or a rule that only matches some requests.
- **Rate limiting or bot protection at your edge** that let the manual resend through because it arrived alone rather than in a burst.

The Deliveries tab has both attempts with their timestamps - compare them against your own deploy and error logs for that minute.

## Signature verification failing

Almost always one of three things:

1. **You hashed re-serialised JSON.** HMAC the **raw** request body bytes exactly as received. Parsing and re-stringifying changes whitespace and key order, and the signature won't match. Most frameworks need explicit configuration to give you the raw body.
2. **Wrong secret.** The signing secret is **per endpoint**, and it is not your API key. Two endpoints have two different secrets.
3. **Wrong encoding assumption.** If you're unsure whether the secret is used as a literal string or decoded first, don't guess - follow [the signature verification reference](https://docs.didit.me/integration/webhooks#signature-verification) exactly, and log the raw body while debugging so you can compare.

> **Warning:** Never "fix" a signature mismatch by skipping verification. An unverified webhook
> endpoint will accept a forged approval from anyone who finds the URL, which turns a
> debugging shortcut into an account-takeover path.

## Two retries is not a queue

When your endpoint answers with a 5xx or a 404, Didit retries twice - roughly 1 minute and then 4 minutes later - and then stops. A delivery that times out (no answer within 5 seconds) or can't connect is not retried automatically. If your endpoint was down for longer than that, those events won't arrive on their own: resend them from the endpoint's Deliveries list, and your team gets a console alert when a delivery fails for good.

Build a reconciliation path: on start-up, poll the decision endpoint for any session you don't hold a terminal status for. Treat webhooks as the fast path and polling as the backstop.

## Testing without running verifications

[Video: Set up API keys and test your webhooks - Didit Academy](https://www.youtube.com/watch?v=thOMIGbNLP0&t=140)

**Send test event** on the Webhooks page sends a fully-formed, signed event of whichever kind you pick - approved, declined, in review, KYB, entity, transaction. Use it to prove your endpoint, signature check, and handler work before a real session depends on them.

Sandbox is the other half of this: a sandbox session emits real webhooks with `"environment": "sandbox"`, so you can exercise the whole path end to end for free. See [testing in sandbox](https://help.didit.me/getting-started/sandbox-and-testing).
