Skip to main content
Version: 0.0.1

Callbacks and Handling the Customer's Return

Handling the Customer's Return​

Your return page should use this trust order:

  1. Verified callback state
  2. Local server-side payment attempt state
  3. Redirect parameters only as secondary UX context
StateShow
Redirect says success, callback not yet verifiedPayment is being confirmed
Callback verified successPayment successful
Callback verified failedPayment failed
Callback validation failedSafe failure message; do not trust the result

Do not rely on redirect alone to mark a payment as complete.


Callbacks: What They Are and Why They Matter​

When a callbackUrl parameter is included in the plugin payload, Zenith Payments POSTs the transaction result to that URL after payment is processed.

Callbacks are the authoritative server-side confirmation path for Hosted Checkout. Redirect is for customer-facing UX; callback validation is what allows your backend to trust the result.

Unlike redirect, a callback:

  • Does not depend on the browser completing the journey
  • Is not controlled by the user
  • Normally includes a validationCode that can be verified server-side (a callback without one is an unsigned notification)

Requirements:

  • Transaction-specific: applies only to the transaction in which callbackUrl was supplied
  • Asynchronous: delivery happens in the background after Zenith finishes processing
  • Integrator-controlled: you decide the destination via callbackUrl
  • Not merchant-wide: does not notify on unrelated transactions (use Webhooks for broader patterns)
  • HTTPS POST endpoint: must be a secure endpoint reachable from Zenith Payments
  • Backend-owned: receive and validate on your backend, not your frontend
  • Validation required: validate validationCode before trusting the payload, and treat a POST with no validationCode as unverified

Callback vs Redirect​

RedirectCallback
ChannelBrowserServer-to-server
TimingSynchronous with browserAsynchronous
ReliabilityNot guaranteed to completeMore reliable
PurposeCustomer-facing UXAuthoritative confirmation
VerifiedNoYes, when it carries a validationCode

For merchant-wide, event-style notifications across broader activity (not transaction-specific), see Webhooks instead.


Callback Payload Reference​

Every callback body is JSON with two top-level keys: response, the transaction result, and validationCode, the hash described in Validating the Callback. What is inside response depends on the mode:

Moderesponse shape
0 (Make Payment), 2 (Custom Payment)Payment result
1 (Tokenise)Tokenise result
3 (Preauthorisation)Preauthorisation result

validationCode is a SHA3-512 hash, sent beside response, not inside it. A callback can arrive without it: see Unsigned notifications.

Payment result (modes 0 and 2)​

The plugin's server package (@zp-hcp/server) requires only paymentReference. Read every other field defensively.

FieldTypeNotes
paymentReferencestringUnique payment reference assigned by Zenith
customerNamestringName of the customer
customerReferencestringReference provided by the merchant
paymentStatusnumber0 Pending · 1 Error · 3 Successful · 4 Failed · 5 Cancelled · 6 Suppressed · 7 InProgress
paymentStatusStringstringHuman-readable payment status
baseAmountnumberBase transaction amount before fees
fundsToMerchantnumberNet amount payable to the merchant
accountOrCardNostringMasked account or card number
paymentAccountstringPayment account type such as Card or BankAccount
processingDatestringUTC ISO-8601 datetime
settlementDatestringUTC ISO-8601 datetime
processorReferencestringReference from the processor or gateway
isPaymentSettledToMerchantbooleanWhether the payment has been settled to the merchant
failureCodestringFailure code if unsuccessful, otherwise empty
failureReasonstringFailure reason if unsuccessful, otherwise empty
paymentCardstringCard brand such as MasterCard or Visa
merchantUniquePaymentIdstringUnique ID supplied by the merchant for the payment
additionalReferencestringSame as the input parameter. Returned when supplied in the request payload
merchantCodestringMerchant identifier code
transactionSourcenumberNumeric identifier for the source channel
transactionSourceStringstringHuman-readable source channel
customerFeenumberFee charged to the customer
processedAmountnumberTotal processed amount including fees
cardCategorystringCategory of card such as Domestic or International
cardInformationSavedbooleanWhether card information was stored for tokenisation
tokenstringReturned only if the payment was processed using the cardProxy input parameter. Same value as cardProxy
cardTypeValuenumberNumeric value of the card or account type
cardTypeStringstringName of the card or account type, matching cardTypeValue, for example MasterCard or PayTo
subCardTypeStringstringSub-type of the card, as text
payToStatusstringPayTo status. PayTo accounts only
sku1stringStock keeping unit field 1
sku2stringStock keeping unit field 2
additionalDataobjectAuthorisation details from the card acquirer: authCode, rrn and stan

Example payload structure (illustrative values only, a subset of the fields above):

