# Webhook receiver

> A webhook receiver in Node and Python (Flask) that verifies Standard Webhooks signatures on the raw body, replies fast, ignores duplicates and fetches job results.

Canonical: https://mobilevalidate.com/docs/recipes/webhook-receiver · Last updated: 2026-09-25

This recipe receives `job.completed` and `lookup.completed` events and verifies them before trusting them. There is a [Node version](https://github.com/brntech/mobilevalidate-sdk/tree/main/examples/webhook-receiver/node) with no framework and a [Python version](https://github.com/brntech/mobilevalidate-sdk/tree/main/examples/webhook-receiver/python) with Flask.

## What rules does the receiver follow?

| Rule | Why |
|---|---|
| Verify the signature on the raw body | Re-serialized JSON changes the bytes, so the signature no longer matches |
| Reply with 2xx quickly, then do the work | Slow or failing endpoints are retried |
| Ignore repeated `webhook-id` values | A delivery can arrive more than once |
| Fetch job results with your API key | Events never contain phone numbers or e-mail addresses |

## What does verification look like?

```js
import { verifyWebhook } from "mobilevalidate";

const raw = Buffer.concat(chunks);                 // the exact bytes received
const event = await verifyWebhook(raw, req.headers, process.env.MOBILEVALIDATE_WEBHOOK_SECRET);
res.writeHead(204).end();                          // reply first
if (event.type === "job.completed") {
  for await (const row of mv.jobs.results(event.data.id)) { /* … */ }
}
```

```python
from mobilevalidate import verify_webhook, WebhookVerificationError

@app.post("/webhooks/mobilevalidate")
def receive():
    try:
        event = verify_webhook(request.get_data(), request.headers, SECRET)
    except WebhookVerificationError as e:
        return str(e), 400
    return "", 204
```

`verifyWebhook` throws when the signature doesn't match or the timestamp is more than 5 minutes off. See [Webhooks](/docs/webhooks) for the signature scheme and retry schedule.

## How do I test it locally?

Sign a sample event with the CLI and send it with curl. No API key is needed for this:

```bash
export MOBILEVALIDATE_WEBHOOK_SECRET=whsec_$(printf 'local-test-secret' | base64)
npx mobilevalidate webhooks sign --secret "$MOBILEVALIDATE_WEBHOOK_SECRET" --file event.json > headers.txt
curl -i localhost:3000/webhooks/mobilevalidate -H 'content-type: application/json' \
  -H "$(sed -n 1p headers.txt)" -H "$(sed -n 2p headers.txt)" -H "$(sed -n 3p headers.txt)" \
  --data-binary @event.json
```

To receive real events, register an endpoint with a [personal test key](/get-test-key) or a live key.

## Frequently asked questions

### Why does my signature check fail?

Usually because the body was parsed and re-serialized before verification. Verify the raw bytes exactly as received, with the endpoint's own whsec_ secret.

### Can I use webhooks with the sandbox key?

No. The public sandbox key has no webhooks. Use a personal test key to register a test endpoint, or sign a sample event yourself with the CLI.
