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.
Moving a sub-merchant to v4? See Migrating Callbacks from v3 to v4 for the payload, signature, and rollout changes.
- v4
- v3
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."
}
| Field | Type | Description |
|---|---|---|
checkout_status | string | Outcome of the checkout attempt — success or failed |
reason | string | Machine-readable reason for the outcome, from a fixed set — see the reason table below |
nimbbl_order_id | string | Nimbbl's unique identifier for the order |
nimbbl_transaction_id | string | null | Nimbbl's unique identifier for the transaction. null if no transaction was created |
invoice_id | string | Your merchant invoice id for the order, echoed back from order creation |
retry | boolean | Whether the customer should be allowed to retry payment for this order. false when there is no transaction |
message | string | A 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_status | reason | message | When |
|---|---|---|---|
success | payment_captured | Your payment was successful. | Payment captured (auto-capture) |
success | payment_authorized | Your payment has been authorized. You'll be charged once the order is confirmed. | Pre-authorization — funds held, not captured |
success | payment_already_authorized | This order's payment has already been authorized. You'll be charged once the order is confirmed. | The order already carries an authorized pre-authorization |
success | payment_already_captured | This order has already been paid. | The order was already paid |
failed | payment_failed | Your payment could not be completed. | Payment attempt failed |
failed | payment_unconfirmed | We're still confirming your payment. Any amount deducted will be refunded if the payment doesn't complete. | Outcome not yet known — resolves asynchronously |
failed | user_cancelled | You cancelled the payment. | Customer closed checkout |
failed | timed_out | The checkout timed out. | Checkout or payment session timed out |
failed | payment_reversing | Your 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 |
failed | validity_expired | This order is no longer valid. Please start a new order. | The order's validity window has passed |
failed | no_payment_methods_configured | No payment method is available for this order. | No payment methods available |
failed | max_retries_exhausted | The maximum number of payment attempts was reached. Please start a new order. | Retry limit reached |
failed | invalid_order | This order is invalid or could not be found. | The order could not be resolved |
failed | authorisation_period_expired | The authorization window for this payment expired before it could be confirmed. | A pre-authorization's confirmation period expired |
failed | serial_number_blocking_failed | Your payment succeeded, but we couldn't complete a required device verification step. Please contact the merchant. | Device verification failed after payment succeeded |
failed | merchant_voided_preauth | The merchant voided the authorization hold on this order. | An operator voided a pre-authorization hold |
payment_authorized Is Not a Completed PaymentA 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
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_status | reason | What to do |
|---|---|---|
success | payment_captured / payment_already_captured | Validate signature, verify via webhook or Transaction Enquiry, then fulfill the order |
success | payment_authorized / payment_already_authorized | Do not fulfill yet — capture the authorization, then verify and fulfill |
failed | any reason | Check 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.
The v3 checkout callback was delivered by the Sonic SDK via your callback_handler function in the browser. The response had event_type: "globalHandleCheckoutResponse" at the top, with the full transaction and order nested inside a payload object.
Unencrypted payload:
{
"event_type": "globalHandleCheckoutResponse",
"payload": {
"status": "success",
"message": "",
"nimbbl_order_id": "<Order_ID>",
"nimbbl_transaction_id": "<Transaction_ID>",
"nimbbl_signature": "<hmac-sha256-hex>",
"transaction": {
"transaction_id": "<Transaction_ID>",
"status": "succeeded",
"transaction_amount": 1250.00,
"transaction_type": "payment",
"transaction_currency": "INR",
"additional_charges": 0.00,
"offer_discount": 0.00,
"offer_id": null,
"payment_partner": "<Payment Partner>",
"psp_transaction_id": "<PSP_Transaction_ID>",
"nimbbl_consumer_message": "",
"nimbbl_merchant_message": "",
"nimbbl_error_code": "",
"signature": "<hmac-sha256-hex>",
"signature_version": "v3"
},
"order": {
"invoice_id": "<Merchant invoice id>",
"status": "completed",
"refund_details": {
"refundable_currency": "INR",
"available_refundable_amount": 1250.00,
"refunded_amount": 0.00,
"total_refundable_amount": 1250.00
},
"currency_conversion": {
"original_currency": "USD",
"converted_currency": "INR",
"exchange_rate": 100.0,
"conversion_reason": "ALTERNATE_CURRENCY_CONFIG",
"original_amount_before_tax": 8.0,
"original_tax": 2.0,
"original_total_amount": 10.0
}
}
}
}
Annotated example (with field descriptions)
{
"event_type": "globalHandleCheckoutResponse",
"payload": {
"status": "success", // this is the Response Status - possible values are `success`, `authorized`, `failed` or `pending`
"message": "",
"nimbbl_order_id": "o_5aezw98nG8KLozP1", // this is the Nimbbl Order ID
"nimbbl_transaction_id": "o_5aezw98nG8KLozP1-230808065657", // this is the Nimbbl Transaction ID
"nimbbl_signature": "7e754ab8859f3ff888545ce73a0848bf131a468e543de5d9872128bc05d085ab", // this is the Signature generated for this transaction
"transaction": { // transaction object contains further information on the Transaction
"transaction_id": "o_5aezw98nG8KLozP1-230808065657", // this is the Nimbbl Transaction ID, same as above
"status": "succeeded", // this is the Transaction Status possible values are `succeeded`, `authorized`, `failed`, `pending`, `reversed` or `reversing`
"transaction_amount": 12.50, // this is the final transaction amount paid by the customer
"transaction_type": "payment", // this is the type of the transaction with possible values `payment`, `capture`, `void`, `partial-refund` and `full-refund`
"transaction_currency": "INR", // this is the currency of the transaction
"additional_charges": 0.00, // this is the convenience fee applied on the transaction
"offer_discount": 0.00, // this is the offer discount applied on the transaction
"offer_id": "offer_fhdjsfmfdbfmsd", // If the offer is applied this will be the id of the offer
"payment_partner": "", // this is the Payment Partner that processed the payment
"psp_transaction_id": "", // this is the Transaction ID of the Payment Partner
"nimbbl_consumer_message": "", // this is the message that can be shown to the customer when transaction has `failed`
"nimbbl_merchant_message": "",
"nimbbl_error_code": "",
"signature": "7e754ab8859f3ff888545ce73a0848bf131a468e543de5d9872128bc05d085ab",
"signature_version": "v3" // this the version of the signature returned
},
"order": { // order object contains further information on the Order
"invoice_id": "9RZvgJOpAyJOd7Yx", // this is the Invoice ID, your unique reference for generating the Order
"status": "completed", // this is the order status, possible values are `new`, `pending`, `completed` or `lapsed`. A `lapsed` order with `lapsed_reason: payment_authorized` is holding an authorized pre-authorization awaiting capture or void
"refund_details": { // this field will only come when the order_status is `completed`. This object will have all the refund details for the order.
"refundable_currency": "INR", // this is the currency in which the refund will be initiated
"available_refundable_amount": 5.0, // this is the amount which is available for refund
"refunded_amount": 0.0, // this is the amount which has been already refunded successfully
"total_refundable_amount": 5.0 // this is the maximum refundable amount available for the order
},
"currency_conversion": { // this object is sent only if the transaction currency was changed.
"original_currency": "USD", // this is the currency in which the order was initiated
"converted_currency": "INR", // this is the currency in which the order will be processed in
"exchange_rate": 100.0, // this is the exchange rate used to convert amounts from original currency to converted currency
"conversion_reason": "ALTERNATE_CURRENCY_CONFIG", // this is the reason for currency conversion
"original_amount_before_tax": 8.0, // this is the `amount_before_tax` received when the order was initiated
"original_tax": 2.0, // this is the `tax` when the order was initiated
"original_total_amount": 10.0 // this is the `total_amount` received when the order was initiate
}
}
}
}
Encrypted payload:
{
"event_type": "globalHandleCheckoutResponse",
"payload": {
"encrypted_response": "3164351ca6195e9871...",
"sub_merchant_id": "123456"
}
}
Decrypt payload.encrypted_response using your AES-GCM key to obtain the full unencrypted payload above. See Using Encrypted Payloads for decryption steps.
Signature validation uses the same v3 field-based approach — extract invoice_id from payload.order, and transaction_id, transaction_amount, transaction_currency, status, transaction_type from payload.transaction, concatenate with |, and HMAC SHA256 with your access_secret. Compare against payload.transaction.signature. See v3 Signature — Checkout Callback for code samples.
Key difference from the v4 checkout callback: The v3 checkout callback carried the full transaction and order objects inside the payload. The v4 checkout callback is a lightweight status response — checkout_status, nimbbl_order_id, nimbbl_transaction_id, invoice_id, retry, reason, message (the invoice_id and reason fields were added in v4).