Skip to main content

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.

Manual capture is a property of the payment method

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_methodBehaviour
automatic (default)The payment authorizes and captures in one step, landing at succeeded.
manualThe 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:

StatusMeaning
requires_captureAuthorized; funds held; awaiting your capture. The authorized value is in amount_capturable.
succeededFully captured. Money movement is complete. amount_received shows the captured total.
partially_capturedCaptured for less than the authorized amount. Money movement is complete at the captured amount; amount_received shows the captured total.
cancelledCancelled 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; 0 once 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 -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" }
}'

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 -X POST https://api.tensorrail.com/payments/pay_N5cPeGw6uS2QIMjnsjVF/capture \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{}'

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_capture cannot exceed amount_capturable. Attempting to capture more than was authorized is rejected.
  • Capturing settles the payment at the captured amount. After a partial capture, amount_received reflects what you captured and the payment moves to partially_captured (not succeeded); the uncaptured remainder of the authorization is not collected.
  • A partial capture fires only the payment_captured webhook — no payment_succeeded event is delivered for it. Key your fulfilment off payment_captured when 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_capture is rejected with ERR_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 -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" }'

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 capturedcancel to release the hold.
  • Money already capturedrefund 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:

TensorRail, Limassol, Cyprus.