> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionicfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Save a card for later

> Vault a card once, then charge it off-session without the buyer present.

A setup intent lets you collect card details during a buyer-initiated session and store them securely, so you can charge the card later — for subscriptions, one-click reorder, or any merchant-initiated payment — without requiring the buyer to re-enter their details.

## How it works

<Steps>
  <Step title="Create a setup intent">
    Your server calls `POST /v1/setup_intents`. The intent starts in `requires_confirmation` and is ready to accept a client-side token.
  </Step>

  <Step title="Tokenize the card in the browser">
    Ionic Hosted Card Fields sends the card details directly to the tokenization service and returns a single-use `payment_token`. Your server never sees the PAN.
  </Step>

  <Step title="Confirm the intent">
    Your server calls `POST /v1/setup_intents/{id}/confirm` with the `payment_token`. On success the intent moves to `succeeded` and a reusable payment method (`pm_…`) is created.
  </Step>

  <Step title="Attach to a customer (optional)">
    Call `POST /v1/payment_methods/{id}/attach` to associate the saved method with a customer record for easy retrieval later.
  </Step>

  <Step title="Charge off-session">
    When you need to collect payment without the buyer present, create a payment intent using the saved `pm_…` and set `initiated_by: merchant`.
  </Step>
</Steps>

On Node, `npm install @ionicfi/sdk` gives you typed calls for every request
below; the [server SDK](/sdks/server) guide has details. Each example also
shows raw curl.

