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

# Verify Signatures

## Overview

Verifying webhook signatures ensures that incoming webhooks are authentic and sent by Coinbase, protecting your application from malicious requests and replay attacks.

## Why verify signatures?

Without signature verification, your webhook endpoint is vulnerable to:

* **Spoofed webhooks**: Attackers could send fake event data to your endpoint
* **Replay attacks**: Old webhook events could be resent to trigger duplicate processing
* **Man-in-the-middle attacks**: Modified webhook payloads could go undetected

<Warning>
  Always verify webhook signatures in production. Unverified webhooks can lead to security vulnerabilities and data integrity issues.
</Warning>

## How it works

When you create a webhook subscription, the response includes a `secret` that serves as your signing key.

Each webhook request includes the signature under the header name: `X-Hook0-Signature`.

```text title="Example signature header" theme={null}
t=1728394718,v0=9f8e7d6c5b4a...,h=content-type x-event-id x-event-type,v1=a1b2c3d4e5f6...
```

The signature header contains four parts:

| Field | Description                                                                      | Example                                |
| ----- | -------------------------------------------------------------------------------- | -------------------------------------- |
| `t`   | Unix timestamp (seconds) when the webhook was sent                               | `1728394718`                           |
| `v0`  | HMAC-SHA256 over `{t}.{body}` — protects the body and timestamp only             | `9f8e7d6c5b4a...`                      |
| `h`   | Space-separated, ordered list of header names included in the `v1` signature     | `content-type x-event-id x-event-type` |
| `v1`  | HMAC-SHA256 over `{t}.{h}.{headerValues}.{body}` — also binds the listed headers | `a1b2c3d4e5f6...`                      |

<Tip>
  `v0` and `v1` are both included on every request. `v0` includes the timestamp and body; `v1` additionally binds the listed headers. Unless you want to bind the headers, which is unnecessary for most use cases, use `v0`.
</Tip>

<Accordion title="Verification process">
  1. **Extract signature components**: Parse the `t`, `h`, `v0`, and `v1` values from the header
  2. **Build signed payload**: Concatenate `timestamp.headerNames.headerValues.rawBody`
  3. **Compute expected signature**: Create HMAC-SHA256 hash using your secret
  4. **Compare signatures**: Use timing-safe comparison to match expected vs. provided
  5. **Verify timestamp**: Ensure the webhook isn't too old (prevents replay attacks)
</Accordion>

### 1. Create a verification function

First, handle the verification logic in a reusable function which will:

* Parse the signature header to extract the timestamp, header names, and signature
* Build the signed payload by concatenating the timestamp, headers, and raw body
* Compute the expected signature using HMAC-SHA256
* Compare the signatures using a timing-safe comparison to prevent timing attacks
* Validate the timestamp to ensure the webhook isn't too old (replay attack prevention)

```javascript title="verify-webhook.js" theme={null}
const crypto = require('crypto');

/**
 * Verify webhook signature and timestamp
 * @param {string} payload - Raw request body as string
 * @param {string} signatureHeader - X-Hook0-Signature header value
 * @param {string} secret - Webhook secret from the subscription response
 * @param {Object} headers - HTTP headers from webhook request
 * @param {number} maxAgeMinutes - Max age for webhook (default: 5 minutes)
 * @returns {boolean} true if webhook is authentic and within allowed time window
 */
function verifyWebhookSignature(payload, signatureHeader, secret, headers, maxAgeMinutes = 5) {
    try {
        // Parse signature header: t=timestamp,h=headers,v1=signature
        const elements = signatureHeader.split(',');
        const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
        const headerNames = elements.find(e => e.startsWith('h=')).split('=')[1];
        const providedSignature = elements.find(e => e.startsWith('v1=')).split('=')[1];

        // Build header values string
        const headerNameList = headerNames.split(' ');
        const headerValues = headerNameList.map(name => headers[name] || '').join('.');

        // Build signed payload
        const signedPayload = `${timestamp}.${headerNames}.${headerValues}.${payload}`;

        // Compute expected signature
        const expectedSignature = crypto
            .createHmac('sha256', secret)
            .update(signedPayload, 'utf8')
            .digest('hex');

        // Compare signatures securely
        const signaturesMatch = crypto.timingSafeEqual(
            Buffer.from(expectedSignature, 'hex'),
            Buffer.from(providedSignature, 'hex')
        );

        // Verify timestamp to prevent replay attacks
        const webhookTime = parseInt(timestamp) * 1000; // Convert to milliseconds
        const currentTime = Date.now();
        const ageMinutes = (currentTime - webhookTime) / (1000 * 60);

        if (ageMinutes > maxAgeMinutes) {
            console.error(`Webhook timestamp exceeds maximum age: ${ageMinutes.toFixed(1)} minutes > ${maxAgeMinutes} minutes`);
            return false;
        }

        return signaturesMatch;

    } catch (error) {
        console.error('Webhook verification error:', error);
        return false;
    }
}
```

### 2. Verify webhooks in your application

Now integrate the verification function into your webhook endpoint. This example shows:

* How to configure Express to preserve the raw request body (required for signature verification)
* How to extract the signature header and webhook secret
* How to call the verification function before processing the webhook
* How to handle both valid and invalid webhooks appropriately

<Info>
  **Important**: You must use `express.raw()` middleware instead of `express.json()` to preserve the raw request body. The signature is computed against the raw bytes, so parsing the JSON first will break verification.
</Info>

