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.
What signature verification proves
A valid signature on a webhook delivery proves two things:- Authenticity. Only a party with the subscription’s
secretcould 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.
- 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):- Virtuous computes
signature = HMAC-SHA256(secret, request_body)at delivery time. - The signature is delivered in a request header (typically named something like
X-Virtuous-Signature). - 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.
- If the signatures match, the request is accepted. If not, the request is rejected with
401 Unauthorized.
- 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 theSIGNATURE_HEADER constant and the body-to-sign format once Virtuous confirms its scheme.
express.raw({ type: 'application/json' })captures the request body as aBufferinstead of parsing it as JSON. Standardexpress.json()middleware parses and discards the raw bytes, breaking signature verification. Userawfor the webhook route only.crypto.timingSafeEqualcompares 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 with401 Unauthorized and log enough context to investigate.
JavaScript
- 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.
- 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.
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
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.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
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
Live replay against the Seeded Sandbox
Subscribe a real webhook against your 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 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.timingSafeEqualin Node.js,hmac.compare_digestin 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
Idempotency and Safe Reprocessing
How to make your handler safe against the duplicate deliveries that signature verification cannot prevent.
Retry Behavior
What Virtuous does when verification or processing fails — and how that interacts with your replay defenses.
Local Testing
Set up a local environment that receives signed webhook deliveries for verification testing.
Webhooks Overview
The subscription model, including how the shared secret is stored and rotated.