> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mapping.travel/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying webhook signatures

> How to verify the X-Webhook-Signature header so you trust only payloads that originated from Mapping.Travel.

Every webhook delivery carries:

```
X-Webhook-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
```

The signature is computed as:

```
HMAC_SHA256( signing_secret, "{unix_timestamp}.{raw_body}" )
```

Where `raw_body` is the **bytes of the request body as received**, not a re-serialized JSON string.

## Verification steps

1. Parse `t=` and `v1=` from the `X-Webhook-Signature` header.
2. Reject the request if `|now - t| > 300` seconds (anti-replay window of ±5 minutes).
3. Recompute the HMAC with your stored signing secret over the string `"{t}.{raw_body}"`.
4. Compare the recomputed signature with `v1` using a **constant-time** equality function.

If any step fails, return `400 Bad Request` and do not process the payload.

## Node.js

```ts theme={null} theme={null}
import crypto from 'crypto';

export function verifyWebhook(
  signatureHeader: string,
  rawBody: string,
  secret: string,
  toleranceSeconds = 300,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('=') as [string, string]),
  );
  const ts = Number(parts.t);
  const v1 = parts.v1;
  if (!ts || !v1) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Note>
  In Express, use `express.raw({ type: 'application/json' })` on the webhook route to get the raw body bytes before JSON parsing. Calling `JSON.parse` then `JSON.stringify` re-serializes the payload and breaks the signature.
</Note>

## Python

```python theme={null} theme={null}
import hmac
import hashlib
import time

def verify_webhook(
    signature_header: str,
    raw_body: bytes,
    secret: str,
    tolerance_seconds: int = 300,
) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts = int(parts.get("t", 0))
    v1 = parts.get("v1", "")

    if abs(int(time.time()) - ts) > tolerance_seconds:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, v1)
```

## Kotlin

```kotlin theme={null} theme={null}
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

fun verifyWebhook(
    signatureHeader: String,
    rawBody: ByteArray,
    secret: String,
    toleranceSeconds: Long = 300,
): Boolean {
    val parts = signatureHeader.split(",").associate {
        val (k, v) = it.split("=", limit = 2)
        k to v
    }
    val ts = parts["t"]?.toLongOrNull() ?: return false
    val v1 = parts["v1"] ?: return false

    val now = System.currentTimeMillis() / 1000
    if (kotlin.math.abs(now - ts) > toleranceSeconds) return false

    val mac = Mac.getInstance("HmacSHA256").apply {
        init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
    }
    val signed = "$ts.".toByteArray() + rawBody
    val expected = mac.doFinal(signed).joinToString("") { "%02x".format(it) }

    return java.security.MessageDigest.isEqual(
        expected.toByteArray(),
        v1.toByteArray(),
    )
}
```

## Go

```go theme={null} theme={null}
package webhook

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strconv"
    "strings"
    "time"
)

func Verify(signatureHeader string, rawBody []byte, secret string, tolerance time.Duration) bool {
    parts := map[string]string{}
    for _, p := range strings.Split(signatureHeader, ",") {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) == 2 {
            parts[kv[0]] = kv[1]
        }
    }
    ts, err := strconv.ParseInt(parts["t"], 10, 64)
    if err != nil {
        return false
    }
    if d := time.Since(time.Unix(ts, 0)); d > tolerance || -d > tolerance {
        return false
    }

    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%d.", ts)
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expected), []byte(parts["v1"]))
}
```

## Common mistakes

* **Re-serializing JSON.** Sign the raw bytes as received, not a pretty-printed or re-normalized JSON string. Middleware in Express, Django, or Spring often re-parses the body; use a raw-body reader specifically on the webhook route.
* **Plain string comparison.** Use a constant-time comparison (`crypto.timingSafeEqual`, `hmac.compare_digest`, `MessageDigest.isEqual`, `hmac.Equal`). Plain `==` leaks timing information that can be exploited.
* **Skipping the timestamp check.** Without the ±5 min check, a captured valid request can be replayed indefinitely by an attacker.
* **Trimming or decoding the header.** Pass the `X-Webhook-Signature` header value verbatim — do not URL-decode or trim whitespace before splitting on `,`.
