Hold (manual capture)¶
On this page
What: hold the money as soon as the person confirms, and take it (capture) or release it (cancel)
only after the goods/service have been handed over. Time: ~30 min. You need: a backend that creates
intents (web, QR, terminal or
mini app).
1. When manual¶
| Case | Why |
|---|---|
| Vending machine, locker, fuel pump | whether the goods come out is known only after the payment (vending) |
| Food made to order, goods that depend on stock | not certain it will be ready |
| Rental, deposit | the final amount comes later |
Otherwise automatic (the default): succeeded as soon as the person confirms.
2. Flow¶
requires_confirmation ──confirmed─────▶ processing ──hold──▶ requires_capture ──capture──▶ succeeded
│
├──cancel──▶ canceled (hold released)
└──capture_before passed────▶ expired
(failure_code: authorization_expired, hold released)
{ "amount": 12000, "description": "Захиалга #5521", "reference": "5521", "presentment": "qr",
"capture_method": "manual", "capture_within": 900 }
capture_within— within how many seconds after the confirmation to capture/cancel: 60 – 604 800 (7 days), default 7 days. Only withmanual(given withautomatic,400 validation_error). Also allowed on a mini app's (app) intent.- On confirmation,
capture_before= the moment of confirmation +capture_within; webhookpayment_intent.requires_capture.
3. Capture, cancel, partial¶
- Capture —
POST /payment-intents/{id}/capture(empty body) →succeeded; in live, if the ledger has not answered,202+processing(thenGET). Capturing one already captured again →200with that intent. If it is notrequires_capture,409 not_capturable(withpayment_intent); ifcapture_beforehas passed,409 expired. - Cancel —
POST /payment-intents/{id}/cancel→canceled, the hold is released (not a refund: nothing was charged to the person). Even if the ledger's release is late, the cancellation stands — the platform keeps retrying until the money is back. - Partial — capture with an amount (
{amount}) «coming soon». For now: capture in full → wait untilsucceeded→POST …/refunds {"amount": <the part not delivered>}+ its ownIdempotency-Key. A refund is only for asucceeded/partially_refundedintent (409 not_refundable).
4. Automatic release¶
If you do not answer, at capture_before the intent becomes expired (authorization_expired) and the hold is
released to the person automatically (payment_intent.expired; for a mini app's intent, payment_intent.canceled
+ status: "expired"). Capture is not possible afterwards. The ledger itself releases the hold at capture_before
+ half the window (≥ 1 min, ≤ 15 min) — only when the platform could not release it.
Choose capture_within longer than the worst-case time to hand over + report, but so as not to hold the
person's money for nothing: minutes for a machine, hours for food made to order.
5. Code¶
const pi = await gerege.paymentIntents.capture(id); // or .cancel(id)
if (pi.status === "processing") scheduleRecheck(id); // 202
await gerege.refunds.create(id, { amount: 3000, reason: "out_of_stock" }, { idempotencyKey: `refund:${orderId}` });
pi, err := c.PaymentIntents.Capture(ctx, id) // or Cancel
var apiErr *miniapp.APIError
if errors.As(err, &apiErr) && apiErr.Code == "expired" { /* the hold was released to the person */ }
_, err = c.PaymentIntents.Refund(ctx, id, "refund:"+orderID, miniapp.RefundParams{Amount: 3000, Reason: "out_of_stock"})