Skip to main content
Version: 0.0.1

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.

Per-transaction callbacks

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.

HeaderValueMeaning
x-signature64 lowercase hex characters, different on every webhookThe 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-version1The webhook format version. It matches Version in the body.
x-platformzenpayNames 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.

Verify the raw body

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​

FieldTypeNotes
VersionnumberPayload version number.
EventstringEvent type (e.g. New, Edit).
PayloadTypestringType of payload delivered (e.g. Payment, Customer).
PaymentReferencestringUnique payment reference assigned by Zenith.
CustomerNamestring | nullName of the customer; null when there is none.
CustomerReferencestringReference provided by the merchant.
PaymentStatusnumberNumeric status code.
PaymentStatusDisplaystringHuman-readable payment status.
BaseAmountnumberBase amount before fees.
FundsToMerchantnumberNet amount payable to the merchant.
CustomerFeenumberFee charged to the customer.
MerchantFeenumberFee charged to the merchant.
PaymentAmountnumberTotal amount of the transaction.
AccountOrCardNostringMasked account or card number.
PaymentMethodnumberNumeric code for the payment method.
PaymentMethodDisplaystringHuman-readable payment method.
PaymentCardTypenumberNumeric code for the card type.
PaymentCardTypeDisplaystringHuman-readable card type.
SubCardTypenumber | nullNumeric code for the sub card type; null when there is none.
SubtCardTypeDisplaystring | nullHuman-readable sub card type; null when there is none. Field name is misspelled, this is not an error in documentation. Also sent as SubCardTypeDisplay.
SubCardTypeDisplaystring | nullHuman-readable sub card type. Same value as SubtCardTypeDisplay.
ProcessingDateTimestringUTC ISO-8601 datetime (yyyy-MM-ddTHH:mm:ss).
SettlementDatestring | nullUTC ISO-8601 date (yyyy-MM-dd), or null.
ProcessorReferencestringReference from the processor/gateway.
IsPaymentSettledToMerchantbooleanIndicates if the payment has been settled.
FailureCodestringFailure code; an empty string when there is none.
FailureReasonstringFailure reason; an empty string when there is none.
MerchantUniquePaymentIdstring | nullUnique payment ID from the merchant; null when there is none.
MerchantCodestringMerchant identifier.
AdditionalReferencestring | nullAdditional reference; null when there is none.
PaymentSourcenumberNumeric source identifier.
PaymentSourceDispalystringHuman-readable payment source. Field name is misspelled, this is not an error in documentation. Also sent as PaymentSourceDisplay.
PaymentSourceDisplaystringHuman-readable payment source. Same value as PaymentSourceDispaly.
IsPaymentRecalledbooleanIndicates if the payment has been recalled.
IsPaymentRetryScheduledbooleanIndicates if a retry of the payment is scheduled.
IsPaymentRefundedbooleanIndicates if the payment has been refunded.
Sku1string | nullnull when empty.
Sku2string | nullnull when empty.
AdditionalDataobjectProcessor details: AuthCode, RRN and STAN. Not on every webhook.
CardTypeValuenumberNumeric code for the card type.
CardTypeStringstringName of the card type, for example MasterCard.
SubCardTypeValuenumber | nullNumeric code for the sub card type; null when there is none.
SubCardTypeStringstring | nullName of the sub card type; null when there is none.
SettlementTypestringSettlement type, for example Net or Gross.
SettlementFrequencyValuenumberNumeric code for the settlement frequency (1 Daily, 2 Monthly).
SettlementFrequencyStringstringSettlement 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-platformzenpay
x-signature53e842e7fe4392ca6813f35bc12ed2da00179aa8c64d1a5cfa946d1fd146d3b8
x-version1