Skip to main content

Pre-auth and Capture

Pre-authorization lets you hold a customer's funds at checkout and decide later whether to capture (collect the money) or void (release the hold). This guide is the step-by-step integration. For the underlying concepts — the authorized state, the lifecycle, the authorization window, and refunds after capture — see Pre-authorization.

This flow applies to Standard Checkout, Custom Checkout, and Plug and Play Payment Links — the detection surface differs (browser callback, server callback, or webhook), but the capture and void steps are identical.

Prerequisites

Pre-authorization (manual capture) must be enabled on your sub-merchant. It is not on by default. Contact [email protected] with the sub-merchant ID(s) you want configured; Nimbbl enables manual capture on those accounts. Once enabled, payments on that sub-merchant hold funds instead of collecting them immediately.

A few related settings are configured with Nimbbl at the same time: your expiry action (capture or void at the deadline — see Authorization Expiry Period), your capture period (how long you have to decide), and whether checkout shows the customer a held-amount message.

Nimbbl handles all payment-partner-specific behaviour on the backend — you integrate against one interface and do not write per-partner logic.

Choosing a Callback Version

Pre-authorization works on either callback version. authorization_details (including your capture deadline) is emitted on both v1–v3 and v4. v4's signature covers the entire payload with a single HMAC instead of a per-field concatenation, which is simpler to validate correctly. See Callback Payloads for both payload structures, and Migrating from v3 to v4 if you want to move.

Whichever version you're on, don't treat the callback as your source of truth for the deadline or the outcome — confirm via the payment_authorized webhook or Transaction Enquiry, per Step 1 below.

End-to-End Flow

Step 1 — Detect the Outcome

A completed payment returns one of two outcomes:

  • authorized — funds are held (pre-authorization). You must capture or void.
  • succeeded — at checkout, this means the payment was processed as a normal sale and is already collected; no capture needed. (A pre-authorization returns authorized here, not succeeded — it only reaches succeeded later, once you capture it.) See Sale vs Pre-authorization.

Branch on the payment status. You can detect authorized on any of these surfaces:

SurfaceWhereUse it to
payment_authorized webhookServer-to-serverReliably trigger your capture/void workflow
Payment callbackYour callback_urlRead the result server-side
Checkout callbackBrowser (Sonic SDK)Update the UI — not a source of truth
Transaction Enquiry APIOn demandPoll / confirm the current state

On an authorized payment, an authorization_details object describes the hold. The field you act on is expiry_time — your capture deadline:

FieldWhat it tells you
mechanismHow the payment was processed (always pre_auth when this object is present)
capture_modeAlways manual when this object is present — it is emitted only for manual-capture (pre-auth) payments, never for a normal sale
authorized_timeWhen the hold was placed
expiry_timeYour deadline — capture or void before this, or the hold is released
captured_amountAmount already captured (0 until you capture)
voided_amountAmount already voided from this authorization (0 until you void)
available_authorized_amountAmount still available to capture or void

A payment_authorized webhook looks like this (trimmed):

{
"event_type": "payment_authorized",
"transaction": {
"transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104614",
"status": "authorized",
"authorization_details": {
"mechanism": "pre_auth",
"capture_mode": "manual",
"authorized_time": "2026-06-22T10:46:14Z",
"expiry_time": "2026-06-29T10:46:14Z",
"captured_amount": 0,
"voided_amount": 0,
"available_authorized_amount": 500.00
}
},
"order": { "status": "lapsed", "lapsed_reason": "payment_authorized" }
}

See Callback Payloads for the full payload.

danger

A checkout callback can report success even when the payment is only authorized. Never fulfill on the checkout callback alone — confirm via the payment_authorized webhook or Transaction Enquiry first.

Step 2 — Hold and Decide

Record the transaction_id and mark the order as awaiting a capture decision. Do not fulfill yet — the funds are held, not collected. Capture or void before authorization_details.expiry_time.

If you do nothing, Nimbbl resolves the hold at the deadline according to your account's expiry action, and sends you a webhook you didn't initiate:

  • void — Nimbbl releases the hold and sends a void_success. See Handle a void you didn't initiate.
  • capture — Nimbbl automatically collects the funds and sends a capture_success; the payment succeeds.

Handle both — and note the capture case means inaction charges the customer, so don't rely on the deadline to release a hold unless your expiry action is void. See Pre-authorization → Authorization Expiry Period for the configuration.

Step 3 — Capture

When you're ready to collect the funds, call the Capture a Payment API from your server with the transaction_id of the authorized payment. You may include a comment to record the reason.

Request:

{
"transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104614",
"comment": "Goods dispatched"
}

