Manual capture
By default a card payment authorizes and captures in one step. With capture_method: "manual" the
two are separated: the create places a hold on the cardholder's funds, and you settle it later with
a capture - or release it with a void.
Use it when you charge on dispatch rather than on order: physical goods, bookings with a cancellation window, anything where the final amount is only known after the fact.
On a direct payment, manual capture currently needs a card that is already on file: the create must
carry payment_method.details.card_token, whether as a
merchant-initiated charge or a
customer-present collect_cvv charge. A card
collected fresh through collect_card cannot use it yet, and it is rejected on a
zero-amount setup.
Hosted checkout sessions accept capture_method: "manual"
without either restriction.
1. Authorize
Add capture_method: "manual" to a POST /v1/payments that charges a token (for a hosted checkout,
see Manual capture on hosted checkout - the capture and void
steps below are the same). This example is a
merchant-initiated charge, so it authorizes in the create itself:
{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "card",
"details": {
"card_token": "tok_1PqRsT2eZvKYlo2C",
"stored_credential": {
"initiator": "merchant",
"reason": "unscheduled"
}
}
},
"capture_method": "manual",
"merchant_reference": "order-1001"
}
A successful authorization returns status: "authorized" - funds are held, nothing has moved:
{
"order_id": "pay_5b4a3c2d",
"transaction_id": "txn_9f8e7d6c",
"merchant_reference": "order-1001",
"status": "authorized",
"amount": 42.00,
"currency": "GBP"
}
Keep transaction_id - it is the authorization's id, and the most precise way to target the
capture later.
With the customer present - the same create without stored_credential - the response is
requires_action with a collect_cvv action
instead: the customer confirms the security code in Upprove.js, pay() resolves with
status: "succeeded" at the authorization (the payment.authorized webhook is the one to wait
for), and the hold is then captured or voided exactly as below. With
collect_cvv: false the create authorizes at once like the example above; if the issuer demands
3-D Secure you get requires_action and complete the challenge as described in
Server-to-server card payments, after which the payment
settles to authorized.
2. Capture
POST /v1/payments/capture. Identify the authorization with exactly one of order_id,
transaction_id, or merchant_reference:
REQ_PATH="/v1/payments/capture"
BODY='{
"transaction_id": "txn_9f8e7d6c"
}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$UPPROVE_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $UPPROVE_SECRET_KEY" \
-H "X-Signature: t=${TS}, v1=${SIG}" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
Omitting amount captures the full remaining authorized amount. The response carries
captured_amount:
{
"order_id": "pay_5b4a3c2d",
"transaction_id": "txn_2c3d4e5f",
"status": "completed",
"amount": 42.00,
"captured_amount": 42.00,
"currency": "GBP"
}
transaction_id must be the authorization'sPassing a capture's or refund's transaction_id is refused. When in doubt, target by order_id or
merchant_reference.
Partial and multiple captures
Send an amount lower than the authorized total to capture part of it. The final flag decides
what happens to the rest:
final | Effect |
|---|---|
true (default) | Settles this amount and releases the uncaptured remainder. The authorization closes. |
false | Settles this amount and keeps the remainder capturable. |
{
"order_id": "pay_5b4a3c2d",
"amount": "30.00",
"final": false
}
Omitting final is the same as true - a capture closes the authorization unless you explicitly
say otherwise.
Multiple captures depend on the provider granting multicapture on the authorization. Where they
haven't, the second capture fails with the decline code multicapture_not_supported.
The authorization closes when a final capture settles, when the captured total reaches amount, or
when you void it.
3. Void
To release a hold you no longer need, POST /v1/payments/void with the same one-of target. There is
no amount - a void always releases whatever remains:
{
"order_id": "pay_5b4a3c2d"
}
{
"order_id": "pay_5b4a3c2d",
"transaction_id": "txn_7a8b9c0d",
"status": "canceled",
"amount": 42.00,
"voided_amount": 42.00,
"currency": "GBP"
}
voided_amount is what was actually released - the whole hold if nothing was captured, or only the
remainder if a partial capture settled first. It is not necessarily amount.
Voiding a payment that is already canceled returns 409 payment_unexpected_state, not a repeat of
the original result. If a void times out, check the payment's state before retrying rather than
firing again.
POST /v1/payments/{id}/3ds/complete is idempotent - don't generalize from one to the other.
Manual capture on hosted checkout
Manual capture works on a hosted checkout session
too. Add capture_method: "manual" to POST /v1/checkout/sessions:
{
"amount": "42.00",
"currency": "GBP",
"capture_method": "manual",
"merchant_reference": "order-1001",
"customer": {
"email": "jane@example.com"
},
"success_url": "https://shop.example.com/checkout/success",
"cancel_url": "https://shop.example.com/checkout/cancel"
}
The response echoes it, and so do GET /v1/checkout/sessions/{id} and the search:
{
"id": "cs_7e6d5c4b",
"url": "https://checkout.sandbox.upprove.com/cs_7e6d5c4b",
"status": "open",
"capture_method": "manual",
"merchant_reference": "order-1001",
"order_id": "pay_5b4a3c2d",
"amount": 42.00,
"currency": "GBP",
"created_at": "2024-06-22T10:00:00Z"
}
When the customer pays by card on the hosted page, the payment stops at the authorization. From the
customer's side that is the end of the checkout: the session becomes completed, they see the success
page and return to your success_url. Nothing has moved yet.
You are told twice, in this order:
| Event | Meaning |
|---|---|
checkout.session.completed | The session is completed. Its payload carries capture_method: "manual" and the order_id and transaction_id you capture with. |
payment.authorized | The authorization succeeded and funds are held. |
Do not fulfil on checkout.session.completed alone when the session is manual capture: the money
arrives only when you capture. Target the capture (or void) with the
session's order_id, its transaction_id (the authorization), or your merchant_reference -
exactly as for a direct payment. The closing capture emits payment.succeeded; the session is
already completed and does not change again.
capture_method applies to card payments. An alternative payment method the customer picks on the
hosted page settles as it always has, and the session completes when that payment succeeds.
Statuses
status | Meaning | captured_amount | voided_amount |
|---|---|---|---|
authorized | Funds held, nothing moved. | - | - |
settling | Capture accepted by the provider, not yet settled. | - | - |
completed | The authorization closed with money settled - by a capture, or by a void behind earlier captures. | Set | Set on a void |
canceled | A void released the whole hold; nothing was captured. | - | Set |
failed | The capture or void did not succeed. | - | - |
Webhooks
Manual capture has its own events. Subscribe to them - on the 3-D Secure path you are not in the
request that decides the outcome, so payment.authorized is your only notification:
| Event | Fires when |
|---|---|
payment.authorized | The authorization succeeded and funds are held. |
payment.partially_captured | A capture settled but left the authorization open (final: false). |
payment.capture_failed | A capture did not settle. No money moved; the authorization stands. |
payment.succeeded | The capture that closed the authorization settled. Fires once per payment. |
payment.canceled | A void released the hold with nothing captured. |
payment.succeeded carries captured_amount and the closing capture's capture_transaction_id and
capture_provider_transaction_id. A void behind settled captures closes the payment the same way.
On a hosted checkout session, checkout.session.completed fires alongside payment.authorized, at
authorization - see Manual capture on hosted checkout.
See Webhook events for the payload.
Errors
code | HTTP | When it happens |
|---|---|---|
payment_unexpected_state | 409 | Capturing an already-captured payment, voiding after a final capture, or operating on a non-card payment. The message names the verb, the current status, and the expected one. |
capture_amount_exceeds_authorized | 400 | The requested amount is more than was authorized, or is not positive. |
capture_amount_exceeds_remaining | 400 | The requested amount is more than the uncaptured remainder, counting captures still in flight. |
refund_requires_transaction_target | 400 | Refunding by order_id a payment captured in parts. Name the specific capture by transaction_id. |
A capture or void the provider refuses is not an error: it returns 200 with
status: "failed". Only the pre-flight checks above produce 4xx.
Refunding a partially captured payment
A refund reverses exactly one settlement. When a payment has more than one settled capture,
refunding by order_id is ambiguous and is refused with refund_requires_transaction_target - pass
the transaction_id of the capture you want to reverse instead.
Next steps
- Server-to-server card payments - the authorization step in full.
- Getting started - creating a hosted checkout session.
- Errors - the complete error-code reference.
- Webhooks - payload shapes for the capture events.