Appearance
Webhook Security
This section explains how to secure and validate webhook notifications sent by HUCH.
Webhook security is critical to ensure that:
- Requests originate from HUCH
- Payloads are not tampered with
- Duplicate or malicious requests are rejected
Endpoint Requirements
Your webhook endpoint must meet the following requirements:
- Must be publicly accessible over HTTPS
- Must accept POST requests
- Must accept
application/jsonpayloads - Must respond quickly with HTTP
200 OK - Must not require authentication redirects
If your endpoint does not return a successful HTTP response, the notification may be retried.
Signature Verification (HMAC-SHA256)
To ensure that webhook requests originate from HUCH and have not been modified, you must verify the request signature.
Each webhook notification is sent with the following HTTP headers:
| Header | Description |
|---|---|
Content-Type | Always application/json. |
X-Timestamp | Unix timestamp (seconds) of when the notification was generated. |
X-Signature | Legacy (v1) signature. Deprecated — kept only for backward compatibility. |
X-Signature-V2 | Recommended (v2) signature. Use this one for all new integrations. |
Both signatures are an HMAC-SHA256 hex digest computed over the raw JSON request body, exactly as sent. They differ only in the secret key used:
| Header | Version | Secret key used | Status |
|---|---|---|---|
X-Signature | v1 | your merchant_id | ⚠️ Deprecated — do not use for new integrations |
X-Signature-V2 | v2 | your client_id | ✅ Recommended |
IMPORTANT
Always verify X-Signature-V2. The v1 X-Signature is keyed by your merchant_id, which is not a secret (it is included in payment payloads), so it does not provide meaningful authentication. X-Signature-V2 is keyed by your client_id — the same secret credential you use for API authentication — and is the only signature you should trust.
X-Signature-V2 is present on every notification for merchants that have a client_id. If it is missing, reject the request rather than falling back to v1.
Why Signature Verification Is Required
Without signature validation:
- Anyone could send fake webhook requests to your endpoint
- Transaction statuses could be manipulated
- Your system could be exposed to fraud
Validation Process
- Retrieve the raw request body exactly as received (do not parse and re-serialize it).
- Read the
X-Signature-V2header from the request. - Compute
HMAC-SHA256(raw_body, your_client_id)and hex-encode it. - Compare your computed value with
X-Signature-V2using a constant-time comparison. - Reject the request if the values do not match, or if
X-Signature-V2is absent.
Important
- Always use the raw request body. Re-stringifying the parsed JSON can change key order or whitespace and will break the signature.
- Use a constant-time comparison method (e.g.
crypto.timingSafeEqual,hash_equals) when comparing signatures. - Your
client_idis the v2 secret — treat it like a password and never expose it client-side. - If verification fails, return an HTTP 400 or 401 response.
Example (Node.js)
Below is a simplified example of how to verify the recommended X-Signature-V2 header using HMAC-SHA256:
js
import crypto from "crypto";
// clientId is your Huch client_id (the v2 signing secret).
function verifySignatureV2(rawBody, headers, clientId) {
const receivedSignature = headers["x-signature-v2"];
if (!receivedSignature) return false; // never fall back to the legacy v1 signature
const computedSignature = crypto
.createHmac("sha256", clientId)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(receivedSignature)
);
}Example (PHP)
php
<?php
// $clientId is your Huch client_id (the v2 signing secret).
function verifySignatureV2(string $rawBody, array $headers, string $clientId): bool
{
$receivedSignature = $headers['X-Signature-V2'] ?? null;
if ($receivedSignature === null) {
return false; // never fall back to the legacy v1 signature
}
$computedSignature = hash_hmac('sha256', $rawBody, $clientId);
return hash_equals($computedSignature, $receivedSignature);
}Replay Protection
A valid signature only proves that a payload was produced by HUCH — it does not prove the payload is recent. Anyone who captures a notification can send the identical bytes again later, and the signature will still verify. You must therefore reject stale and duplicate deliveries yourself.
1. Check the timestamp
Every notification carries X-Timestamp, a Unix timestamp in seconds taken when the notification was generated. Reject any request whose timestamp is too far from your own clock.
A tolerance of 5 minutes (300 seconds) is recommended: wide enough to absorb clock skew and delivery latency, narrow enough to make captured payloads useless quickly.
js
const TOLERANCE_SECONDS = 300;
function isFresh(headers) {
const timestamp = Number(headers["x-timestamp"]);
if (!Number.isFinite(timestamp)) return false;
return Math.abs(Math.floor(Date.now() / 1000) - timestamp) <= TOLERANCE_SECONDS;
}php
<?php
const TOLERANCE_SECONDS = 300;
function isFresh(array $headers): bool
{
$timestamp = $headers['X-Timestamp'] ?? null;
if (!is_numeric($timestamp)) {
return false;
}
return abs(time() - (int) $timestamp) <= TOLERANCE_SECONDS;
}Make sure your server clock is synchronised with NTP, otherwise valid notifications will be rejected.
2. Deduplicate deliveries
Notifications are retried whenever your endpoint does not return a 2XX, so the same status change can legitimately arrive more than once. Processing must be idempotent.
NOTE
The notification payload does not currently contain a dedicated unique event id. Build your deduplication key from the fields that are present.
Recommended key: payment_id + payment_status (plus status_change.old → status_change.new when present).
- On receipt, verify the signature and the timestamp.
- Compute the deduplication key and look it up in a store of already-processed keys.
- If it is already present, return
200 OKand do nothing else. - Otherwise process the notification, then record the key.
Keep processed keys for at least as long as your retry window (a few days is a safe default). Always return 200 OK for a duplicate — returning an error causes further retries.
Statuses do not always arrive in order. Guard your own state machine so that a late PAID cannot overwrite an already-recorded PAID_RECEIVED.
IP Whitelisting
For additional security, merchants should restrict incoming webhook requests to HUCH production IP addresses.
Production IP Addresses
- Payin: 52.47.150.158
- Withdrawal: 51.44.195.207
It is recommended to configure firewall rules so that only these IP addresses are allowed to access your webhook endpoint.
Payment Status Validation
When processing webhook notifications, always rely on the payment_status field to determine the transaction state.
The status field is deprecated and must not be used.
Final Statuses to Consider
PAID– The customer's bank confirmed that the transaction was initiated.PAID_RECEIVED– Funds have landed on the merchant’s bank account.FAILED– The transaction failed.PROCESSING– The bank is still processing the transaction.
For maximum accuracy, merchants should treat PAID_RECEIVED as the fully settled status.
Best Practices
- Always verify the
X-Signature-V2webhook signature; never rely on the deprecated v1X-Signature. - Implement IP whitelisting.
- Log all webhook payloads for audit purposes.
- Ensure idempotent processing to prevent duplicate handling.
- Monitor webhook failures and retries.