When UPayments sends real-time transaction updates to your notificationUrl (webhook), an HMAC signature is included in the request headers. Validating this signature on your server ensures that the webhook originated from UPayments and that the payload was not tampered with during transit.
How Webhook Validation Works
To verify an incoming webhook, follow the exact same steps used for generating outbound request signatures:
- Extract Headers & Body: Retrieve the
X-Timestamp(timestamp in UTC seconds) andX-Signatureheaders, along with the raw, unparsed JSON request body sent by UPayments. - Reconstruct the Payload String: Build the string-to-sign using the timestamp, HTTP method (
POST), webhook URL, and raw request body. - Generate the Expected Signature: Calculate the HMAC SHA-256 hash using your API Secret Key and Base64-encode the result.
- Compare Signatures: Compare your generated signature against the
X-Signatureheader value received in the request. If they match, the request is authentic.
Key Difference: Full URL vs. Relative PathWhen generating signatures for outbound API requests, the payload uses only the relative endpoint path following
/api/v1/(e.g.,charge).However, for incoming Webhook signature validation, the payload string must contain the full notification URL starting with
https://orhttp://.
Webhook Payload Format
timestamp + HTTP_METHOD + FULL_NOTIFICATION_URL + RAW_REQUEST_BODY
timestamp: The value from theX-Timestampheader.HTTP_METHOD: AlwaysPOSTfor webhooks.FULL_NOTIFICATION_URL: The complete, absolute webhook URL configured in your request (e.g.,https://yourdomain.com/api/v1/webhook).RAW_REQUEST_BODY: The exact raw JSON body received in the HTTP request.
Webhook Verification Examples
const crypto = require('crypto');
function verifyWebhookSignature(req, apiSecret) {
const receivedSignature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
const httpMethod = 'POST';
// Use the full URL for webhooks (e.g., https://yourdomain.com/webhook)
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
const rawBody = JSON.stringify(req.body); // Ensure raw unformatted string
// Construct payload
const payload = timestamp + httpMethod + fullUrl + rawBody;
// Calculate HMAC SHA-256
const expectedSignature = crypto
.createHmac('sha256', apiSecret)
.update(payload)
.digest('base64');
return receivedSignature === expectedSignature;
}const crypto = require('crypto');
/**
* Validates the HMAC signature of an incoming UPayments webhook request.
* @param {Object} req - Express request object.
* @param {string} apiSecret - Your Merchant API Secret Key.
* @returns {boolean} True if the signature is authentic.
*/
function verifyWebhookSignature(req, apiSecret) {
const receivedSignature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
const httpMethod = req.method.toUpperCase(); // 'POST'
// Full notification URL (e.g., https://yourdomain.com/api/v1/webhook)
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
// Raw unparsed JSON string
const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
// Payload: timestamp + HTTP_METHOD + FULL_NOTIFICATION_URL + RAW_REQUEST_BODY
const payload = timestamp + httpMethod + fullUrl + rawBody;
// Calculate HMAC SHA-256 Base64 signature
const expectedSignature = crypto
.createHmac('sha256', apiSecret)
.update(payload)
.digest('base64');
// Secure constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSignature || ''),
Buffer.from(expectedSignature)
);
}<?php
/**
* Validates the HMAC signature of an incoming UPayments webhook request.
*
* @param string $apiSecret Your Merchant API Secret Key.
* @return bool True if the signature is authentic.
*/
function verifyWebhookSignature($apiSecret) {
// Retrieve HTTP headers
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedSignature = $headers['x-signature'] ?? '';
$timestamp = $headers['x-timestamp'] ?? '';
$httpMethod = $_SERVER['REQUEST_METHOD']; // 'POST'
// Reconstruct full notification URL
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$fullUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
// Read raw input body from stream
$rawBody = file_get_contents('php://input');
// Payload: timestamp + HTTP_METHOD + FULL_NOTIFICATION_URL + RAW_REQUEST_BODY
$payload = $timestamp . $httpMethod . $fullUrl . $rawBody;
// Calculate HMAC SHA-256 and Base64 encode
$hash = hash_hmac('sha256', $payload, $apiSecret, true);
$expectedSignature = base64_encode($hash);
// Timing-attack safe comparison
return hash_equals($expectedSignature, $receivedSignature);
}import hmac
import hashlib
import base64
def verify_webhook_signature(headers, full_url, raw_body_bytes_or_str, api_secret, http_method="POST"):
"""
Validates the HMAC signature of an incoming UPayments webhook request.
:param headers: Request headers dict (e.g. request.headers)
:param full_url: Full notification URL (e.g. 'https://yourdomain.com/webhook')
:param raw_body_bytes_or_str: Raw unparsed HTTP request body
:param api_secret: Merchant API Secret Key
:param http_method: HTTP Method ('POST')
:return: bool indicating signature validity
"""
received_signature = headers.get('X-Signature') or headers.get('x-signature', '')
timestamp = headers.get('X-Timestamp') or headers.get('x-timestamp', '')
if isinstance(raw_body_bytes_or_str, bytes):
raw_body_str = raw_body_bytes_or_str.decode('utf-8')
else:
raw_body_str = str(raw_body_bytes_or_str)
# Payload: timestamp + HTTP_METHOD + FULL_NOTIFICATION_URL + RAW_REQUEST_BODY
payload = f"{timestamp}{http_method.upper()}{full_url}{raw_body_str}"
# Calculate HMAC SHA-256 and Base64 encode
signature_hash = hmac.new(
api_secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature = base64.b64encode(signature_hash).decode('utf-8')
# Timing-attack safe comparison
return hmac.compare_digest(expected_signature, received_signature)