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

# Verifying signatures

> Check that a delivery came from Routeur and is recent.

Every delivery carries:

```text theme={null}
Routeur-Signature: t=1789567200,v1=5f1c0b…
```

`v1` is the hex HMAC-SHA256 of `<t>.<raw body>`, keyed by your subscription's signing secret.

<Steps>
  <Step title="Read the raw body">
    Compute the signature over the exact bytes received, before any JSON parsing.
  </Step>

  <Step title="Recompute and compare">
    HMAC-SHA256 `t + "." + body` with your secret, and compare it with `v1` in constant time.
  </Step>

  <Step title="Reject stale deliveries">
    Reject the request if `t` is more than five minutes from your clock, so a captured delivery cannot be replayed later.
  </Step>
</Steps>

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from 'node:crypto';

  export function verify(rawBody, header, secret, toleranceSeconds = 300) {
    const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
    const t = Number(parts.t);
    if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
    const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(parts.v1, 'hex');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

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


  def verify(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      try:
          t = int(parts["t"])
      except (KeyError, ValueError):
          return False
      if abs(time.time() - t) > tolerance_seconds or "v1" not in parts:
          return False
      expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts["v1"])
  ```

  ```go Go theme={null}
  package webhooks

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

  func Verify(rawBody []byte, header, secret string, tolerance time.Duration) bool {
  	var t, v1 string
  	for _, part := range strings.Split(header, ",") {
  		key, value, _ := strings.Cut(part, "=")
  		switch key {
  		case "t":
  			t = value
  		case "v1":
  			v1 = value
  		}
  	}
  	seconds, err := strconv.ParseInt(t, 10, 64)
  	if err != nil || v1 == "" || math.Abs(time.Since(time.Unix(seconds, 0)).Seconds()) > tolerance.Seconds() {
  		return false
  	}
  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write([]byte(t + "."))
  	mac.Write(rawBody)
  	got, err := hex.DecodeString(v1)
  	return err == nil && hmac.Equal(mac.Sum(nil), got)
  }
  ```
</CodeGroup>

<Warning>
  Frameworks that parse JSON before your handler runs change the bytes. Read the raw body for verification, for example with `express.raw({ type: 'application/json' })` in Express.
</Warning>
