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

> ## Agent Instructions
> Money is always a decimal string at the currency's precision (for example "29.00"), paired with an ISO 4217 currency field. Never use minor units.
> Build against the sandbox first: base URL https://sandbox-api.bachs.io with sk_sandbox_ keys. Production is https://api.bachs.io with sk_live_ keys; going live is a key swap.
> Treat webhooks (for example collection.succeeded) as the source of truth for fulfilment, never client-side events or redirects.
> Subscriptions are created by completing a checkout for a recurring product. There is no direct create-subscription endpoint.
> IDs carry resource prefixes (cust_, prod_, sub_, chk_, inv_, ref_) and timestamps are ISO 8601 UTC.

# Charge a saved card

> Save a customer's card at checkout, then charge it off-session later with no customer present.

In this guide you'll save a customer's card during a checkout, then charge that card later from your server with nobody on a payment page. By the end you'll have a working flow for the payments a customer agrees to once and you collect many times: a renewal you bill yourself, a usage invoice at the end of the month, a top-up when a balance runs low.

A payment taken with the customer away is called an **off-session** charge, as opposed to an on-session one where they are on a payment page and can answer their bank. The distinction matters: an off-session charge cannot ask the customer to authenticate, so a card whose issuer demands it will be refused rather than prompt anyone.

There are two halves, and they happen at different times:

* **Save the card.** A customer completes a checkout, and Bachs keeps their card against their customer record.
* **Charge it.** Days or months later, you call `POST /v1/charges` with that customer and an amount.

<Note>
  Saving and charging cards this way is in beta. The behavior below might change, including field names and the shape of the response. Pin your integration to what you test, and check back before you rely on it in production.
</Note>

<Note>
  If you want Bachs to run the billing cycle for you, use [Subscriptions](/guides/subscriptions/overview) instead. This guide is for when you decide what to charge and when.
</Note>

## Your side of keeping a card

Storing a customer's card and charging it later carries obligations that are yours, not ours.

* **Tell them, and let them agree.** Card network rules require the cardholder's consent to store their card for later use. Our checkout page states this on the card form, but your own terms, and how you present the choice, are yours to get right.
* **Say what you will charge and when.** A customer who agreed to one amount has not agreed to any amount. Be specific about what you will bill and how often.
* **Let them stop it.** Give them a way to remove a saved card and to cancel whatever it is paying for.

You are responsible for your own compliance with the laws, regulations and card network rules that apply to you.

## Before you start

* A **sandbox API key** (`sk_sandbox_...`) with the `payments:write` permission. See [Authentication](/authentication) and [Permissions](/api-reference/permissions).
* A **webhook endpoint** to receive the result. See [Set up webhooks](/guides/webhooks/overview).

<Tip>
  Everything below uses the sandbox base URL, so you can run the whole flow without moving real money.
</Tip>

## Steps

