Skip to content
bitzorcas
EN

Reference

Webhook signing

HMAC-SHA256 webhook request signing — signature generation, timestamp validation, subscriber verification guide, and secret rotation.

Last updated

BitzOrcas signs every webhook delivery with HMAC-SHA256 to ensure payload integrity and authenticity. Subscribers verify signatures to confirm payloads originated from BitzOrcas.

Signature generation

BitzOrcas generates signatures using:

signature = HMAC-SHA256(secret, payload + timestamp)

Request headers

Every webhook delivery includes:

HeaderExampleDescription
X-Webhook-Signaturesha256=abc123...HMAC-SHA256 hex digest
X-Webhook-Timestamp2026-06-22T10:00:00ZISO 8601 UTC timestamp
X-Webhook-Eventticket.createdEvent type
X-Webhook-Delivery-Iddel-789Unique delivery identifier
X-Webhook-Subscription-Idsub-456Subscription identifier

WebhookSignature utility

public static class WebhookSignature
{
public static string Compute(
string secret, // Subscription's signing secret
string payload, // JSON body
DateTimeOffset timestamp)
{
var message = $"{payload}{timestamp:O}";
var key = Encoding.UTF8.GetBytes(secret);
var data = Encoding.UTF8.GetBytes(message);
using var hmac = new HMACSHA256(key);
var hash = hmac.ComputeHash(data);
return $"sha256={Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

Subscriber verification guide

Subscribers should:

  1. Extract headers: Read X-Webhook-Signature, X-Webhook-Timestamp, and the raw request body
  2. Validate timestamp: Reject requests older than 5 minutes
  3. Compute expected signature: Using the same secret and algorithm
  4. Compare signatures: Constant-time comparison to prevent timing attacks
// Subscriber-side verification
var timestamp = DateTimeOffset.Parse(request.Headers["X-Webhook-Timestamp"]);
if (DateTimeOffset.UtcNow - timestamp > TimeSpan.FromMinutes(5))
return StatusCode(400); // Stale request
var expectedSig = WebhookSignature.Compute(subscriptionSecret, rawBody, timestamp);
var actualSig = request.Headers["X-Webhook-Signature"];
if (!ConstantTimeEquals(expectedSig, actualSig))
return StatusCode(401); // Invalid signature

Secret rotation

Subscriptions support secret rotation without downtime:

POST /api/webhooks/subscriptions/{id}/rotate-secret

This generates a new secret while keeping the old one valid for a transition period. Update your verification code to use the new secret.

Security best practices

  • Constant-time comparison: Use CryptographicOperations.FixedTimeEquals to prevent timing attacks
  • Timestamp validation: Reject stale requests (5-minute window)
  • HTTPS only: Always use HTTPS for webhook callback URLs
  • Secret storage: Store secrets in a secure vault (not in code or config files)
  • IP filtering: Configure IWebhookIpAllowlistPolicy to restrict source IPs

See also