VipterHelp Center

Migrating from Stripe to Vipter

A guide for teams that already bill with Stripe and will bill through Vipter, entirely or only in Brazil - what maps to what (Price → offer, Invoice → order), what does not migrate (saved cards and running subscriptions), the changes to checkout, webhook, off-cycle charge and metered usage code, with Node.js diffs, and a five-step cut-over plan that keeps both side by side during the transition.

Admin or OwnerAll plans

Vipter's API was designed for teams that have already integrated Stripe: the same object and event names, the same error format, the same pagination, the same Idempotency-Key, the same webhook signature scheme. What changes is what sits behind it: in Vipter the store sells through its own Brazilian acquirers (PIX, cards with installments, boleto) and the checkout is always the hosted one. This page lists the correspondences, what has no equivalent, what to swap in each piece of code and in which order to cut over.

Before you start

  • A Vipter store with its payment providers connected and, for testing, a test connection.
  • An API key with the write scope and a webhook endpoint on version 2026-11-01.
  • The list of the Stripe price_… ids your system uses today and of the events it handles.

The migration is of the billing layer, not necessarily of the acquirer: the store can connect its own Stripe account to Vipter to keep processing cards through it, and add Pagar.me, Mercado Pago or Asaas for PIX and installments. Your system's code talks only to Vipter's API, whatever the acquirer behind it.

What maps to what

StripeVipterWhat changes
sk_live_…vk_live_…Same use (Authorization: Bearer). There is no vk_test_: see Testing without a test mode.
Stripe-VersionVipter-VersionDates, as in Stripe. Current: 2026-11-01.
Productproduct (prd_)Same.
Priceoffer (ofr_)The offer carries the price per currency, the cycle, the free trial, the cycle limit and the pack. There is no inline price_data: the price is born in the dashboard or the catalog, never in the call.
Customercustomer (cust_)Requires a document (CPF or CNPJ) and a phone, which Brazilian acquirers demand.
Subscriptionsubscription (sub_)Same status (trialing, active, past_due, paused, canceled) plus expired. offer instead of items[].price. One offer per subscription.
Invoice and PaymentIntentorder (ord_)Every charge is an order, with billing_reason (purchase, renewal, manual, usage). There is no separate PaymentIntent or Charge.
InvoiceItem + Invoice paid right awaysubscription_charge (sch_)A single call: POST /v1/subscriptions/{id}/charges with the amount. Creates an order.
Checkout Sessioncheckout.session (cs_)Same main fields (client_reference_id, customer_email, metadata, success_url with {CHECKOUT_SESSION_ID}, cancel_url). One line_item only; quantity is the pack.
Billing Portal Sessionbilling_portal.sessionSame: customer, return_url, single-use url.
Coupon and Promotion Codediscounts[0].coupon with the coupon codeThe coupon is created in the dashboard.
Billing Meter and Meter Eventbilling.meter, billing.meter_eventSame fields. payload.customer_id; stripe_customer_id is accepted as the key too, so the code can stay.
Price with recurring.usage_type: meteredusage_item on the offer or on the subscriptionUnit price, allowance, graduated tiers, billing threshold.
Webhook Endpoint, whsec_…, Stripe-Signaturewebhook_endpoint, whsec_…, Vipter-SignatureSame t=…,v1=… scheme, HMAC SHA-256 of t.body. See Signature.
Event (evt_)event (evt_)Same names in the 2026-11-01 catalog. data.object is the Vipter object.

No equivalent

  • PaymentIntent, PaymentMethod, SetupIntent, Charge. The order is the unit; the saved card is implicit in the subscription and is only charged by POST /v1/subscriptions/{id}/charges or by renewals.
  • expand[], Search API, Tax Rates, draft Invoices, Quotes, Payment Links by API. Objects already carry what they need (offer and product inside the subscription, for instance). Checkout links come from the dashboard and accept URL parameters.
  • Several line_items in one session. One offer per session. To sell a bundle, create the bundle's offer.
  • Test mode. Tests run on the same store, with the provider's test connection; the orders come out with livemode: false.

