Manual Capture
By default, a payment settles the moment it confirms: the card is charged and the money is on its way. Manual capture splits that into two decisions. Confirming the intent places an authorization, a hold on the buyer's card for the full amount, and no money moves. Capturing, up to seven days later, settles some or all of the held amount. Voiding releases the hold without charging anything.
Reach for it when the final amount isn't known at the moment the buyer hands you their card: rentals and bookings with damage deposits, hotels with incidentals, field services quoted before the work is done, tabs that close at the end of the night. It's equally useful when the amount is known but you want to hold off charging until you're sure you can deliver: pre-orders, stock checks, or a manual review before committing a high-value payment.
If you always charge the full amount immediately, stay on the default ("capture_method": "automatic") and skip this page. Every hold you place is a promise to resolve it, and an unmanaged hold ties up the buyer's money for a week before expiring.
Manual capture is available where your server drives the payment: intents you create through the Payments API, standalone or attached to an order. Flint-hosted Checkout Sessions and Payment Links always capture automatically, and invoice payments reject manual capture.
How a Hold Works#
A manual-capture intent follows the normal payment intent lifecycle with one extra stop: after confirmation it parks in requires_capture instead of settling, and stays there until you capture, void, or the authorization expires.
create confirm capture
| | |
v v v
requires_payment_method --> requires_capture --------> succeeded
| |
cancel| |7 days pass
v v
canceled expired
While the intent is in requires_capture, four money fields on the payment intent tell you exactly where the funds stand, and they keep telling the story after it resolves:
| Field | Meaning |
|---|---|
authorized_amount_money | What the hold was placed for. Set at confirmation, never changes. |
capturable_amount_money | What you can still capture. Equals the authorized amount until you capture or void, then drops to zero. |
captured_amount_money | What actually settled. Zero until capture. |
released_amount_money | What went back to the buyer: the uncaptured remainder after a partial capture, or the full hold after a void or expiry. |
Timestamps ride along: authorized_at and authorization_expires_at are set when the hold is placed, and captured_at when it settles. authorization_expires_at is the deadline that should drive your capture scheduling; more on it below.
Place a Hold#
Create the intent with "capture_method": "manual". Everything else about creation is unchanged from an automatic payment.
curl -X POST https://api.withflintpay.com/v1/payment-intents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: hold-camera-rental-001" \
-d '{
"amount_money": {"amount": 12900, "currency": "USD"},
"payment_method_types": ["card"],
"capture_method": "manual"
}'
{
"data": {
"payment_intent": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_payment_method",
"amount_money": {"amount": 12900, "currency": "USD"},
"capture_method": "manual",
"source": "api"
},
"client_confirmation": {
"stripe": {...}
}
}
}
One rule applies at creation: manual capture requires an explicit confirmation step, so "auto_confirm": true is rejected with MANUAL_CAPTURE_NOT_ALLOWED_WITH_AUTO_CONFIRM.
Confirming is what places the hold. Confirm with a payment_source_token (a pm_... token your frontend created with Stripe.js, exactly as in Embedded Payments) or a payment_method_id for a returning customer's saved card. In test mode, Stripe's shorthand token pm_card_visa stands in for the frontend:
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/confirm \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: confirm-camera-rental-001" \
-d '{"payment_source_token": "pm_card_visa"}'
{
"data": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_capture",
"amount_money": {"amount": 12900, "currency": "USD"},
"capture_method": "manual",
"authorized_amount_money": {"amount": 12900, "currency": "USD"},
"capturable_amount_money": {"amount": 12900, "currency": "USD"},
"captured_amount_money": {"amount": 0, "currency": "USD"},
"released_amount_money": {"amount": 0, "currency": "USD"},
"authorized_at": "2026-07-03T02:57:20Z",
"authorization_expires_at": "2026-07-10T02:57:19Z",
"payment_method_brand": "visa",
"payment_method_last_four": "4242"
}
}
The hold is on: status is requires_capture, the full amount is capturable, and nothing has been charged. If the card requires 3D Secure, the intent passes through requires_action first and lands in requires_capture once the buyer completes authentication; the payment_intent.requires_capture webhook is the durable signal that the hold is in place.
Store payment_intent_id and authorization_expires_at with the work you're holding the money for. That deadline is now your problem to beat.
Capture the Payment#
When you're ready to charge, capture. Omit the body to capture the full authorized amount:
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/capture \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: capture-camera-rental-001" \
-d '{}'
Or pass amount_money to capture part of the hold. Here the rental came back a day early, so the merchant settles $98.00 of the $129.00 hold:
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/capture \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: capture-camera-rental-001" \
-d '{"amount_money": {"amount": 9800, "currency": "USD"}}'
{
"data": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "succeeded",
"amount_money": {"amount": 9800, "currency": "USD"},
"capture_method": "manual",
"authorized_amount_money": {"amount": 12900, "currency": "USD"},
"capturable_amount_money": {"amount": 0, "currency": "USD"},
"captured_amount_money": {"amount": 9800, "currency": "USD"},
"released_amount_money": {"amount": 3100, "currency": "USD"},
"authorized_at": "2026-07-03T02:57:20Z",
"captured_at": "2026-07-03T02:57:40Z",
"authorization_expires_at": "2026-07-10T02:57:19Z"
}
}
The intent is succeeded, amount_money now reflects what actually settled, and the uncaptured $31.00 shows up in released_amount_money on its way back to the buyer.
Capture is a one-shot decision. A hold supports exactly one capture; the uncaptured remainder is released to the buyer in the same operation, and there is no second bite. Capturing an intent that isn't in requires_capture (including one you already captured) returns INVALID_STATUS_FOR_CAPTURE. If you might need to charge in installments, capture the first amount and collect the rest with a separate payment.
The amount you capture must also pass three checks, each with its own error code:
- Greater than zero, or
INVALID_CAPTURE_AMOUNT. - Same currency as the authorization, or
CAPTURE_CURRENCY_MISMATCH. - No more than
authorized_amount_money, orCAPTURE_AMOUNT_EXCEEDS_AUTHORIZED. You can never capture more than you held; if the final bill grew past the hold, capture the full authorization and collect the difference as a separate payment.
Send an Idempotency-Key on every capture. If the request times out, retrying with the same key replays the original result instead of tripping INVALID_STATUS_FOR_CAPTURE against your own earlier success. See Idempotency.
Void the Hold#
If you won't be charging (the booking canceled, the review failed, the buyer changed their mind), cancel the intent to release the hold immediately:
curl -X POST https://api.withflintpay.com/v1/payment-intents/pi_1kmn0aExample/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: void-camera-rental-001" \
-d '{"cancellation_reason": "requested_by_customer"}'
{
"data": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "canceled",
"cancellation_reason": "requested_by_customer",
"authorized_amount_money": {"amount": 12900, "currency": "USD"},
"capturable_amount_money": {"amount": 0, "currency": "USD"},
"captured_amount_money": {"amount": 0, "currency": "USD"},
"released_amount_money": {"amount": 12900, "currency": "USD"}
}
}
cancellation_reason is optional and takes requested_by_customer, duplicate, fraudulent, or abandoned. No money ever moved, so there is nothing to refund; the full hold lands in released_amount_money. Flint releases the authorization immediately, but the pending charge can take a few days to disappear from the buyer's statement depending on their bank, so it's worth telling them that.
Canceling only works before capture. A captured intent returns CANNOT_CANCEL_SUCCEEDED_PAYMENT (use a refund instead), an already-canceled one returns PAYMENT_ALREADY_CANCELED, and an expired one returns CANNOT_CANCEL_EXPIRED_PAYMENT (expiry already released the funds; there's nothing left to void).
Authorization Expiry#
Card networks don't hold funds forever. Each authorization carries a deadline in authorization_expires_at, typically seven days from authorized_at for cards. When the deadline passes without a capture, the intent moves to status expired, the hold is released to the buyer, and cancellation_reason reads authorization_expired. Attempting to capture after that fails with PAYMENT_AUTHORIZATION_EXPIRED.
An expired hold is not an error state so much as a missed decision, and it's the expensive kind: the buyer walked away thinking they paid, and you delivered without collecting. Treat the deadline as an operational input:
- Read
authorization_expires_atfrom the confirm response and schedule capture ahead of it. Don't hardcode seven days; the field is the contract. - If the final amount won't be known before the deadline, capture what you can justify before expiry, or void and re-authorize closer to fulfillment.
- Subscribe to the expiry webhooks so a hold never silently evaporates. Expiry means the money is gone from the hold; collecting now requires a brand new payment from the buyer.
Manual Capture on Orders#
When the payment belongs to an order, the same hold-then-capture model runs through the order's payment flow, and the order tracks the authorization for you. The flow mirrors Embedded Payments with one change at creation and one extra call at the end:
curl -X POST https://api.withflintpay.com/v1/payment-intents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pi-order-rental-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"capture_method": "manual"
}'
Order-linked intents never take an amount; Flint derives it from the order balance. From here the buyer pays exactly as in the embedded flow: your frontend collects a PaymentMethod token and your backend calls the order's pay endpoint. Because the intent is manual capture, the pay call places the authorization instead of settling:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/pay \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pay-order-rental-001" \
-d '{
"payment_intent_ids": ["pi_1kmn0aExample"],
"payment_source_tokens": {"pi_1kmn0aExample": "pm_card_visa"},
"expected_amount_money": {"amount": 18500, "currency": "USD"}
}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"authorization_amounts": {
"authorized_money": {"amount": 18500, "currency": "USD"},
"capturable_money": {"amount": 18500, "currency": "USD"},
"expires_at": "2026-07-10T02:58:26Z"
},
"settlement_amounts": {
"paid_money": {"amount": 0, "currency": "USD"},
"amount_due_money": {"amount": 18500, "currency": "USD"}
}
}
}
The order stays open with nothing in paid_money, and authorization_amounts shows the live hold. When you're ready, capture through the order's own route (the standalone capture and cancel endpoints reject order-linked intents with ORDER_LINKED_PAYMENT_INTENT):
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/payment-intents/pi_1kmn0aExample/capture \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: capture-order-rental-001" \
-d '{}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "paid",
"settlement_amounts": {
"paid_money": {"amount": 18500, "currency": "USD"},
"amount_due_money": {"amount": 0, "currency": "USD"}
}
}
}
A full capture settles the balance and the order flips to paid. To release the hold instead, void it through the order route with an empty body: POST /v1/orders/{order_id}/payment-intents/{payment_intent_id}/void.
Rules specific to orders:
- Get the order total right before you pay. The authorization equals the order balance at pay time, and the order's financial fields lock while the hold is active; edits return
409 PAYMENT_ATTEMPT_IN_PROGRESSuntil the authorization resolves. Tips, coupons, and line-item changes belong before the pay call. - Partial capture leaves the difference due on the order. Passing
amount_moneyto the order capture route works, and the uncaptured remainder is released to the buyer, but the order staysopenwith the difference inamount_due_money. Reserve it for genuinely settling less than the order total, and collect or resolve the remainder deliberately. - Retried captures must match. Once an order authorization is captured, repeating the capture call replays cleanly with the same amount but returns
409 CAPTURE_AMOUNT_MISMATCHfor a different one. - Strict inventory blocks delayed capture. Orders holding line items with strict inventory tracking return
STRICT_INVENTORY_DELAYED_CAPTURE_UNSUPPORTED; use automatic capture for those items.
Webhooks#
Holds resolve minutes or days after they're placed, usually from a scheduled job rather than a request path, so webhooks are the durable record of each transition:
| Event | Fires when |
|---|---|
payment_intent.requires_capture | The hold is placed and awaiting capture. |
payment_intent.succeeded | The capture settled. |
payment_intent.canceled | The hold was voided or expired; cancellation_reason distinguishes the two (authorization_expired for expiry). |
order.payment_authorized | Order flow: the hold is placed, with authorized_money, capturable_money, and authorization_expires_at. |
order.payment_captured | Order flow: a capture settled, with captured_money. |
order.payment_succeeded | Order flow: the order became fully paid. A partial capture fires order.payment_captured without this one. |
order.payment_authorization_voided | Order flow: the hold was voided, with released_money. |
order.payment_authorization_expired | Order flow: the hold expired before capture, with released_money. |
Two shapes worth internalizing: there is no separate payment_intent.captured event, because a capture is the intent succeeding, and there is no separate payment_intent.expired event, because expiry is a cancellation with cancellation_reason set to authorization_expired. Key your handlers on payment_intent.succeeded and payment_intent.canceled and read the reason.
Refund or Void?#
Which unwind tool applies depends on whether money has moved:
- Before capture: void. Cancel the intent (or void through the order route). The buyer was never charged, nothing appears on their statement beyond the expiring hold, and no fees apply.
- After capture: refund. The captured amount settled, so reversing any of it is a refund, and only
captured_amount_moneyis refundable. An uncaptured intent returnsPAYMENT_INTENT_NOT_REFUNDABLE. - The released remainder is neither. After a partial capture, the uncaptured portion was released without ever being charged; it needs no refund and can't be recovered. If you released too much, that's a new payment, not a reversal.
Errors You Will Hit#
| Status | Code | What to do |
|---|---|---|
| 400 | INVALID_CAPTURE_METHOD | capture_method takes automatic or manual only. |
| 400 | MANUAL_CAPTURE_NOT_ALLOWED_WITH_AUTO_CONFIRM | Drop auto_confirm; confirm explicitly to place the hold. |
| 400 | MANUAL_CAPTURE_NOT_ALLOWED_WITH_INVOICE | Invoice payments always capture automatically. |
| 400 | INVALID_STATUS_FOR_CAPTURE | The intent isn't in requires_capture. It may be unconfirmed, already captured, canceled, or expired; fetch it and check status. |
| 400 | CAPTURE_AMOUNT_EXCEEDS_AUTHORIZED | You can't capture more than the hold. Capture the full authorization and collect the rest separately. |
| 400 | CAPTURE_CURRENCY_MISMATCH | Capture in the authorization's currency. |
| 400 | INVALID_CAPTURE_AMOUNT | The capture amount must be greater than zero. |
| 400 | PAYMENT_AUTHORIZATION_EXPIRED | The hold lapsed before capture. The funds are released; collect with a new payment. |
| 400 | CANNOT_CANCEL_SUCCEEDED_PAYMENT | Money moved. Issue a refund instead of voiding. |
| 409 | ORDER_LINKED_PAYMENT_INTENT | Use the order's pay, capture, and void routes for order-linked intents. |
| 409 | PAYMENT_ATTEMPT_IN_PROGRESS | The order's financials are locked while a hold is active. Capture or void first. |
| 409 | CAPTURE_AMOUNT_MISMATCH | This order authorization was already captured for a different amount; a retry only replays with the same amount. |
| 400 | STRICT_INVENTORY_DELAYED_CAPTURE_UNSUPPORTED | Strict-inventory line items can't sit behind a hold; use automatic capture. |
The Error Handling guide covers the envelope these arrive in and the request ids to log.
Common Mistakes#
- Treating the hold as revenue.
requires_capturemeans the money is reserved, not collected. Untilcaptured_amount_moneyis positive, you haven't been paid. - Letting authorizations expire silently. Every hold needs a scheduled capture-or-void decision before
authorization_expires_at. Alert onorder.payment_authorization_expiredandpayment_intent.canceledwith reasonauthorization_expired; each one is money you meant to collect. - Planning to capture twice. One hold, one capture. The remainder is released the moment you capture partially. Installments are separate payments.
- Waiting for a
payment_intent.capturedwebhook. It doesn't exist; a successful capture arrives aspayment_intent.succeeded. - Expecting manual capture on hosted surfaces. Checkout Sessions and Payment Links always capture automatically. Manual capture means the payment intent flow, standalone or on an order.
- Adjusting an order under an active hold. Financial edits are rejected with
PAYMENT_ATTEMPT_IN_PROGRESSwhile the authorization is open. Final total first, then pay. - Refunding a hold. Before capture there is nothing to refund; void instead. Refunds apply only to captured amounts.
Next Steps#
- Embedded Payments: the frontend half of collecting the card whose hold you're placing.
- Orders First: why the order is the right anchor for a hold with real line items.
- Refunds: reversing money after capture.
- Webhooks: signature verification and retries for the events above.
- Testing: test cards and sandbox flows for exercising every branch on this page.
- Payments API reference: every field on the payment intent, capture, and cancel endpoints.
