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. Canceling 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, cancel, 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_money | What the hold was placed for. Set at confirmation, never changes. |
capturable_money | What you can still capture. Equals the authorized amount until you capture or cancel, then drops to zero. |
captured_money | What actually settled. Zero until capture. |
released_money | What went back to the buyer: the uncaptured remainder after a partial capture, or the full hold after a cancellation 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_options": ["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",
"origin": "api"
},
"payment_collection": {
"stripe": {
"account_id": "acct_Example",
"publishable_key": "pk_test_Example",
"elements": {
"next_step": "create_confirmation_token",
"submit_to": "confirm_payment_intent",
"mode": "payment",
"payment_method_types": ["card"],
"payment_method_creation": "manual"
}
}
}
}
}
Confirming is what places the hold. Initialize Stripe Elements from payment_collection, call elements.submit(), and create a ConfirmationToken in the browser. Send only its ctoken_... ID to your backend. The browser never confirms the PaymentIntent directly. See Server-Confirmed Payments for the complete collection and action-continuation flow.
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 '{"confirmation_token": "ctoken_Example"}'
{
"data": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_capture",
"amount_money": {"amount": 12900, "currency": "USD"},
"capture_method": "manual",
"authorized_money": {"amount": 12900, "currency": "USD"},
"capturable_money": {"amount": 12900, "currency": "USD"},
"captured_money": {"amount": 0, "currency": "USD"},
"released_money": {"amount": 0, "currency": "USD"},
"authorized_at": "2026-07-03T02:57:20Z",
"authorization_expires_at": "2026-07-10T02:57:19Z",
"payment_source": {
"card": {
"brand": "visa",
"last4": "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_money": {"amount": 12900, "currency": "USD"},
"capturable_money": {"amount": 0, "currency": "USD"},
"captured_money": {"amount": 9800, "currency": "USD"},
"released_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_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 four 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
capturable_money, orCAPTURE_AMOUNT_EXCEEDS_CAPTURABLE. 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. - Within the processing price authorized for this merchant, or
PROCESSING_FEE_PRICING_NOT_AUTHORIZED. A tiny partial capture can fail here with the hold already placed; cancel instead. Pricing configuration is not part of the public API. See Processing Fees.
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.
Cancel 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: cancel-camera-rental-001" \
-d '{"cancellation_reason": "requested_by_customer"}'
{
"data": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "canceled",
"cancellation_reason": "requested_by_customer",
"authorized_money": {"amount": 12900, "currency": "USD"},
"capturable_money": {"amount": 0, "currency": "USD"},
"captured_money": {"amount": 0, "currency": "USD"},
"released_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_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).
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. Capturing a standalone intent after that fails with INVALID_STATUS_FOR_CAPTURE, because the intent is no longer in requires_capture. On the order-scoped capture route, the same lapsed hold reports 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 cancel 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#
Order-linked PaymentIntents are created and managed through the order. Create one manual-capture leg for the outstanding balance:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/payment-intents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pi-order-rental-001" \
-d '{"capture_method": "manual"}'
Submit the buyer's token through PayOrder. Manual capture supports one leg per attempt; split delayed capture is rejected.
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_intents": [{
"payment_intent_id": "pi_1kmn0aExample",
"token": "pm_card_visa"
}],
"expected_outstanding_money": {"amount": 18500, "currency": "USD"}
}'
{
"data": {
"order": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"payment_status": "unpaid",
"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"},
"outstanding_money": {"amount": 18500, "currency": "USD"}
}
},
"payment_attempt": {
"payment_attempt_id": "opat_1kmn0aExample",
"status": "requires_capture",
"is_resumable": true
}
}
}
The authorization has not advanced payment_status; only captured money does that. Capture with the active attempt ID:
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 '{"payment_attempt_id": "opat_1kmn0aExample"}'
A full capture returns an order with status: "closed", payment_status: "paid", and settlement_amounts.outstanding_money.amount: 0.
To release the hold instead, call the order-scoped cancel route with the same attempt ID:
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/payment-intents/pi_1kmn0aExample/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: cancel-order-rental-001" \
-d '{
"payment_attempt_id": "opat_1kmn0aExample",
"cancellation_reason": "requested_by_customer"
}'
Rules specific to orders:
- Finalize pricing before authorization. Financial mutations are rejected while the authorization attempt is active.
- Partial capture leaves a partially paid order. The uncaptured remainder is released,
statusremainsopen,payment_statusbecomespartially_paid, andoutstanding_moneycarries the remainder. - Retried captures must match. Repeating a completed capture with a different amount returns
CAPTURE_AMOUNT_MISMATCH. - Reserved inventory stays claimed through authorization. A single manual-capture leg keeps its inventory reservation claims until capture, cancellation, or expiration. Capture consumes the committed quantity; cancellation and expiration release it.
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 canceled 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.paid | Order flow: the order became fully paid. A partial capture fires order.payment_captured without this one. |
order.payment_authorization_canceled | Order flow: the hold was canceled, 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 Cancel?#
Which unwind tool applies depends on whether money has moved:
- Before capture: cancel. Cancel the intent through its owning 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_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_CAPTURABLE | You can't capture more than capturable_money. 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 | PROCESSING_FEE_PRICING_NOT_AUTHORIZED | No authorized price covers the capture amount, usually because it is too small. Cancel the hold and collect the amount separately. See Processing Fees. |
| 400 | PAYMENT_AUTHORIZATION_EXPIRED | Order-scoped capture only: the hold lapsed before capture. The funds are released; collect with a new payment. Standalone capture reports the lapse as INVALID_STATUS_FOR_CAPTURE. |
| 400 | CANNOT_CANCEL_SUCCEEDED_PAYMENT | Money moved. Issue a refund instead of canceling. |
| 409 | ORDER_PAYMENT_FLOW_REQUIRED | Use the order's pay, capture, and cancel routes for order-linked intents. |
| 409 | PAYMENT_ATTEMPT_IN_PROGRESS | The order's financials are locked while a hold is active. Capture or cancel first. |
| 409 | CAPTURE_AMOUNT_MISMATCH | This order authorization was already captured for a different amount; a retry only replays with the same amount. |
| 400 | ORDER_DELAYED_CAPTURE_SPLIT_PAYMENT_UNSUPPORTED | Use one PaymentIntent for delayed 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_moneyis positive, you haven't been paid. - Letting authorizations expire silently. Every hold needs a scheduled capture-or-cancel 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; cancel 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.
