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

# Signature Verification

> Verify webhook authenticity using HMAC-SHA256 signatures to prevent forgery and replay attacks

Every webhook request from Paxos Labs includes a cryptographic signature that lets you confirm the request is authentic and hasn't been tampered with. Always verify signatures before processing webhook payloads.

## Headers

Each webhook `POST` request includes these headers:

| Header                   | Example                    | Description                                                                             |
| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------- |
| `X-PAXOS-LABS-TIMESTAMP` | `2026-04-07T18:06:40.000Z` | RFC 3339 UTC timestamp of event creation. Concatenated with the payload before signing. |
| `X-PAXOS-LABS-SIGNATURE` | `a3f2...9b01`              | HMAC-SHA256 hex digest of the timestamp and payload                                     |
| `Content-Type`           | `application/json`         | Always JSON                                                                             |
| `User-Agent`             | `PaxosLabs-Webhooks/1.0`   | Identifies the sender                                                                   |

## Signature Scheme

`X-PAXOS-LABS-SIGNATURE` contains the raw hex-encoded HMAC-SHA256 digest. The timestamp used in the HMAC computation is sent in the separate `X-PAXOS-LABS-TIMESTAMP` header.

## Verification Algorithm

<Steps>
  <Step title="Read the timestamp and signature headers">
    Read `X-PAXOS-LABS-TIMESTAMP` for the RFC 3339 timestamp and `X-PAXOS-LABS-SIGNATURE` for the hex signature.
  </Step>

  <Step title="Check the timestamp">
    Reject requests where the timestamp is more than **5 minutes** from your server's current time. This protects against replay attacks.
  </Step>

  <Step title="Construct the signed payload">
    Concatenate the timestamp, a literal period (`.`), and the **raw request body** (the exact bytes received — do not parse and re-serialize):

    ```
    {timestamp}.{raw_body}
    ```
  </Step>

  <Step title="Compute the expected signature">
    Calculate an HMAC-SHA256 using your endpoint's signing secret as the key and the constructed string as the message. Hex-encode the result.
  </Step>

  <Step title="Compare signatures">
    Use a **constant-time comparison** to check that your computed signature matches the `v1` value from the signature header. This prevents timing attacks.
  </Step>
</Steps>

## Implementation Examples

<Tabs>
  <Tab title="Node.js" icon="node-js">
    ```js theme={null}
    import { createHmac, timingSafeEqual } from 'node:crypto'

    const TOLERANCE_SECONDS = 300

    function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
      const timestamp = new Date(timestampHeader)
      if (Number.isNaN(timestamp.getTime())) {
        throw new Error('Invalid timestamp header')
      }

      if (Math.abs(Date.now() - timestamp.getTime()) / 1000 > TOLERANCE_SECONDS) {
        throw new Error('Timestamp outside tolerance — possible replay attack')
      }

      const signedPayload = `${timestampHeader}.${rawBody}`
      const expected = createHmac('sha256', secret)
        .update(signedPayload)
        .digest('hex')

      const received = Buffer.from(signatureHeader, 'hex')
      const expectedBuf = Buffer.from(expected, 'hex')

      if (received.length !== expectedBuf.length ||
          !timingSafeEqual(received, expectedBuf)) {
        throw new Error('Invalid signature')
      }
    }
    ```

    <Info>
      Use `timingSafeEqual` instead of `===` to prevent timing side-channel attacks.
    </Info>
  </Tab>

  <Tab title="Python" icon="python">
    ```python theme={null}
    import hashlib, hmac
    from datetime import datetime, timezone

    TOLERANCE_SECONDS = 300

    def verify_webhook_signature(raw_body, signature_header, timestamp_header, secret):
        ts = datetime.fromisoformat(timestamp_header)
        now = datetime.now(timezone.utc)

        if abs((now - ts).total_seconds()) > TOLERANCE_SECONDS:
            raise ValueError("Timestamp outside tolerance — possible replay attack")

        signed_payload = f"{timestamp_header}.{raw_body}".encode()
        expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()

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

    <Info>
      `hmac.compare_digest` provides constant-time comparison in Python.
    </Info>
  </Tab>

  <Tab title="Go" icon="code">
    ```go theme={null}
    package webhook

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

    const ToleranceSeconds = 300

    func VerifySignature(body []byte, sigHeader, tsHeader, secret string) error {
    	ts, err := time.Parse(time.RFC3339, tsHeader)
    	if err != nil {
    		return fmt.Errorf("invalid timestamp header: %w", err)
    	}

    	if math.Abs(time.Since(ts).Seconds()) > ToleranceSeconds {
    		return fmt.Errorf("timestamp outside tolerance")
    	}

    	mac := hmac.New(sha256.New, []byte(secret))
    	fmt.Fprintf(mac, "%s.%s", tsHeader, body)
    	expected := hex.EncodeToString(mac.Sum(nil))

    	if !hmac.Equal([]byte(sigHeader), []byte(expected)) {
    		return fmt.Errorf("invalid signature")
    	}

    	return nil
    }
    ```

    <Info>
      `hmac.Equal` provides constant-time comparison in Go's standard library.
    </Info>
  </Tab>
</Tabs>

## Best Practices

<AccordionGroup>
  <Accordion title="Always verify before processing">
    Never process a webhook payload without verifying the signature first. An unverified payload could be forged by a malicious actor.
  </Accordion>

  <Accordion title="Use constant-time comparison">
    Standard string comparison (`===`, `==`) leaks timing information that attackers can exploit. Always use `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), or `hmac.Equal` (Go).
  </Accordion>

  <Accordion title="Enforce a replay window">
    Reject events where the timestamp is more than 5 minutes from your server's clock. This prevents captured requests from being replayed later.
  </Accordion>

  <Accordion title="Use the raw request body">
    Compute the HMAC over the exact bytes received in the HTTP body. Parsing to JSON and re-serializing can change whitespace or key ordering, producing a different signature.
  </Accordion>

  <Accordion title="Store secrets securely">
    Keep your signing secret in a secrets manager or encrypted environment variable — never hard-code it or commit it to source control.
  </Accordion>

  <Accordion title="Respond quickly">
    Return a `2xx` status code within **10 seconds**. Move heavy processing to a background queue so the webhook handler returns immediately.
  </Accordion>

  <Accordion title="Handle duplicates idempotently">
    Use the event `id` field to deduplicate. Store processed event IDs and skip any that you've already handled.
  </Accordion>
</AccordionGroup>

## Troubleshooting

| Symptom              | Cause                                   | Fix                                                               |
| -------------------- | --------------------------------------- | ----------------------------------------------------------------- |
| Signature mismatch   | Re-serialized body instead of raw bytes | Use the raw HTTP body for HMAC computation                        |
| Timestamp rejection  | Server clock drift                      | Sync your server with NTP; widen tolerance if needed              |
| `401` on test events | Wrong secret                            | Verify you're using the correct `pxlwh_` secret for this endpoint |
| Secret lost          | Secret was not saved at creation        | Delete the endpoint and create a new one                          |
