Skip to main content

Checkout Callback

The checkout callback is the response the Sonic SDK fires in the browser after the customer completes checkout. Your client receives it via your callback_handler function and must forward it to your server for signature validation before acting on it. For the server-side payment result, see Payment Callback. For configuration and signature validation shared by both callbacks, see Callback Payloads.

Migration From v3 to v4

Moving a sub-merchant to v4? See Migrating Callbacks from v3 to v4 for the payload, signature, and rollout changes.

The response uses event_type: "globalHandleCheckoutResponse". Inside, the payload object is the signed v4 result — a Base64-encoded compact JSON string in payload, an HMAC-SHA256 signature in nimbbl_signature, and the version tag in version.

Outer Envelope

{
"event_type": "globalHandleCheckoutResponse",
"payload": {
"payload": "<base64>",
"nimbbl_signature": "<hmac-sha256-hex>",
"version": "v400"
}
}

The checkout callback uses the same field name as the payment callback — nimbbl_signature, not signature. sub_merchant_id is not part of the unencrypted response; it's added only when payload encryption is enabled for the sub-merchant (see the encrypted example below).

When payload encryption is enabled, the compact result is AES-GCM encrypted, wrapped as {"encrypted_response": "<hex>"}, JSON-serialised, and Base64-encoded into the same payload field — and no signature is sent:

{
"event_type": "globalHandleCheckoutResponse",
"payload": {
"payload": "<base64( {\"encrypted_response\": \"<hex>\"} )>",
"sub_merchant_id": "<sub_merchant_id>"
}
}

Base64-decode payload.payload and parse it to get {"encrypted_response": "<hex>"}, then decrypt encrypted_response with your AES-GCM key to recover the plain compact payload object directly — the successful decryption itself authenticates the data, so there is no signature to validate.

Decoded payload

Base64-decode payload.payload to get the compact JSON string, then parse it. The example below shows a pre-authorization result (funds authorized, not yet captured):

{
"checkout_status": "success",
"reason": "payment_authorized",
"nimbbl_order_id": "<Order_ID>",
"nimbbl_transaction_id": "<Transaction_ID>",
"invoice_id": "<Merchant invoice id>",
"retry": false,
"message": "Your payment has been authorized. You will be charged when the order is confirmed."
}
FieldTypeDescription
checkout_statusstringOutcome of the checkout attempt — success or failed
reasonstringMachine-readable reason for the outcome, from a fixed set — see the reason table below
nimbbl_order_idstringNimbbl's unique identifier for the order
nimbbl_transaction_idstring | nullNimbbl's unique identifier for the transaction. null if no transaction was created
invoice_idstringYour merchant invoice id for the order, echoed back from order creation
retrybooleanWhether the customer should be allowed to retry payment for this order. false when there is no transaction
messagestringA consumer-friendly message describing the outcome, paired with the reason

reason Values

reason and its paired message come from a fixed set of 17 values. Switch on reason — not on message, which is illustrative and may be reworded — in your code:

checkout_statusreasonmessageWhen
successpayment_capturedYour payment was successful.Payment captured (auto-capture)
successpayment_authorizedYour payment has been authorized. You'll be charged once the order is confirmed.Pre-authorization — funds held, not captured
successpayment_already_authorizedThis order's payment has already been authorized. You'll be charged once the order is confirmed.The order already carries an authorized pre-authorization
successpayment_already_capturedThis order has already been paid.The order was already paid
failedpayment_failedYour payment could not be completed.Payment attempt failed
failedpayment_unconfirmedWe're still confirming your payment. Any amount deducted will be refunded if the payment doesn't complete.Outcome not yet known — resolves asynchronously
faileduser_cancelledYou cancelled the payment.Customer closed checkout
failedtimed_outThe checkout timed out.Checkout or payment session timed out
failedpayment_reversingYour payment is being reversed. Please contact the merchant if you have questions.A delayed success arrived after the confirmation window and is being auto-reversed
failedvalidity_expiredThis order is no longer valid. Please start a new order.The order's validity window has passed
failedno_payment_methods_configuredNo payment method is available for this order.No payment methods available
failedmax_retries_exhaustedThe maximum number of payment attempts was reached. Please start a new order.Retry limit reached
failedinvalid_orderThis order is invalid or could not be found.The order could not be resolved
failedauthorisation_period_expiredThe authorization window for this payment expired before it could be confirmed.A pre-authorization's confirmation period expired
failedserial_number_blocking_failedYour payment succeeded, but we couldn't complete a required device verification step. Please contact the merchant.Device verification failed after payment succeeded
failedmerchant_voided_preauthThe merchant voided the authorization hold on this order.An operator voided a pre-authorization hold
payment_authorized Is Not a Completed Payment

A pre-authorization returns checkout_status: "success" with reason: "payment_authorized" — the funds are only held, not collected. Do not fulfill the order on this callback. Capture the authorization first, and confirm the outcome via the payment_authorized webhook or the Transaction Enquiry API. See Pre-Authorization and Pre-Auth and Capture.

Handling the Response

Required Step

Never fulfill an order based on the client-side callback alone. Always validate the signature and verify the final payment status server-side using webhooks or the Transaction Enquiry API before completing the order.

Use checkout_status, reason, and retry to decide your next action:

checkout_statusreasonWhat to do
successpayment_captured / payment_already_capturedValidate signature, verify via webhook or Transaction Enquiry, then fulfill the order
successpayment_authorized / payment_already_authorizedDo not fulfill yet — capture the authorization, then verify and fulfill
failedany reasonCheck retry — if true, allow the customer to retry (with a different payment mode if the failure was mode-specific); if false, inform the customer and do not offer a retry

Regardless of retry, treat every failed outcome the same way operationally: don't fulfill, and confirm the final state server-side before writing it to your records — some failed reasons (like payment_unconfirmed or payment_reversing) describe an in-flight state that can still resolve, not a hard stop.

Edge Case: Client-Side Failure Fallback

If the client can't reach Nimbbl to build the signed callback at all (for example, a session timeout), the Sonic SDK falls back to a flat, unsigned payload with no payload field, no nimbbl_signature, and no version — regardless of your callback version:

{
"status": "failed",
"message": "Sorry, your payment could not be processed. Please try again or another payment option.",
"nimbbl_order_id": "<Order_ID>",
"nimbbl_transaction_id": "<Transaction_ID or omitted>"
}

A handler written strictly against the v4 shape will throw on this. Check for the presence of a payload key before parsing it as the signed shape — treat its absence as an unverified client-side failure, not a parse error, and confirm the real outcome server-side rather than trusting this fallback's status.