{
"response": {
"paymentReference": "95408",
"customerName": "Leah Adria",
"customerReference": "a12b1926-811f-411c-b10a-1b330cc50d3b",
"paymentStatus": 3,
"paymentStatusString": "Successful",
"baseAmount": 635.72,
"fundsToMerchant": 635.72,
"accountOrCardNo": "555555XXXXXX4444",
"paymentAccount": "Card",
"processingDate": "2025-08-27T21:48:59.447",
"settlementDate": "2025-08-29T00:00:00",
"processorReference": "ddb3a3436cbc7d77c241",
"isPaymentSettledToMerchant": false,
"failureCode": "",
"failureReason": "",
"paymentCard": "MasterCard",
"merchantUniquePaymentId": "35917c6c-ee6b-4e52-866b-bea51841e796",
"merchantCode": "1337",
"transactionSource": 36,
"transactionSourceString": "Public_Customer_OnlineOneOffPayment",
"customerFee": 63.57,
"processedAmount": 699.29,
"cardCategory": "International Cards",
"cardInformationSaved": false
},
"validationCode": "1dbe5ddcc72b8207e839da42308d0596cbbb72e5bf7074520ec885e3c615bb519848fce4a0c073a9a7e6544854338cab9c0c87bc8c82c54bb3706a91c21a7410"
}

Tokenise result (mode 1)​

A Tokenise callback saves a payment method. It returns a token, not a payment result, so there is no paymentReference or paymentStatus. @zp-hcp/server requires only token.

FieldTypeNotes
tokenstringThe token for the saved payment method. Use it to process a payment later through the API or the payment plugin. It is also the reference in the validation hash
merchantUniquePaymentIdstringSame as the input parameter
cardTypestringType of card or account chosen: Bank Account, PayID, PayTo or UnionPay in the captured examples
cardTypeValuenumberNumeric value of cardType. In the captured examples: Bank Account 4, PayID 13, PayTo 16, UnionPay 21
cardTypeStringstringName matching cardTypeValue
paymentDetailobjectFees and amounts, see below
paymentDetail.paymentAmountnumberSame as the paymentAmount input parameter
paymentDetail.customerFeenumberCustomer fee for a payment of paymentAmount
paymentDetail.merchantFeenumberMerchant fee for a payment of paymentAmount
paymentDetail.processingAmountnumberThe total that will be processed: paymentAmount plus customerFee
isRestrictedCardbooleanWhether the card is restricted
doRedirectbooleantrue in every captured example. Its meaning is not documented
accountNamestringAccount name entered by the customer. Bank Account and PayTo only
accountNumberstringObfuscated account number. Bank Account and PayTo only
payIdstringThe PayID. PayID only
payIdNamestringDisplay name for the PayID. PayID only
cardHolderNamestringCard holder name entered by the customer. Card payment methods only, for example UnionPay
cardNumberstringObfuscated card number. Card payment methods only
cardExpirystringCard expiry, MM/CCYY. Card payment methods only

Examples of each payment type are in Callback Examples.

Preauthorisation result (mode 3)​

In @zp-hcp/server, a Preauthorisation response is the Payment result above with these differences.

Left out: paymentReference, paymentStatus, paymentStatusString, fundsToMerchant, settlementDate, isPaymentSettledToMerchant, processedAmount, token, cardInformationSaved, payToStatus, sku1, sku2 and additionalData.

Added: the fields below. The package requires only preauthReference.

FieldTypeNotes
preauthReferencestringPreauthorisation reference. It is the reference in the validation hash
preauthStatusnumberSame codes as paymentStatus: 0 Pending · 1 Error · 3 Successful · 4 Failed · 5 Cancelled · 6 Suppressed · 7 InProgress
preauthStatusStringstringHuman-readable preauthorisation status
preauthAmountnumberBase amount plus customer fee
preauthExpiryAtstringWhen the preauthorisation expires. The format is not documented

Validating the Callback​

The validationCode is a SHA3-512 hash of 7 pipe-separated fields, in exact order:

apiKey|userName|password|mode|paymentAmount|merchantUniquePaymentId|reference
  • The first 6 fields are the same values used in the original fingerprint hash
  • Only the last field changes: from timestamp (fingerprint) to the returned transaction reference (validation code)
  • reference depends on mode: for Mode 1 (Tokenise), this is the Token output parameter

Reference field and amount format by mode​

ModeReference fieldAmount in hash
0 (Make Payment)paymentReferencewhole cents
1 (Tokenise)token"0" when the launch amount is 0, 0.00 or blank, otherwise whole cents
2 (Custom Payment)paymentReference"0" (literal zero)
3 (Preauthorisation)preauthReferencewhole cents

