Skip to main content

Webhook signature verification

The payment notification arrives at an address only your store and our service know. But the address can leak—from logs, from configuration, from development history. To tell a genuine notification from a forgery, every request is signed with the webhook secret.

Verify the signature before fulfilling the order or changing its status.

Signature headers

HeaderValue
X-TimestampSend time, unix time in seconds
X-SignatureThe sha256= prefix and the HMAC-SHA256 in hex

The signed string is the send time, a dot, and the request body:

1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}

The signing key is the Webhook secret field in the project settings. You can reissue it there if the value is compromised.

Verification steps

  1. Take the raw request body, before parsing the JSON.
  2. Concatenate the X-Timestamp value, a dot, and the body.
  3. Compute the HMAC-SHA256 with the webhook secret.
  4. Compare the result with X-Signature using a constant-time comparison.
  5. Reject the request if X-Timestamp deviates too far from the current time.

Implementation examples

<?php
$secret = 'webhook secret from your project settings';

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit;
}

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

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);

Things to keep in mind

The raw body is signed. If you parse the JSON and rebuild it, the key order and spacing change—the signature will not match. Compute the HMAC before parsing.

Every retry has its own signature. The send time is new on each retry, so the signature differs too. Do not store a signature to compare against future requests.

The secret can be reissued. The old secret stops working immediately after reissue, so update the value in your store at the same time.

Signature verification does not replace idempotency. The signature proves the request is authentic, not that you are seeing it for the first time. A retry arrives with a valid signature—check against invoice.id.