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 returnsauthorizedhere, notsucceeded— it only reachessucceededlater, once you capture it.) See Sale vs Pre-authorization.
Branch on the payment status. You can detect authorized on any of these surfaces:
| Surface | Where | Use it to |
|---|---|---|
payment_authorized webhook | Server-to-server | Reliably trigger your capture/void workflow |
| Payment callback | Your callback_url | Read the result server-side |
| Checkout callback | Browser (Sonic SDK) | Update the UI — not a source of truth |
| Transaction Enquiry API | On demand | Poll / 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:
| Field | What it tells you |
|---|---|
mechanism | How the payment was processed (always pre_auth when this object is present) |
capture_mode | Always manual when this object is present — it is emitted only for manual-capture (pre-auth) payments, never for a normal sale |
authorized_time | When the hold was placed |
expiry_time | Your deadline — capture or void before this, or the hold is released |
captured_amount | Amount already captured (0 until you capture) |
voided_amount | Amount already voided from this authorization (0 until you void) |
available_authorized_amount | Amount 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.
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 avoid_success. See Handle a void you didn't initiate.capture— Nimbbl automatically collects the funds and sends acapture_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_status | Meaning | What to do |
|---|---|---|
succeeded | Funds collected, settlement triggered | Fulfil the order |
pending | Capture is processing (a normal asynchronous outcome) — the response includes a next action pointing at Transaction Enquiry | Wait for the capture_success webhook or poll Transaction Enquiry; handle capture_failed too |
failed | Capture was declined | Check 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 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 longerauthorized). Treat that as a no-op rather than an error, and key your own logic on thetransaction_id. - Make webhook handlers idempotent.
payment_authorized,capture_success, andvoid_successmay be redelivered — dedupe ontransaction_idso you don't fulfill or release twice. - Decide before the deadline. Once
expiry_timepasses the hold is gone; a late capture will fail with the transaction no longerauthorized.
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:
| Situation | Result |
|---|---|
| Transaction not found / not yours | Error |
Transaction is not in the authorized state — already captured, voided, or the hold expired | Error — 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.