Mode 3 note: Preauthorisation callbacks carry preauthStatus and preauthReference in place of paymentStatus and paymentReference.

Steps to validate:

  1. Receive the callback payload
  2. Extract merchantUniquePaymentId, the returned reference, and validationCode
  3. Rebuild the input string in the exact order above
  4. Generate SHA3-512 server-side
  5. Compare your computed value against the received validationCode

If they match, the callback is authentic, so update payment state. If they don't match, reject the callback, do not trust the result, and log for investigation.

Recovering launch context​

To verify a callback you need the original mode, paymentAmount, and merchantUniquePaymentId from the launch. The callback does not say which mode it belongs to, so keep the mode with your launch record rather than guessing it from the body. The hash uses the paymentAmount you launched with. merchantUniquePaymentId is in the callback body (Tokenise included), but the plugin's server package treats it as optional, so do not rely on it alone to find the launch.

The stateless approach is a signed callback token: embed a short-lived signed token in the callbackUrl as a query parameter (e.g. ?t=…). The token carries the launch context and is verified by your server before the validationCode check runs. This avoids any session store or database lookup:

callbackUrl: https://your-server/callbacks/zenith?t=<signed-token>

Unsigned notifications​

A POST to your callbackUrl can arrive with no validationCode. It is a notification only: it cannot be verified, so do not mark the payment complete on it. Answer 200 and wait for the signed POST. Both carry merchantUniquePaymentId, which pairs them.

What the sandbox sent when this was tested (September and October 2026, so check it again before relying on it):

  • Modes 0, 1 and 2 each sent one signed POST. Mode 0 sometimes sent an unsigned POST before the signed one.
  • Mode 3 sent an unsigned POST first. On one test the signed POST followed about 1.4 seconds later. On two later tests only the unsigned POST arrived, so those Mode 3 callbacks could not be verified.
  • In one Mode 3 unsigned POST, preauthStatus was the text "Successful" instead of the number 3. Do not assume the type.

Backend handling checklist​

  • Accept POST requests over HTTPS
  • Answer 200 to every POST, signed or not, and fulfil only on one that verifies
  • Validate validationCode before trusting any field
  • Process callbacks idempotently
  • Update local payment state safely
  • Log failures and mismatches clearly

Do not trust callback data without validation. Do not rely on redirect as final truth. Do not expose credentials or validation logic to the browser.

Timing and idempotency​

Callbacks can arrive before redirect completes, after redirect completes, without redirect completing, or more than once.

  • Treat callbacks as idempotent
  • Use merchantUniquePaymentId to prevent double-processing
  • Show a pending / confirming state if redirect returns before callback validation completes

Callback Examples​

These are callbacks captured from sandbox payments, grouped by mode. Each entry is one payment's callback as the API Server stored it. A Mode 1 (Tokenise) callback carries a different response from the payment callback above: it returns the token for the saved payment method, not a payment result. A Mode 3 (Pre-Auth) callback returns pre-authorisation fields. Pick a mode filter or search, then select an entry to see its payload.

Callback reference

{
"response": {
"paymentReference": "250905",
"customerName": "Jaina Proudmoore",
"customerReference": "CUST-523YU4BE",
"paymentStatus": 3,
"paymentStatusString": "Successful",
"baseAmount": 214.18,
"fundsToMerchant": 214.18,
"accountOrCardNo": "625094XXXXXX0016",
"paymentAccount": "Card",
"processingDate": "2026-11-03T20:33:18.187",
"settlementDate": "2026-11-05T00:00:00",
"processorReference": "1456-P-BVFV6A4PZBSBIMWE",
"isPaymentSettledToMerchant": false,
"failureCode": "",
"failureReason": "",
"paymentCard": "UnionPay",
"merchantUniquePaymentId": "OOLrdYj3SNVXY9uQBfM4Vw",
"merchantCode": "1337",
"transactionSource": 36,
"transactionSourceString": "Public_Customer_OnlineOneOffPayment",
"customerFee": 0,
"processedAmount": 214.18,
"cardCategory": "International Cards",
"cardInformationSaved": false,
"additionalReference": "ADD-QQ8KDNW3",
"merchantFee": 0,
"payToStatus": "NotApplicable",
"cardTypeValue": 21,
"cardTypeString": "UnionPay",
"subCardTypeString": "",
"settlementType": "Gross",
"settlementFrequencyString": "Monthly",
"settlementFrequencyValue": 2
},
"validationCode": "5287cdab1c3a6e7c7e62a7cc5aed33354a05f601979d7c92e105da7b95cc6bcf3af3ed73859ac15e6e1ea229074a5f8c5a565fc89ac55e3a5f76c048acf85d86"
}