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.
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
writescope and a webhook endpoint on version2026-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
| Stripe | Vipter | What changes |
|---|---|---|
sk_live_… | vk_live_… | Same use (Authorization: Bearer). There is no vk_test_: see Testing without a test mode. |
Stripe-Version | Vipter-Version | Dates, as in Stripe. Current: 2026-11-01. |
| Product | product (prd_) | Same. |
| Price | offer (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. |
| Customer | customer (cust_) | Requires a document (CPF or CNPJ) and a phone, which Brazilian acquirers demand. |
| Subscription | subscription (sub_) | Same status (trialing, active, past_due, paused, canceled) plus expired. offer instead of items[].price. One offer per subscription. |
| Invoice and PaymentIntent | order (ord_) | Every charge is an order, with billing_reason (purchase, renewal, manual, usage). There is no separate PaymentIntent or Charge. |
| InvoiceItem + Invoice paid right away | subscription_charge (sch_) | A single call: POST /v1/subscriptions/{id}/charges with the amount. Creates an order. |
| Checkout Session | checkout.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 Session | billing_portal.session | Same: customer, return_url, single-use url. |
| Coupon and Promotion Code | discounts[0].coupon with the coupon code | The coupon is created in the dashboard. |
| Billing Meter and Meter Event | billing.meter, billing.meter_event | Same fields. payload.customer_id; stripe_customer_id is accepted as the key too, so the code can stay. |
Price with recurring.usage_type: metered | usage_item on the offer or on the subscription | Unit price, allowance, graduated tiers, billing threshold. |
Webhook Endpoint, whsec_…, Stripe-Signature | webhook_endpoint, whsec_…, Vipter-Signature | Same 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}/chargesor by renewals. expand[], Search API, Tax Rates, draft Invoices, Quotes, Payment Links by API. Objects already carry what they need (offerandproductinside the subscription, for instance). Checkout links come from the dashboard and accept URL parameters.- Several
line_itemsin 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_daysequal 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
-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
-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
-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
| Event | In Stripe, data.object is | In Vipter, data.object is | What to adjust |
|---|---|---|---|
checkout.session.completed | Checkout Session | checkout.session | subscription and customer are already ids; order instead of payment_intent/invoice. |
invoice.paid | Invoice | order (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_failed | Invoice | order with status: failed | Same as above. |
customer.subscription.created / updated / deleted | Subscription | subscription | offer instead of items.data[0].price. updated carries data.previous_attributes as in Stripe. |
customer.subscription.paused / resumed | Subscription | subscription | Same. |
customer.created / updated | Customer | customer | document 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
-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
-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
-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_amounton a checkout session. For a new price, create an offer. invoice.paidcarries an order, not an invoice. The field isobject: "order", withamount_total,linesandbilling_reason.- One
customer.subscription.updatedfor many things: renewal, plan change, dunning recovery and card change. Tell them apart bydata.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: pendingand the payment confirms later throughcheckout.session.async_payment_succeeded. - No
expand[]: the subscription already comes withofferandproductsummarized; the rest is a second call. - Limits: 100 requests every 2 seconds per key, with
X-RateLimit-*and429. See conventions.
Checklist
-
price_… → ofr_…table complete and offers active (GET /v1/offers). -
vk_live_…key withwritein an environment variable;GET /v1/accountanswers 200. -
2026-11-01endpoint created; test withPOST /v1/webhook_endpoints/{id}/testaccepted by your server. - Test checkout paid with the test connection;
checkout.session.completedreceived;client_reference_idchecked 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
- Follow the full guide with code in SaaS: from sign-up to dashboard.
- See every field in the API reference and every event in the catalog.
- Let an AI agent do the code swap with the API skill.