TAŇBA-OOH Developers

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:

HeaderMeaning
X-Tanba-DeliveryThe id of the delivery. It is the same on every retry
X-Tanba-EventThe event name, for example approval.issued
X-Tanba-Signaturet=<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

  1. Read the raw body bytes before any JSON parsing.
  2. Split the header into t and v1.
  3. Reject the delivery if t differs from your clock by more than 5 minutes.
  4. Compute the HMAC-SHA256 of t, a dot, and the raw body, keyed with the secret. Write it in hex.
  5. Compare it with v1 in 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.