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

# Signature Verification

> How to verify that an incoming webhook request was sent by Virtuous and was not modified in transit — the security foundation for every webhook receiver.

A webhook receiver must verify that every incoming request was actually sent by Virtuous and was not tampered with in transit. Without verification, anyone who discovers your `payloadUrl` could send fake events to it, potentially driving incorrect behavior in your integration — fraudulent gift records, unauthorized contact updates, manipulated reporting data.

CRM+ webhook deliveries carry a signature derived from the request body and your subscription `secret`. Verifying the signature on every incoming request is the security foundation for every production webhook receiver.

<Warning>
  The exact signature mechanism used by Virtuous webhooks (header name, signing algorithm, signed string format) is **not documented** in the CRM+ OpenAPI spec. The patterns described on this page reflect the industry-standard approach (HMAC-SHA256 over the request body, signature delivered in a request header) and the conservative implementation a partner integration should adopt. Before going to production, confirm the exact signature scheme with Virtuous engineering and adjust your verifier accordingly.
</Warning>

## What signature verification proves

A valid signature on a webhook delivery proves two things:

* **Authenticity.** Only a party with the subscription's `secret` could have generated the signature. Since the secret is known only to your integration and Virtuous, a valid signature confirms the request originated from Virtuous.
* **Integrity.** The signature is computed over the request body. Any modification of the body in transit invalidates the signature.

What signature verification does **not** prove:

* **Freshness.** A captured-and-replayed request has a valid signature. Use a delivery timestamp and a short window (e.g., reject events older than five minutes) to defend against replay attacks. Combined with idempotency on event IDs, this is sufficient for most production receivers.
* **Authorization.** The signature only proves the request is genuine. Whether the event represents something your integration should act on is a separate concern — handled by your business logic, not your verifier.

***

## The verification pattern

The industry-standard pattern (which CRM+ is presumed to follow until confirmed otherwise):

1. Virtuous computes `signature = HMAC-SHA256(secret, request_body)` at delivery time.
2. The signature is delivered in a request header (typically named something like `X-Virtuous-Signature`).
3. Your receiver reads the **raw** request body (before JSON parsing), computes the same HMAC using your stored secret, and compares the result to the signature header in **constant time**.
4. If the signatures match, the request is accepted. If not, the request is rejected with `401 Unauthorized`.

The two non-obvious requirements:

* **Use the raw body.** Signature verification operates on the exact bytes Virtuous sent. JSON-parsing-then-re-serializing changes whitespace, key ordering, and number formatting — all of which invalidate the signature.
* **Compare in constant time.** A naive string comparison reveals timing information that lets an attacker iterate toward a valid signature. Use a constant-time comparison primitive from your language's crypto library.

***

## Verification code

The example below shows the canonical verifier shape. Adjust the `SIGNATURE_HEADER` constant and the body-to-sign format once Virtuous confirms its scheme.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import crypto from 'crypto';

  // The exact header name Virtuous uses is not yet confirmed.
  // Update once published.
  const SIGNATURE_HEADER = 'x-virtuous-signature';

  export function verifyVirtuousSignature(rawBody, headers, secret) {
    const receivedSignature = headers[SIGNATURE_HEADER];
    if (!receivedSignature) {
      return false;
    }

    // Compute the expected signature over the raw body.
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    // Constant-time comparison to defend against timing attacks.
    const expectedBuffer = Buffer.from(expectedSignature, 'hex');
    const receivedBuffer = Buffer.from(receivedSignature, 'hex');

    if (expectedBuffer.length !== receivedBuffer.length) {
      return false;
    }

    return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
  }
  ```

  ```javascript Express Receiver theme={null}
  import express from 'express';
  import { verifyVirtuousSignature } from './signature-verification.js';

  const app = express();

  app.post(
    '/virtuous/webhook',
    // Capture the raw body — required for signature verification.
    // Do NOT use express.json() here; it consumes the body before verification.
    express.raw({ type: 'application/json' }),
    async (req, res) => {
      const secret = process.env.VIRTUOUS_WEBHOOK_SECRET;
      const valid = verifyVirtuousSignature(req.body, req.headers, secret);

      if (!valid) {
        console.warn('Webhook signature verification failed', {
          ip: req.ip,
          userAgent: req.headers['user-agent'],
        });
        return res.status(401).send('Invalid signature');
      }

      // Body is the raw Buffer — parse it now that the signature is verified.
      const event = JSON.parse(req.body.toString('utf8'));

      // Acknowledge immediately, then queue for async processing.
      res.status(200).send('OK');
      await enqueueForProcessing(event);
    }
  );

  app.listen(3000);
  ```
</CodeGroup>

The two implementation details worth calling out:

* **`express.raw({ type: 'application/json' })`** captures the request body as a `Buffer` instead of parsing it as JSON. Standard `express.json()` middleware parses and discards the raw bytes, breaking signature verification. Use `raw` for the webhook route only.
* **`crypto.timingSafeEqual`** compares two equal-length Buffers in constant time. Standard `===` comparison short-circuits on the first differing byte, leaking timing information.

***

## Handling signature failures

When signature verification fails, the safe response is to reject the request with `401 Unauthorized` and log enough context to investigate.

```javascript JavaScript theme={null}
if (!valid) {
  console.warn('Webhook signature verification failed', {
    ip: req.ip,
    userAgent: req.headers['user-agent'],
    timestamp: new Date().toISOString(),
    // Do NOT log the raw body or the secret — both could leak sensitive data.
  });
  return res.status(401).send('Invalid signature');
}
```

A small number of legitimate signature failures are expected during operation:

* **Secret rotation in flight.** During a secret rotation, deliveries in transit may still be signed with the old secret. Accept both old and new secrets during the rotation window. See [Webhooks Overview](/crm/webhooks/overview#rotating-the-secret).
* **Body modification by an intermediate proxy.** Some load balancers and WAFs modify request bodies (re-encoding, header normalization). If you see consistent signature failures from a specific source, suspect a proxy. Configure the proxy to pass the body through unmodified.

Sustained signature failures from non-rotation sources are a strong signal of an attack or misconfiguration. Set up alerting on `401` responses from your webhook endpoint.

***

## Replay protection

Even with a valid signature, a captured webhook delivery can be replayed by anyone who recorded it. Two complementary defenses:

### 1. Timestamp window

If the webhook payload includes a delivery timestamp, reject events older than a short window (typically five minutes). The timestamp itself must be signed — otherwise an attacker can forward an old event with a new timestamp.

```javascript JavaScript theme={null}
const MAX_AGE_SECONDS = 5 * 60; // 5 minutes

