Skip to content

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/json payloads
  • 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:

HeaderDescription
Content-TypeAlways application/json.
X-TimestampUnix timestamp (seconds) of when the notification was generated.
X-SignatureLegacy (v1) signature. Deprecated — kept only for backward compatibility.
X-Signature-V2Recommended (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:

HeaderVersionSecret key usedStatus
X-Signaturev1your merchant_id⚠️ Deprecated — do not use for new integrations
X-Signature-V2v2your 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

  1. Retrieve the raw request body exactly as received (do not parse and re-serialize it).
  2. Read the X-Signature-V2 header from the request.
  3. Compute HMAC-SHA256(raw_body, your_client_id) and hex-encode it.
  4. Compare your computed value with X-Signature-V2 using a constant-time comparison.
  5. Reject the request if the values do not match, or if X-Signature-V2 is 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_id is 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.oldstatus_change.new when present).

  1. On receipt, verify the signature and the timestamp.
  2. Compute the deduplication key and look it up in a store of already-processed keys.
  3. If it is already present, return 200 OK and do nothing else.
  4. 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-V2 webhook signature; never rely on the deprecated v1 X-Signature.
  • Implement IP whitelisting.
  • Log all webhook payloads for audit purposes.
  • Ensure idempotent processing to prevent duplicate handling.
  • Monitor webhook failures and retries.