Go
Verify webhooks
Check the signature of a delivery before you trust what it says.
Verify a delivery
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
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.