---
title: "Java webhook signature verifier"
description: "Run the complete Java raw-byte notification verifier with authenticated event and environment checks."
slug: "/docs/v1/guides/verify-webhook-signatures/java"
section: "Webhook verification examples"
order: 220
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"]
draft: false
---
Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Java verifier.

## What you will learn

- Install and execute this exact sample.
- Verify raw bytes, event metadata and the pinned environment.
- Identify the boundary between authentication and durable business processing.

## Prerequisites

- Java 21 with Gson 2.14.0 on both the compile and run classpaths.
- The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally.
- The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret.

## Prepare a request

1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json.
2. Set KDP_WEBHOOK_SECRET through your private process environment.
3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox.
4. Use Python 3.11+ to create a fresh private synthetic request.

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

Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap.

## Build and run

Download [Gson 2.14.0 from the official artifact repository](https://repo1.maven.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar). The tested JAR SHA-256 is 2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f. On Windows replace the classpath separator with a semicolon: -cp ".;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
```

Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed.

## Complete source

```java
// Notification authenticity, not license validity. Java 21 + Gson 2.14.0.
// Compile: javac -cp gson-2.14.0.jar VerifyWebhook.java
// From this folder: java -cp .:gson-2.14.0.jar VerifyWebhook body.json headers.json
// Set KDP_WEBHOOK_SECRET/KDP_WEBHOOK_ENVIRONMENT; Windows classpath separator is ;.
// Expected: verified=true JSON; invalid input prints verified=false and exits 1.
import com.google.gson.*;
import java.nio.file.*;
import java.nio.charset.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HexFormat;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class VerifyWebhook {
    private static byte[] bounded(String path, int limit) throws Exception {
        try(var input=Files.newInputStream(Path.of(path))){var value=input.readNBytes(limit+1);if(value.length>limit)throw new IOException("Oversized input");return value;}
    }
    private static String utf8(byte[] raw) throws Exception {
        return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(raw)).toString();
    }
    private static String text(JsonObject object,String key) {
        var value=object.get(key);if(value==null||!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isString())throw new IllegalArgumentException("String required");return value.getAsString();
    }
    private static boolean includes(JsonArray values,String candidate){for(var value:values)if(candidate.equals(value.getAsString()))return true;return false;}
    public static JsonObject verify(byte[] body,JsonObject headers,String secret,String environment,long now,JsonObject contract) throws Exception {
        if(secret==null||secret.isEmpty()||!includes(contract.getAsJsonArray("environments"),environment)||body.length>contract.get("body_limit").getAsInt())throw new IllegalArgumentException("Invalid configuration or body size");
        var match=Pattern.compile("^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$").matcher(text(headers,"x-lmp-signature"));
        if(!match.matches()||Math.abs(now-Long.parseLong(match.group(1)))>contract.get("tolerance_seconds").getAsInt())throw new IllegalArgumentException("Invalid or stale signature");
        var mac=Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));mac.update((match.group(1)+".").getBytes(StandardCharsets.US_ASCII));
        if(!MessageDigest.isEqual(mac.doFinal(body),HexFormat.of().parseHex(match.group(2))))throw new IllegalArgumentException("Signature mismatch");
        var event=JsonParser.parseString(utf8(body)).getAsJsonObject();
        if(!text(event,"id").matches("evt_[A-Za-z0-9_-]{1,100}")||!includes(contract.getAsJsonArray("events"),text(event,"event"))||!environment.equals(text(event,"environment"))||!text(event,"id").equals(text(headers,"x-lmp-event-id"))||!text(event,"event").equals(text(headers,"x-lmp-event")))throw new IllegalArgumentException("Event metadata mismatch");
        if(text(event,"created_at").isEmpty()||!event.has("data")||!event.get("data").isJsonObject())throw new IllegalArgumentException("Invalid envelope");
        return event;
    }
    public static void main(String[] args){
        try{
            if(args.length!=2)throw new IllegalArgumentException("Two file paths required");
            var contract=JsonParser.parseString(utf8(bounded("contract.json",32768))).getAsJsonObject();
            var headers=JsonParser.parseString(utf8(bounded(args[1],32768))).getAsJsonObject();
            var environment=System.getenv("KDP_WEBHOOK_ENVIRONMENT");if(environment==null)environment="";
            var event=verify(bounded(args[0],1048576),headers,System.getenv("KDP_WEBHOOK_SECRET"),environment.toLowerCase(java.util.Locale.ROOT),Instant.now().getEpochSecond(),contract);
            var out=new JsonObject();out.addProperty("verified",true);out.add("event_id",event.get("id"));out.add("event",event.get("event"));out.add("environment",event.get("environment"));System.out.println(out);
        }catch(Exception failure){System.out.println("{\"verified\":false}");System.exit(1);}
    }
}
```

## Connect to an HTTP adapter

1. Read a bounded raw HTTP body before JSON middleware changes it.
2. Reject duplicate signature, event-ID and event-name header fields.
3. Normalize header names to lowercase without changing their values.
4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier.
5. Atomically persist the verified event ID and work item before sending a 2xx response.

Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary.

## Expected result

All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above.

## Common mistakes

- Reading contract.json from a different working directory or copying only one source without its companion files.
- Re-encoding JSON before HMAC verification.
- Reusing an expired test signature instead of generating a current one.
- Assuming successful authentication deduplicates an additive renewal.

## Security notes

Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration |
| Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks |
| The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect |

## Next steps

Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally).

## Related

- [All verifier languages](/docs/v1/guides/verify-webhook-signatures)
- [Delivery and retries](/docs/v1/guides/webhook-delivery)
- [Webhook event catalog](/docs/v1/guides/webhook-events)
