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.

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_moneyWhat the hold was placed for. Set at confirmation, never changes.
capturable_moneyWhat you can still capture. Equals the authorized amount until you capture or cancel, then drops to zero.
captured_moneyWhat actually settled. Zero until capture.
released_moneyWhat 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.

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_options": ["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",
      "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.

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

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_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, or CAPTURE_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:

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: cancel-camera-rental-001" \
  -d '{"cancellation_reason": "requested_by_customer"}'
JSON
{
  "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_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 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:

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

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_intents": [{
      "payment_intent_id": "pi_1kmn0aExample",
      "token": "pm_card_visa"
    }],
    "expected_outstanding_money": {"amount": 18500, "currency": "USD"}
  }'
JSON
{
  "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:

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

Bash
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, status remains open, payment_status becomes partially_paid, and outstanding_money carries 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:

EventFires when
payment_intent.requires_captureThe hold is placed and awaiting capture.
payment_intent.succeededThe capture settled.
payment_intent.canceledThe hold was canceled 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.paidOrder flow: the order became fully paid. A partial capture fires order.payment_captured without this one.
order.payment_authorization_canceledOrder flow: the hold was canceled, 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 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_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_CAPTURABLEYou can't capture more than capturable_money. 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.
400PROCESSING_FEE_PRICING_NOT_AUTHORIZEDNo authorized price covers the capture amount, usually because it is too small. Cancel the hold and collect the amount separately. See Processing Fees.
400PAYMENT_AUTHORIZATION_EXPIREDOrder-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.
400CANNOT_CANCEL_SUCCEEDED_PAYMENTMoney moved. Issue a refund instead of canceling.
409ORDER_PAYMENT_FLOW_REQUIREDUse the order's pay, capture, and cancel routes for order-linked intents.
409PAYMENT_ATTEMPT_IN_PROGRESSThe order's financials are locked while a hold is active. Capture or cancel first.
409CAPTURE_AMOUNT_MISMATCHThis order authorization was already captured for a different amount; a retry only replays with the same amount.
400ORDER_DELAYED_CAPTURE_SPLIT_PAYMENT_UNSUPPORTEDUse 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_capture means the money is reserved, not collected. Until captured_money is positive, you haven't been paid.
  • Letting authorizations expire silently. Every hold needs a scheduled capture-or-cancel 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; 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.
Rate this doc