Verify webhook signatures

Verify every incoming webhook before processing its payload. The API token used to manage subscriptions is not the signing secret: use the subscription’s secret.

Headers and signature

Deliveries are HTTP POST requests with JSON content, X-DocuWriter-Event, and X-DocuWriter-Signature. The signature value has the form sha256=<hex digest>. Compute HMAC-SHA256 over the exact raw request body using the subscription secret, then compare with a constant-time comparison.

<?php
$rawBody = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_DOCUWRITER_SIGNATURE'] ?? '';
$secret = getenv('DOCUWRITER_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}
$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
// Persist or enqueue the verified event before acknowledging it.
http_response_code(204);

Keep the raw bytes available: decoding and re-encoding JSON before verification can change whitespace or escaping and break the signature. Reject requests with missing/invalid signatures. Only accept event types your integration handles.

Receiver behavior

Use a publicly reachable HTTPS endpoint, respond promptly with a 2xx status after accepting the event, and process longer work asynchronously. The delivery timeout is 30 seconds. Design for duplicate events and reconciliation with the API; do not treat delivery as exactly-once or guaranteed.

The signed payload contains a timestamp, but the signature header has no separate timestamp/replay-window format. Your receiver can enforce its own freshness policy using the verified payload timestamp and record processed events. Do not use webhook_id alone as a unique delivery ID: it identifies the subscription.

See Webhook management for secret provisioning and Event payloads for the envelope.

Updated