go 145 lines · 3 tabs

Verifying and Dispatching Stripe-Style Webhooks by Event Type in Go

Shared by codesnips Aug 2026
3 tabs
package webhook

import (
	"encoding/json"
	"io"
	"net/http"
)

type Event struct {
	ID      string          `json:"id"`
	Type    string          `json:"type"`
	Created int64           `json:"created"`
	Data    json.RawMessage `json:"data"`
}

type Handler struct {
	Secret     string
	Dispatcher *Dispatcher
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil {
		http.Error(w, "cannot read body", http.StatusBadRequest)
		return
	}

	sig := r.Header.Get("Webhook-Signature")
	if err := VerifySignature(body, sig, h.Secret); err != nil {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var evt Event
	if err := json.Unmarshal(body, &evt); err != nil {
		http.Error(w, "malformed payload", http.StatusBadRequest)
		return
	}

	if err := h.Dispatcher.Dispatch(r.Context(), evt); err != nil {
		http.Error(w, "handler failed", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}
3 files · go Explain with highlit

This snippet shows the shape of a real webhook receiver: a signed HTTP endpoint that verifies the payload, decodes a common envelope, and dispatches to a per-event-type handler. The three tabs collaborate — the HTTP handler owns the request, the verifier owns the security, and the dispatcher owns the routing.

In webhook_handler.go, Handler.ServeHTTP reads the raw body exactly once with io.ReadAll before any parsing. This ordering matters: signature verification must run against the exact bytes that were signed, so the code never unmarshals first and re-serializes. The raw body is passed to VerifySignature, and only after that check passes is the JSON decoded into an Event envelope. Rejecting unsigned or tampered requests with 401 before touching business logic is the whole point of a webhook boundary.

signature.go implements the verification. VerifySignature parses the t= timestamp and v1= scheme from the signature header, rebuilds the signed payload as timestamp.body, and computes an HMAC-SHA256 over it with the shared secret. The comparison uses hmac.Equal, a constant-time compare, to avoid timing side channels that a naive == on the hex string would leak. It also enforces a tolerance window so replayed captures older than five minutes are rejected even if their signature is still valid.

dispatcher.go is the routing layer. Dispatcher holds a map[string]HandlerFunc keyed by the event Type string, registered via On. Dispatch looks up the handler and falls back to a no-op for unknown types rather than erroring — a deliberate choice, because providers add new event types over time and a receiver should tolerate types it does not care about. Each handler receives the already-decoded Event, so it can json-unmarshal only the Data field it needs.

The design separates three concerns that are often tangled together: transport, trust, and behavior. Keeping VerifySignature independent makes it unit-testable with fixed vectors, and keeping Dispatch map-based means adding an event type is one On call rather than another switch branch. A pitfall worth noting: the body must be buffered fully, so extremely large payloads should be bounded with http.MaxBytesReader in production.


Related snips

Share this code

Here's the card — post it anywhere.

Verifying and Dispatching Stripe-Style Webhooks by Event Type in Go — share card
Link copied