What does not migrate

  • Saved cards. A card token belongs to the acquirer and to the integration that created it. Vipter does not import tokens, not even those of your Stripe account when it is connected to Vipter: cards saved by your direct integration are not reused. Each customer needs to pay once through Vipter's checkout; from then on the card is saved on the new subscription. The cut-over plan below is designed around this.
  • Running subscriptions. There is no subscription import endpoint. The new subscription is born from a paid checkout session. To avoid charging the same period twice, use a migration offer with trial_days equal to the days left in the Stripe cycle, or cancel the Stripe subscription with a prorated refund on the day the session completes.
  • Invoice and event history. It stays in Stripe. Export what you need before closing the account and keep the (provider, subscription_id) pair of each customer in your database.

What to swap in the code

The examples use Node.js with fetch. The switch by event type, user ids in client_reference_id and the "one endpoint per provider" structure come from Coexisting with Stripe.

Creating the checkout

checkout.mjs
-const session = await stripe.checkout.sessions.create({
-  mode: 'subscription',
-  line_items: [{ price: 'price_1Pq…', quantity: 1 }],
-  client_reference_id: user.id,
-  customer_email: user.email,
-  metadata: { user_id: user.id, plan: 'pro' },
-  success_url: 'https://app.example.com/billing/success?session_id={CHECKOUT_SESSION_ID}',
-  cancel_url: 'https://app.example.com/billing/plans',
-});
-redirect(session.url);
+const res = await fetch('https://api.vipter.com/v1/checkout/sessions', {
+  method: 'POST',
+  headers: {
+    Authorization: `Bearer ${process.env.VIPTER_API_KEY}`,
+    'Content-Type': 'application/json',
+    'Idempotency-Key': `${user.id}:pro:${Date.now()}`,
+  },
+  body: JSON.stringify({
+    offer: 'ofr_6e2b8d4f1a9c3e7b',          // the offer that replaces the price
+    client_reference_id: user.id,
+    customer_email: user.email,
+    metadata: { user_id: user.id, plan: 'pro' },
+    success_url: 'https://app.example.com/billing/success?session_id={CHECKOUT_SESSION_ID}',
+    cancel_url: 'https://app.example.com/billing/plans',
+  }),
+});
+const session = await res.json();
+redirect(session.url);

mode goes away: Vipter derives it from the offer. Keep a price_… → ofr_… table in your code or database during the transition.

Confirming the payment

success.mjs
-const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ['subscription'] });
-if (session.payment_status === 'paid') activate(session.client_reference_id, session.subscription.id);
+const session = await vipter(`/checkout/sessions/${sessionId}`);
+if (session.payment_status === 'paid') activate(session.client_reference_id, session.subscription);

session.subscription is already the sub_…; for the whole object, GET /v1/subscriptions/{id}. A PIX stays payment_status: pending until it is paid and then checkout.session.async_payment_succeeded fires, the same name Stripe uses for boleto.

Verifying the webhook