function isWithinFreshnessWindow(event) {
  if (!event.timestamp) return false;
  const eventTime = new Date(event.timestamp).getTime();
  const ageSeconds = (Date.now() - eventTime) / 1000;
  return ageSeconds < MAX_AGE_SECONDS;
}
```

<Note>
  Whether Virtuous includes a signed timestamp in its delivery is not confirmed. If the payload's `timestamp` field is **inside** the signed body, the replay-window check is meaningful. If the timestamp is in an unsigned header, an attacker can rewrite it.
</Note>

### 2. Event ID idempotency

Even without a timestamp, you can defend against replay by tracking the event IDs you have already processed. Reject a second delivery of an event ID you have already seen.

```javascript JavaScript theme={null}
async function processWithIdempotency(event) {
  const alreadyProcessed = await idempotencyStore.has(event.eventId);
  if (alreadyProcessed) {
    return; // Already handled — drop the duplicate.
  }
  await idempotencyStore.set(event.eventId, { processedAt: new Date() });
  await processVirtuousEvent(event);
}
```

The idempotency store can be Redis, a database table, or any persistent key-value store. Keep records for at least the maximum replay window you want to defend against — 24 hours is a reasonable default.

See [Idempotency and Safe Reprocessing](/crm/webhooks/idempotency-and-safe-reprocessing) for the full pattern, including the retry-induced duplicates that arrive even from legitimate Virtuous deliveries.

***

## Testing your verifier

Two patterns for testing signature verification logic without depending on live Virtuous deliveries:

### Self-signed test fixtures

Generate test payloads in your own test suite by computing the signature with a known secret. This validates your verifier's logic against payloads you control:

```javascript JavaScript theme={null}
import crypto from 'crypto';

function generateTestSignature(body, secret) {
  return crypto.createHmac('sha256', secret).update(body).digest('hex');
}

// In a test
const testBody = Buffer.from(JSON.stringify({ eventType: 'contact.created' }));
const testSecret = 'test-secret';
const testHeaders = {
  'x-virtuous-signature': generateTestSignature(testBody, testSecret),
};

expect(verifyVirtuousSignature(testBody, testHeaders, testSecret)).toBe(true);
expect(verifyVirtuousSignature(testBody, testHeaders, 'wrong-secret')).toBe(false);
```

### Live replay against the Seeded Sandbox

Subscribe a real webhook against your [Seeded Sandbox](/crm/base-urls#seeded-sandbox), capture a few real deliveries, save them as fixtures, and run your verifier against them. This validates that your code agrees with what Virtuous actually sends — not just with your test-generated approximation.

See [Local Testing](/crm/webhooks/local-testing) for the full local-development workflow.

***

## Security checklist

Before deploying a webhook receiver to production, confirm:

* Signature verification runs on **every** request before any processing.
* The raw request body is used for signature computation (not the parsed JSON).
* Signature comparison uses a constant-time primitive (`crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python).
* The shared secret is loaded from a secrets manager — never hardcoded.
* Failed signature attempts are logged with enough context to investigate, **but** the raw body, the secret, and the signature value are **not** logged.
* The endpoint is HTTPS-only with a valid public certificate.
* Replay protection is in place: timestamp window, event ID idempotency, or both.
* Secret rotation is supported: your verifier can accept multiple valid secrets simultaneously during a rotation window.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Idempotency and Safe Reprocessing" icon="copy" href="/crm/webhooks/idempotency-and-safe-reprocessing">
    How to make your handler safe against the duplicate deliveries that signature verification cannot prevent.
  </Card>

  <Card title="Retry Behavior" icon="arrows-rotate" href="/crm/webhooks/retry-behavior">
    What Virtuous does when verification or processing fails — and how that interacts with your replay defenses.
  </Card>

  <Card title="Local Testing" icon="flask" href="/crm/webhooks/local-testing">
    Set up a local environment that receives signed webhook deliveries for verification testing.
  </Card>

  <Card title="Webhooks Overview" icon="bell" href="/crm/webhooks/overview">
    The subscription model, including how the shared secret is stored and rotated.
  </Card>
</CardGroup>
