---
title: "Save a card"
description: "Save a card with a SetupIntent and charge it later, with the buyer away."
url: https://docs.fluveo.com/payments/save-a-card/
---

> 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/save-a-card.curl.md (cURL), https://docs.fluveo.com/payments/save-a-card.node.md (Node.js), https://docs.fluveo.com/payments/save-a-card.python.md (Python), https://docs.fluveo.com/payments/save-a-card.php.md (PHP).

# Save a card

Save a card with a SetupIntent and charge it later, with the buyer away.

A saved card is a card PaymentMethod (`pm_...`) attached to a Customer (`cus_...`), which you can charge again while the buyer is not on your site. Keep a card on file when you know a later payment is coming: a balance due at delivery, a top-up the buyer agreed to, a one-click repeat order. A one-off payment does not need any of this.

There are two ways to save a card. A SetupIntent (`seti_...`) saves it and charges nothing. A PaymentIntent with `setup_future_usage` saves the card it is paid with. Either way the card ends up attached to the Customer, and the later payment is the same call:

| Step | Call | What you get |
| --- | --- | --- |
| Keep the buyer | `POST /v1/customers` | a `cus_` |
| Save the card | `POST /v1/setup_intents` with `confirm=true` | a `succeeded` SetupIntent whose `payment_method` is attached to the Customer |
| See what is saved | `GET /v1/customers/{customer}/payment_methods` | the Customer's cards |
| Charge it later | `POST /v1/payment_intents` with `off_session=true` | a `succeeded` PaymentIntent, or a `402` when the bank wants the buyer |
| Remove it | `POST /v1/payment_methods/{payment_method}/detach` | the card with `customer: null` |

Every request on this page runs on a fresh test store in the order shown, and each id comes from an earlier answer. Every amount is in the smallest unit of the currency: `2500` is 25.00. Send an `Idempotency-Key` with each POST. For retries, reuse the same key and request within the key's retention window (see [Idempotency](https://docs.fluveo.com/start/idempotency.md)).

## Create a Customer

