Save and reuse a card
Save and reuse a card — a runnable, end-to-end guide verified against the VINR sandbox.
Saving a card lets returning customers pay in one tap and powers off-session charges like subscription renewals and top-ups. This guide takes you from collecting consent through charging a stored method, all runnable against the VINR sandbox.
OverviewAsk
A saved card is a payment method attached to a customer (cust_…). You collect and tokenize the card once with the customer present, then reuse the resulting pm_… token for future charges — either with the customer on your page (on-session) or without them (off-session, e.g. a renewal).
your server VINR customer
│ create setup │ │
│─────────────────►│ │
│ setupUrl │ │
│◄─────────────────│ redirect │
│──────────────────────────────────────►│ enters card + SCA
│ payment_method.saved (webhook) │
│◄─────────────────│◄──────────────────│
│ store pm_ id │ │
│ charge later (off-session) │
│─────────────────►│ │Two rules drive everything below: a customer must explicitly consent before you store a card, and you must record the intended future usage so VINR can apply the right authentication when you charge later.
Create a customerAsk
A saved card always belongs to a customer. Create one (or reuse an existing cust_…) before saving the method.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const customer = await vinr.customers.create({
email: 'ada@example.com',
name: 'Ada Lovelace',
metadata: { userId: 'u_8842' },
});
// customer.id → "cust_…"Save a payment methodAsk
Use a setup to collect and authenticate the card without taking a payment. Set usage to declare how you'll charge it later — this is what determines the SCA exemption VINR can request.
export async function POST(req: Request) {
const { customerId } = await req.json();
const setup = await vinr.setups.create(
{
customer: customerId,
usage: 'off_session', // renewals / merchant-initiated
returnUrl: 'https://yoursite.com/wallet',
},
{ idempotencyKey: `setup-${customerId}` },
);
return Response.json({ setupUrl: setup.setupUrl });
}Redirect the customer to setup.setupUrl. VINR hosts card entry and runs the 3D Secure challenge needed to authorize future off-session use.
Always capture and store proof of consent (timestamp, IP, the agreement text shown). Off-session charges without recorded mandate consent are the most common cause of disputes — see Compliance notes.
Confirm the saved methodAsk
When the setup completes, VINR emits payment_method.saved. Persist the pm_… id against your customer record from the webhook so it's recorded exactly once.
export async function POST(req: Request) {
const event = vinr.webhooks.verify(
await req.text(),
req.headers.get('x-vinr-signature'),
);
if (event.type === 'payment_method.saved') {
await db.savePaymentMethod({
customerId: event.data.customer,
paymentMethodId: event.data.id, // "pm_…"
brand: event.data.card.brand, // "visa"
last4: event.data.card.last4, // "4242"
});
}
return new Response('OK', { status: 200 });
}Charge a saved methodAsk
To charge later, create a payment referencing the customer and stored pm_…. Set offSession: true when the customer is not present so VINR knows to use the saved mandate.
const payment = await vinr.payments.create(
{
amount: 4999, // €49.99
currency: 'EUR',
customer: 'cust_…',
paymentMethod: 'pm_…',
offSession: true,
description: 'Monthly plan renewal',
metadata: { invoiceId: 'inv_…' },
},
{ idempotencyKey: 'renewal-2026-05' },
);
if (payment.status === 'completed') {
// charge succeeded with no customer interaction
}Some banks still require authentication even for stored cards. When that happens the payment returns status: 'requires_action' with an authenticationUrl — bring the customer back on-session to complete it.
if (payment.status === 'requires_action') {
await emailCustomer({
invoiceId: payment.metadata.invoiceId,
link: payment.authenticationUrl, // "Confirm your payment"
});
}Manage stored methodsAsk
List, inspect, and remove a customer's saved cards. Always offer customers a way to remove a card from your UI.
const methods = await vinr.customers.paymentMethods.list('cust_…');
// → [{ id: 'pm_…', card: { brand, last4, expMonth, expYear } }, …]
await vinr.paymentMethods.detach('pm_…'); // forget the cardProp
Type
Test itAsk
Use these sandbox cards on the hosted setup page:
| Card | Result |
|---|---|
4242 4242 4242 4242 | Saved successfully |
4000 0000 0000 3220 | Requires 3D Secure to save |
4000 0000 0000 0002 | Declined at save time |
To simulate a later off-session charge that needs re-authentication, charge a method saved with the 3220 card — the payment returns requires_action.
Compliance notesAsk
Storing and reusing cards carries obligations beyond the API call:
- Consent and mandate. Record explicit consent, the date, and the terms shown before the first off-session charge. Keep it retrievable for the life of the agreement.
- SCA. In the EEA, the initial save is authenticated with 3D Secure so subsequent merchant-initiated charges can claim an exemption. VINR requests this automatically when
usageisoff_session. - Network mandates. For recurring charges, send a pre-debit notification per card-network rules. VINR stores the mandate reference returned at save time and attaches it to each charge.
- PCI scope. Because card entry happens on VINR-hosted pages and you only ever handle
pm_…tokens, you stay within the reduced SAQ A scope. See PCI compliance.
This page is informational and not legal advice; consult your compliance counsel for binding decisions.
Next stepsAsk
Handle 3D Secure
Complete SCA at save and at charge time.
Payment methods
Everything VINR can store and charge.
Subscriptions
Use saved cards for recurring billing.
Last updated on