Capture and cancel
By default a payment is captured automatically: the customer pays and the collection completes in one step. When you need to authorize now and collect later, use manual capture: the payment is authorized at checkout, funds are held, and you capture (fully or partially) when you are ready, or cancel to release the hold if you never do.
Use manual capture when the collectible amount is not final at checkout: authorize when the customer commits, and capture on fulfilment or shipment.
Authorize-now-collect-later only exists on payment methods that separate authorization from
capture. Methods where the collection is an immediate transfer have no authorization to hold,
so they are always automatic: there is nothing to capture, and nothing to cancel either,
because a collection that is never paid simply expires.
Which behaviour applies to you depends on the payment methods enabled for your account. You can see them in your dashboard, and support can confirm the detail for any method you plan to use.
What you will see if you ask anyway. Sending capture_method: "manual" on a method that
cannot authorize separately is not rejected. The payment comes back
requires_payment_method (payment_method_awaited on the attempt) with no error, and the
subsequent capture fails with ERR_3004 because nothing was ever authorized. UPI is the
common case: it is a collection, not an authorization, so there is no hold to capture.
Automatic vs manual capture
You choose the behaviour with capture_method on POST /payments.
capture_method | Behaviour |
|---|---|
automatic (default) | The payment authorizes and captures in one step, landing at succeeded. |
manual | The payment authorizes and stops at requires_capture; the authorization stays in place until you capture or cancel. |
The statuses involved
A manual-capture payment moves through these states:
| Status | Meaning |
|---|---|
requires_capture | Authorized; funds held; awaiting your capture. The authorized value is in amount_capturable. |
succeeded | Fully captured. Money movement is complete. amount_received shows the captured total. |
partially_captured | Captured for less than the authorized amount. Money movement is complete at the captured amount; amount_received shows the captured total. |
cancelled | Cancelled before capture; the authorization hold is released. Nothing is collected. |
Two fields on the payment object track the amounts:
amount_capturable: how much of the authorization is still capturable. Nonzero only while a manual-capture payment awaits capture;0once fully captured.amount_received: the total captured so far.
Authorize with manual capture
Create the payment with capture_method: "manual", then confirm it as usual. On an authorized
manual-capture payment the confirm lands at requires_capture rather than succeeded.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 6540,
"currency": "USD",
"capture_method": "manual",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": { "order_id": "12345" }
}'
const r = await fetch("https://api.tensorrail.com/payments", {
method: "POST",
headers: {
"api-key": process.env.TENSORRAIL_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 6540,
currency: "USD",
capture_method: "manual",
return_url: "https://yourshop.com/checkout/complete",
metadata: { order_id: "12345" },
}),
});
const payment = await r.json();
import requests
r = requests.post(
"https://api.tensorrail.com/payments",
headers={"api-key": "rail_full_test_xxx"},
json={
"amount": 6540,
"currency": "USD",
"capture_method": "manual",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": {"order_id": "12345"},
},
)
payment = r.json()
Once authorized, the payment sits at requires_capture with the authorized value in
amount_capturable:
{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "requires_capture",
"amount": 6540,
"amount_capturable": 6540,
"amount_received": null,
"currency": "USD",
"capture_method": "manual"
}
Capture
POST /payments/{payment_id}/capture captures a payment in requires_capture, moving it to
succeeded. The request must send Content-Type: application/json and a JSON body; an empty
object ({}) captures the full authorized amount.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/capture \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{}'
const r = await fetch(
"https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/capture",
{
method: "POST",
headers: {
"api-key": process.env.TENSORRAIL_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
const captured = await r.json();
import requests
r = requests.post(
"https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/capture",
headers={"api-key": "rail_full_test_xxx"},
json={},
)
captured = r.json()
A payment_succeeded webhook is delivered for a full capture (and only payment_succeeded;
no payment_captured event fires for a full capture).
Partial capture
Capture less than the authorized amount by passing amount_to_capture (minor units). It
defaults to the full authorized amount and cannot exceed amount_capturable.
# Authorized USD 65.40; capture USD 50.00.
curl -X POST https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/capture \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "amount_to_capture": 5000 }'
Partial-capture rules:
amount_to_capturecannot exceedamount_capturable. Attempting to capture more than was authorized is rejected.- Capturing settles the payment at the captured amount. After a partial capture,
amount_receivedreflects what you captured and the payment moves topartially_captured(notsucceeded); the uncaptured remainder of the authorization is not collected. - A partial capture fires only the
payment_capturedwebhook — nopayment_succeededevent is delivered for it. Key your fulfilment offpayment_capturedwhen you capture partially. - Refunds are bounded by what you captured, not what you authorized. A later refund cannot
exceed the captured amount (
ERR_3005,refund_exceeds_capture). See Refunds. - Capturing a payment that is not in
requires_captureis rejected withERR_3004(invalid_state_transition).
Cancel an uncaptured payment
POST /payments/{payment_id}/cancel voids a payment before capture, releasing the
authorization hold. The payment moves to cancelled and nothing is collected. An optional
cancellation_reason is stored on the payment.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/cancel \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "cancellation_reason": "customer abandoned checkout" }'
const r = await fetch(
"https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/cancel",
{
method: "POST",
headers: {
"api-key": process.env.TENSORRAIL_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ cancellation_reason: "customer abandoned checkout" }),
},
);
const cancelled = await r.json();
import requests
r = requests.post(
"https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/cancel",
headers={"api-key": "rail_full_test_xxx"},
json={"cancellation_reason": "customer abandoned checkout"},
)
cancelled = r.json()
A payment_cancelled webhook follows. Cancel is valid from the pre-capture states, including
requires_capture for an uncaptured authorization.
Cancel vs refund
A captured payment cannot be cancelled. Once money has moved (succeeded), issue a
refund instead; attempting to cancel it is rejected with ERR_3004. The rule
of thumb:
- Money not yet captured → cancel to release the hold.
- Money already captured → refund to return it.
Worked example: authorize at checkout, capture on shipment
const SECRET = process.env.TENSORRAIL_SECRET_KEY;
async function tr(path, body) {
const r = await fetch(`https://api.tensorrail.com${path}`, {
method: "POST",
headers: {
"api-key": SECRET,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
return r.json();
}
// At checkout: authorize the full order total.
async function authorize(order) {
const payment = await tr("/payments", {
amount: order.total_minor,
currency: order.currency,
capture_method: "manual",
confirm: true,
payment_method: order.method,
payment_method_type: order.method_type,
payment_method_data: order.method_data,
return_url: "https://yourshop.com/checkout/complete",
metadata: { order_id: order.id },
});
return payment; // status: requires_capture (once authorized)
}
// On shipment: capture what actually shipped (may be less than authorized).
async function captureOnShipment(paymentId, shippedMinor) {
return tr(`/payments/${paymentId}/capture`, { amount_to_capture: shippedMinor });
}
// If the order is abandoned or cancelled before shipment: release the hold.
async function releaseHold(paymentId) {
return tr(`/payments/${paymentId}/cancel`, { cancellation_reason: "order not fulfilled" });
}
Next steps
You can authorize, capture, and release a hold. What usually comes next:
- Refunds: return money after a capture has cleared.
- Handle webhooks: drive capture and cancel outcomes off the authoritative event.
- API reference: Payments: full field tables for capture and cancel.
TensorRail, Limassol, Cyprus.