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.

text
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:

FieldMeaning
authorized_amount_moneyWhat the hold was placed for. Set at confirmation, never changes.
capturable_amount_moneyWhat you can still capture. Equals the authorized amount until you capture or void, then drops to zero.
captured_amount_moneyWhat actually settled. Zero until capture.
released_amount_moneyWhat 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.

Bash
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"
  }'
JSON
{
  "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:

Bash
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"}'
JSON
{
  "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:

Bash
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:

Bash
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"}}'
JSON
{
  "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, or CAPTURE_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:

Bash
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"}'
JSON
{
  "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_at from 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:

Bash
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:

Bash
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"}
  }'
JSON
{
  "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):

Bash
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 '{}'
JSON
{
  "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_PROGRESS until 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_money to the order capture route works, and the uncaptured remainder is released to the buyer, but the order stays open with the difference in amount_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_MISMATCH for 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:

EventFires when
payment_intent.requires_captureThe hold is placed and awaiting capture.
payment_intent.succeededThe capture settled.
payment_intent.canceledThe hold was voided or expired; cancellation_reason distinguishes the two (authorization_expired for expiry).
order.payment_authorizedOrder flow: the hold is placed, with authorized_money, capturable_money, and authorization_expires_at.
order.payment_capturedOrder flow: a capture settled, with captured_money.
order.payment_succeededOrder flow: the order became fully paid. A partial capture fires order.payment_captured without this one.
order.payment_authorization_voidedOrder flow: the hold was voided, with released_money.
order.payment_authorization_expiredOrder 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_money is refundable. An uncaptured intent returns PAYMENT_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#

StatusCodeWhat to do
400INVALID_CAPTURE_METHODcapture_method takes automatic or manual only.
400MANUAL_CAPTURE_NOT_ALLOWED_WITH_AUTO_CONFIRMDrop auto_confirm; confirm explicitly to place the hold.
400MANUAL_CAPTURE_NOT_ALLOWED_WITH_INVOICEInvoice payments always capture automatically.
400INVALID_STATUS_FOR_CAPTUREThe intent isn't in requires_capture. It may be unconfirmed, already captured, canceled, or expired; fetch it and check status.
400CAPTURE_AMOUNT_EXCEEDS_AUTHORIZEDYou can't capture more than the hold. Capture the full authorization and collect the rest separately.
400CAPTURE_CURRENCY_MISMATCHCapture in the authorization's currency.
400INVALID_CAPTURE_AMOUNTThe capture amount must be greater than zero.
400PAYMENT_AUTHORIZATION_EXPIREDThe hold lapsed before capture. The funds are released; collect with a new payment.
400CANNOT_CANCEL_SUCCEEDED_PAYMENTMoney moved. Issue a refund instead of voiding.
409ORDER_LINKED_PAYMENT_INTENTUse the order's pay, capture, and void routes for order-linked intents.
409PAYMENT_ATTEMPT_IN_PROGRESSThe order's financials are locked while a hold is active. Capture or void first.
409CAPTURE_AMOUNT_MISMATCHThis order authorization was already captured for a different amount; a retry only replays with the same amount.
400STRICT_INVENTORY_DELAYED_CAPTURE_UNSUPPORTEDStrict-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_capture means the money is reserved, not collected. Until captured_amount_money is positive, you haven't been paid.
  • Letting authorizations expire silently. Every hold needs a scheduled capture-or-void decision before authorization_expires_at. Alert on order.payment_authorization_expired and payment_intent.canceled with reason authorization_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.captured webhook. It doesn't exist; a successful capture arrives as payment_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_PROGRESS while 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.
Rate this doc