---
title: "Capture or cancel"
description: "Hold an amount on a card, capture all or part of it later, or cancel the payment."
url: https://docs.fluveo.com/payments/capture-and-cancel/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/payments/capture-and-cancel.curl.md (cURL), https://docs.fluveo.com/payments/capture-and-cancel.node.md (Node.js), https://docs.fluveo.com/payments/capture-and-cancel.python.md (Python), https://docs.fluveo.com/payments/capture-and-cancel.php.md (PHP).

# Capture or cancel

Hold an amount on a card, capture all or part of it later, or cancel the payment.

A hold reserves money on the buyer's card without taking it. You decide later: capture the whole amount, capture a smaller part, or cancel and let the buyer have it all back. Use a hold when the final amount or the sale itself is not settled at checkout, such as an order you still have to pack, or a booking you confirm by hand.

Three calls cover it, all on the PaymentIntent:

| Step | Call | The PaymentIntent after it |
| --- | --- | --- |
| Hold | `POST /v1/payment_intents` with `capture_method=manual` | `requires_capture` |
| Capture | `POST /v1/payment_intents/{intent}/capture` | `succeeded` |
| Cancel | `POST /v1/payment_intents/{intent}/cancel` | `canceled` |

Every amount on this page is in the smallest unit of the currency: `5000` is 50.00. Send an `Idempotency-Key` with each call, so a retry after a timeout never holds, captures or cancels twice (see [Idempotency](https://docs.fluveo.com/start/idempotency.md)).

## Hold an amount

Create and confirm a card PaymentIntent with `capture_method=manual`. The bank approves the card and reserves the amount, and nothing reaches your balance yet.

**Hold 50.00 on a card**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-1043-hold" \
  -d amount=5000 \
  -d currency=usd \
  -d "payment_method_types[]=card" \
  -d payment_method=pm_card_visa \
  -d capture_method=manual \
  -d confirm=true
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_intents", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "order-1043-hold",
  },
  body: new URLSearchParams({
    amount: "5000",
    currency: "usd",
    "payment_method_types[]": "card",
    payment_method: "pm_card_visa",
    capture_method: "manual",
    confirm: "true",
  }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.status, paymentIntent.amount_capturable);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "order-1043-hold",
    },
    data={
        "amount": 5000,
        "currency": "usd",
        "payment_method_types[]": "card",
        "payment_method": "pm_card_visa",
        "capture_method": "manual",
        "confirm": "true",
    },
)
payment_intent = response.json()
print(payment_intent["status"], payment_intent["amount_capturable"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: order-1043-hold",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "amount" => 5000,
        "currency" => "usd",
        "payment_method_types" => ["card"],
        "payment_method" => "pm_card_visa",
        "capture_method" => "manual",
        "confirm" => "true",
    ]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["status"] . " " . $paymentIntent["amount_capturable"];
```

The answer shows the hold. `status` is `requires_capture`, `amount_capturable` is what you may still capture, and `amount_received` stays `0` until you do.

*200 response (trimmed)*

```json
{
  "id": "pi_...",
  "object": "payment_intent",
  "amount": 5000,
  "amount_capturable": 5000,
  "amount_received": 0,
  "capture_method": "manual",
  "currency": "usd",
  "latest_charge": "ch_...",
  "status": "requires_capture"
}
```

A hold does not last forever: the bank keeps it open for a limited time. The Charge of the hold (the PaymentIntent's `latest_charge`) tells you until when; [Cards](https://docs.fluveo.com/payments/cards.md#manual-capture) shows where to read it. Capture before then.

A card that asks for 3D Secure answers `requires_action` first, and reaches `requires_capture` only after the buyer completes the check; see [3D Secure](https://docs.fluveo.com/payments/cards.md#3d-secure).

## Capture all or part

Capture a PaymentIntent in `requires_capture`. Leave `amount_to_capture` out to take everything that is held. Send a smaller `amount_to_capture` to take only part: here 30.00 of the 50.00.

**Capture 30.00 of the hold**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_intents/pi_.../capture \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-1043-capture" \
  -d amount_to_capture=3000
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_intents/pi_.../capture", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "order-1043-capture",
  },
  body: new URLSearchParams({ amount_to_capture: "3000" }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.status, paymentIntent.amount_received);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents/pi_.../capture",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "order-1043-capture",
    },
    data={"amount_to_capture": 3000},
)
payment_intent = response.json()
print(payment_intent["status"], payment_intent["amount_received"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents/pi_.../capture");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: order-1043-capture",
    ],
    CURLOPT_POSTFIELDS => http_build_query(["amount_to_capture" => 3000]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["status"] . " " . $paymentIntent["amount_received"];
```

The PaymentIntent is now `succeeded`. `amount_received` is the part you took, and `amount_capturable` drops to `0`.

*200 response (trimmed)*

```json
{
  "id": "pi_...",
  "object": "payment_intent",
  "amount": 5000,
  "amount_capturable": 0,
  "amount_received": 3000,
  "capture_method": "manual",
  "currency": "usd",
  "status": "succeeded"
}
```

A PaymentIntent is captured once. The 20.00 you did not take is released to the buyer at that moment, and no later call can capture it. `amount` keeps the 50.00 you held. On the Charge, `amount_captured` is `3000` and `captured` is `true`. To give back part of what you took, [refund it](https://docs.fluveo.com/payments/refunds.md): a refund can return up to the captured amount.

The same call also takes `metadata` and the statement text of the captured payment, listed under [Parameters](https://docs.fluveo.com/payments/capture-and-cancel.md#parameters).

If the capture answers `processing`, it is not confirmed yet. Wait for `payment_intent.succeeded`, or read the PaymentIntent again. If a capture does not succeed, read the PaymentIntent's current status before deciding what to do next.

## Cancel a payment

Cancel a PaymentIntent you will not complete. On a hold, the cancel releases the whole held amount to the buyer: `amount_capturable` becomes `0` and nothing reaches your balance. `cancellation_reason` is optional and records why.

The first hold is captured now, so place a second one to cancel. It is the same call with its own `Idempotency-Key`.

**Hold another 50.00**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-1044-hold" \
  -d amount=5000 \
  -d currency=usd \
  -d "payment_method_types[]=card" \
  -d payment_method=pm_card_visa \
  -d capture_method=manual \
  -d confirm=true
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_intents", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "order-1044-hold",
  },
  body: new URLSearchParams({
    amount: "5000",
    currency: "usd",
    "payment_method_types[]": "card",
    payment_method: "pm_card_visa",
    capture_method: "manual",
    confirm: "true",
  }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.id, paymentIntent.status);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "order-1044-hold",
    },
    data={
        "amount": 5000,
        "currency": "usd",
        "payment_method_types[]": "card",
        "payment_method": "pm_card_visa",
        "capture_method": "manual",
        "confirm": "true",
    },
)
payment_intent = response.json()
print(payment_intent["id"], payment_intent["status"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: order-1044-hold",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "amount" => 5000,
        "currency" => "usd",
        "payment_method_types" => ["card"],
        "payment_method" => "pm_card_visa",
        "capture_method" => "manual",
        "confirm" => "true",
    ]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["id"] . " " . $paymentIntent["status"];
```

Then cancel it with the new `pi_...` id.

**Cancel the hold**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_intents/pi_.../cancel \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-1044-cancel" \
  -d cancellation_reason=requested_by_customer
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_intents/pi_.../cancel", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "order-1044-cancel",
  },
  body: new URLSearchParams({ cancellation_reason: "requested_by_customer" }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.status, paymentIntent.cancellation_reason);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents/pi_.../cancel",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "order-1044-cancel",
    },
    data={"cancellation_reason": "requested_by_customer"},
)
payment_intent = response.json()
print(payment_intent["status"], payment_intent["cancellation_reason"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents/pi_.../cancel");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: order-1044-cancel",
    ],
    CURLOPT_POSTFIELDS => http_build_query(["cancellation_reason" => "requested_by_customer"]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["status"] . " " . $paymentIntent["cancellation_reason"];
```

*200 response (trimmed)*

```json
{
  "id": "pi_...",
  "object": "payment_intent",
  "amount": 5000,
  "amount_capturable": 0,
  "amount_received": 0,
  "canceled_at": 1762862400,
  "cancellation_reason": "requested_by_customer",
  "status": "canceled"
}
```

`canceled` is final: the PaymentIntent takes no capture, confirm or second cancel after it. To charge the buyer again, create a new PaymentIntent.

### When a cancel works

The cancel looks at the PaymentIntent's `status` when it arrives:

| `status` | What the cancel answers |
| --- | --- |
| `requires_payment_method` | 200, `canceled`. |
| `requires_confirmation` | 200, `canceled`. |
| `requires_action` | 200, `canceled`. A Boleto that waits for its payment is the exception: it can still be paid at a bank, so the cancel answers 400 `invalid_request_error` with no `code` ([errors without a code](https://docs.fluveo.com/errors.md#no-code)). |
| `requires_capture` | 200, `canceled`. The hold is released. |
| `processing` | Not canceled. 400 `payment_intent_unexpected_state`, or, while a capture of it is still being confirmed, 200 with the PaymentIntent unchanged. |
| `succeeded` | 400 `payment_intent_unexpected_state`. The money was taken: [refund it](https://docs.fluveo.com/payments/refunds.md) instead. |
| `canceled` | 400 `payment_intent_unexpected_state`. |

## Parameters

### Hold

**Body parameters of POST /v1/payment_intents**

- `capture_method` (string, optional): `manual` holds the amount until you capture or cancel. `automatic` and `automatic_async` capture as soon as the card is approved. Card only.
- `amount` (integer, required): The amount to hold, in the smallest unit of the currency.
- `currency` (string, required): Three-letter code in lowercase, one your store accepts.
- `payment_method_types` (array, optional): `card` for a hold.
- `payment_method` (string, optional): The card's `pm_...` id. In test mode, `pm_card_visa`.
- `confirm` (boolean, optional): `true` creates and confirms in one call.

### Capture

**Body parameters of POST /v1/payment_intents/{intent}/capture**

- `amount_to_capture` (integer, optional): How much of `amount_capturable` to take, in the smallest unit of the currency. Leave it out to take all of it. The rest is released.
- `metadata` (object, optional): Your own key-value pairs, sent as `metadata[key]=value`. They are added to the PaymentIntent's.
- `statement_descriptor` (string, optional): Statement text for the captured payment, up to 22 characters.
- `statement_descriptor_suffix` (string, optional): Text added after your store's descriptor on the buyer's card statement.

### Cancel

**Body parameters of POST /v1/payment_intents/{intent}/cancel**

- `cancellation_reason` (string, optional): `requested_by_customer`, `duplicate`, `fraudulent` or `abandoned`. The PaymentIntent shows it in `cancellation_reason`; without it, that field is `null`.

The reference lists every parameter of the [create](https://docs.fluveo.com/api/payment_intents.md#post-v1-payment-intents), the [capture](https://docs.fluveo.com/api/payment_intents.md#post-v1-payment-intents-intent-capture) and the [cancel](https://docs.fluveo.com/api/payment_intents.md#post-v1-payment-intents-intent-cancel).

## Statuses and events

| `status` | What it means here |
| --- | --- |
| `requires_capture` | The amount is held. Capture or cancel it. |
| `processing` | A capture was accepted and is still being confirmed. |
| `succeeded` | Captured. `amount_received` is what you took. |
| `canceled` | Canceled. Nothing was taken, and any hold is released. |

| Event | When |
| --- | --- |
| `payment_intent.created` | The PaymentIntent was created. |
| `charge.succeeded` + `payment_intent.amount_capturable_updated` | The card was approved and the amount is held. The Charge has `captured: false`. |
| `charge.captured` + `payment_intent.succeeded` | You captured the hold. |
| `payment_intent.processing` | A capture is still being confirmed. |
| `payment_intent.canceled` | The PaymentIntent was canceled. |
| `charge.expired` | A hold ran out before anyone captured it. |

Each event's payload is on [Event types](https://docs.fluveo.com/webhooks/events.md).

## Errors you may see

The three 400 responses below leave the PaymentIntent unchanged. Each includes the PaymentIntent as it stands, inside the error.

**Capture twice.** After a capture, the PaymentIntent is `succeeded` and a second capture is refused with [`payment_intent_unexpected_state`](https://docs.fluveo.com/errors.md#payment_intent_unexpected_state). After a partial capture the message says the rest was released; after a full one it says the PaymentIntent was already captured.

*400 response (trimmed)*

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "payment_intent_unexpected_state",
    "message": "The remaining amount on this PaymentIntent could not be captured because the remainder of the authorized amount has been released.",
    "payment_intent": { "id": "pi_...", "status": "succeeded" }
  }
}
```

**Capture more than was held.** An `amount_to_capture` above `amount_capturable` is refused with [`amount_too_large`](https://docs.fluveo.com/errors.md#amount_too_large). Nothing is captured, and you may capture again with a smaller amount.

*400 response (trimmed)*

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "amount_too_large",
    "message": "The payment could not be captured because the requested capture amount is greater than the amount you can capture for this charge. Contact us via https://docs.fluveo.com/support/ for help.",
    "payment_intent": { "id": "pi_...", "status": "requires_capture" }
  }
}
```

**Cancel after capture.** A `succeeded` PaymentIntent cannot be canceled; the answer is [`payment_intent_unexpected_state`](https://docs.fluveo.com/errors.md#payment_intent_unexpected_state). Refund the payment instead. The table under [When a cancel works](https://docs.fluveo.com/payments/capture-and-cancel.md#cancel-statuses) is the rule for every status.

*400 response (trimmed)*

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "payment_intent_unexpected_state",
    "message": "You cannot cancel this PaymentIntent because it has a status of succeeded. Only a PaymentIntent with one of the following statuses may be canceled: requires_payment_method, requires_capture, requires_reauthorization, requires_confirmation, requires_action, expired, processing.",
    "payment_intent": { "id": "pi_...", "status": "succeeded" }
  }
}
```

Capturing an already `canceled` PaymentIntent also answers `payment_intent_unexpected_state`. See [Errors](https://docs.fluveo.com/errors.md) for the shape of every error.

## Test it

Use a `sk_test_...` key and the test card `pm_card_visa`.

1. Hold 50.00 as in [Hold an amount](https://docs.fluveo.com/payments/capture-and-cancel.md#hold): `200`, `status` `requires_capture`, `amount_capturable` `5000`. Your endpoint gets `charge.succeeded` and `payment_intent.amount_capturable_updated`.
2. Capture `amount_to_capture=6000` on that PaymentIntent: `400` `amount_too_large`, and the PaymentIntent is still `requires_capture`.
3. Capture `amount_to_capture=3000`: `200`, `status` `succeeded`, `amount_received` `3000`. Your endpoint gets `charge.captured` and `payment_intent.succeeded`.
4. Capture it again: `400` `payment_intent_unexpected_state`.
5. Cancel it: `400` `payment_intent_unexpected_state`, since it is `succeeded`.
6. Hold 50.00 again with a new `Idempotency-Key`, then cancel it with `cancellation_reason=requested_by_customer`: `200`, `status` `canceled`, `amount_capturable` `0`. Your endpoint gets `payment_intent.canceled`.
7. Send step 6's cancel again with the same `Idempotency-Key`: the same answer comes back with `Idempotent-Replayed: true`.

> **For AI agents:**
>
> Hold: POST /v1/payment_intents with capture_method=manual, payment_method_types[]=card, payment_method, confirm=true and an Idempotency-Key; expect status requires_capture and amount_capturable equal to amount. Capture only from requires_capture, once: POST /v1/payment_intents/{id}/capture, with no body for all or amount_to_capture (at most amount_capturable, smallest currency unit) for part; the rest is released for good. Never try to capture the remainder later and never retry a capture with a new key: reuse the first key. Cancel works from requires_payment_method, requires_confirmation, requires_action (not a waiting Boleto) and requires_capture; never from processing, succeeded or canceled. To give money back after a capture, refund (POST /v1/refunds); never cancel. A capture that answers processing is not done: wait for payment_intent.succeeded. Branch on error.code: amount_too_large means capture less; payment_intent_unexpected_state means read the PaymentIntent and act on its status.

## Next

- [Cards](https://docs.fluveo.com/payments/cards.md): How a card payment moves, 3D Secure, and where the hold's deadline shows.
- [Refund a payment](https://docs.fluveo.com/payments/refunds.md): Give back all or part of a captured payment.
- [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md): The one call for card, Pix and Boleto.
- [PaymentIntents reference](https://docs.fluveo.com/api/payment_intents.md): Every parameter and field of the PaymentIntent.
- [Event types](https://docs.fluveo.com/webhooks/events.md): The payload of each event on this page.
