VipterCentro de Ayuda

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.

Admin o PropietarioTodos los planes

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 write y un endpoint de webhook en la versión 2026-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é

StripeVipterQué cambia
sk_live_…vk_live_…Mismo uso (Authorization: Bearer). No existe vk_test_: consulta Probar sin modo de prueba.
Stripe-VersionVipter-VersionFechas, como en Stripe. Actual: 2026-11-01.
Productproduct (prd_)Igual.
Priceoffer (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.
Customercustomer (cust_)Pide documento (CPF o CNPJ) y teléfono, exigidos por los adquirentes brasileños.
Subscriptionsubscription (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 PaymentIntentorder (ord_)Cada cobro es un pedido, con billing_reason (purchase, renewal, manual, usage). No hay PaymentIntent ni Charge separados.
InvoiceItem + Invoice pagada al momentosubscription_charge (sch_)Una sola llamada: POST /v1/subscriptions/{id}/charges con el valor. Genera un pedido.
Checkout Sessioncheckout.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 Sessionbilling_portal.sessionIgual: customer, return_url, url de un solo uso.
Coupon y Promotion Codediscounts[0].coupon con el código del cupónEl cupón se crea en el panel.
Billing Meter y Meter Eventbilling.meter, billing.meter_eventMismos campos. payload.customer_id; stripe_customer_id también se acepta como clave, para no tocar el código.
Price con recurring.usage_type: meteredusage_item en la oferta o en la suscripciónPrecio por unidad, franquicia, tramos graduados, umbral de cobro.
Webhook Endpoint, whsec_…, Stripe-Signaturewebhook_endpoint, whsec_…, Vipter-SignatureMismo 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}/charges o por las renovaciones.
  • expand[], Search API, Tax Rates, Invoice en borrador, Quotes, Payment Links por API. Los objetos ya vienen con lo que necesitan (offer y product dentro de la suscripción, por ejemplo). Los enlaces de checkout salen del panel y aceptan parámetros de URL.
  • Varios line_items en 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_days igual 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

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',          // 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

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 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

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);

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

EventoEn Stripe, data.object esEn Vipter, data.object esQué ajustar
checkout.session.completedCheckout Sessioncheckout.sessionsubscription y customer ya son IDs; order en lugar de payment_intent/invoice.
invoice.paidInvoiceorder (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_failedInvoiceorder con status: failedIgual que el anterior.
customer.subscription.created / updated / deletedSubscriptionsubscriptionoffer en lugar de items.data[0].price. updated trae data.previous_attributes como en Stripe.
customer.subscription.paused / resumedSubscriptionsubscriptionIguales.
customer.created / updatedCustomercustomerdocument 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

charge.mjs
-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

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 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

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);

Plan de corte en cinco etapas

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_amount en una sesión de checkout. Para un precio nuevo, crea una oferta.
  • invoice.paid trae un pedido, no una factura. El campo es object: "order", con amount_total, lines y billing_reason.
  • Un customer.subscription.updated para muchas cosas: renovación, cambio de plan, recuperación de morosidad y cambio de tarjeta. Distínguelos por data.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: pending y el pago se confirma después por checkout.session.async_payment_succeeded.
  • Sin expand[]: la suscripción ya viene con offer y product resumidos; el resto es una segunda llamada.
  • Límites: 100 solicitudes cada 2 segundos por clave, con X-RateLimit-* y 429. Consulta convenciones.

Lista de verificación

  • Tabla price_… → ofr_… completa y ofertas activas (GET /v1/offers).
  • Clave vk_live_… con write en variable de entorno; GET /v1/account responde 200.
  • Endpoint 2026-11-01 creado; prueba con POST /v1/webhook_endpoints/{id}/test aceptada por tu servidor.
  • Checkout de prueba pagado con la conexión de prueba; checkout.session.completed recibido; client_reference_id verificado 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

¿Te ayudó esta página?

En esta página

Idioma