Migrate to VINR
Migrate to VINR — a runnable, end-to-end guide verified against the VINR sandbox.
This guide moves customers, saved payment methods, active subscriptions, and loyalty balances from a previous provider onto VINR with zero double-charges and a clean rollback path. Every step is runnable against the sandbox before you touch production data.
Plan the migrationAsk
Migrations fail on the details, not the API. Decide three things up front: what moves, in what order, and how you cut over.
- Customers and payment methods first. Subscriptions and loyalty accounts reference customers, so import those before anything that points at them.
- Run dual-write, not big-bang. Keep your old provider live while VINR shadows it. Only flip traffic once balances reconcile.
- Preserve external IDs. Store every old-provider ID in
metadataso you can reconcile and roll back by lookup.
Payment-method (card) data is PCI-scoped. You cannot export raw PANs. VINR imports cards through a provider-to-provider network token transfer — see below — so cardholders are never re-prompted.
old provider your importer VINR (sandbox)
│ export CSV / API │ │
│─────────────────────►│ create customer │
│ │─────────────────────►│ cust_…
│ │ request card import │
│ │─────────────────────►│ pm_… (tokenized)
│ reconcile by metadata│◄────────────────────│Import customersAsk
Create each customer with the old-provider ID in metadata. The idempotencyKey makes the whole import safe to re-run after a partial failure.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
async function importCustomer(row: LegacyCustomer) {
return vinr.customers.create(
{
email: row.email,
name: row.name,
metadata: { legacyId: row.id, source: 'acme-billing' },
},
{ idempotencyKey: `migrate-cust-${row.id}` },
);
}Keep a mapping table (legacyId -> cust_…) — every later step joins on it.
Import payment methodsAsk
Saved cards move as network tokens, never raw card numbers. You request the import; VINR coordinates the secure transfer with your previous processor.
const pm = await vinr.paymentMethods.import(
{
customer: customerId, // cust_…
transfer: {
provider: 'acme', // your prior processor
reference: row.legacyPaymentMethodId,
},
metadata: { legacyId: row.legacyPaymentMethodId },
},
{ idempotencyKey: `migrate-pm-${row.legacyPaymentMethodId}` },
);A token transfer can take several days to clear the card networks. Start payment-method imports early and treat any method still in pending at cutover as "charge on next cycle, not now."
Migrate subscriptionsAsk
Recreate each subscription against an existing price, then anchor the billing cycle so the customer is not charged twice for a period the old provider already billed.
const sub = await vinr.subscriptions.create(
{
customer: customerId, // cust_…
price: priceId, // price_…
defaultPaymentMethod: pmId, // pm_…
billingCycleAnchor: row.nextRenewalAt, // ISO date of next charge
prorationBehavior: 'none', // no charge at migration
metadata: { legacyId: row.legacySubscriptionId },
},
{ idempotencyKey: `migrate-sub-${row.legacySubscriptionId}` },
);The billingCycleAnchor set to the next renewal date is what prevents a duplicate charge: VINR issues the first invoice on the date the old provider would have, not at creation.
Cancel the subscription on the old provider only after confirming the VINR subscription is active. Cancel old subscriptions without auto-refunding so the final paid period is honored.
Import loyalty balancesAsk
Move point balances as a single adjusting transaction per account so the ledger stays auditable. Create the loyalty account, then post the opening balance.
const account = await vinr.loyalty.accounts.create({
program: programId, // prog_…
customer: customerId, // cust_…
});
await vinr.loyalty.points.create(
{
account: account.id, // loy_…
amount: row.pointsBalance, // integer points
reason: 'migration_opening_balance',
metadata: { legacyId: row.legacyLoyaltyId },
},
{ idempotencyKey: `migrate-loy-${row.legacyLoyaltyId}` },
);Reconcile by summing imported ptx_ transactions per program against your old totals before going live.
Test the importerAsk
Run the full pipeline against https://sandbox.api.vinr.com first. Use sandbox cards to exercise the first post-migration charge once anchors elapse:
| Card | Result |
|---|---|
4242 4242 4242 4242 | First renewal succeeds |
4000 0000 0000 0002 | First renewal declines — test dunning |
4000 0000 0000 3220 | 3D Secure on first renewal |
Verify the first live-cycle invoice fires by listening for invoice.paid and loyalty.points.earned:
export async function POST(req: Request) {
const event = vinr.webhooks.verify(
await req.text(),
req.headers.get('x-vinr-signature'),
);
if (event.type === 'invoice.paid') {
await reconcile(event.data.metadata.legacyId);
}
return new Response('OK', { status: 200 });
}Cut over and roll backAsk
Freeze writes on the old provider
Pause new charges and subscription edits on the source system to stop divergence during the final sync.
Run a final delta import
Re-run the importer; the idempotency keys skip everything already migrated and only the delta is created.
Reconcile, then flip traffic
Confirm customer, subscription, and loyalty counts match. Point your application at VINR live keys from the Dashboard.
Keep rollback ready
For 30 days, leave old subscriptions canceled-but-restorable. To roll back, reactivate on the source by legacyId and pause VINR subscriptions — no data is lost because every record carries its metadata.legacyId.
Next stepsAsk
Go-live checklist
Final checks before flipping live keys.
Subscriptions
Cycles, anchors, and proration in depth.
Loyalty accounts
How point ledgers and transactions work.
Last updated on