Skip to main content

Webhook Security

Every webhook request from ConsentForge includes an HMAC-SHA256 signature. Verify this signature before processing the payload to ensure the request is genuine and hasn't been tampered with.

Request headers

HeaderDescription
X-ConsentForge-SignatureHMAC-SHA256 hex digest of {timestamp}.{body}
X-ConsentForge-TimestampUnix timestamp (seconds) when the event was sent
X-ConsentForge-Delivery-IDUnique ID for this delivery (use for idempotency)

Verification algorithm

  1. Read X-ConsentForge-Timestamp from the request header
  2. Read X-ConsentForge-Signature from the request header
  3. Build the signing string: {timestamp}.{raw_request_body}
  4. Compute HMAC-SHA256 of the signing string using your webhook secret
  5. Compare (timing-safe) with the received signature
  6. Reject if timestamp is more than 5 minutes old (replay protection)

Code examples

function verifyConsentForgeWebhook(
string $rawBody,
string $signature,
string $timestamp,
string $secret
): bool {
// Reject if too old (5 minutes)
if (abs(time() - (int)$timestamp) > 300) {
return false;
}

$signingString = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signingString, $secret);

return hash_equals($expected, $signature);
}

// Usage in a Laravel controller:
$rawBody = $request->getContent();
$signature = $request->header('X-ConsentForge-Signature');
$timestamp = $request->header('X-ConsentForge-Timestamp');
$secret = config('services.consentforge.webhook_secret');

if (!verifyConsentForgeWebhook($rawBody, $signature, $timestamp, $secret)) {
return response('Unauthorized', 401);
}

$payload = $request->json()->all();

Idempotency

Use X-ConsentForge-Delivery-ID to deduplicate retried deliveries. Store processed delivery IDs and skip duplicates.

Secret rotation

To rotate your webhook secret:

  1. Generate a new secret in the Dashboard (old secret still active for 24h)
  2. Update your server to use the new secret
  3. After 24h, the old secret is invalidated