# Receiving signed webhooks

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

Guide - Since version 1.5.0 - Flexway v1.6.3 - https://flexway.io/en/docs/developer-webhooks/

> 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](https://flexway.io/en/docs/product-async/); operations side: [Publishing and consuming on a broker](https://flexway.io/en/docs/operations-async/).

## 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

```python
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())
```

```go
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**.
