---
title: "Verify webhook signatures"
description: "Execute raw-byte HMAC verifiers in Node, Python, PHP, C#, Go, Java and Ruby with positive and negative notification tests."
slug: "/docs/v1/guides/verify-webhook-signatures"
section: "Webhooks and integrations"
order: 68
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/guides/webhook-delivery","/docs/v1/guides/test-webhooks-locally"]
draft: false
---
Verify the original request bytes before trusting a notification or storing work for it.

## What you will learn

- Run the same complete verifier contract in seven languages.
- Generate a current signed synthetic test request.
- Distinguish signature verification from durable deduplication and [license](/docs/v1/start/glossary#license) authorization.

## Prerequisites

- Your endpoint's signing secret from [setup](/docs/v1/guides/webhooks-overview).
- The [source archive](/docs/downloads/webhook-receivers.zip), extracted locally.
- A language runtime from the requirements below.
- A private process environment; do not publish its secret values.

The [download manifest](/docs/webhook-examples.json) lists the archive SHA-256 and every included source-file hash. Generated private requests, compiled programs and local inbox databases are not included.

## Prepare the files

1. Extract the archive and work inside its [webhooks](/docs/v1/start/glossary#webhook) directory.
2. Keep contract.json beside the verifier files.
3. Configure KDP_WEBHOOK_SECRET from your secret store with a [Sandbox](/docs/v1/start/glossary#sandbox) test endpoint secret.
4. Set KDP_WEBHOOK_ENVIRONMENT to sandbox.
5. Generate a new request directory with the following command.

```bash
python3 make-test.py events/test-ping.json private/request-1
```

Expected output: Created private test request files. The program creates body.json and headers.json without printing the secret or signature. It refuses to overwrite existing request files; use a new directory for another test.

This complete test body is a real captured synthetic local notification. It is old by design; make-test.py signs its exact bytes with the current attempt timestamp. Checking its created_at against the current clock would reject a valid delayed event incorrectly.

```json
{
  "id": "evt_ffa11b785e0ede73d9e69801",
  "data": {
    "message": "Signed webhook test",
    "webhook_id": "whk_2bf9c0559190a2101b3b9d1b"
  },
  "event": "test.ping",
  "created_at": "2026-10-14T17:46:36+00:00",
  "environment": "sandbox"
}
```

## Shared receiver contract

The archive includes contract.json beside the sources. Keep it unchanged with the [local test helper](/docs/v1/guides/test-webhooks-locally#generate-and-replay-a-request); all seven programs read the same file.

The event list is checked against the real sender. The 300-second freshness window and 1048576-byte body limit are explicit receiver example policy, not sender inbound limits. Header-file input is capped at 32768 bytes. The verifier rejects a missing or malformed timestamp/signature pair, wrong HMAC, stale/future attempt timestamp, unknown event, header/body mismatch and wrong pinned environment.

Use the plaintext endpoint secret as the HMAC key, not its hash and not an Ed25519 public key. Verification is over timestamp, one period, and raw body bytes. No JSON canonicalization step belongs in HMAC verification.

## Requirements and commands

| Language | Requirements | Run after request generation |
| --- | --- | --- |
| Node.js | Node 22+, standard library | node verify.mjs private/request-1/body.json private/request-1/headers.json |
| Python | Python 3.11+, standard library | python3 verify.py private/request-1/body.json private/request-1/headers.json |
| PHP | PHP 8.2+, JSON/hash | php verify.php private/request-1/body.json private/request-1/headers.json |
| Ruby | JSON/OpenSSL, compatible syntax from 2.6; deploy maintained Ruby | ruby verify.rb private/request-1/body.json private/request-1/headers.json |
| Go | Go 1.24+, standard library | Build verify.go, then run verify-go with both file paths |
| Java | Java 21, Gson 2.14.0 | Compile, then run VerifyWebhook with both file paths |
| C# | .NET 7 target, standard library | Build the project, then run its DLL with both file paths |

Go build:

```bash
go build -o verify-go verify.go
./verify-go private/request-1/body.json private/request-1/headers.json
```

Java build and run, after downloading the exact Gson JAR from the [official artifact repository](https://repo1.maven.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar):

```bash
javac -cp gson-2.14.0.jar VerifyWebhook.java
java -cp .:gson-2.14.0.jar VerifyWebhook private/request-1/body.json private/request-1/headers.json
```

The tested JAR SHA-256 is 2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f. On Windows use a semicolon in the Java classpath: -cp ".;gson-2.14.0.jar", and run verify-go.exe instead of ./verify-go. Use the same working directory containing contract.json for Go, Java and C#.

```bash
dotnet build csharp/WebhookVerifier.csproj -c Release
dotnet csharp/bin/Release/net7.0/WebhookVerifier.dll private/request-1/body.json private/request-1/headers.json
```

The .NET target matches the current SDK baseline; verify an upgrade to your supported deployment runtime before launch. This phase executes the verifiers on macOS, not native Windows or Linux. Ruby was executed on the legacy macOS runtime. Prefer maintained Ruby with the native fixed_length_secure_compare primitive for deployment; the older fallback compares every fixed-size byte position without early exit but does not establish a managed-VM constant-time guarantee.

## Complete verifier sources

Choose a language to read its complete executed source and setup. Separate pages keep the initial download small without truncating or hiding code. All are public documentation and work without JavaScript.

:::cards
- [Node.js verifier](/docs/v1/guides/verify-webhook-signatures/node)
- [Python verifier](/docs/v1/guides/verify-webhook-signatures/python)
- [PHP verifier](/docs/v1/guides/verify-webhook-signatures/php)
- [C# verifier](/docs/v1/guides/verify-webhook-signatures/csharp)
- [Go verifier](/docs/v1/guides/verify-webhook-signatures/go)
- [Java verifier](/docs/v1/guides/verify-webhook-signatures/java)
- [Ruby verifier](/docs/v1/guides/verify-webhook-signatures/ruby)
:::

## HTTP adapter checklist

1. Require POST on a dedicated route and application/json content.
2. Reject duplicate signature, event-name and event-ID HTTP headers before joining header values.
3. Enforce body/header limits and a read timeout before buffering.
4. Pass exact raw body bytes and normalized lowercase header names to the verifier.
5. Pass only the secret and expected environment assigned to that endpoint.
6. On success, atomically record the event ID and durable work item.
7. Return a generic refusal on failure without echoing secrets, signatures or payloads.

Expected result: an authenticated duplicate is handled by your durable inbox, not by calling the verifier alone. The CLI tools authenticate but deliberately do not perform any business effect or store a deduplication record. The [local HTTP inbox](/docs/v1/guides/test-webhooks-locally) shows the receive/commit/acknowledge boundary.

## Expected result

For test-ping.json, each program exits 0 and prints JSON containing verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Object property ordering is not part of the result contract.

A changed body byte, wrong secret, stale/future attempt, invalid envelope or wrong environment prints exactly {"verified":false} and exits 1. Test every failure before connecting a business action. Regenerate the request when its signature timestamp is outside the 300-second window.

## Common mistakes

- Signing a pretty-printed reconstruction instead of preserving the request bytes.
- Passing the webhook secret hash instead of the original secret.
- Reusing a saved signature indefinitely instead of generating a new test timestamp.
- Assuming the CLI verifier prevents duplicate emails or license renewals.

## Security notes

The header JSON file is a local testing interface, not a public endpoint. Securely remove private generated requests under your retention policy; do not check them into Git. Reject duplicated HTTP signature headers at the adapter. Application validity uses Ed25519-signed license responses, not these HMAC notification verifiers.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| verified=false in all languages | Wrong secret, stale signature or changed body | Regenerate a private request and use its exact files |
| Java cannot find Gson | JAR absent from compile/run classpath | Install the pinned artifact and use the OS-specific separator |
| Go or C# cannot read contract.json | Wrong working directory | Run from the extracted webhooks directory |
| A duplicate verifies twice | Authentication is not deduplication | Add a durable unique event-ID inbox before business processing |

## Next steps

Run [local replay and failure tests](/docs/v1/guides/test-webhooks-locally) before enabling your production notification workflow.

## Related

- [Delivery contract](/docs/v1/guides/webhook-delivery)
- [Event catalog](/docs/v1/guides/webhook-events)
- [Ed25519 response verification](/docs/v1/security/verify-ed25519)