A card is saved to a Customer, so start with one. The [Customers](https://docs.fluveo.com/payments/customers.md) guide covers every field.

**Create a Customer**

cURL:

```bash
curl https://api.fluveo.com/v1/customers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: crm-a2001-create" \
  -d email=grace@example.com \
  -d name="Grace Hopper"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/customers", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "crm-a2001-create",
  },
  body: new URLSearchParams({
    email: "grace@example.com",
    name: "Grace Hopper",
  }),
});
const customer = await response.json();
console.log(customer.id);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/customers",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "crm-a2001-create",
    },
    data={
        "email": "grace@example.com",
        "name": "Grace Hopper",
    },
)
customer = response.json()
print(customer["id"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: crm-a2001-create",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "email" => "grace@example.com",
        "name" => "Grace Hopper",
    ]),
]);
$customer = json_decode(curl_exec($ch), true);
echo $customer["id"];
```

*200 response (shortened)*

```json
{
  "id": "cus_...",
  "object": "customer",
  "email": "grace@example.com",
  "name": "Grace Hopper",
  "livemode": false
}
```

## Save a card without charging

Create a SetupIntent with the Customer, the card and `usage=off_session`, which says you will charge the card while the buyer is away. `confirm=true` sets the card up in the same call. Here the card is the test PaymentMethod `pm_card_visa`; in live mode it is a `pm_` your checkout collected. Send `return_url` too: it is where the buyer comes back if their bank asks them to authenticate (see [When the bank asks the buyer](https://docs.fluveo.com/payments/save-a-card.md#authentication)).

**Save a card to the Customer**

cURL:

```bash
curl https://api.fluveo.com/v1/setup_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: a2001-save-card-1" \
  -d customer=cus_... \
  -d payment_method=pm_card_visa \
  -d "payment_method_types[]=card" \
  -d usage=off_session \
  -d confirm=true \
  --data-urlencode "return_url=https://shop.example.com/account/card-saved"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/setup_intents", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "a2001-save-card-1",
  },
  body: new URLSearchParams({
    customer: "cus_...",
    payment_method: "pm_card_visa",
    "payment_method_types[]": "card",
    usage: "off_session",
    confirm: "true",
    return_url: "https://shop.example.com/account/card-saved",
  }),
});
const setupIntent = await response.json();
console.log(setupIntent.status, setupIntent.payment_method);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/setup_intents",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "a2001-save-card-1",
    },
    data={
        "customer": "cus_...",
        "payment_method": "pm_card_visa",
        "payment_method_types[]": "card",
        "usage": "off_session",
        "confirm": "true",
        "return_url": "https://shop.example.com/account/card-saved",
    },
)
setup_intent = response.json()
print(setup_intent["status"], setup_intent["payment_method"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/setup_intents");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: a2001-save-card-1",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "customer" => "cus_...",
        "payment_method" => "pm_card_visa",
        "payment_method_types" => ["card"],
        "usage" => "off_session",
        "confirm" => "true",
        "return_url" => "https://shop.example.com/account/card-saved",
    ]),
]);
$setupIntent = json_decode(curl_exec($ch), true);
echo $setupIntent["status"] . " " . $setupIntent["payment_method"];
```

The card is set up. `status` is `succeeded`, and `payment_method` names the saved card, a new `pm_` now attached to the Customer. Keep that `pm_`: it is what you charge later. Nothing was charged and no Charge exists.

*200 response (shortened)*

```json
{
  "id": "seti_...",
  "object": "setup_intent",
  "customer": "cus_...",
  "payment_method": "pm_...",
  "payment_method_types": ["card"],
  "usage": "off_session",
  "status": "succeeded",
  "next_action": null,
  "latest_attempt": "setatt_...",
  "last_setup_error": null
}
```

Your endpoint gets `setup_intent.created` and `setup_intent.succeeded`. A card the bank declines answers `402` instead, with the SetupIntent inside the error in `requires_payment_method` and the reason in its `last_setup_error`; see [Errors you may see](https://docs.fluveo.com/payments/save-a-card.md#errors).

### Confirm in a second call

You can also create the SetupIntent without `confirm=true`, for instance before the buyer has picked a card. It waits in `requires_payment_method`, or in `requires_confirmation` when it already names a card. Confirm it with `POST /v1/setup_intents/{intent}/confirm`, sending `payment_method` if it has none yet and the `return_url` of the buyer's session. `return_url` belongs on the confirm in that case: a create that sends it without `confirm=true` is refused with `400` [`setup_intent_invalid_parameter`](https://docs.fluveo.com/errors.md#setup_intent_invalid_parameter).

### When the bank asks the buyer to authenticate

Some banks want the cardholder to prove it is them before a card is saved, with 3D Secure. The confirm then answers `200` with `status` `requires_action`, and `next_action` holds a page for the buyer:

*200 response, the buyer must authenticate (shortened)*

```json
{
  "id": "seti_...",
  "object": "setup_intent",
  "customer": "cus_...",
  "status": "requires_action",
  "next_action": {
    "type": "redirect_to_url",
    "redirect_to_url": {
      "url": "https://api.fluveo.com/v1/card_actions/ca_...",
      "return_url": "https://shop.example.com/account/card-saved"
    }
  }
}
```

1. Send the buyer to `next_action.redirect_to_url.url`. Their bank's check opens there.
2. When they finish, they land on your `return_url`, with the `setup_intent` id and a `redirect_status` in the query.
3. Do not trust the query alone. Read the SetupIntent with `GET /v1/setup_intents/{intent}`, or wait for its event: `setup_intent.succeeded` when the card is saved, `setup_intent.setup_failed` when the check failed. A failed check leaves the SetupIntent in `requires_payment_method` with `last_setup_error`, and no card is attached.

Your endpoint gets `setup_intent.requires_action` while the SetupIntent waits. Send `return_url` on every confirm made while the buyer is with you, so `next_action` always has a page to send them to.

### Cancel a SetupIntent

A SetupIntent you no longer need, such as one the buyer left in `requires_action`, is canceled with `POST /v1/setup_intents/{intent}/cancel`, with an optional `cancellation_reason` of `abandoned`, `duplicate` or `requested_by_customer`. It answers `status` `canceled`, sends `setup_intent.canceled`, and the page in its `next_action` no longer opens. Only a SetupIntent that has not succeeded can be canceled: a `succeeded` or already `canceled` one answers `400`. To stop using a card that is already saved, [remove it](https://docs.fluveo.com/payments/save-a-card.md#detach) instead.

## Save a card while charging

When the buyer pays now and you will charge them again later, save the card with that first payment instead of a separate SetupIntent. Add `customer` and `setup_future_usage=off_session` to the card PaymentIntent you already create (see [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md)). Once the payment succeeds, its card is attached to that Customer, the PaymentIntent shows `setup_future_usage`, and its `payment_method` is the `pm_` you charge later. `on_session` saves the card for payments the buyer makes on your site; use `off_session` when you will charge it without them.

## See the saved cards

`GET /v1/customers/{customer}/payment_methods` lists the cards attached to a Customer, newest first. Narrow it with `type=card`, and page it with `limit`, `starting_after` and `ending_before`.

**List the Customer's cards**

cURL:

```bash
curl -G https://api.fluveo.com/v1/customers/cus_.../payment_methods \
  -H "Authorization: Bearer sk_test_..." \
  -d type=card
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../payment_methods?type=card", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const cards = await response.json();
console.log(cards.data.map((pm) => pm.id));
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/cus_.../payment_methods",
    headers={"Authorization": "Bearer sk_test_..."},
    params={"type": "card"},
)
cards = response.json()
print([pm["id"] for pm in cards["data"]])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../payment_methods?type=card");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
    ],
]);
$cards = json_decode(curl_exec($ch), true);
echo implode(", ", array_column($cards["data"], "id"));
```

*200 response (shortened)*

```json
{
  "object": "list",
  "data": [
    {
      "id": "pm_...",
      "object": "payment_method",
      "type": "card",
      "customer": "cus_...",
      "card": { "brand": "visa", "last4": "4242" }
    }
  ],
  "has_more": false
}
```

Show the buyer `card.brand` and `card.last4` so they know which card is on file.

## Charge the card later

When the payment is due, create and confirm a PaymentIntent with the Customer, the saved `pm_`, `off_session=true` and `confirm=true`. `off_session=true` tells Fluveo the buyer is not there to answer anything, so no authentication page is offered. Use a new `Idempotency-Key` for each payment you mean to take.

**Charge 25.00 on the saved card**

cURL:

```bash
curl https://api.fluveo.com/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: a2001-order-2002" \
  -d amount=2500 \
  -d currency=usd \
  -d customer=cus_... \
  -d payment_method=pm_... \
  -d "payment_method_types[]=card" \
  -d off_session=true \
  -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": "a2001-order-2002",
  },
  body: new URLSearchParams({
    amount: "2500",
    currency: "usd",
    customer: "cus_...",
    payment_method: "pm_...",
    "payment_method_types[]": "card",
    off_session: "true",
    confirm: "true",
  }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.status);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "a2001-order-2002",
    },
    data={
        "amount": 2500,
        "currency": "usd",
        "customer": "cus_...",
        "payment_method": "pm_...",
        "payment_method_types[]": "card",
        "off_session": "true",
        "confirm": "true",
    },
)
payment_intent = response.json()
print(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: a2001-order-2002",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "amount" => 2500,
        "currency" => "usd",
        "customer" => "cus_...",
        "payment_method" => "pm_...",
        "payment_method_types" => ["card"],
        "off_session" => "true",
        "confirm" => "true",
    ]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["status"];
```

*200 response (shortened)*

```json
{
  "id": "pi_...",
  "object": "payment_intent",
  "amount": 2500,
  "amount_received": 2500,
  "currency": "usd",
  "customer": "cus_...",
  "payment_method": "pm_...",
  "latest_charge": "ch_...",
  "status": "succeeded"
}
```

The payment went through: `payment_intent.succeeded` and `charge.succeeded` follow. A decline answers `402` `card_error` as on any card payment (see [Cards](https://docs.fluveo.com/payments/cards.md)), and the PaymentIntent goes back to `requires_payment_method`.

### When the bank wants the buyer present

Even a saved card can meet a bank that insists on 3D Secure for this payment. Off session there is nobody to authenticate, so the confirm fails at once with `402`, `code` and `decline_code` both [`authentication_required`](https://docs.fluveo.com/errors.md#authentication_required):

*402 response (trimmed)*

```json
{
  "error": {
    "type": "card_error",
    "code": "authentication_required",
    "decline_code": "authentication_required",
    "payment_intent": {
      "id": "pi_...",
      "status": "requires_payment_method",
      "latest_charge": null
    },
    "payment_method": { "id": "pm_...", "type": "card" }
  }
}
```

No Charge is made and no money moves. The PaymentIntent keeps the reason in `last_payment_error`, and the only events are `payment_intent.created` and `payment_intent.payment_failed`. Do not send the same off-session payment again: the bank will answer the same way. Instead:

1. Tell the buyer a payment needs them, by email or in your app, with a link back to your site.
2. When they are there, confirm the same PaymentIntent with `POST /v1/payment_intents/{intent}/confirm`, sending the saved card in `payment_method` and a `return_url`, and without `off_session`. Use a new `Idempotency-Key`: this is a new attempt.
3. The answer is `requires_action` with a `next_action` page, as when saving. Send the buyer there; once they pass the check the PaymentIntent goes on to `succeeded`, or the bank declines it with its own reason.

The buyer can also pick another card on that visit: confirm with its `pm_` in `payment_method`.

## Remove a card

Detach a card when the buyer removes it from their account or asks you to forget it. The card leaves the Customer.

**Remove the saved card**

cURL:

```bash
curl -X POST https://api.fluveo.com/v1/payment_methods/pm_.../detach \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: a2001-remove-card-1"
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/payment_methods/pm_.../detach", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "a2001-remove-card-1",
  },
});
const paymentMethod = await response.json();
console.log(paymentMethod.customer);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_methods/pm_.../detach",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "a2001-remove-card-1",
    },
)
payment_method = response.json()
print(payment_method["customer"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_methods/pm_.../detach");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: a2001-remove-card-1",
    ],
]);
$paymentMethod = json_decode(curl_exec($ch), true);
var_dump($paymentMethod["customer"]);
```

*200 response (shortened)*

```json
{
  "id": "pm_...",
  "object": "payment_method",
  "type": "card",
  "customer": null,
  "card": { "brand": "visa", "last4": "4242" }
}
```

Your endpoint gets `payment_method.detached`, and the card no longer shows in the Customer's list. To charge this buyer again later, save a card again.

## Parameters

**Save a card (POST /v1/setup_intents)**

- `customer` (string, optional): The `cus_` the card is saved to. Once the SetupIntent succeeds, its `payment_method` is attached to this Customer.
- `payment_method` (string, optional): The card to save: a `pm_` of your store, or in test mode a test PaymentMethod such as `pm_card_visa`.
- `payment_method_types` (array, optional): Use `card` to restrict this SetupIntent to cards, as in the examples above.
- `usage` (string, optional): `off_session` (the default) when you will charge the card without the buyer, or `on_session`.
- `confirm` (boolean, optional): `true` sets the card up in the same call.
- `return_url` (string, optional): Where the buyer comes back after their bank's check. Only with `confirm=true`, or on `POST /v1/setup_intents/{intent}/confirm`.

**Charge it later (POST /v1/payment_intents)**

- `amount` (integer, required): In the smallest unit of the currency: `2500` is 25.00.
- `currency` (string, required): A currency your account accepts.
- `customer` (string, optional): The `cus_` the card is saved to.
- `payment_method` (string, optional): The saved `pm_`.
- `off_session` (string, optional): `true` when the buyer is not there. Only with `confirm=true`. No authentication page is offered: a card that needs one fails with `402` `authentication_required`.
- `confirm` (boolean, optional): `true` charges in the same call.
- `setup_future_usage` (string, optional): `off_session` or `on_session`: saves the card of a payment that succeeds to its `customer`. See [Save a card while charging](https://docs.fluveo.com/payments/save-a-card.md#setup-future-usage).

The [SetupIntents reference](https://docs.fluveo.com/api/setup_intents.md) lists every parameter and field of the SetupIntent.

## Statuses and events

| SetupIntent `status` | Meaning |
| --- | --- |
| `requires_payment_method` | No card yet, or the last one was declined or failed its check (see `last_setup_error`). |
| `requires_confirmation` | It names a card and waits for a confirm. |
| `requires_action` | The buyer must pass their bank's check at `next_action`. |
| `processing` | The result is not known yet. Read the SetupIntent again or wait for its event. |
| `succeeded` | The card is saved and attached to the Customer. |
| `canceled` | Canceled; nothing was saved. |

| Event | When |
| --- | --- |
| [`setup_intent.created`](https://docs.fluveo.com/webhooks/events.md#setup_intent.created) | A SetupIntent was created. |
| [`setup_intent.requires_action`](https://docs.fluveo.com/webhooks/events.md#setup_intent.requires_action) | The buyer must pass 3D Secure before the card is set up. |
| [`setup_intent.succeeded`](https://docs.fluveo.com/webhooks/events.md#setup_intent.succeeded) | The card was set up and attached to the SetupIntent's Customer. |
| [`setup_intent.setup_failed`](https://docs.fluveo.com/webhooks/events.md#setup_intent.setup_failed) | Setting up the card failed, by a decline or a failed check; `last_setup_error` says which. |
| [`setup_intent.canceled`](https://docs.fluveo.com/webhooks/events.md#setup_intent.canceled) | A SetupIntent was canceled. |
| [`payment_method.attached`](https://docs.fluveo.com/webhooks/events.md#payment_method.attached) | A PaymentMethod was attached to a Customer. |
| [`payment_method.updated`](https://docs.fluveo.com/webhooks/events.md#payment_method.updated) | A PaymentMethod changed. |
| [`payment_method.detached`](https://docs.fluveo.com/webhooks/events.md#payment_method.detached) | A PaymentMethod was detached from a Customer. |

## Errors you may see

| Error | Why |
| --- | --- |
| 402 [`card_declined`](https://docs.fluveo.com/errors.md#card_declined) on a SetupIntent | The bank refused to save the card. The error carries the SetupIntent in `requires_payment_method`, with the reason in `last_setup_error`. Ask for another card. |
| 402 [`authentication_required`](https://docs.fluveo.com/errors.md#authentication_required) on an off-session charge | The bank wants the buyer for this payment. See [When the bank wants the buyer present](https://docs.fluveo.com/payments/save-a-card.md#buyer-needed). |
| 400 [`setup_intent_invalid_parameter`](https://docs.fluveo.com/errors.md#setup_intent_invalid_parameter) | `return_url` on a create without `confirm=true`. |
| 400 [`payment_method_unexpected_state`](https://docs.fluveo.com/errors.md#payment_method_unexpected_state) | The card belongs to a deleted Customer. Ask the buyer for a card again. |
| 400 [`parameter_unknown`](https://docs.fluveo.com/errors.md#parameter_unknown) | A parameter the call does not take. `param` names it. |
| 400 or 404 [`resource_missing`](https://docs.fluveo.com/errors.md#resource_missing) | The `seti_`, `pm_` or `cus_` is not one of your store's. The status depends on the operation and whether the id is in the path or a parameter. |

See [Errors](https://docs.fluveo.com/errors.md) for the shape of every error.

## Test it

Use a `sk_test_...` key.

1. Create a Customer and save `pm_card_visa` to it as above: the SetupIntent answers `succeeded` with a new `pm_`, and your endpoint gets `setup_intent.created` and `setup_intent.succeeded`.
2. List the Customer's cards: the `pm_` is there, `visa` ending in `4242`.
3. Charge it with `off_session=true`: `200`, `status` `succeeded`.
4. Save `pm_card_authenticationRequiredOnSetup` with a `return_url`: `requires_action` with a `next_action` page. Open it, press Complete, and you land on your `return_url`; the SetupIntent is now `succeeded`. Press Fail instead and it goes back to `requires_payment_method`.
5. Save `pm_card_chargeDeclined`: `402` `card_declined`, the SetupIntent in `requires_payment_method`, and `setup_intent.setup_failed`.
6. Create a PaymentIntent with `pm_card_threeDSecure2Required`, `off_session=true` and `confirm=true`: `402` `authentication_required`, and no Charge.
7. Detach the card from step 1: `customer` is `null`, and the Customer's list no longer shows it.
8. Create a SetupIntent with no `confirm`, then cancel it with `cancellation_reason=abandoned`: `status` `canceled`. Cancel it again: `400`.

> **For AI agents:**
>
> Save a card: POST /v1/setup_intents with customer=cus_..., payment_method=pm_..., payment_method_types[]=card, usage=off_session, confirm=true, return_url and an Idempotency-Key. Keep the returned payment_method (pm_...) only when status is succeeded. requires_action means the buyer must open next_action.redirect_to_url.url; never treat the card as saved until GET /v1/setup_intents/{intent} shows succeeded or setup_intent.succeeded arrives. Never send return_url on a create without confirm=true. Never put a raw card number in a request: use a pm_. Charge later: POST /v1/payment_intents with amount (smallest currency unit), currency, customer, payment_method=<saved pm_>, payment_method_types[]=card, off_session=true, confirm=true and a new Idempotency-Key per payment. A 402 authentication_required means the bank wants the buyer: no Charge exists; never retry it off session, and never retry with a new key in a loop. Ask the user to bring the buyer back, then confirm the same PaymentIntent on POST /v1/payment_intents/{intent}/confirm with return_url and without off_session. Remove a card with POST /v1/payment_methods/{payment_method}/detach; never charge a card after it is detached.

## Next

- [Customers](https://docs.fluveo.com/payments/customers.md): Create, find and update the buyer the card is saved to.
- [Cards](https://docs.fluveo.com/payments/cards.md): How a card payment moves, its declines and 3D Secure.
- [Capture or cancel](https://docs.fluveo.com/payments/capture-and-cancel.md): Hold an amount on a card and capture or cancel it later.
- [Checkout](https://docs.fluveo.com/payments/checkout.md#setup): Let a hosted page collect and save the card.
- [SetupIntents reference](https://docs.fluveo.com/api/setup_intents.md): Every parameter and field of the SetupIntent.
- [Event types](https://docs.fluveo.com/webhooks/events.md): The payload of each event on this page.
