Skip to content
Receiver guide

Build a webhook receiver in Go

A small net/http handler that reads the raw body once, verifies the HMAC with a constant-time compare, and acknowledges before doing the work.

Go’s standard library has everything a webhook receiver needs — net/http for the endpoint and crypto/hmac for verification. No framework required.

A minimal receiver

Read the body once with io.ReadAll, verify the HMAC with a constant-time hmac.Equal, return 200, and process in a goroutine. Reject a bad signature with 401.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
)

var secret = []byte(os.Getenv("WEBHOOK_SECRET"))

func webhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body) // read the RAW body once
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}
	if !verify(body, r.Header.Get("X-Webhook-Signature")) {
		http.Error(w, "bad signature", http.StatusUnauthorized)
		return
	}
	w.WriteHeader(http.StatusOK) // acknowledge fast
	go process(body)             // then process asynchronously
}

func verify(body []byte, sig string) bool {
	mac := hmac.New(sha256.New, secret)
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	// hmac.Equal is constant-time.
	return hmac.Equal([]byte(expected), []byte(sig))
}

Acknowledge fast, process async

w.WriteHeader(http.StatusOK) returns immediately; the go process(body) goroutine does the slow work. For durability, enqueue the job instead of relying on an in-process goroutine that a restart would drop.

Register it with http.HandleFunc("/webhooks", webhook), then point the provider at a HookWatch endpoint. Every delivery is captured, so a failure is something you can inspect and replay rather than reproduce by hand.

Get started

Start debugging your webhooks.

Point one endpoint at HookWatch, capture a failure, and replay it once it’s fixed. Free during beta.