---
title: "Verify webhooks"
description: "Check the signature of a delivery before you trust what it says."
url: "https://openemail.uk/docs/java/webhooks/verify"
area: "Java"
category: "Guides"
---

# Verify webhooks

Check the signature of a delivery before you trust what it says.

## Verify a delivery

**WebhookHandler.java**

```
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import uk.openemail.OpenEmail;
import uk.openemail.exception.WebhookSignatureException;

public final class WebhookHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String rawBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
        String secret = System.getenv("OPENEMAIL_WEBHOOK_SECRET");
        int status = 204;

        try {
            Map<String, Object> event = OpenEmail.verifyWebhookSignature(rawBody, exchange.getRequestHeaders(), secret);

            System.out.println(event.get("type") + " " + event.get("id"));
        } catch (WebhookSignatureException error) {
            status = 400;
        }

        exchange.sendResponseHeaders(status, -1);
        exchange.close();
    }
}
```

`OpenEmail.verifyWebhookSignature` reads the `X-OpenEmail-Signature` header, checks its HMAC over the timestamp and the body in constant time, and returns the event as a `Map<String, Object>`. The secret is the one `client.webhooks().create` returned.

The headers are a map with names in any case and text or a list of text as each value, so `exchange.getRequestHeaders()` and the `HttpHeaders` of Spring fit as they are. The value of the header itself works too.

> Pass the raw body, exactly as it arrived. A body that was parsed and encoded again no longer matches its signature.

## Old deliveries

**Tolerance.java**

```
String signature = "t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd";
Map<String, String> headers = Map.of("X-OpenEmail-Signature", signature);

try {
    OpenEmail.verifyWebhookSignature("{}", headers, System.getenv("OPENEMAIL_WEBHOOK_SECRET"), Duration.ofMinutes(1));
} catch (WebhookSignatureException error) {
    System.err.println(error.getMessage());
}
```

A delivery more than five minutes old is refused, so a captured request cannot be replayed later. A `Duration` as the last argument changes the five minutes, and `Duration.ZERO` accepts a delivery of any age.

- A missing or malformed header, a signature that does not match and a delivery that is too old all throw `WebhookSignatureException`.
- After a secret is rotated, a delivery can carry more than one signature, and any one that matches passes.
- Answer with a 2xx status quickly. A delivery that gets any other answer is tried again later.
