Webhook signatures
Anyone can send a request to your HTTPS address. Check the signature before you trust a delivery.
What arrives
Every delivery is a POST with a JSON body {"event", "occurred_at", "data"} and three headers:
| Header | Meaning |
|---|---|
X-Tanba-Delivery | The id of the delivery. It is the same on every retry |
X-Tanba-Event | The event name, for example approval.issued |
X-Tanba-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
The events are listed on the webhooks page.
The secret
When an organisation administrator creates a webhook in the console, the platform shows its secret once. It begins with whsec_. Use the whole string as the key. Store it on your server only.
How to verify
- Read the raw body bytes before any JSON parsing.
- Split the header into
tandv1. - Reject the delivery if
tdiffers from your clock by more than 5 minutes. - Compute the HMAC-SHA256 of
t, a dot, and the raw body, keyed with the secret. Write it in hex. - Compare it with
v1in constant time.
Python:
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False
mac = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
Go:
func verify(secret string, header string, body []byte, now time.Time) bool {
var t, sig string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(part, "=")
switch k {
case "t":
t = v
case "v1":
sig = v
}
}
secs, err := strconv.ParseInt(t, 10, 64)
if err != nil || now.Sub(time.Unix(secs, 0)).Abs() > 5*time.Minute {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(body)
want, err := hex.DecodeString(sig)
return err == nil && hmac.Equal(mac.Sum(nil), want)
}
Retries and duplicates
Answer with any 2xx status within 10 seconds. Anything else is retried: the first retry after 1 minute, then the wait doubles up to 6 hours, at most 12 attempts. After that the delivery is marked failed.
A delivery can arrive more than once. Store X-Tanba-Delivery and skip a delivery you have already handled. Do the slow work after you have answered.
Addresses that are refused
Loopback, private, link-local and other internal addresses are refused, when you create the webhook and again at every delivery, because the name is resolved at send time.