# Save Cards & Charge Later (/vi/online/integration/saved_payment_methods)



<SavedCardsBanner />

## Two ways to use a saved card [#two-ways-to-use-a-saved-card]

<SavedCardModes />

Want to see a whole flow for a real business first? Try [Use cases](/online/integration/use_cases).

## Implementation [#implementation]

Your server uses the same [merchant credentials](/online/integration/api_integration#authentication) for checkout, saved cards, and later charges.

<Steps>
  <Step>
    ### Save the card at checkout [#save-the-card-at-checkout]

    Add three fields when you create a hosted session or an embedded payment:

    ```ts
    const session = await tapaya.checkout.sessions.create({
    	mode: 'embedded',
    	merchantOrderId: 'order-42',
    	amount: 1200,
    	currency: 'EUR',
    	customerReference: 'buyer-42',
    	savePaymentMethod: 'optional',
    	paymentMethodUsage: 'off_session',
    }, { idempotencyKey: 'order-42:initial' })
    ```

    | Field                | What it does                                                                                                                   |
    | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
    | `customerReference`  | Your own ID for the customer. Authenticate the customer on your server before you use it.                                      |
    | `savePaymentMethod`  | `optional` shows an unchecked checkbox. `required` saves the card with consent you already collected. `none` turns saving off. |
    | `paymentMethodUsage` | `off_session` if you will charge later. `on_session` for faster repeat checkout. Omit it when saving is `none`.                |
    | `savingTerms`        | Optional `{ text, url }` with your own consent wording and terms link.                                                         |

    For a hosted session, omit `mode` and set its [return destinations](/online/integration/checkout_settings#configure-return-destinations).

    <Callout title="Get consent first with required saving" type="warn">
      `required` does not ask the customer. Collect agreement on your own page or in your subscription terms before you create the payment. If the customer declines your checkbox, create the payment with saving set to `none`.
    </Callout>

    <Accordions>
      <Accordion title="Use your own consent wording">
        Tapaya shows localized consent text by default. `savingTerms.text` replaces it and is not translated, so send the shopper's language and set `locale` on hosted sessions that supply it. `savingTerms.url` must use HTTPS on an allowed checkout website.

        `labels.saveCardOnSession` overrides the checkbox label. `labels.saveCardOffSession` adds an off-session disclosure. Server-side `checkout.sessions.confirm()` accepts `{ saveCard: true }` only after the shopper accepts the consent wording.
      </Accordion>

      <Accordion title="Embedded payments created on submission">
        Show the same policy in the browser before a payment exists:

        ```ts
        const payment = browserTapaya.createPayment({
        	prepare: '/api/payments',
        	cardSaving: {
        		savePaymentMethod: 'optional',
        		paymentMethodUsage: 'off_session',
        		savingTerms: {
        			text: 'Save my card for Pro plan renewals.',
        			url: 'https://shop.example/terms#billing',
        		},
        	},
        })
        ```

        Pass the same `savingTerms` when creating the payment on your server. The frame checks the displayed policy before confirmation. A changed policy clears the checkbox and requires review. If optional saving first becomes available during submission, payment proceeds without saving.
      </Accordion>
    </Accordions>
  </Step>

  <Step>
    ### Store the saved card ID [#store-the-saved-card-id]

    Read the session or payment on your server until `paymentMethodId` is not null, and store it with your customer. Saving can finish a moment after the payment succeeds. A failed save does not undo the payment; never charge again to retry saving.

    The method's `consent` record holds `type`, `acceptedAt`, `wordingVersion`, `savingTerms`, `ipAddress`, and `userAgent`. `type` is `checkbox` for Tapaya's optional checkbox or `merchant_asserted` for required saving. Browser details can be null for server confirmations. Keep this record with your billing records.
  </Step>

  <Step>
    ### Charge the saved card [#charge-the-saved-card]

    <Tabs defaultValue="merchant" groupId="saved-card-charge" className="border-0 bg-transparent">
      <TabsList>
        <TabsTrigger value="merchant">
          You charge later
        </TabsTrigger>

        <TabsTrigger value="customer">
          Customer pays faster
        </TabsTrigger>
      </TabsList>

      <TabsContent value="merchant">
        Use a method saved for `off_session` and an amount the customer agreed to. Give each renewal or top-up its own
        `merchantOrderId`: an order allows one unresolved payment at a time.

        ```ts
        const renewal = await tapaya.payments.create({
        	merchantOrderId: 'sub-42-2026-11',
        	amount: 990,
        	currency: 'EUR',
        	paymentMethodId: 'pm_7Hq2x9LmR4',
        	initiator: 'merchant',
        	merchantInitiatedReason: 'recurring',
        }, { idempotencyKey: 'sub-42-2026-11' })
        ```

        Set `merchantInitiatedReason` to one of:

        * `recurring` for a fixed series the customer agreed to, such as a monthly plan.
        * `unscheduled` for charges that vary in amount or timing, such as top-ups or a hotel bill.

        Automatic capture charges the card before the call returns. Check `status`: `declined` includes `declineReason`, and `pending` needs another server read. HTTP 200 alone does not prove payment.
      </TabsContent>

      <TabsContent value="customer">
        For a saved typed card, create a customer-initiated payment on your server:

        ```ts
        const payment = await tapaya.payments.create({
        	merchantOrderId: 'order-1002',
        	amount: 2500,
        	currency: 'EUR',
        	paymentMethodId: 'pm_7Hq2x9LmR4',
        	initiator: 'customer',
        	returnUrl: 'https://shop.example/checkout/return',
        }, { idempotencyKey: 'order-1002:charge' })
        ```

        Return only the payment ID and client secret to the signed-in shopper. `returnUrl` is required even for inline verification and must match an allowed checkout website. Then complete any 3-D Secure step in the browser:

        ```ts
        import { loadTapaya } from '@tapayadot/checkout/client'

        const browserTapaya = await loadTapaya(publishableKey, { apiOrigin })
        const result = await browserTapaya.handleNextAction({ clientSecret })
        ```

        Verification opens a cancellable dialog, also for frictionless approval. The result is `successful`, `authorized`, `declined`, `action_required`, `pending`, or `cancelled`. Retry an unfinished action with the same secret. If an exception has `submitted: true`, read the payment before offering another attempt.

        Without Tapaya.js, redirect to `payment.nextAction.url`. It returns to `returnUrl` with `paymentId`. Inline and hosted verification cannot charge twice. Unfinished actions expire after 15 minutes, at `nextAction.expiresAt`.
      </TabsContent>
    </Tabs>

    <Accordions>
      <Accordion title="When a charge you started is declined" id="when-a-charge-is-declined">
        A declined merchant-initiated charge returns `status: 'declined'` with a `declineReason`. You can try the same order
        again while `retryAllowed` is `true`:

        * Retry with a new idempotency key, such as `sub-42-2026-11:retry-1`. Reusing the first key returns the declined
          attempt.
        * An order accepts at most five declined attempts.
        * After `do_not_retry`, Tapaya rejects further merchant-initiated charges of that card for the order. Ask the customer
          to pay with another card.

        | `declineReason`                                                                   | Suggested next step                                                                                              |
        | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
        | <span className="whitespace-nowrap">`insufficient_funds`, `limit_exceeded`</span> | Retry later, for example after a few days.                                                                       |
        | `processing_error`                                                                | Retry later.                                                                                                     |
        | `card_expired`                                                                    | Ask the customer for an updated card.                                                                            |
        | `authentication_required`                                                         | The bank wants the customer present. Ask them to pay with the [customer-initiated](#charge-the-saved-card) flow. |
        | <span className="whitespace-nowrap">`do_not_retry`, `fraud_suspected`</span>      | Do not charge this card again. Ask for another card.                                                             |
        | `card_declined`                                                                   | Ask the customer to contact their bank or use another card.                                                      |

        Treat any other value like `card_declined`.
      </Accordion>
    </Accordions>
  </Step>

  <Step>
    ### Confirm on your server [#confirm-on-your-server]

    Read `tapaya.payments.get(paymentId)` and compare its order reference, amount, and currency before you fulfill. Only `successful` proves a captured payment. Checkout has no webhooks yet, so keep reconciling unresolved payments on a schedule.
  </Step>
</Steps>

## Save Apple Pay and Google Pay [#save-apple-pay-and-google-pay]

Wallets use the same fields. To charge an Apple Pay card later, also pass a `walletAgreement`, which the native sheet shows to the customer:

```ts
const walletAgreement = {
	type: 'recurring' as const,
	description: 'Pro plan',
	managementUrl: 'https://shop.example/account/billing',
	recurring: { amount: 990, interval: 'month' as const, intervalCount: 1 },
}
```

* Pass it on the server and as `cardSaving.walletAgreement` in the browser.
* `managementUrl` must use HTTPS on an allowed checkout website.
* For top-ups, use `type: 'automatic_reload'` with `automaticReload: { amount, thresholdAmount }`, in minor units.
* Without an agreement, Apple Pay is hidden when saving for later charges.

Saved wallet methods expose `card.wallet.type` and `card.wallet.deviceBound`, and support merchant-initiated charges only. A device-bound method can stop working after the customer changes device.

## Check a card without charging it [#check-a-card-without-charging-it]

For free trials and bookings, save the card with a €0 verification. Use a unique order reference and required saving:

```ts
const setup = await tapaya.payments.create({
	merchantOrderId: 'setup-buyer-42',
	amount: 0,
	currency: 'EUR',
	customerReference: 'buyer-42',
	savePaymentMethod: 'required',
	paymentMethodUsage: 'off_session',
}, { idempotencyKey: 'setup-buyer-42:initial' })
```

Pass `setup.clientSecret` to the normal embedded form. Hosted sessions also accept a zero amount with required saving. Cards, Apple Pay, and Google Pay all work, without `shippingOptions`. For wallets, pass `wallets: { amount: 0, currency: 'EUR' }` and the matching saving policy; Apple Pay still needs its agreement. Later charges need a positive amount.

## Hold, capture, and refund [#hold-capture-and-refund]

To hold funds first, as a hotel or rental does, add `captureMethod: 'manual'`. `authorized` means the amount is held; capture it before `authorizationExpiresAt`.

```ts
const hold = await tapaya.payments.create({
	merchantOrderId: 'stay-310',
	amount: 20000,
	currency: 'EUR',
	paymentMethodId: 'pm_7Hq2x9LmR4',
	initiator: 'merchant',
	merchantInitiatedReason: 'unscheduled',
	captureMethod: 'manual',
	allowIncrementalAuthorization: true,
}, { idempotencyKey: 'stay-310' })

// After a server read confirms authorization, increase the hold by EUR 50.
await tapaya.payments.increment(hold.id, { amount: 5000 }, {
	idempotencyKey: 'stay-310:extra-night',
})
await tapaya.payments.capture(hold.id, { amount: 18000 }, {
	idempotencyKey: 'stay-310:checkout',
})
```

| Action         | Call                                              | Notes                                                                                                                                                                    |
| -------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Raise the hold | `payments.increment(id, { amount })`              | Adds to the hold. A decline keeps the original authorization and sets `declineReason`.                                                                                   |
| Capture        | `payments.capture(id, { amount })`                | Runs once. A partial capture releases the rest; omit `amount` to capture everything.                                                                                     |
| Release        | `payments.cancel(id, options)`                    | Frees the held amount without charging. Works only before capture; refund a captured payment instead.                                                                    |
| Refund         | `payments.refunds.create(id, { amount, reason })` | Refund in parts, as often as needed, up to `amountCaptured`. Omit `amount` to refund what is left. Reasons: `requested_by_customer`, `duplicate`, `fraudulent`, `other`. |

To refund a Hosted Checkout payment, pass the session's `paymentReference` as the payment ID. Read a refund until `status` is `succeeded` or `failed`; `pending` is unresolved and holds back its amount. Use a separate idempotency key for each create, increment, capture, cancel, or refund, and reuse it with the same body after a lost response. Track `amountAuthorized`, `amountCaptured`, and `amountRefunded` on server reads.

## Manage saved cards [#manage-saved-cards]

```ts
const page = await tapaya.paymentMethods.list({
	customerReference: 'buyer-42',
	limit: 25,
})
const method = await tapaya.paymentMethods.get('pm_7Hq2x9LmR4')
await tapaya.paymentMethods.update(method.id, { expMonth: 12, expYear: 2028 })
await tapaya.paymentMethods.delete(method.id)
```

* Lists return `{ data, nextCursor }`. Pass `cursor: page.nextCursor` for the next page while it is not null. Filter `wallet` by `none`, `apple_pay`, or `google_pay`.
* Updates accept `expMonth`, `expYear`, `cardholderName`, and `billingAddress`. Omitted and null fields stay unchanged.
  A card saved through Apple Pay or Google Pay cannot be updated and returns `409`; the wallet keeps it current.
* Deleting a method stops future charges and keeps past payments. Deleting it again returns the same confirmation.
* Always check that the requested method belongs to the signed-in customer and merchant.

<Accordions type="multiple">
  <Accordion title="In the merchant portal" id="in-the-merchant-portal">
    Merchants open **Saved cards** to filter by customer reference and see each card's status, expiry, usage, and payment history. Selecting a card lets them delete it or use **Charge card**, which appears for cards saved for later charges. Enter the order reference, amount, currency, and the recurring or unscheduled reason, and charge only amounts the customer agreed to.

    A transaction's **Card authorization** shows manual holds. A valid hold offers **Capture**, for all or part of the amount, or **Release**. An expired authorization cannot be captured, and an operation in progress must finish before another starts.

    Integrators can view a merchant's **Saved cards** list. Managing and charging cards belongs to the merchant portal.
  </Accordion>
</Accordions>
