Webhooks
Webhooks are merchant-wide, event-based notifications configured once at the merchant account level by Zenith. The merchant provides an HTTPS endpoint URL, and Zenith registers it against the merchant account.
Once registered, Zenith will POST JSON payloads whenever:
- A transaction status changes (e.g. Pending → Successful) across any payment method or channel.
- PayTo mandate status changes
- isSettledToMerchant status changes
Webhooks are dispatched on a scheduler/timer that evaluates states and posts updates. All payment methods and channels trigger the same webhook mechanism.
If you are using the Payment Plugin and need immediate, per-transaction notifications, see Callbacks and Validation. Callbacks are plugin-only and apply to a single transaction.
Callbacks vs Webhooks
Requirements
- HTTPS POST endpoint – Must be reachable from Zenith's servers.
- Stable URL – The endpoint is configured once by Zenith; changes require contacting Zenith.
Webhook Headers
Every webhook carries three headers set by ZenPay.
| Header | Value | Meaning |
|---|---|---|
x-signature | 64 lowercase hex characters, different on every webhook | The signature of the request body. Use it to check the webhook came from ZenPay and was not changed on the way. See Verifying the Signature. |
x-version | 1 | The webhook format version. It matches Version in the body. |
x-platform | zenpay | Names the sender. |
Verifying the Signature
x-signature is the HMAC-SHA256 of the raw request body, written as lowercase hex. The key is your API key for the program and environment that sent the webhook.
Read the body as text and verify it before parsing it. ZenPay writes whole-number amounts with a .0 ("CustomerFee":0.0), and parsing then re-serialising the JSON drops it (0). The text changes, so the signature no longer matches.
This runs in Node 20+ and in Cloudflare Workers. crypto.subtle.verify compares in constant time.
export async function verifyZenPaySignature(rawBody: string, apiKey: string, signatureHeader: string): Promise<boolean> {
if (!/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey('raw', encoder.encode(apiKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
const signature = Uint8Array.from(signatureHeader.match(/../g) ?? [], (pair) => parseInt(pair, 16));
return crypto.subtle.verify('HMAC', key, signature, encoder.encode(rawBody));
}
In a Hono handler, read the text first, then parse:
const rawBody = await c.req.text();
const valid = await verifyZenPaySignature(rawBody, apiKey, c.req.header('x-signature') ?? '');
if (!valid) return c.json({ error: 'invalid signature' }, 401);
const webhook = JSON.parse(rawBody);
Only the body is signed. There is no timestamp, so a webhook that is replayed carries the same valid signature. Make your handler safe to run twice for the same payment status, for example by keying on PaymentReference and PaymentStatus.
Webhook Payload Fields
| Field | Type | Notes |
|---|---|---|
Version | number | Payload version number. |
Event | string | Event type (e.g. New, Edit). |
PayloadType | string | Type of payload delivered (e.g. Payment, Customer). |
PaymentReference | string | Unique payment reference assigned by Zenith. |
CustomerName | string | null | Name of the customer; null when there is none. |
CustomerReference | string | Reference provided by the merchant. |
PaymentStatus | number | Numeric status code. |
PaymentStatusDisplay | string | Human-readable payment status. |
BaseAmount | number | Base amount before fees. |
FundsToMerchant | number | Net amount payable to the merchant. |
CustomerFee | number | Fee charged to the customer. |
MerchantFee | number | Fee charged to the merchant. |
PaymentAmount | number | Total amount of the transaction. |
AccountOrCardNo | string | Masked account or card number. |
PaymentMethod | number | Numeric code for the payment method. |
PaymentMethodDisplay | string | Human-readable payment method. |
PaymentCardType | number | Numeric code for the card type. |
PaymentCardTypeDisplay | string | Human-readable card type. |
SubCardType | number | null | Numeric code for the sub card type; null when there is none. |
SubtCardTypeDisplay | string | null | Human-readable sub card type; null when there is none. Field name is misspelled, this is not an error in documentation. Also sent as SubCardTypeDisplay. |
SubCardTypeDisplay | string | null | Human-readable sub card type. Same value as SubtCardTypeDisplay. |
ProcessingDateTime | string | UTC ISO-8601 datetime (yyyy-MM-ddTHH:mm:ss). |
SettlementDate | string | null | UTC ISO-8601 date (yyyy-MM-dd), or null. |
ProcessorReference | string | Reference from the processor/gateway. |
IsPaymentSettledToMerchant | boolean | Indicates if the payment has been settled. |
FailureCode | string | Failure code; an empty string when there is none. |
FailureReason | string | Failure reason; an empty string when there is none. |
MerchantUniquePaymentId | string | null | Unique payment ID from the merchant; null when there is none. |
MerchantCode | string | Merchant identifier. |
AdditionalReference | string | null | Additional reference; null when there is none. |
PaymentSource | number | Numeric source identifier. |
PaymentSourceDispaly | string | Human-readable payment source. Field name is misspelled, this is not an error in documentation. Also sent as PaymentSourceDisplay. |
PaymentSourceDisplay | string | Human-readable payment source. Same value as PaymentSourceDispaly. |
IsPaymentRecalled | boolean | Indicates if the payment has been recalled. |
IsPaymentRetryScheduled | boolean | Indicates if a retry of the payment is scheduled. |
IsPaymentRefunded | boolean | Indicates if the payment has been refunded. |
Sku1 | string | null | null when empty. |
Sku2 | string | null | null when empty. |
AdditionalData | object | Processor details: AuthCode, RRN and STAN. Not on every webhook. |
CardTypeValue | number | Numeric code for the card type. |
CardTypeString | string | Name of the card type, for example MasterCard. |
SubCardTypeValue | number | null | Numeric code for the sub card type; null when there is none. |
SubCardTypeString | string | null | Name of the sub card type; null when there is none. |
SettlementType | string | Settlement type, for example Net or Gross. |
SettlementFrequencyValue | number | Numeric code for the settlement frequency (1 Daily, 2 Monthly). |
SettlementFrequencyString | string | Settlement frequency, for example Daily or Monthly. |
Webhook Examples
These are webhooks captured from sandbox payments with a recorded checkout mode. Each entry is one payment, and its buttons select the messages in the order they arrived. Select a mode and payment to see the indented JSON body and its headers together. The headers are those ZenPay sets (x-signature, x-version, x-platform). Signature verification uses the original received body, including its whitespace.
Webhook reference
{
"Version": 1,
"Event": "New",
"PayloadType": "Payment",
"Payload": {
"PaymentReference": "250905",
"CustomerName": "Jaina Proudmoore",
"CustomerReference": "CUST-523YU4BE",
"PaymentStatus": 3,
"PaymentStatusDisplay": "Successful",
"BaseAmount": 214.18,
"FundsToMerchant": 214.18,
"CustomerFee": 0,
"MerchantFee": 0,
"PaymentAmount": 214.18,
"AccountOrCardNo": "625094XXXXXX0016",
"PaymentMethod": 0,
"PaymentMethodDisplay": "Credit / Debit Card",
"PaymentCardType": 21,
"PaymentCardTypeDisplay": "UnionPay - International Cards",
"SubCardType": null,
"SubtCardTypeDisplay": null,
"SubCardTypeDisplay": null,
"ProcessingDateTime": "2026-11-03T20:33:18",
"SettlementDate": "2026-11-05",
"ProcessorReference": "1456-P-BVFV6A4PZBSBIMWE",
"IsPaymentSettledToMerchant": false,
"MerchantUniquePaymentId": "OOLrdYj3SNVXY9uQBfM4Vw",
"MerchantCode": "1337",
"AdditionalReference": "ADD-QQ8KDNW3",
"PaymentSource": 36,
"PaymentSourceDispaly": "Pub.Online Payment - Customer",
"PaymentSourceDisplay": "Pub.Online Payment - Customer",
"IsPaymentRecalled": false,
"IsPaymentRefunded": false,
"Sku1": null,
"Sku2": null,
"CardTypeValue": 21,
"CardTypeString": "UnionPay",
"SubCardTypeValue": null,
"SubCardTypeString": null,
"SettlementType": "Gross",
"SettlementFrequencyValue": 2,
"SettlementFrequencyString": "Monthly"
}
}
x-platformzenpayx-signature53e842e7fe4392ca6813f35bc12ed2da00179aa8c64d1a5cfa946d1fd146d3b8x-version1