Papers Verifying Signed Webhooks
Verifying Signed Webhooks
How to verify that a payment event really came from U.CASH Pay: the signature header, the HMAC, and the replay guard. Re-authored from the canonical merchant doc.
The request
When a payment is confirmed, U.CASH Pay sends an HTTP POST to your webhook URL (configured in Settings, then Webhook). The body is JSON carrying the transaction object and an event_id, a 32-character hex idempotency key. Two headers matter: X-Webhook-Signature and X-Webhook-Event-Id, which repeats the body event_id.
POST <your webhook URL>
Content-Type: application/json
X-Webhook-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
X-Webhook-Event-Id: <32-char hex>
{
"transaction": { "id": "...", "status": "C", "...": "..." },
"event_id": "<32-char hex idempotency key>"
}
The signature
The signature is an HMAC-SHA256 of the string "<t>.<raw-body>", keyed with your webhook secret key from Settings, then Webhook. Verify in three steps: parse the t and v1 components from the header; reject anything whose timestamp is more than five minutes stale (replay protection); recompute the HMAC over the RAW request body, never a re-encoded copy, and compare in constant time.
<?php
$signature_header = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input'); // MUST be the raw body
$t = null; $v1 = null;
foreach (explode(',', $signature_header) as $component) {
[$k, $v] = explode('=', $component, 2) + [null, null];
if ($k === 't') { $t = $v; }
if ($k === 'v1') { $v1 = $v; }
}
if ($t === null || $v1 === null) { http_response_code(401); exit('Missing signature'); }
// Replay protection: reject anything older than 5 minutes.
if (abs(time() - (int) $t) > 300) { http_response_code(401); exit('Stale timestamp'); }
// Recompute and compare in constant time.
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
if (!hash_equals($expected, $v1)) { http_response_code(401); exit('Invalid signature'); }
// Verified: fulfill against the event_id, idempotently.
http_response_code(200);
Idempotency
Deliveries can be retried. Treat the event_id as the idempotency key: fulfill each event exactly once, and answer 200 once done. Retries that arrive after fulfillment should be acknowledged, not re-processed.
The canonical doc
This paper is a condensate. The canonical, always-current version, including key rotation, lives in the merchant documentation.
This paper condenses a canonical source. When details drift, the upstream wins: pay.u.cash/docs/webhook-signing.