Response — the capture is recorded as its own transaction (a new transaction_id) linked back to the authorized payment via original_payment_transaction_id. The response echoes back the comment you supplied — note this differs from the capture_success webhook, which does not carry it:

{
"original_payment_transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104614",
"transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104615",
"capture_status": "succeeded",
"transaction_type": "capture",
"order_id": "o_Rz4Zx2WeyooEpyxa",
"comment": "Goods dispatched"
}

Branch on capture_status:

capture_statusMeaningWhat to do
succeededFunds collected, settlement triggeredFulfil the order
pendingCapture is processing (a normal asynchronous outcome) — the response includes a next action pointing at Transaction EnquiryWait for the capture_success webhook or poll Transaction Enquiry; handle capture_failed too
failedCapture was declinedCheck the error object's retry_allowed flag before deciding whether to retry the same capture call or void instead — see Error Handling

Capture is always for the full authorized amount — partial capture is not currently supported.

Step 4 — Void

To release the hold without charging the customer, call the Void a Payment API with the transaction_id. You may include a comment. This is one call for every payment partner — Nimbbl handles the partner-specific mechanics behind the scenes.

Request:

{
"transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104614",
"comment": "Customer cancelled booking"
}

Response — like capture, the void is its own transaction and carries a void_status (succeeded / pending / failed); a pending response includes a next enquiry action. As with capture, the response echoes back the comment you supplied, unlike the void_success webhook:

{
"original_payment_transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104614",
"transaction_id": "o_Rz4Zx2WeyooEpyxa-221117104616",
"void_status": "succeeded",
"transaction_type": "void",
"order_id": "o_Rz4Zx2WeyooEpyxa",
"comment": "Customer cancelled booking"
}

A voided payment moves to reversed and the order to lapsed. Handle the void_success / void_failed webhooks for the asynchronous outcome.

Handle a Void You Didn't Initiate

A void_success can also arrive without a void call from you — Nimbbl voids the hold automatically at expiry, or as a system safeguard.

The Webhook Doesn't Say — Check Transaction Enquiry Instead

The void_success webhook itself carries no field that distinguishes the two — there's no comment, notes, or reversal_reason on this event. Call Transaction Enquiry for the same transaction instead: its comment field carries "Auto-voided by the Nimbbl system" for a system-initiated void, or whatever comment you passed (or none) for one you triggered.

Treat every void_success the same way regardless of cause: it is terminal for that authorization — release the order on your side and do not attempt a capture. See Payment Webhooks for the full payload.

Step 5 — Confirm and Fulfill

Only fulfill after a capture is confirmed — a capture_status: succeeded response or a capture_success webhook. If you later need to refund a captured pre-authorization, the refundable amount is the captured amount, not the original authorized hold. See Refunds.

Integrating Safely

Capture and void act on a single authorization, and only an authorized transaction can be captured or voided. Build for that:

  • Repeat or out-of-order calls are rejected. Capturing an already-captured payment, or voiding one that's already captured/voided/expired, returns a 4XX (the transaction is no longer authorized). Treat that as a no-op rather than an error, and key your own logic on the transaction_id.
  • Make webhook handlers idempotent. payment_authorized, capture_success, and void_success may be redelivered — dedupe on transaction_id so you don't fulfill or release twice.
  • Decide before the deadline. Once expiry_time passes the hold is gone; a late capture will fail with the transaction no longer authorized.

Via Command Center

You can also capture or void manually: open the payment in Payments, find the transaction in the authorized state, and use the Capture or Void action (optionally adding a comment).

Error Handling

Capture and void return a uniform error contract across partners. When capture_status / void_status is failed, read the error object (nimbbl_error_code, nimbbl_consumer_message, nimbbl_merchant_message, retry_allowed). retry_allowed reflects the underlying partner failure, not the transaction's own state: a false value means the partner-side failure is unlikely to succeed on a repeat attempt (consider voiding instead), while true means retrying the same still-authorized transaction is reasonable. This is a different situation from "Repeat or out-of-order calls are rejected" below, which covers calling capture or void on a transaction that is no longer authorized at all. Common validation errors:

SituationResult
Transaction not found / not yoursError
Transaction is not in the authorized state — already captured, voided, or the hold expiredError — only an authorized payment can be captured or voided

A pending status is not an error — it means the outcome will arrive asynchronously via webhook.

Testing in Sandbox

In test mode, complete a checkout on a pre-auth-enabled sub-merchant to produce an authorized payment, then exercise capture and void from your server (or Command Center) and confirm your webhook handler reacts to payment_authorized, capture_success, and void_success. See Testing and Going Live for test credentials.