```javascript title="webhook-endpoint.js" theme={null}
const express = require("express");
const app = express();

// Important: Get raw body for signature verification
app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
    // Step 1: Extract the raw payload (must be string for signature verification)
    const payload = req.body.toString();

    // Step 2: Get the signature from the X-Hook0-Signature header
    const signature = req.headers["x-hook0-signature"];

    // Step 3: Get your webhook secret (from the subscription response)
    const secret = process.env.WEBHOOK_SECRET;

    // Step 4: Verify the webhook signature
    if (verifyWebhookSignature(payload, signature, secret, req.headers)) {
        console.log("✅ Authentic webhook");

        // Step 5: Parse the JSON payload (only after verification!)
        const event = JSON.parse(payload);

        // Step 6: Process your webhook event
        console.log("Transaction detected:", event.data.transactionHash);
        // Add your business logic here...

        // Step 7: Return 200 to acknowledge receipt
        res.status(200).send("OK");
    } else {
        console.log("❌ Invalid webhook - rejected");
        res.status(400).send("Invalid signature");
    }
});
```

### Example webhook payload

Here's what a complete webhook request looks like:

```json title="webhook-payload.json" theme={null}
{
  "id": "evt_1a2b3c4d5e6f",
  "type": "onchain.activity.detected",
  "createdAt": "2025-10-08T13:58:38.681893Z",
  "data": {
    "subscriptionId": "sub_abc123",
    "networkId": "base-mainnet",
    "blockNumber": 12345678,
    "blockHash": "0xabc123...",
    "transactionHash": "0xdef456...",
    "logIndex": 42,
    "contractAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "eventName": "Transfer",
    "from": "0xf20d2e37514195ebedb0bc735ba6090ce103d38c",
    "to": "0x1234567890123456789012345678901234567890",
    "value": "1000000"
  }
}
```

The webhook request will include these HTTP headers:

```text title="HTTP headers" theme={null}
POST /webhook HTTP/1.1
Host: your-webhook-url.com
Content-Type: application/json
X-Event-Id: 3a3dd092-55ce-4832-97d9-0e1ab928c79b
X-Event-Type: onchain.activity.detected
X-Hook0-Signature: t=1728394718,v0=9f8e7d6c5b4a...,h=content-type x-event-id x-event-type,v1=a1b2c3d4e5f6...
Content-Length: 512
```

## Security best practices

<AccordionGroup>
  <Accordion title="Store secrets securely">
    Never hardcode webhook secrets in your code. Use environment variables or a secure secrets manager:

    ```javascript theme={null}
    // ✅ Good - using environment variables
    const secret = process.env.WEBHOOK_SECRET;

    // ❌ Bad - hardcoded secret
    const secret = "whsec_abc123...";
    ```
  </Accordion>

  <Accordion title="Use HTTPS only">
    Always use HTTPS endpoints for your webhooks. HTTP endpoints expose your webhook data to interception and tampering.
  </Accordion>

  <Accordion title="Implement rate limiting">
    Add rate limiting to your webhook endpoint to prevent abuse:

    ```javascript theme={null}
    const rateLimit = require('express-rate-limit');

    const webhookLimiter = rateLimit({
      windowMs: 1 * 60 * 1000, // 1 minute
      max: 100 // limit each IP to 100 requests per minute
    });

    app.post('/webhook', webhookLimiter, (req, res) => {
      // Your webhook handler
    });
    ```
  </Accordion>

  <Accordion title="Validate timestamp window">
    The default 5-minute window prevents replay attacks. Adjust based on your needs, but don't make it too large:

    ```javascript theme={null}
    // Default 5 minutes is recommended
    verifyWebhookSignature(payload, signature, secret, headers, 5);

    // For high-security applications, use a shorter window
    verifyWebhookSignature(payload, signature, secret, headers, 1);
    ```
  </Accordion>

  <Accordion title="Log verification failures">
    Track failed verification attempts to detect potential security issues:

    ```javascript theme={null}
    if (!verifyWebhookSignature(payload, signature, secret, headers)) {
      console.error('Webhook verification failed', {
        timestamp: new Date().toISOString(),
        ip: req.ip,
        signature: signature,
        // Don't log the payload as it may contain sensitive data
      });
      res.status(400).send("Invalid signature");
      return;
    }
    ```
  </Accordion>
</AccordionGroup>

## Error handling

Handle common verification failures gracefully:

```javascript title="error-handling.js" theme={null}
app.post('/webhook', (req, res) => {
  const payload = req.body.toString();
  const signature = req.headers['x-hook0-signature'];
  const secret = process.env.WEBHOOK_SECRET;

  // Check for missing signature
  if (!signature) {
    console.error('Missing X-Hook0-Signature header');
    return res.status(400).send('Missing signature');
  }

  // Check for missing secret
  if (!secret) {
    console.error('Webhook secret not configured');
    return res.status(500).send('Server configuration error');
  }

  try {
    const isValid = verifyWebhookSignature(payload, signature, secret, req.headers);

    if (!isValid) {
      console.error('Invalid webhook signature');
      return res.status(400).send('Invalid signature');
    }

    // Process the webhook
    const event = JSON.parse(payload);

    // Handle specific event types
    if (event.type === 'onchain.activity.detected') {
      // Process transaction event
      console.log('Transaction detected:', event.data.transactionHash);
    }

    res.status(200).send('OK');

  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Processing error');
  }
});
```

## What to read next

* **[Quickstart](/webhooks/quickstart)**: Set up your first webhook subscription.
* **[Webhooks overview](/webhooks/overview)**: All webhook surfaces and shared concepts.
