Save Cards & Charge Later
Save a card, Apple Pay, or Google Pay with the customer's consent, then charge it for renewals, top-ups, hotel holds, or faster repeat checkout.
Two ways to use a saved card
You charge later
Merchant-initiated (MIT)
- Who starts it
- Your server, while the customer is away
- Typical use
- Subscriptions, free trials, auto top-ups, hotel holds
- Methods
- Cards, Apple Pay, and Google Pay
// Save the cardpaymentMethodUsage: 'off_session'// Charge itinitiator: 'merchant'Customer pays faster
Customer-initiated (CIT)
- Who starts it
- The customer, on your site
- Typical use
- One-click repeat checkout
- Methods
- Typed cards only
// Save the cardpaymentMethodUsage: 'on_session'// Charge itinitiator: 'customer'Want to see a whole flow for a real business first? Try Use cases.
Implementation
Your server uses the same merchant credentials for checkout, saved cards, and later charges.
Save the card at checkout
Add three fields when you create a hosted session or an embedded payment:
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.
Get consent first with required saving
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.
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.
Charge the saved card
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.
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:
recurringfor a fixed series the customer agreed to, such as a monthly plan.unscheduledfor 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.
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.
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:
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.walletAgreementin the browser. managementUrlmust use HTTPS on an allowed checkout website.- For top-ups, use
type: 'automatic_reload'withautomaticReload: { 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
For free trials and bookings, save the card with a €0 verification. Use a unique order reference and required saving:
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
To hold funds first, as a hotel or rental does, add captureMethod: 'manual'. authorized means the amount is held; capture it before authorizationExpiresAt.
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
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 }. Passcursor: page.nextCursorfor the next page while it is not null. Filterwalletbynone,apple_pay, orgoogle_pay. - Updates accept
expMonth,expYear,cardholderName, andbillingAddress. Omitted and null fields stay unchanged. A card saved through Apple Pay or Google Pay cannot be updated and returns409; 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.