---
title: "Webhook signature examples"
description: "Verify Wazapin webhook deliveries on your server using the endpoint signing secret."
---

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

# Webhook signature examples

Verify every `POST` to your webhook URL **before** you process the body.

## Requirements

1. Read the **raw** request body bytes (do not re-serialize JSON).
2. Read signature headers from the delivery (typically `webhook-id` / `svix-id`, `webhook-timestamp` / `svix-timestamp`, and `webhook-signature` / `svix-signature`).
3. Use the **endpoint signing secret** from Wazapin (format `whsec_…`). Store it as a server secret, not in client code.
4. Reject requests outside the allowed timestamp window (replay protection).
5. Compare expected and received signatures with a **constant-time** comparison.

The signing scheme matches the [Svix](https://docs.svix.com/receiving/verifying-payloads/how) standard used for outbound deliveries. You can use the official Svix libraries or implement the same HMAC steps below.

## Node.js

### Express

```javascript
import { Webhook } from 'svix';

const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET);

app.post('/webhooks/wazapin', express.raw({ type: 'application/json' }), (req, res) => {
  try {
wh.verify(req.body, req.headers);
  } catch {
return res.status(403).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.status(200).send('ok');
});
```
### Manual (no SDK)

```javascript
import crypto from 'crypto';

function verifyWazapinWebhook(rawBody, headers, secret) {
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const msgId = headers['svix-id'] || headers['webhook-id'];
  const timestamp = headers['svix-timestamp'] || headers['webhook-timestamp'];
  const sigHeader = headers['svix-signature'] || headers['webhook-signature'];
  if (!msgId || !timestamp || !sigHeader) return false;

  const signed = `${msgId}.${timestamp}.${rawBody.toString('utf8')}`;
  const expected = crypto.createHmac('sha256', key).update(signed).digest('base64');

  for (const part of sigHeader.split(' ')) {
const [version, sig] = part.split(',');
if (version !== 'v1' || !sig) continue;
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length === b.length && crypto.timingSafeEqual(a, b)) return true;
  }
  return false;
}
```

## Python

### FastAPI + svix

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])

@app.post("/webhooks/wazapin")
async def wazapin_webhook(request: Request):
payload = await request.body()
try:
    wh.verify(payload, dict(request.headers))
except WebhookVerificationError:
    raise HTTPException(status_code=403)
return {"ok": True}
```
### Manual HMAC

```python
import hmac
import hashlib
import base64

def verify_wazapin_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
msg_id = headers.get("svix-id") or headers.get("webhook-id")
timestamp = headers.get("svix-timestamp") or headers.get("webhook-timestamp")
sig_header = headers.get("svix-signature") or headers.get("webhook-signature")
if not msg_id or not timestamp or not sig_header:
    return False

signed = f"{msg_id}.{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8")
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode("ascii")

for part in sig_header.split():
    version, sig = part.split(",", 1)
    if version == "v1" and hmac.compare_digest(sig, expected):
        return True
return False
```

## Go

Use [github.com/svix/svix-webhooks](https://github.com/svix/svix-webhooks) with your endpoint `whsec_` secret, or implement the same signed content: `msgID + "." + timestamp + "." + string(body)` with HMAC-SHA256 and base64, matching `v1` entries in the signature header.

## Failure handling

- Return `403` when the signature is invalid.
- Return `400` for malformed JSON after verification succeeds.
- Return `200` for duplicate events after your idempotency check.

:::warning
Never verify signatures on re-encoded JSON. Always use the raw HTTP body.
:::

## Related pages

- [Webhooks](/api/webhooks)
- [Webhook payload examples](/api/inbound-webhook-examples)
- [Example: handle webhook events](/recipes/handle-webhook-events)

Source: https://docs.wazapin.id/api/webhook-signature-examples/index.mdx
