Skip to content

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 with manual (given with automatic, 400 validation_error). Also allowed on a mini app's (app) intent.
  • On confirmation, capture_before = the moment of confirmation + capture_within; webhook payment_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 (then GET). Capturing one already captured again → 200 with that intent. If it is not requires_capture, 409 not_capturable (with payment_intent); if capture_before has 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 until succeeded → POST …/refunds {"amount": <the part not delivered>} + its own Idempotency-Key. A refund is only for a succeeded / partially_refunded intent (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"})