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

# Verify webhooks

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

## Verify a delivery

**webhook.go**

```
package main

import (
	"io"
	"log"
	"net/http"
	"os"

	"github.com/bfzli/openemail-go"
)

func main() {
	secret := os.Getenv("OPENEMAIL_WEBHOOK_SECRET")

	http.HandleFunc("/webhooks/openemail", func(writer http.ResponseWriter, request *http.Request) {
		payload, err := io.ReadAll(http.MaxBytesReader(writer, request.Body, 1<<20))
		if err != nil {
			writer.WriteHeader(http.StatusBadRequest)

			return
		}

		event, err := openemail.VerifyWebhookSignature(payload, request.Header, secret)
		if err != nil {
			writer.WriteHeader(http.StatusBadRequest)

			return
		}

		log.Println(event.String("type"), event.ID())
		writer.WriteHeader(http.StatusNoContent)
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

`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 an `openemail.Object`. The secret is the one `client.Webhooks.Create` returned.

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

## Old deliveries

**tolerance.go**

```
payload := []byte(`{"id":"evt_1","type":"email.received"}`)
headers := http.Header{"X-OpenEmail-Signature": {"t=1767225600,v1=5f2d"}}

_, err := openemail.VerifyWebhookSignature(payload, headers, "whsec_example", openemail.WithTolerance(time.Minute))

if errors.Is(err, openemail.ErrWebhookSignature) {
	fmt.Println("refused:", err)
}
```

A delivery more than five minutes old is refused, so a captured request cannot be replayed later. `openemail.WithTolerance` changes the five minutes, and 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 return an error that matches `openemail.ErrWebhookSignature`.
- 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.
