Skip to content

Verifying Webhook Signatures

Every signed delivery lets the receiver prove it came from Sparrow and wasn’t replayed or modified. This page shows how to verify it in Go, plus a sample helper for other common languages: one file you copy into your project, with no package to install.

Sparrow signs in the Standard Webhooks format. A signed delivery carries three headers:

HeaderValue
webhook-idmsg_<delivery-id>
webhook-timestampUnix seconds when the delivery was sent
webhook-signatureSpace-separated signatures, e.g. v1,<base64> v1a,<base64>

The signed message is {webhook-id}.{webhook-timestamp}.{raw body}:

  • v1, is HMAC-SHA256 keyed with the webhook’s webhook_secret. A whsec_… secret (the default) is base64-decoded first; any other secret is used as raw bytes.
  • v1a, is Ed25519, verified with the webhook’s hex signing_public_key, which is present when the webhook uses signature_type: "ed25519". There’s no shared secret, so receivers only ever hold a public key.

Every helper rejects a delivery when:

  • a header is missing;
  • the timestamp is more than 5 minutes off in either direction (replay protection);
  • no signature of the requested scheme matches.

Several signatures may appear during secret rotation, and matching any one of them is enough.

Pick your language: the choice applies to every example on this page.

Terminal window
go get github.com/sarathsp06/sparrow/pkg/signature
import (
"io"
"net/http"
"github.com/sarathsp06/sparrow/pkg/signature"
)
func webhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) // raw bytes
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := signature.VerifyHMAC(body, r.Header, webhookSecret); err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// Ed25519 instead: signature.VerifyEd25519(body, r.Header, signingPublicKeyHex)
w.WriteHeader(http.StatusOK)
}

Custom clock or tolerance: signature.Verifier{Tolerance: 10 * time.Minute}.VerifyHMAC(...).

  • sparrow listen registers a temporary webhook and prints each delivery with its signature check, which makes it an easy way to see real signed requests (see the CLI guide). Point it at your receiver with --forward to confirm a sample accepts genuine deliveries and rejects tampered ones.
  • Each helper takes an optional clock and tolerance (now / nowSeconds / verify_hmac_at / signature.Verifier), so you can unit-test your handler with a fixed request instead of a live one.