Receive and Verify Webhooks in Go
Learn how to securely receive and verify webhooks in Go using HMAC-SHA256 signatures, timestamp validation, and timing-safe comparisons. Complete tutorial with production-ready code examples.

Receive and Verify Webhooks in Go
Go's standard library provides everything for secure webhook receivers: HTTP handling, cryptographic verification, and high-throughput performance. This tutorial covers building secure endpoints, HMAC-SHA256 verification, timestamp validation, and middleware patterns.
Setting Up the HTTP Handler
Read the raw request body before any parsing—you need exact bytes for signature verification.
package main
import (
"io"
"log"
"net/http"
)
func main() {
http.HandleFunc("/webhooks", handleWebhook)
log.Println("Webhook server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Only accept POST requests
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Read the entire request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "Error reading request", http.StatusBadRequest)
return
}
defer r.Body.Close()
log.Printf("Received webhook: %s", string(body))
// Respond immediately
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received": true}`))
}Use io.ReadAll to capture exact request bytes. Any modification—whitespace, key ordering—invalidates HMAC verification.
Reading and Preserving the Request Body
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Limit body size to prevent memory exhaustion attacks
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB limit
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "Request body too large or unreadable", http.StatusBadRequest)
return
}
// Get signature from header
signature := r.Header.Get("X-Webhook-Signature")
if signature == "" {
http.Error(w, "Missing signature header", http.StatusUnauthorized)
return
}
// Verify signature using the raw body bytes
if !verifySignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Only parse JSON after verification succeeds
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
log.Printf("Error parsing JSON: %v", err)
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
return
}
// Process the verified event
processEvent(event)
w.WriteHeader(http.StatusOK)
}http.MaxBytesReader prevents memory exhaustion attacks.
HMAC-SHA256 Signature Verification
HMAC-SHA256 verification uses Go's crypto/hmac, crypto/sha256, and crypto/subtle packages for timing-safe comparison.
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"strings"
)
var webhookSecret = []byte("whsec_your_secret_key_here")
func verifySignature(payload []byte, signatureHeader string, secret []byte) bool {
// Parse the signature header (format: "v1=abc123...")
parts := strings.Split(signatureHeader, "=")
if len(parts) != 2 || parts[0] != "v1" {
return false
}
receivedSig, err := hex.DecodeString(parts[1])
if err != nil {
return false
}
// Compute expected signature
mac := hmac.New(sha256.New, secret)
mac.Write(payload)
expectedSig := mac.Sum(nil)
// Timing-safe comparison
return subtle.ConstantTimeCompare(receivedSig, expectedSig) == 1
}Use subtle.ConstantTimeCompare—naive comparison returns early on mismatch, letting attackers discover signatures through timing. Constant-time comparison always takes the same duration.
Timestamp Validation for Replay Protection
Signature verification alone doesn't prevent replay attacks—an attacker who captures a legitimate webhook can resend it indefinitely. Including a timestamp in the signed payload and validating its freshness solves this problem.
Most webhook providers, including Hook Mesh, use a signature format that incorporates the timestamp:
import (
"fmt"
"strconv"
"time"
)
const maxTimestampAge = 5 * time.Minute
func verifySignatureWithTimestamp(payload []byte, signatureHeader string, secret []byte) error {
// Parse signature header (format: "t=1234567890,v1=abc123...")
parts := strings.Split(signatureHeader, ",")
if len(parts) < 2 {
return errors.New("invalid signature format")
}
var timestamp int64
var signature string
for _, part := range parts {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
ts, err := strconv.ParseInt(kv[1], 10, 64)
if err != nil {
return errors.New("invalid timestamp")
}
timestamp = ts
case "v1":
signature = kv[1]
}
}
if timestamp == 0 || signature == "" {
return errors.New("missing timestamp or signature")
}
// Validate timestamp freshness
webhookTime := time.Unix(timestamp, 0)
if time.Since(webhookTime).Abs() > maxTimestampAge {
return errors.New("timestamp outside acceptable range")
}
// Reconstruct signed payload (timestamp + payload)
signedPayload := fmt.Sprintf("%d.%s", timestamp, string(payload))
// Compute expected signature
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(signedPayload))
expectedSig := mac.Sum(nil)
receivedSig, err := hex.DecodeString(signature)
if err != nil {
return errors.New("invalid signature encoding")
}
if subtle.ConstantTimeCompare(receivedSig, expectedSig) != 1 {
return errors.New("signature mismatch")
}
return nil
}A 5-minute tolerance accommodates clock drift while limiting replay attacks. For complete protection, store processed event IDs to build idempotent webhook handlers.
Responding Quickly to Webhooks
Acknowledge immediately (within 5-30 seconds), handle work asynchronously:
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Webhook-Signature")
if err := verifySignatureWithTimestamp(body, signature, webhookSecret); err != nil {
log.Printf("Verification failed: %v", err)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Respond immediately before processing
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received": true}`))
// Process asynchronously
go func() {
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
log.Printf("Error parsing event: %v", err)
return
}
processEvent(event)
}()
}Use job queues for production (not bare goroutines).
Middleware Pattern for Verification
Use middleware to centralize verification and ensure it's never accidentally skipped:
package main
import (
"bytes"
"context"
"io"
"net/http"
)
type contextKey string
const bodyKey contextKey = "webhookBody"
func webhookVerificationMiddleware(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Read and preserve body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request", http.StatusBadRequest)
return
}
r.Body.Close()
// Verify signature
signature := r.Header.Get("X-Webhook-Signature")
if err := verifySignatureWithTimestamp(body, signature, secret); err != nil {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Store body in context for handler access
ctx := context.WithValue(r.Context(), bodyKey, body)
r.Body = io.NopCloser(bytes.NewReader(body))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func getWebhookBody(r *http.Request) []byte {
body, _ := r.Context().Value(bodyKey).([]byte)
return body
}Using this middleware with your routes:
func main() {
mux := http.NewServeMux()
// Apply verification middleware to webhook routes
webhookHandler := webhookVerificationMiddleware(webhookSecret)(
http.HandlerFunc(handleVerifiedWebhook),
)
mux.Handle("/webhooks/", webhookHandler)
mux.HandleFunc("/health", healthCheck)
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleVerifiedWebhook(w http.ResponseWriter, r *http.Request) {
// Signature already verified by middleware
body := getWebhookBody(r)
var event WebhookEvent
json.Unmarshal(body, &event)
// Handle the verified event
switch event.Type {
case "payment.completed":
handlePaymentCompleted(event)
case "subscription.canceled":
handleSubscriptionCanceled(event)
}
w.WriteHeader(http.StatusOK)
}Hook Mesh Signature Verification
func verifyHookMeshSignature(payload []byte, signatureHeader string) error {
secret := os.Getenv("HOOKMESH_WEBHOOK_SECRET")
if secret == "" {
return errors.New("webhook secret not configured")
}
return verifySignatureWithTimestamp(payload, signatureHeader, []byte(secret))
}Hook Mesh includes event IDs, making idempotency straightforward. Combined with automatic retries and delivery guarantees, you get reliable delivery without building infrastructure.
Complete Production Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type WebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Timestamp int64 `json:"timestamp"`
Data json.RawMessage `json:"data"`
}
func main() {
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
log.Fatal("WEBHOOK_SECRET environment variable required")
}
mux := http.NewServeMux()
handler := webhookVerificationMiddleware([]byte(secret))(
http.HandlerFunc(handleWebhook),
)
mux.Handle("/webhooks", handler)
log.Println("Starting webhook server on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body := getWebhookBody(r)
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
log.Printf("Processing event %s: %s", event.ID, event.Type)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received": true}`))
}Conclusion
Building secure webhook receivers in Go requires: reading raw request bytes, HMAC-SHA256 verification, subtle.ConstantTimeCompare for timing-safe comparison, and timestamp validation. The middleware pattern centralizes verification.
Go's standard library provides everything needed: io.ReadAll, crypto/hmac, crypto/sha256, and crypto/subtle. Combined with fast response times and async processing, you have production-grade webhook handling.
Hook Mesh provides automatic signature generation, timestamp inclusion, and reliable delivery with retries. To send webhooks from Go, see our sending webhooks with Go guide. For broader security patterns, read our webhook security best practices. For more language tutorials, explore our webhook implementation guides.
Related Posts
Send Webhooks with Go: A Complete Tutorial
Learn how to send webhooks with Go using the standard library. This hands-on tutorial covers JSON marshaling, HMAC-SHA256 signatures, HTTP client configuration, timeouts, retries, and production-ready patterns.
HMAC-SHA256 Webhook Signatures: Implementation Guide
Learn how to implement secure webhook signature verification using HMAC-SHA256. Complete guide with code examples for signing and verifying webhook payloads in Node.js and Python.
Webhook Security Best Practices: The Complete Guide
Learn how to secure your webhook implementations with HMAC signature verification, replay attack prevention, SSRF mitigation, and more. Includes code examples in Node.js and Python.
Receive and Verify Webhooks in Node.js: A Complete Guide
Learn how to securely receive and verify webhooks in Node.js using Express.js. Covers HMAC signature verification, timestamp validation, replay attack prevention, and common mistakes to avoid.
Webhook Idempotency: Why It Matters and How to Implement It
A comprehensive technical guide to implementing idempotency for webhooks. Learn about idempotency keys, deduplication strategies, and implementation patterns with Node.js and Python code examples.