webhook.mjs
-const event = stripe.webhooks.constructEvent(rawBody, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
+const event = verifyVipter(rawBody, req.headers['vipter-signature'], process.env.VIPTER_WEBHOOK_SECRET);

The scheme is the same (t=…,v1=…, HMAC SHA-256 over `${t}.${rawBody}`), so the function you already have works with another header and another secret. The full verification code is in Webhook signatures.

Handling the events

EventIn Stripe, data.object isIn Vipter, data.object isWhat to adjust
checkout.session.completedCheckout Sessioncheckout.sessionsubscription and customer are already ids; order instead of payment_intent/invoice.
invoice.paidInvoiceorder (object: "order")amount_total instead of amount_paid; subscription and customer the same; billing_reason says whether it is a renewal, a purchase or an off-cycle charge.
invoice.payment_failedInvoiceorder with status: failedSame as above.
customer.subscription.created / updated / deletedSubscriptionsubscriptionoffer instead of items.data[0].price. updated carries data.previous_attributes as in Stripe.
customer.subscription.paused / resumedSubscriptionsubscriptionSame.
customer.created / updatedCustomercustomerdocument and address in Vipter's format.

The events that only exist in Vipter, subscription_charge.succeeded|failed, order.refunded|partially_refunded|charged_back and billing.meter.error_report_triggered, are in the event catalog.

Off-cycle charge

charge.mjs
-await stripe.invoiceItems.create({ customer, amount: 1990, currency: 'brl', description: 'Usage overage' });
-const invoice = await stripe.invoices.create({ customer, auto_advance: true });
-await stripe.invoices.pay(invoice.id);
+const charge = await vipter(`/subscriptions/${subscriptionId}/charges`, {
+  method: 'POST',
+  idempotencyKey: `usage:${subscriptionId}:2026-09`,
+  body: { amount: 1990, description: 'Usage overage', metadata: { period: '2026-09' } },
+});
+// charge.status: succeeded | pending | failed; charge.order is the ord_…

A card refusal comes back as 402 card_error, with the failed charge in error.subscription_charge. The Idempotency-Key is mandatory. See Charge the saved card.

Metered usage

usage.mjs
-await stripe.billing.meterEvents.create({
-  event_name: 'api_calls',
-  payload: { stripe_customer_id: customerId, value: '250' },
-  identifier: requestId,
-});
+await vipter('/billing/meter_events', {
+  method: 'POST',
+  body: { event_name: 'api_calls', payload: { customer_id: customerId, value: '250' }, identifier: requestId },
+});

payload.stripe_customer_id is accepted too, so even that line can stay as it is. The usage price comes from the offer or from POST /v1/subscriptions/{id}/usage_items, and the charge happens at the end of the cycle or when the threshold is reached. See Metered usage.

Customer portal

portal.mjs
-const portal = await stripe.billingPortal.sessions.create({ customer, return_url });
+const portal = await vipter('/billing_portal/sessions', { method: 'POST', body: { customer, return_url } });
 redirect(portal.url);

Five-step cut-over plan

1. Inventory and catalog

List the product_… and price_… ids in use. Create the equivalent products and offers in Vipter, with the price in reais, the cycle and the free trial. Check through the API with GET /v1/offers?type=recurring and build the price_… → ofr_… table.

2. Key, endpoint and code

Create the key and a webhook endpoint on version 2026-11-01, pointing at a new route (/webhooks/vipter). Apply the code swaps above behind a per-user flag (billing_provider: 'stripe' | 'vipter'). Test the checkout session with the provider's test connection.

3. New customers on Vipter

Turn the flag on for everyone who signs up from now on. Stripe keeps billing the old customers. The two webhooks arrive at separate routes, with separate secrets, and client_reference_id identifies the user in both.

4. Migrate old customers at renewal

For each Stripe subscription, close to the renewal, send the customer a link to a Vipter checkout session with client_reference_id equal to their id and metadata.stripe_subscription with the old sub_…. On checkout.session.completed, read that metadata, cancel the Stripe subscription (cancel_at_period_end: true or del with a prorated refund) and flip the user's flag. Whoever does not pay stays on Stripe until you decide.

5. Switch Stripe off

When the list of active subscriptions in Stripe reaches zero, disable the Stripe endpoint, revoke the sk_live_… keys and export invoices and customers to your archive. Remove the flag from the code.

Differences that tend to surprise

  • The amount comes from the offer, not from the call. There is no unit_amount on a checkout session. For a new price, create an offer.
  • invoice.paid carries an order, not an invoice. The field is object: "order", with amount_total, lines and billing_reason.
  • One customer.subscription.updated for many things: renewal, plan change, dunning recovery and card change. Tell them apart by data.previous_attributes, as in Stripe.
  • Document and phone are required when creating customers through the API, because Brazilian acquirers require them.
  • Installments are the buyer's decision at checkout; the order reports the number of installments, and the subscription charges the full amount each cycle.
  • PIX and boleto are asynchronous: the session completes with payment_status: pending and the payment confirms later through checkout.session.async_payment_succeeded.
  • No expand[]: the subscription already comes with offer and product summarized; the rest is a second call.
  • Limits: 100 requests every 2 seconds per key, with X-RateLimit-* and 429. See conventions.

Checklist

  • price_… → ofr_… table complete and offers active (GET /v1/offers).
  • vk_live_… key with write in an environment variable; GET /v1/account answers 200.
  • 2026-11-01 endpoint created; test with POST /v1/webhook_endpoints/{id}/test accepted by your server.
  • Test checkout paid with the test connection; checkout.session.completed received; client_reference_id checked on the subscription.
  • Test off-cycle charge with Idempotency-Key; a repeat returns the same charge.
  • Event deduplication by (provider, id).
  • Per-user flag and migration-at-renewal flow.
  • Stripe export stored before switching off.

What to do next

Was this page helpful?

On this page

Language