Skip to main content
Print

Every webhook request is signed so you can confirm it came from Zencommerce and was not altered. The signature is in X-Webhook-Signature.

Values you need

Variable Source
$webhookSecret Returned once when you create (or rotate) the subscription
$appSecret Your partner Webhook App Secret, issued by Zencommerce during partner onboarding
$shopHash X-Shop-Hash header
$webhookId X-Webhook-Id header
$rawBody Raw request body bytes (do not decode and re-encode before verifying)

Per shop secret

$shopAwareSecret = hash_hmac(
    'sha512',
    $shopHash . ':' . $webhookSecret,
    $appSecret
);

Request signature

$expectedSignature = sha1(
$webhookId . ':' . $shopAwareSecret . ':' . $rawBody
);

Compare with hash_equals($expectedSignature, $receivedSignature).

PHP example

<?php

function verifyZencommerceWebhook(
    string $rawBody,
    array $headers,
    string $webhookSecret,
    string $appSecret
): bool {
    $shopHash = $headers['X-Shop-Hash']
        ?? $headers['x-shop-hash']
        ?? '';
    $webhookId = $headers['X-Webhook-Id']
        ?? $headers['x-webhook-id']
        ?? '';
    $received = $headers['X-Webhook-Signature']
        ?? $headers['x-webhook-signature']
        ?? '';

    if ($shopHash === '' || $webhookId === '' || $received === '') {
        return false;
    }

    $shopAwareSecret = hash_hmac(
        'sha512',
        $shopHash . ':' . $webhookSecret,
        $appSecret
    );

    $expected = sha1($webhookId . ':' . $shopAwareSecret . ':' . $rawBody);

    return hash_equals($expected, $received);
}

Always use a constant-time comparison (hash_equals or equivalent), not ===.

Fallback mode

If a partner Webhook App Secret is not configured for the environment, deliveries may use:

X-Webhook-Signature = HMAC-SHA256(rawBody, webhookSecret)

Production partner integrations should use the primary (shop-aware) signing mode with a provisioned Webhook App Secret. The app secret is never included in webhook payloads.

Implementation tips

  • Use the lowercase hex string returned by hash_hmac in the SHA-1 construction (not raw binary).
  • Normalize header names (HTTP headers are case-insensitive).
  • Look up the subscription secret from your own secure store using X-Webhook-Id, then verify.
  • Verify against the unmodified raw body before parsing JSON.
  • Configure one expected signing mode — do not fall back automatically after a failed primary check.
  • Signature verification alone does not prevent replay; design idempotent processing.
Scroll to Top