<Steps>
  <Step title="Create the customer">
    A saved card belongs to a customer, and charging it later names that customer by id. Create them first, and keep the `cust_` id: you will use it twice.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl https://sandbox-api.bachs.io/v1/customers \
        -H "Authorization: Bearer $BACHS_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "email": "jane@example.com",
          "name": "Jane Doe"
        }'
      ```

      ```json Response theme={"dark"}
      {
        "customer_id": "cust_1a2b3c4d5e6f",
        "email": "jane@example.com",
        "name": "Jane Doe",
        "created_at": "2026-04-27T11:58:00Z"
      }
      ```
    </CodeGroup>

    Already have the customer? Reuse their id and skip this step. See [the customer object](/api-reference/customers/object).
  </Step>

  <Step title="Save the card during a checkout">
    You have two ways to save a card, and they differ only in whether the customer pays at the same time.

    <Tabs>
      <Tab title="Save without charging">
        Send `save_payment_method` with a customer and no price. The checkout collects a card, charges nothing, and saves it. Use this when a customer signs up before they owe you anything.

        <CodeGroup>
          ```bash Request theme={"dark"}
          curl https://sandbox-api.bachs.io/v1/checkout-sessions \
            -H "Authorization: Bearer $BACHS_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "customer": { "customer_id": "cust_1a2b3c4d5e6f" },
              "save_payment_method": true,
              "success_url": "https://shop.example.com/card-saved"
            }'
          ```

          ```json Response theme={"dark"}
          {
            "checkout_id": "chk_u6FNakbY9shGPBC4",
            "checkout_url": "https://checkout.bachs.io/c/udK0GuvyweLeCnI",
            "save_payment_method": true,
            "status": "open",
            "amount": "0.00",
            "expires_at": "2026-04-27T13:00:00Z",
            "created_at": "2026-04-27T12:00:00Z"
          }
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Save while they pay">
        Add `save_payment_method: true` to a normal checkout. The customer pays what they owe today, and their card is kept for later.

        <CodeGroup>
          ```bash Request theme={"dark"}
          curl https://sandbox-api.bachs.io/v1/checkout-sessions \
            -H "Authorization: Bearer $BACHS_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "pricing": { "currency": "USD", "amount": "29.00" },
              "customer": { "customer_id": "cust_1a2b3c4d5e6f" },
              "save_payment_method": true,
              "success_url": "https://shop.example.com/thanks"
            }'
          ```

          ```json Response theme={"dark"}
          {
            "checkout_id": "chk_2N3o4P5q6R7s8T9u",
            "checkout_url": "https://checkout.bachs.io/c/V8xQ2mZpLj9RfTa",
            "save_payment_method": true,
            "status": "open",
            "amount": "29.00",
            "currency": "USD",
            "expires_at": "2026-04-27T13:00:00Z",
            "created_at": "2026-04-27T12:00:00Z"
          }
          ```
        </CodeGroup>

        Only card payments leave something you can charge again, so a checkout that offers no card is refused rather than accepted and quietly broken.
      </Tab>
    </Tabs>

    Send the customer to the `checkout_url` either way. See [Accept a payment with Checkout](/guides/checkout/checkout-sessions) for the full checkout flow.
  </Step>

  <Step title="Wait for the card to be saved">
    The card is saved when the customer finishes the checkout, not when you create it. Bachs sends `payment_method.saved` when the card is ready to charge.

    ```json payment_method.saved theme={"dark"}
    {
      "id": "evt_3ab4e0d5d2",
      "type": "payment_method.saved",
      "created_at": "2026-04-27T12:04:00Z",
      "organization_id": "acct_7KpQ2mNv4XbR9dLc",
      "data": {
        "payment_method_id": "pm_4f2c9a1b8e3d5a7c6b04",
        "customer": {
          "customer_id": "cust_1a2b3c4d5e6f",
          "email": "jane@example.com",
          "name": "Jane Doe"
        },
        "type": "card",
        "card_brand": "visa",
        "card_last4": "4242",
        "card_exp_month": 12,
        "card_exp_year": 2034,
        "currency": "USD",
        "is_default": true,
        "created_at": "2026-04-27T12:04:00Z"
      }
    }
    ```

    Store the `pm_` id against your own record of the customer. You can charge without it, but then you are charging whichever card is their default, and you cannot show them which card you are about to bill.

    `is_default` tells you whether this is the card a charge picks when you name none. The first card a customer saves becomes their default.

    <Warning>
      Do not call `POST /v1/charges` straight after creating the checkout. The customer has not entered a card yet, and the charge is refused with `NO_SAVED_PAYMENT_METHOD`.
    </Warning>
  </Step>

  <Step title="Charge the saved card">
    Call `POST /v1/charges` with the customer and an amount. There is no checkout and no page for the customer to visit.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl https://sandbox-api.bachs.io/v1/charges \
        -H "Authorization: Bearer $BACHS_API_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: invoice-2026-04-cust_1a2b3c4d5e6f" \
        -d '{
          "customer": "cust_1a2b3c4d5e6f",
          "payment_method": "pm_4f2c9a1b8e3d5a7c6b04",
          "amount": "29.00",
          "currency": "USD",
          "description": "April usage",
          "reference": "INV-2026-04-881"
        }'
      ```

      ```json Response theme={"dark"}
      {
        "payment_id": "ch_389305e973a841cc",
        "status": "processing",
        "amount": "30.85",
        "amount_paid": "0.00",
        "amount_remaining": "30.85",
        "currency": "USD",
        "fees": { "amount": "1.85", "currency": "USD" },
        "payment_method": "CARD",
        "checkout_id": null,
        "narration": "April usage",
        "reference": "INV-2026-04-881",
        "customer": { "name": "Jane Doe", "email": "jane@example.com" },
        "created_at": "2026-04-27T12:05:00Z"
      }
      ```
    </CodeGroup>

    `amount` is what the card is charged. It is larger than the `29.00` you asked for because this account passes the processing fee to the customer. On an account that absorbs the fee, the card is charged `29.00` and you settle less. See [Fees](/for-you/fees).

    Leave `payment_method` out and Bachs charges the customer's default saved card.

    <Warning>
      Always send an `Idempotency-Key`. Without one, a retried request after a timeout charges the customer twice. Key it to the thing you are billing for, not to the attempt. See [Idempotency](/guides/idempotency).
    </Warning>
  </Step>

  <Step title="Confirm the outcome with a webhook">
    The charge comes back `processing`, which means the card has been submitted and nobody has told us yet whether it worked. The answer arrives as a webhook.

    ```json collection.succeeded theme={"dark"}
    {
      "id": "evt_9f21c7e4a8",
      "type": "collection.succeeded",
      "created_at": "2026-04-27T12:05:04Z",
      "organization_id": "acct_7KpQ2mNv4XbR9dLc",
      "data": {
        "charge_id": "ch_389305e973a841cc",
        "status": "succeeded",
        "amount": "30.85",
        "currency": "USD"
      }
    }
    ```

    If the card is refused you get `collection.failed` instead, and the charge ends at `failed` with `amount_paid` still `"0.00"`. No money moved and nothing is owed.
  </Step>
</Steps>

## Charging in a currency the card does not use

You do not have to bill in the currency the card was saved in. Ask for the amount you are owed and Bachs converts it.

Say a customer saved a card that bills in `USD`, and you invoice in `NGN`:

```json theme={"dark"}
{
  "customer": "cust_1a2b3c4d5e6f",
  "amount": "45000.00",
  "currency": "NGN"
}
```

Bachs converts `45000.00 NGN` into `USD` at the prevailing rate and charges the card that amount. `currency` is the currency you are owed and settle in; the card is billed in its own. This is the same conversion a normal checkout does when a customer pays you in their currency.

## A declined card is an answer, not an error

`POST /v1/charges` always answers with a charge. A refused card is an outcome you read from `status`, never an exception you catch.

A refused card returns `201` with a charge like this:

```json Refused card theme={"dark"}
{
  "payment_id": "ch_1791fa89510846e2",
  "status": "failed",
  "amount": "30.85",
  "amount_paid": "0.00",
  "amount_remaining": "30.85",
  "currency": "USD",
  "payment_method": "CARD"
}
```

So branch on `status`, and do not rely on the request raising.

<Warning>
  Do not treat a `201` as payment received. A charge is `processing` at that moment and can still fail. Only `succeeded` means you have the money, and it reaches you as `collection.succeeded`. Code that fulfils an order on the `201` will ship goods it was never paid for.
</Warning>

Most charges come back `processing` and settle a few seconds later. Some come back already `failed`, when the card is refused while your request is still open. Both are normal, and both are the same response shape, so read `status` rather than assuming which one you got.

## Retrying a failed charge

A failed charge is final. To try again, create a new charge with a new `Idempotency-Key`.

Before you retry, consider why it failed. A card refused for insufficient funds may work in three days; a card refused because it expired will never work, and the customer has to save a new one. Send them to a new checkout that saves a card to replace it.

<Warning>
  Do not retry a failed charge in a tight loop. Repeated attempts against a refused card can get your account flagged by the card networks.
</Warning>

## Test it in the sandbox

The sandbox takes test cards, so you can run the whole flow, save a card and charge it, without moving real money.

| Card number           | What it does                           |
| --------------------- | -------------------------------------- |
| `4242 4242 4242 4242` | Saves, and later charges succeed.      |
| `4000 0000 0000 0341` | Saves, and later charges are declined. |

Any future expiry date and any CVC work.

The second one is worth spending time on. A card that saves and then fails is the case most integrations get wrong, because it only goes wrong long after the customer has gone.

<Warning>
  Watch for your browser autofilling a card you used earlier. Check the field holds the card you meant before you submit, or you will test the wrong one.
</Warning>

## Errors

These are refusals at the request itself, before any card is charged. They are the ones worth handling.

| Error code                            | Cause                                                            | What to do                                                                                                                 |
| ------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `NO_SAVED_PAYMENT_METHOD`             | The customer has no saved card.                                  | Send them to a checkout that saves one. A customer who has paid you before has not necessarily saved a card.               |
| `SAVED_PAYMENT_METHOD_NOT_FOUND`      | The `payment_method` you named does not belong to this customer. | Check the `pm_` id. Cards are scoped to one customer, so another customer's card reads as missing.                         |
| `PAYMENT_METHOD_UNUSABLE`             | The saved card has expired or been removed.                      | Ask the customer to save a new card.                                                                                       |
| `PAYMENT_METHOD_NOT_ENABLED`          | Your account cannot take card payments in this currency.         | Check which currencies your account can collect in. See [Payment method support](/guides/payments/payment-method-support). |
| `CHECKOUT_CANNOT_SAVE_PAYMENT_METHOD` | The checkout offers no card, so nothing could be saved.          | Cards are the only method that can be charged again. Check your account can take card payments.                            |
| `NOT_FOUND`                           | No customer with that id.                                        | Customer ids start with `cust_`.                                                                                           |

<Warning>
  If the request fails with a network error or a `5xx`, do not assume nothing happened. The charge may have gone through. Retry with the same `Idempotency-Key`, which returns the original charge instead of creating a second one.
</Warning>

## Next steps

* [The payment object](/api-reference/payments/object) for every field on a charge
* [Set up webhooks](/guides/webhooks/overview) to receive `payment_method.saved`, `collection.succeeded` and `collection.failed`
* [Idempotency](/guides/idempotency) for safe retries
* [Subscriptions](/guides/subscriptions/overview) if you want Bachs to run the billing cycle
* [Refunds](/guides/refunds) to return money from a charge
