Migrar de Stripe a Vipter
Guía para quien ya cobra con Stripe y va a cobrar por Vipter, del todo o solo en Brasil - qué corresponde a qué (Price → oferta, Invoice → pedido), qué no migra (tarjetas guardadas y suscripciones en curso), los cambios en el código de checkout, webhooks, cobros adicionales y uso medido, con diffs en Node.js, y un plan de corte en cinco etapas que mantiene los dos lado a lado durante la transición.
La API de Vipter fue diseñada para quien ya integró Stripe: los mismos nombres de objeto y de evento, el mismo formato de error, la misma paginación, la misma Idempotency-Key, el mismo esquema de firma de webhook. Lo que cambia es lo que hay detrás: en Vipter la tienda vende por sus propios adquirentes brasileños (PIX, tarjeta en cuotas, boleto) y el checkout es siempre el alojado. Esta página lista las correspondencias, lo que no tiene equivalente, qué cambiar en cada fragmento de código y en qué orden hacer el corte.
Antes de empezar
- Una tienda en Vipter con los proveedores de pago conectados y, para probar, una conexión de prueba.
- Una clave de API con el alcance
writey un endpoint de webhook en la versión2026-11-01. - La lista de los
price_…de Stripe que tu sistema usa hoy y de los eventos que trata.
La migración es de la capa de cobro, no necesariamente del adquirente: la tienda puede conectar su propia cuenta de Stripe a Vipter para seguir procesando tarjetas por ella, y sumar Pagar.me, Mercado Pago o Asaas para PIX y cuotas. El código de tu sistema habla solo con la API de Vipter, sea cual sea el adquirente detrás.
Qué corresponde a qué
| Stripe | Vipter | Qué cambia |
|---|---|---|
sk_live_… | vk_live_… | Mismo uso (Authorization: Bearer). No existe vk_test_: consulta Probar sin modo de prueba. |
Stripe-Version | Vipter-Version | Fechas, como en Stripe. Actual: 2026-11-01. |
| Product | product (prd_) | Igual. |
| Price | offer (ofr_) | La oferta lleva el precio por moneda, el ciclo, la prueba gratis, el límite de ciclos y el paquete. No hay price_data en línea: el precio nace en el panel o en el catálogo, nunca en la llamada. |
| Customer | customer (cust_) | Pide documento (CPF o CNPJ) y teléfono, exigidos por los adquirentes brasileños. |
| Subscription | subscription (sub_) | Mismo status (trialing, active, past_due, paused, canceled) más expired. offer en lugar de items[].price. Una oferta por suscripción. |
| Invoice y PaymentIntent | order (ord_) | Cada cobro es un pedido, con billing_reason (purchase, renewal, manual, usage). No hay PaymentIntent ni Charge separados. |
| InvoiceItem + Invoice pagada al momento | subscription_charge (sch_) | Una sola llamada: POST /v1/subscriptions/{id}/charges con el valor. Genera un pedido. |
| Checkout Session | checkout.session (cs_) | Mismos campos principales (client_reference_id, customer_email, metadata, success_url con {CHECKOUT_SESSION_ID}, cancel_url). Un solo line_item; quantity es el paquete. |
| Billing Portal Session | billing_portal.session | Igual: customer, return_url, url de un solo uso. |
| Coupon y Promotion Code | discounts[0].coupon con el código del cupón | El cupón se crea en el panel. |
| Billing Meter y Meter Event | billing.meter, billing.meter_event | Mismos campos. payload.customer_id; stripe_customer_id también se acepta como clave, para no tocar el código. |
Price con recurring.usage_type: metered | usage_item en la oferta o en la suscripción | Precio por unidad, franquicia, tramos graduados, umbral de cobro. |
Webhook Endpoint, whsec_…, Stripe-Signature | webhook_endpoint, whsec_…, Vipter-Signature | Mismo esquema t=…,v1=…, HMAC SHA-256 de t.cuerpo. Consulta Firma. |
Event (evt_) | event (evt_) | Mismos nombres en el catálogo 2026-11-01. El data.object es el objeto de Vipter. |
Sin equivalente
- PaymentIntent, PaymentMethod, SetupIntent, Charge. El pedido es la unidad; la tarjeta guardada está implícita en la suscripción y solo se cobra por
POST /v1/subscriptions/{id}/chargeso por las renovaciones. expand[], Search API, Tax Rates, Invoice en borrador, Quotes, Payment Links por API. Los objetos ya vienen con lo que necesitan (offeryproductdentro de la suscripción, por ejemplo). Los enlaces de checkout salen del panel y aceptan parámetros de URL.- Varios
line_itemsen una sesión. Una oferta por sesión. Para vender un conjunto, crea la oferta del conjunto. - Modo de prueba. Las pruebas corren en la misma tienda, con una conexión de prueba del proveedor; los pedidos salen con
livemode: false.
Qué no migra
- Tarjetas guardadas. El token de una tarjeta pertenece al adquirente y a la integración que lo creó. Vipter no importa tokens, ni siquiera los de tu cuenta de Stripe cuando está conectada a Vipter: las tarjetas guardadas por tu integración directa no se reutilizan. Cada cliente necesita pagar una vez por el checkout de Vipter; a partir de ahí la tarjeta queda guardada en la nueva suscripción. El plan de corte de abajo está diseñado alrededor de esto.
- Suscripciones en curso. No hay endpoint de importación de suscripciones. La suscripción nueva nace de una sesión de checkout pagada. Para no cobrar dos veces el mismo período, usa una oferta de migración con
trial_daysigual a los días que faltan en el ciclo de Stripe, o cancela la suscripción de Stripe con reembolso proporcional el día en que la sesión se complete. - Historial de facturas y eventos. Se queda en Stripe. Exporta lo que necesites antes de cerrar la cuenta y guarda en tu base de datos el par
(provider, subscription_id)de cada cliente.
Qué cambiar en el código
Los ejemplos usan Node.js con fetch. El switch por tipo de evento, los IDs de usuario en client_reference_id y la estructura de "un endpoint por proveedor" vienen de Convivir con Stripe.
Crear el 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', // la oferta que reemplaza al 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);El mode desaparece: Vipter lo deduce de la oferta. Guarda una tabla price_… → ofr_… en tu código o en la base de datos durante la transición.
Confirmar el pago
-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 ya es el sub_…; para el objeto entero, GET /v1/subscriptions/{id}. Un PIX queda payment_status: pending hasta que se paga y entonces sale checkout.session.async_payment_succeeded, el mismo nombre que Stripe usa para el boleto.
Verificar el 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);El esquema es el mismo (t=…,v1=…, HMAC SHA-256 sobre `${t}.${rawBody}`), así que la función que ya tienes sirve con otro encabezado y otro secreto. El código completo de la verificación está en Firma de los webhooks.
Tratar los eventos
| Evento | En Stripe, data.object es | En Vipter, data.object es | Qué ajustar |
|---|---|---|---|
checkout.session.completed | Checkout Session | checkout.session | subscription y customer ya son IDs; order en lugar de payment_intent/invoice. |
invoice.paid | Invoice | order (object: "order") | amount_total en lugar de amount_paid; subscription y customer iguales; billing_reason dice si es renovación, compra o cobro adicional. |
invoice.payment_failed | Invoice | order con status: failed | Igual que el anterior. |
customer.subscription.created / updated / deleted | Subscription | subscription | offer en lugar de items.data[0].price. updated trae data.previous_attributes como en Stripe. |
customer.subscription.paused / resumed | Subscription | subscription | Iguales. |
customer.created / updated | Customer | customer | document y address en el formato de Vipter. |
Los eventos que solo existen en Vipter, subscription_charge.succeeded|failed, order.refunded|partially_refunded|charged_back y billing.meter.error_report_triggered, están en el catálogo de eventos.
Cobro adicional
-await stripe.invoiceItems.create({ customer, amount: 1990, currency: 'brl', description: 'Excedente de uso' });
-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: 'Excedente de uso', metadata: { period: '2026-09' } },
+});
+// charge.status: succeeded | pending | failed; charge.order es el ord_…El rechazo de la tarjeta vuelve como 402 card_error, con el cobro fallido en error.subscription_charge. La Idempotency-Key es obligatoria. Consulta Cobrar la tarjeta guardada.
Uso medido
-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 también se acepta, así que hasta esa línea puede quedarse como está. El precio del uso sale de la oferta o de POST /v1/subscriptions/{id}/usage_items, y el cobro ocurre al final del ciclo o al alcanzar el umbral. Consulta Uso medido.
Portal del cliente
-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);Plan de corte en cinco etapas
1. Inventario y catálogo
Lista los product_… y price_… en uso. Crea en Vipter los productos y las ofertas equivalentes, con precio en reales, ciclo y prueba gratis. Verifica por la API con GET /v1/offers?type=recurring y arma la tabla price_… → ofr_….
2. Clave, endpoint y código
Crea la clave y un endpoint de webhook en la versión 2026-11-01, apuntando a una ruta nueva (/webhooks/vipter). Aplica los cambios de código de arriba detrás de un indicador por usuario (billing_provider: 'stripe' | 'vipter'). Prueba la sesión de checkout con la conexión de prueba del proveedor.
3. Clientes nuevos en Vipter
Activa el indicador para quien se registre a partir de ahora. Stripe sigue cobrando a los clientes antiguos. Los dos webhooks llegan a rutas separadas, con secretos separados, y el client_reference_id identifica al usuario en ambos.
4. Migrar a los clientes antiguos en la renovación
Para cada suscripción de Stripe, cerca de la renovación, envíale al cliente un enlace a una sesión de checkout de Vipter con client_reference_id igual a su ID y metadata.stripe_subscription con el sub_… antiguo. En el checkout.session.completed, lee ese metadata, cancela la suscripción de Stripe (cancel_at_period_end: true o del con reembolso proporcional) y cambia el indicador del usuario. Quien no pague sigue en Stripe hasta que decidas.
5. Apagar Stripe
Cuando la lista de suscripciones activas en Stripe llegue a cero, desactiva el endpoint de Stripe, revoca las claves sk_live_… y exporta facturas y clientes a tu archivo. Quita el indicador del código.
Diferencias que suelen sorprender
- El valor viene de la oferta, no de la llamada. No existe
unit_amounten una sesión de checkout. Para un precio nuevo, crea una oferta. invoice.paidtrae un pedido, no una factura. El campo esobject: "order", conamount_total,linesybilling_reason.- Un
customer.subscription.updatedpara muchas cosas: renovación, cambio de plan, recuperación de morosidad y cambio de tarjeta. Distínguelos pordata.previous_attributes, como en Stripe. - Documento y teléfono son obligatorios al crear clientes por la API, porque los adquirentes brasileños los exigen.
- Las cuotas son decisión del comprador en el checkout; el pedido informa el número de cuotas, y la suscripción cobra el valor completo en cada ciclo.
- PIX y boleto son asíncronos: la sesión se completa con
payment_status: pendingy el pago se confirma después porcheckout.session.async_payment_succeeded. - Sin
expand[]: la suscripción ya viene conofferyproductresumidos; el resto es una segunda llamada. - Límites: 100 solicitudes cada 2 segundos por clave, con
X-RateLimit-*y429. Consulta convenciones.
Lista de verificación
- Tabla
price_… → ofr_…completa y ofertas activas (GET /v1/offers). - Clave
vk_live_…conwriteen variable de entorno;GET /v1/accountresponde 200. - Endpoint
2026-11-01creado; prueba conPOST /v1/webhook_endpoints/{id}/testaceptada por tu servidor. - Checkout de prueba pagado con la conexión de prueba;
checkout.session.completedrecibido;client_reference_idverificado en la suscripción. - Cobro adicional de prueba con
Idempotency-Key; la repetición devuelve el mismo cobro. - Deduplicación de eventos por
(provider, id). - Indicador por usuario y flujo de migración en la renovación.
- Exportación de Stripe guardada antes de apagar.
Qué hacer a continuación
- Sigue la guía completa con código en SaaS: del registro al dashboard.
- Mira cada campo en la Referencia de la API y cada evento en el catálogo.
- Deja que un agente de IA haga el cambio de código con la skill de la API.