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.
Manual capture currently needs a server-to-server charge: the create must
carry payment_method.details.card_token. Browser-collected card payments cannot use it yet, and it
is rejected on a zero-amount setup.
1. Authorize
Add capture_method: "manual" to a POST /v1/payments that charges a token:
{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "card",
"details": {"card_token": "tok_1PqRsT2eZvKYlo2C"}
},
"capture_method": "manual",
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel"
}
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.
If the issuer demands 3-D Secure you get requires_action instead; 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 "$ORCHESTR_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $ORCHESTR_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.
Statuses
status | Meaning | captured_amount | voided_amount |
|---|---|---|---|
authorized | Funds held, nothing moved. | — | — |
settling | Capture accepted by the provider, not yet settled. | — | — |
completed | The capture settled and closed the authorization. | Set | — |
canceled | A void released the hold. | Set if a capture settled first | 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. |
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.
- Errors — the complete error-code reference.
- Webhooks — payload shapes for the capture events.