Payment Callback
The payment callback delivers the result of a payment to your server. It is delivered to your callback_url — as a redirect for redirect integrations, or as a direct POST for no-redirect integrations. For the checkout callback (the browser-side response), see Checkout 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 v4 payment callback carries a compact {order, transaction} payload, Base64-encoded and signed.
How Nimbbl Prepares the Payload
- Nimbbl builds the payment result as a compact JSON object with an
orderobject and atransactionobject. - That JSON is serialised to a compact string (no extra spaces). It is Base64-encoded into the
payloadfield, and HMAC-SHA256 signed with youraccess_secretinto thenimbbl_signaturefield. - If encryption is enabled, the signed result is AES-GCM encrypted and delivered as
encrypted_response, along with yoursub_merchant_id.
What You Receive
The fields are the same either way — payload, nimbbl_signature, version — but how they arrive depends on your callback mode:
Redirect integrations — Nimbbl redirects the browser to your callback_url with the fields spread as query parameters, not a POST body:
callback_url?payload=<base64>&nimbbl_signature=<hmac-sha256-hex>&version=v4
No-redirect / webhook integrations (callback_mode is callback_url_noredirect or callback_handler) — Nimbbl POSTs the same fields as a JSON body to your callback_url:
callback_mode Has Four Valuescallback_mode is set per order and determines how the result reaches the client: callback_handler (posts a message to the parent frame), callback_url_redirect (redirects the browser with the payload in the URL), callback_mobile (used by the mobile SDKs — redirects to an internal mobile handoff route), and callback_url_noredirect (the default — closes the checkout window). This callback page describes the server-side result Nimbbl also POSTs to your callback_url, which is independent of callback_mode. Two failure modes are silent: an unrecognised callback_mode value falls back to closing the window with no callback delivered, and a missing callback_mode throws client-side, so the browser interaction never completes — validate that callback_mode is set correctly on every order.
{
"payload": "<base64>",
"nimbbl_signature": "<hmac-sha256-hex>",
"version": "v4"
}
payload is the Base64-encoded compact JSON below; nimbbl_signature is the HMAC-SHA256 of that compact JSON string — not of individual extracted fields, and not of the query string itself. Validate it the same way regardless of whether it arrived as query parameters or a JSON body.
Unlike the webhook, the unencrypted v4 payment callback does not include sub_merchant_id — the checkout callback works the same way. If you run multiple sub-merchants against a single callback URL, there's nothing in the response telling you which access_secret to verify against. Route by URL (one callback URL per sub-merchant) or enable encryption, which does add sub_merchant_id.
When encryption is enabled, both delivery modes carry the encrypted variant instead — as query parameters (?encrypted_response=<hex>&sub_merchant_id=<id>) for redirects, or as a JSON body for no-redirect/webhook:
{
"encrypted_response": "<hex>",
"sub_merchant_id": "<id>"
}
Decoded payload — Unencrypted
Base64-decode the payload value to get the compact JSON string, then parse it. The example below shows a pre-authorization result:
{
"order": {
"invoice_id": "<Merchant invoice id>",
"order_id": "<Order_ID>",
"currency": "INR",
"total_amount": 5000.00,
"status": "lapsed",
"cancellation_reason": null,
"lapsed_reason": "payment_authorized"
},
"transaction": {
"transaction_id": "<Transaction_ID>",
"status": "authorized",
"transaction_type": "payment",
"transaction_amount": 5500.00,
"transaction_currency": "INR",
"nimbbl_error_code": "",
"nimbbl_consumer_message": "",
"nimbbl_merchant_message": "",
"authorization_details": {
"mechanism": "pre_auth",
"capture_mode": "manual",
"authorized_time": "2026-06-18 10:52:26",
"expiry_time": "2026-06-25 10:52:26",
"captured_amount": 0.00,
"voided_amount": 0.00,
"available_authorized_amount": 5500.00
}
}
}
Here the transaction status is authorized and the order status is lapsed with lapsed_reason payment_authorized. The funds are held, not collected — see the callout below.
authorization_details
The transaction.authorization_details block is present only for pre-authorization payments — that is, payment transactions where the sub-merchant's capture_mode is manual. For automatically captured payments it is omitted entirely.
| Field | Type | Description |
|---|---|---|
mechanism | string | Always pre_auth for a pre-authorized transaction |
capture_mode | string | manual — the authorization must be captured explicitly |
authorized_time | string | null | When the authorization was granted |
expiry_time | string | null | When the authorization lapses if not captured |
captured_amount | number | Total already captured from this authorization (successful captures only) |
voided_amount | number | Total already voided from this authorization (successful voids only) |
available_authorized_amount | number | Amount still available to capture or void |
When a transaction comes back with status: "authorized", the funds are only held on the customer's card — they have not been collected. You must capture the authorization before fulfilling the order, and you must not treat this callback as a successful payment.
Always confirm the outcome via the payment_authorized webhook or the Transaction Enquiry API before acting. For the full flow, see Pre-Authorization and Pre-Auth and Capture.
order.cancellation_reason and order.lapsed_reason are null unless the order was cancelled or lapsed. transaction.authorization_details appears only for pre-authorization (manual-capture) payments.
Decoded payload — Encrypted
If your account has encryption enabled, the v4 payment callback is delivered as an encrypted result — and no nimbbl_signature is sent:
{
"encrypted_response": "3164351ca6195e9871...",
"sub_merchant_id": "123456"
}
Important: Decrypt encrypted_response using your AES-GCM key to obtain the plain {order, transaction} object directly — there is no Base64 payload field and no signature to validate. A successful AES-GCM decryption is itself proof of authenticity. See Using Encrypted Payloads for decryption steps and code.
The v3 payment callback delivered a rich, full payload to your redirect URL as a Base64-encoded response query parameter — no signature or sub_merchant_id outside it. The decoded payload included a nimbbl_signature field and a transaction.signature field for validation.
Delivery — redirect integrations:
callback_url?response=<base64>
Delivery — no-redirect / webhook integrations: the same full payload below is POSTed as a plain JSON body to your callback_url.
Decoded payload:
{
"status": "authorized",
"message": "Payment Authorized",
"nimbbl_order_id": "<Order_ID>",
"nimbbl_transaction_id": "<Transaction_ID>",
"is_callback": true,
"transaction": {
"transaction_id": "<Transaction_ID>",
"status": "authorized",
"payment_partner": "Easebuzz",
"psp_transaction_id": "<PSP_Transaction_ID>",
"payment_mode": "Credit Card",
"retry_allowed": null,
"nimbbl_consumer_message": "",
"nimbbl_merchant_message": "",
"nimbbl_error_code": "",
"sub_payment_mode": {
"card_input_type": "card_pan",
"issuer": "UNKNOWN",
"card_type": "retail",
"scheme": "MasterCard",
"masked_card": "**** **** **** 2346",
"expiry": "**/****",
"card_holder": "R****** S*****"
},
"processing_payment_partner": "Easebuzz",
"payment_acquirer": null,
"psp_credential_label": "easebuzz_2",
"payment_arn": "<ARN>",
"transaction_amount": 5500.0,
"transaction_type": "payment",
"transaction_currency": "INR",
"additional_charges": 500.0,
"shipping_charges": 0.0,
"cod_charges": 0.0,
"offer_discount": 0.0,
"offer_id": null,
"authorization_details": {
"mechanism": "pre_auth",
"capture_mode": "manual",
"authorized_time": "2026-06-18 10:52:26",
"expiry_time": "2026-06-25 10:52:26",
"captured_amount": 0.00,
"voided_amount": 0.00,
"available_authorized_amount": 5500.00
},
"signture_version": "v3",
"signature": "<hmac-sha256-hex>"
},
"order": {
"invoice_id": "<Merchant invoice id>",
"order_date": "2026-06-18 10:52:26",
"amount_before_tax": 5000.0,
"tax": 1.0,
"total_amount": 5000.0,
"convenience_fee": 0,
"additional_charges": 0,
"grand_total": 5000.0,
"offer_discount": 0.0,
"attempts": 1,
"status": "lapsed",
"currency": "INR",
"description": "",
"cancellation_reason": "",
"shopfront_domain": "server_to_server",
"referrer_platform": "string",
"referrer_platform_version": "string",
"device_user_agent": "Mozilla/5.0 ...",
"device": {
"browser_name": "Firefox",
"device_name": "Other",
"os_name": "Windows",
"ip_address": ""
},
"shipping_charges": 0.0,
"cod_charges": 0.0,
"shipping_address": {
"first_name": "N*",
"last_name": "N*",
"street": "M**************",
"landmark": "O********************",
"area": "El****",
"city": "At******",
"state": "Tamil Nadu",
"state_code": "IN-TN",
"country": "India",
"pincode": "60****",
"address_type": "BeachHouse",
"label": null,
"source": "merchant"
},
"billing_address": null
},
"user": {
"name": "<Customer Name>",
"mobile": "******4261",
"email": "ra************[email protected]",
"user_id": "<user_id>",
"source": "merchant_provided"
},
"nimbbl_signature": "<hmac-sha256-hex>"
}
Key differences from the v4 payment callback:
| v3 Payment Callback | v4 Payment Callback | |
|---|---|---|
| Delivery | response=<base64> query parameter (redirect only) | payload/nimbbl_signature/version as query params (redirect) or the same fields as a JSON body (no-redirect/webhook) |
| Where the signature lives | Inside the decoded JSON, as nimbbl_signature | A separate field alongside payload, not inside the decoded JSON |
| Payload size | Full payload — transaction, order, user objects | Compact — order and transaction summary. authorization_details for pre-auth is present on both — it is not a v4 addition. |
| Signature input | Per-field concatenation (invoice_id|transaction_id|...) | Entire compact JSON string |