Migrate to VINR

Migrate to VINR — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

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 metadata so 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:

CardResult
4242 4242 4242 4242First renewal succeeds
4000 0000 0000 0002First renewal declines — test dunning
4000 0000 0000 32203D 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

Was this page helpful?
Edit on GitHub

Last updated on

On this page