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

# Security

> Make sure you only accept trusted webhooks

## Introduction

This guide explains how to validate webhook payloads from Quartr by verifying their cryptographic signatures. We **strongly recommend** that all consumers implement signature verification to prevent unauthorized or tampered webhook events. In addition to verifying the payload’s signature, you should also validate the timestamp to mitigate replay attacks.

## How our webhook signatures work

Every webhook event sent by Quartr includes a cryptographic signature in the Webhook-Signature header. Signatures are computed over three elements:

`webhook-id.webhook-timestamp.payload`

* `webhook-id`: A unique message identifier (not user-controlled).
* `webhook-timestamp`: The Unix timestamp (in seconds) at which the message was generated.
* `payload`: The raw request body (minified JSON).

For example, for the following webhook:

```http theme={null}
HTTP/1.1 204 No Content
Content-Type: application/json
Webhook-Id: msg_2uU6k60RnPzWIUeqUjueBJOboBl
Webhook-Timestamp: 1742290945
Webhook-Signature: v1,h6YyrYs32RDl7KWxtQsv7GNw+f5enUNSmvjT6GKbeYM=

{
  "timestamp": "2025-03-18T09:42:22.000Z",
  "type": "document.report.created",
  "data": {
    "id": 1871575,
    "typeId": 10,
    "eventId": 87664,
    "createdAt": "2025-03-18T09:42:22",
    "updatedAt": "2025-03-18T09:42:22",
    "fileUrl": "https://files.quartr.com/reports/abc.pdf",
    "companyId": 14960
  },
  "previousAttributes": {}
}
```

The string you must sign is

```plaintext theme={null}
msg_2uU6k60RnPzWIUeqUjueBJOboBl.1742290945.{"timestamp":"2025-03-18T09:42:22.000Z","type":"document.report.created","data":{"id":1871575,"typeId":10,"eventId":87664,"createdAt":"2025-03-18T09:42:22","updatedAt":"2025-03-18T09:42:22","fileUrl":"https://files.quartr.com/reports/abc.pdf","companyId":14960},"previousAttributes":{}}
```

<Info>
  Multiple signatures can appear when a user rotates their secrets. See Rotating
  webhook secrets below.
</Info>

## Rotating webhook secrets

If you rotate your webhook secret via the portal, we will start signing requests with both the old and new secret for 24 hours to allow a seamless transition. During that window:

* The Webhook-Signature header will include multiple signatures (e.g., `v1,<new_signature> v1,<old_signature>`).
* You can verify against both secrets. Once you find a match, you can trust the request.

After the rotation window (24 hours), we drop the old secret from the payload entirely. At that point, only the new secret is used, and the old secret should be removed from your verification logic.

## Verifying webhook signatures

<Steps>
  <Step title="Parse the headers and body">
    Extract the following from the incoming request:

    * `Webhook-Id`
    * `Webhook-Timestamp`
    * `Webhook-Signature`
    * The raw request body (minified JSON)
  </Step>

  <Step title="Validate the timestamp (optional)">
    Compare the webhook-timestamp header to the current time. If it is too old (e.g., more than 5 minutes or 10 minutes), you should reject it to protect against replay attacks. For example:

    ```javascript js theme={null}
    const FIVE_MINUTES = 5 * 60; // 5 minutes
    const nowInSeconds = Math.floor(Date.now() / 1000);
    if (nowInSeconds - parseInt(webhookTimestamp, 10) > FIVE_MINUTES) {
      // The timestamp is too old; reject the request.
      throw new Error('Webhook timestamp is too old');
    }
    ```
  </Step>

  <Step title="Verify the HMAC signature">
    1. Split the Webhook-Signature header by spaces, because there may be multiple signatures following a secret rotation. For example, `v1,h6Yyyr... v1,XYZ...`

    2. Construct the signed payload:

    ```js js theme={null}
    const signedPayload = `${webhookId}.${webhookTimestamp}.${payload}`;
    ```

    3. Compute the HMAC-SHA256 of `signedPayload` using your secret. When using the secret one should remove the whsec\_ prefix and decode the base64 string. For example:

    ```javascript js theme={null}
    const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
    const signature = crypto
      .createHmac('sha256', secretBytes)
      .update(signedPayload)
      .digest('base64');
    ```

    4. Compare the result with all the available signatures in the Webhook-Signature header. If any of the available versions match, the payload is authentic and can be trusted.

    ```javascript js theme={null}
    const signatures = webhookSignature.replaceAll('v1,', '').split(' ');
    const isValid = signatures.some((sig) => {
      return sig === signature;
    });
    ```
  </Step>
</Steps>

<Warning>
  Any modification of the payload, webhook-id, or webhook-timestamp will break
  the signature validation.
</Warning>

### Examples

Here's an example of how to verify webhook signatures in Node.js:

<CodeGroup>
  ```javascript js [expandable] theme={null}
  const crypto = require('node:crypto');

  const FIVE_MINUTES = 5 * 60; // 5 minutes
  const nowInSeconds = Math.floor(Date.now() / 1000);
  if (nowInSeconds - parseInt(webhookTimestamp, 10) > FIVE_MINUTES) {
    // The timestamp is too old; reject the request.
    throw new Error('Webhook timestamp is too old');
  }

  const signedPayload = `${webhookId}.${webhookTimestamp}.${payload}`;

  const secretBytes = Buffer.from(secret.split('_')[1], 'base64');

  const signature = crypto
    .createHmac('sha256', secretBytes)
    .update(signedPayload)
    .digest('base64');

  const signatures = webhookSignature.replaceAll('v1,', '').split(' ');

  const isValid = signatures.some((sig) => {
    return sig === signature;
  });

  if (isValid) {
    console.log('Signature is valid');
  } else {
    console.log('Signature is invalid');
  }
  ```
</CodeGroup>
