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

# Signature Verification

> Verify webhook signatures to ensure payloads are authentically from AgentRef.

Every webhook delivery is signed with HMAC-SHA256. Verify the signature to confirm the payload came from AgentRef and wasn't tampered with.

<Info>
  The Node.js and Python SDKs do not currently expose a webhook signature helper. Use the manual verification algorithm below, or wrap it in your own helper for your framework.
</Info>

## Signature Format

AgentRef sends three headers with each webhook delivery:

| Header           | Description                                        |
| ---------------- | -------------------------------------------------- |
| `svix-id`        | Unique message ID                                  |
| `svix-timestamp` | Unix timestamp (seconds) when the message was sent |
| `svix-signature` | `v1,{base64_signature}`                            |

## Verification Algorithm

<Steps>
  <Step title="Get the raw request body">
    Read the body as a raw string – do **not** parse JSON first. Parsing and re-serializing can change whitespace or key order, breaking the signature.
  </Step>

  <Step title="Read the headers">
    Extract `svix-id`, `svix-timestamp`, and `svix-signature` from the request headers.
  </Step>

  <Step title="Compute the expected signature">
    Build the signed content string: `{svix-id}.{svix-timestamp}.{body}`

    Compute HMAC-SHA256 of this string using your webhook signing secret (the part after `whsec_`, base64url-decoded).
  </Step>

  <Step title="Compare signatures">
    Base64-encode your HMAC result and compare it to the signature value (after the `v1,` prefix) using constant-time comparison.
  </Step>
</Steps>

## Code Examples

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import crypto from 'crypto'

  function verifyWebhookSignature(req, secret) {
    const msgId = req.headers['svix-id']
    const timestamp = req.headers['svix-timestamp']
    const signature = req.headers['svix-signature']

    // Check timestamp to prevent replay attacks (5 min tolerance)
    const now = Math.floor(Date.now() / 1000)
    if (Math.abs(now - parseInt(timestamp)) > 300) {
      throw new Error('Timestamp too old')
    }

    // Decode the secret (remove whsec_ prefix, base64url decode)
    const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64url')

    // Build the signed content
    const signedContent = `${msgId}.${timestamp}.${req.body}`

    // Compute expected signature
    const expected = crypto
      .createHmac('sha256', secretBytes)
      .update(signedContent)
      .digest('base64')

    // Extract actual signature (remove v1, prefix)
    const actual = signature.split(',')[1]

    // Constant-time comparison
    const expectedBytes = Buffer.from(expected)
    const actualBytes = Buffer.from(actual)
    if (expectedBytes.length !== actualBytes.length || !crypto.timingSafeEqual(expectedBytes, actualBytes)) {
      throw new Error('Invalid signature')
    }

    return JSON.parse(req.body)
  }

  // Express route
  app.post('/webhooks/agentref', express.raw({ type: 'application/json' }), (req, res) => {
    try {
      const event = verifyWebhookSignature(req, process.env.AGENTREF_WEBHOOK_SECRET)
      console.log('Verified event:', event.type)
      res.status(200).send('OK')
    } catch (err) {
      console.error('Verification failed:', err.message)
      res.status(400).send('Invalid signature')
    }
  })
  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  import base64
  import json
  import os
  import time
  from flask import Flask, request

  app = Flask(__name__)

  def verify_webhook_signature(payload, headers, secret):
      msg_id = headers.get("svix-id")
      timestamp = headers.get("svix-timestamp")
      signature = headers.get("svix-signature")

      # Check timestamp (5 min tolerance)
      now = int(time.time())
      if abs(now - int(timestamp)) > 300:
          raise ValueError("Timestamp too old")

      # Decode secret (remove whsec_ prefix, base64url decode)
      secret_bytes = base64.urlsafe_b64decode(secret.removeprefix("whsec_") + "==")

      # Build signed content
      signed_content = f"{msg_id}.{timestamp}.{payload}"

      # Compute expected signature
      expected = base64.b64encode(
          hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()
      ).decode()

      # Extract actual (remove v1, prefix)
      actual = signature.split(",")[1]

      if not hmac.compare_digest(expected, actual):
          raise ValueError("Invalid signature")

      return json.loads(payload)

  @app.route("/webhooks/agentref", methods=["POST"])
  def webhook():
      try:
          event = verify_webhook_signature(
              request.get_data(as_text=True),
              request.headers,
              os.environ["AGENTREF_WEBHOOK_SECRET"],
          )
          print(f"Verified event: {event['type']}")
          return "OK", 200
      except ValueError as e:
          return str(e), 400
  ```
</CodeGroup>

## Common Pitfalls

<Warning>
  **Don't parse the body before verifying.** If your framework automatically parses JSON, use a raw body parser for the webhook route. `JSON.parse(JSON.stringify(body))` may reorder keys or change whitespace, producing a different signature.
</Warning>

* **Use `express.raw()` in Express** (not `express.json()`) for the webhook route
* **In Next.js App Router**, use `await request.text()` to get the raw body
* **Always use constant-time comparison** (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python) to prevent timing attacks
* **Validate the timestamp** – reject events older than 5 minutes to prevent replay attacks