## Create a setup intent

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Ionic } from "@ionicfi/sdk";

  const ionic = new Ionic({ token: process.env.IONIC_SECRET_KEY });

  const intent = await ionic.setupIntents.create({
    "Idempotency-Key": "setup_user_42_2026",
    currency: "usd",
    customer: "cus_000000000000000000000001",
    metadata: { signup_flow: "checkout" },
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/setup_intents \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: setup_user_42_2026" \
    -H "Content-Type: application/json" \
    -d '{
      "currency": "usd",
      "customer": "cus_000000000000000000000001",
      "metadata": {
        "signup_flow": "checkout"
      }
    }'
  ```
</CodeGroup>

The response is a setup intent in `requires_confirmation` status:

```json theme={null}
{
  "id": "seti_000000000000000000000001",
  "object": "setup_intent",
  "status": "requires_confirmation",
  "currency": "usd",
  "customer": "cus_000000000000000000000001",
  "payment_method": null,
  "livemode": false
}
```

### Create request fields

| Field      | Type   | Notes                                                                                                                             |
| ---------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `currency` | string | Required. Three-letter ISO currency code (e.g. `usd`).                                                                            |
| `customer` | string | Customer id (`cus_…`). When present, a mandate is created on confirm and the resulting payment method is linked to this customer. |
| `metadata` | object | Up to 50 key-value pairs of arbitrary metadata.                                                                                   |

## Confirm the setup intent

After your client-side card form returns a `payment_token`, pass it to the confirm endpoint from your server.

<CodeGroup>
  ```ts TypeScript theme={null}
  // The payment token is single-use, so it makes a natural Idempotency-Key:
  // retries of this attempt replay the original result, and a fresh token
  // after a failed vault is automatically a new key.
  const paymentToken = "tok_..."; // from your client-side card form

  const confirmed = await ionic.setupIntents.confirm({
    id: intent.id,
    "Idempotency-Key": `confirm-${paymentToken}`,
    payment_token: paymentToken,
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/setup_intents/seti_000000000000000000000001/confirm \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: confirm-tok_..." \
    -H "Content-Type: application/json" \
    -d '{
      "payment_token": "tok_..."
    }'
  ```
</CodeGroup>

Key the confirm to the attempt, not to a constant. A fixed key blocks every
later confirmation with a `422` conflict once the first one is recorded;
deriving it from the single-use token scopes each key to one attempt.

On success the intent moves to `succeeded` and `payment_method` is populated:

```json theme={null}
{
  "id": "seti_000000000000000000000001",
  "object": "setup_intent",
  "status": "succeeded",
  "payment_method": "pm_000000000000000000000001",
  "customer": "cus_000000000000000000000001"
}
```

If vaulting fails, the intent returns to `requires_confirmation` so you can retry with a fresh token from the client. A `422` response means the intent is not in a confirmable state.

### Setup intent status values

| Status                  | Meaning                                                                                   |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| `requires_confirmation` | Initial state; awaiting a client-side token. Also the state after a failed vault attempt. |
| `processing`            | Vault operation is in progress.                                                           |
| `succeeded`             | Card vaulted and reusable payment method created.                                         |
| `canceled`              | Intent was canceled before completion.                                                    |

## Inspect the saved payment method

Retrieve the saved method to confirm the card details. The response includes brand, last four digits, and expiration — never the PAN.

<CodeGroup>
  ```ts TypeScript theme={null}
  const method = await ionic.paymentMethods.retrieve({
    id: "pm_000000000000000000000001",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/payment_methods/pm_000000000000000000000001 \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

```json theme={null}
{
  "id": "pm_000000000000000000000001",
  "object": "payment_method",
  "type": "card",
  "status": "active",
  "reusable": true,
  "card": {
    "brand": "visa",
    "last4": "4242",
    "exp_month": 12,
    "exp_year": 2029,
    "funding_type": "credit"
  },
  "customer": "cus_000000000000000000000001",
  "fingerprint": "fp_..."
}
```

### Payment method card fields

| Field          | Notes                                                       |
| -------------- | ----------------------------------------------------------- |
| `brand`        | One of `visa`, `mastercard`, `amex`, `discover`, `unknown`. |
| `last4`        | Last four digits of the card number.                        |
| `exp_month`    | Expiration month (1–12).                                    |
| `exp_year`     | Expiration year (four digits).                              |
| `funding_type` | One of `credit`, `debit`, `prepaid`, `unknown`.             |

## Attach to a customer

If you did not supply a `customer` when creating the intent, attach the method to a customer now. Attachment is write-once — a method can only be attached to one customer.

<CodeGroup>
  ```ts TypeScript theme={null}
  await ionic.paymentMethods.attach({
    id: "pm_000000000000000000000001",
    "Idempotency-Key": "attach_pm_user_42",
    customer: "cus_000000000000000000000001",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/payment_methods/pm_000000000000000000000001/attach \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: attach_pm_user_42" \
    -H "Content-Type: application/json" \
    -d '{
      "customer": "cus_000000000000000000000001"
    }'
  ```
</CodeGroup>

## Charge off-session

When you need to charge the buyer without them present, create a payment intent using the saved method. Set `initiated_by: merchant` — merchant-initiated transactions require a secret key.

<CodeGroup>
  ```ts TypeScript theme={null}
  const payment = await ionic.paymentIntents.create({
    "Idempotency-Key": "pi_renewal_order_789",
    amount: 4900,
    currency: "usd",
    payment_method: "pm_000000000000000000000001",
    customer: "cus_000000000000000000000001",
    initiated_by: "merchant",
    mandate: "mand_000000000000000000000001",
    reference: "order_789",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/payment_intents \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: pi_renewal_order_789" \
    -H "Content-Type: application/json" \
    -d '{
      "amount": 4900,
      "currency": "usd",
      "payment_method": "pm_000000000000000000000001",
      "customer": "cus_000000000000000000000001",
      "initiated_by": "merchant",
      "mandate": "mand_000000000000000000000001",
      "reference": "order_789"
    }'
  ```
</CodeGroup>

A successful off-session charge returns a payment intent in `succeeded` status. A decline returns `402` with a `card_error` type and the current payment intent embedded in the response body so you can inspect its status without a second request.

### Off-session payment intent fields

| Field            | Type    | Notes                                                                                                                           |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `amount`         | integer | Amount in minor units (e.g. cents for USD).                                                                                     |
| `currency`       | string  | Three-letter ISO currency code.                                                                                                 |
| `payment_method` | string  | The saved payment method id (`pm_…`).                                                                                           |
| `initiated_by`   | string  | Use `merchant` for off-session charges. Requires a secret key.                                                                  |
| `mandate`        | string  | Mandate id authorizing the merchant-initiated charge. Created automatically when a setup intent is confirmed with a `customer`. |
| `customer`       | string  | Customer id (`cus_…`) to attach.                                                                                                |
| `reference`      | string  | Your order number or other reference (max 256 chars).                                                                           |

## Operational guidance

* Always send an `Idempotency-Key` on setup-intent create, confirm, and payment-intent create. Retried requests without a key can vault the same card twice or charge a buyer more than once.
* Tokenize card data in the browser with Ionic Hosted Card Fields. Your server only ever receives an opaque `payment_token` — never raw card details.
* Store the `pm_…` id in your database against the customer or subscription record after the setup intent succeeds. You will need it for every subsequent off-session charge.
* Use `GET /v1/payment_methods?customer=cus_…` to list all saved methods for a customer before charging, so you can handle expired or revoked cards gracefully.
* A payment method's `fingerprint` is stable across multiple vaultings of the same physical card. Use it to detect duplicate cards on a customer before saving a second copy.
* When an off-session charge returns `402`, check the embedded `payment_intent.status`. A `requires_payment_method` status means the card was declined and the buyer needs to update their payment details.
