flexway
FRENTalk to an expert

Receiving signed webhooks

GuideDevelopersSince version 1.5.0

Markdown

On this page

The gateway delivers your tenant's events to your URL, signed in the Standard Webhooks format. Verify the signature before anything else.

Product side: Asynchronous plane and brokers; operations side: Publishing and consuming on a broker.

What you receive

Element Value
Method and body POST, a JSON CloudEvent (Content-Type: application/cloudevents+json)
webhook-id The event's id (the CloudEvent's id), stable across attempts
webhook-timestamp Unix seconds of the attempt
webhook-signature v1,<base64>; two values separated by a space during a secret rotation
Secret whsec_<base64>, handed out by the operator

The signature is HMAC-SHA256(decoded secret, "<webhook-id>.<webhook-timestamp>.<raw body>") in base64.

Verifying

import base64, hashlib, hmac, time
def verify(secret: str, headers, body: bytes, tolerance=300) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > tolerance:
        return False
    mac = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(mac, s.split(",", 1)[1]) for s in headers["webhook-signature"].split())
func verify(secret string, h http.Header, body []byte) bool {
	key, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	m := hmac.New(sha256.New, key)
	fmt.Fprintf(m, "%s.%s.", h.Get("webhook-id"), h.Get("webhook-timestamp"))
	m.Write(body)
	want := base64.StdEncoding.EncodeToString(m.Sum(nil))
	for _, s := range strings.Fields(h.Get("webhook-signature")) {
		if v, ok := strings.CutPrefix(s, "v1,"); ok && hmac.Equal([]byte(v), []byte(want)) {
			return true
		}
	}
	return false // check the webhook-timestamp skew too
}

The official Standard Webhooks libraries (standardwebhooks for Python, JavaScript, Go...) do the same check. Read the raw body, before any JSON decoding.

Answering

Your response What the gateway does
2xx Delivered
408, 429, 5xx, timeout, connection refused Another attempt, honouring your Retry-After (capped)
Any other 4xx No further attempt
Attempts exhausted The event goes to the operator's dead-letter channel

Answer fast and process afterwards; deduplicate on webhook-id: delivery is at least once.

Next steps