Ir al contenido
AppsSDAR

Suscripciones recurrentes en Perú: Culqi frente a Izipay

actualizada el 10 min de lectura Pagos y normativa peruana

Un sistema que se vende por mes tiene que cobrar por mes. Pedirle la tarjeta al cliente cada vez que vence su plan funciona hasta que alguien se olvida y su servicio se corta un domingo. Las dos pasarelas con las que cobramos en producción, Culqi e Izipay, tienen suscripciones nativas: la pasarela guarda la tarjeta y la cobra sola. Las usamos en Arca (las dos), en SortealoPE y en wapi (Izipay). Esta nota junta lo que cambia entre una y otra y las trampas que ya nos costaron tiempo.

Qué hay que construir en cualquier caso

La idea es la misma en ambas: una tarjeta tokenizada, una regla de frecuencia y un monto. El número de la tarjeta nunca pasa por nuestro servidor; en la base solo guardamos el token, y cifrado (AES-256-GCM en SortealoPE). El cobro recurrente lo dispara la pasarela, no un cron nuestro. Nosotros solo nos enteramos y movemos la fecha hasta la que el cliente tiene servicio. Si el sistema no se entera, el cliente paga y el plan vence igual. Por eso cada integración lleva tres piezas: el aviso de la pasarela (webhook o IPN), un cron que le pregunta a la pasarela por las suscripciones vivas, y un procesamiento idempotente que acepte el mismo cobro por cualquiera de los dos caminos.

Culqi: tres objetos en cadena

En Culqi el flujo es explícito y obligatorio: Customer, luego Card (necesita el customer_id) y luego Subscription (necesita el card_id y un Plan). No hay atajo. Lo que aprendimos al armarlo en Arca:

  • Solo tarjeta. Yape, PagoEfectivo y las demás billeteras son cargo único. No se pueden asociar a una suscripción.
  • El cliente pide más datos de los que uno recolecta. POST /v2/customers exige nombre, apellido, dirección, ciudad, país y teléfono. Culqi solo valida el formato: sin envíos físicos, dirección y ciudad van con un valor fijo.
  • La tarjeta, sin validación. POST /v2/cards con validate: true (el valor por defecto) genera un cargo real de S/ 3. Como el primer cobro lo hace la creación de la suscripción, mandamos validate: false.
  • El plan lleva el intervalo adentro. interval_unit_time vale 1 diario, 2 semanal, 3 mensual, 4 anual, 5 trimestral, 6 semestral. Un plan de tu negocio con cuatro ciclos son cuatro planes en Culqi.
  • Un plan no se cambia. El PATCH de una suscripción solo acepta card_id o metadata. Cambiar de plan es cancelar (DELETE) y crear otra.

Izipay: el primer pago crea la tarjeta

Izipay tiene otra forma. El cobro inicial es un checkout normal (Charge/CreatePayment) con formAction: REGISTER_PAY, que cobra y guarda la tarjeta en el mismo paso. Con el paymentMethodToken de esa respuesta se llama a Charge/CreateSubscription. El detalle fino está en la opción: ASK_REGISTER_PAY deja que el comprador decida con un checkbox si se guarda la tarjeta, y quien no lo marca paga pero no queda ninguna suscripción posible.

Charge/CreateSubscription
  amount              monto en céntimos
  currency            PEN
  effectDate          fecha ISO 8601, no pasada
  paymentMethodToken  el de la tarjeta registrada
  rrule               RRULE:FREQ=MONTHLY;INTERVAL=1

La recurrencia se expresa con una regla RFC-5545. En SortealoPE los cuatro ciclos de pago se cobran solos con INTERVAL=1, 3, 6 o 12 sobre FREQ=MONTHLY. Para el anual, el aviso de recurrencia bajo el formulario no es decoración: es lo que evita la disputa por cargo no reconocido.

En Arca llegamos a escribir un cron propio que reutilizaba el token mes a mes. Cobraba bien, pero no aparecía en Gestión > Suscripciones del back office, que solo muestra las creadas con este objeto.

Trampas de Izipay que solo se ven con una suscripción real

  • El effectDate no admite milisegundos. El toISOString() de JavaScript los incluye siempre y Izipay responde INT_069 con "Unknown effectDate". Hay que recortarlos: .replace(/\.\d3Z$/, 'Z').
  • Ignora la hora y cambia de huso. Toma la fecha en la zona de la tienda y la fija a medianoche en la de la plataforma. Midiendo contra la API real, enviar 2026-10-08T04:32Z devolvió 2026-10-06T22:00Z: el primer cobro caía casi un día antes de terminar lo ya pagado. Sumamos 24 horas de margen: cobrar unas horas antes no perjudica al cliente; cobrar después lo dejaría sin servicio aunque haya pagado.
  • No se cambia el importe. Para subir o bajar el monto de una suscripción viva hay que cancelarla y crear otra. En wapi solo creamos la nueva si Izipay confirmó la cancelación de la vieja; antes se ignoraba la respuesta y con la pasarela caída quedaban dos suscripciones vivas, o sea cobro doble.
  • El token que manda es el de esta transacción. Un sistema tomaba el token guardado del cliente y miraba el de la transacción solo si no había ninguno. Con un token viejo la pasarela no lo encontraba y la suscripción nunca se creaba. Lo contamos en la nota del cobro de un sol.
  • Cancelar tiene códigos. Subscription/Cancel devuelve responseCode 0 (cancelada), 30 (token no encontrado), 32 (suscripción no encontrada) o 99 (error). Los códigos 30 y 32 equivalen a "ya no existe" y no deben bloquear la cancelación local; el 99 o un error de red, sí.

Enterarse del cobro: la regla que falta en el panel

En Izipay los cobros de una recurrencia se autorizan por lote, no como un pago interactivo. En producción ocurren una vez al día entre las 00:00 y las 05:00 UTC, es decir, entre las 19:00 y la medianoche en Lima (ver la nota del servidor en UTC). En pruebas, la primera cuota se crea dentro de la hora siguiente.

En Configuración > Reglas de notificaciones hay que activar tres: al final del pago, al crear una suscripción y, la decisiva, al autorizar por lote. Con solo la primera, el cobro mensual ocurre y tu sistema nunca se entera. Trae de fábrica la condición "Origen del evento = Proceso automático de autorización"; no la borres, o la regla también dispara en pagos interactivos y duplica el aviso.

En el IPN de una renovación el orderId lo genera la pasarela: no hay una orden pendiente nuestra y la empresa se resuelve por el subscriptionId. Si falla la base, responde 500: la pasarela reintenta y el procesamiento idempotente evita duplicados.

Culqi también avisa tarde

En Arca, una suscripción de Culqi quedaba ACTIVA y realmente cobrada (se confirmaba con GET /recurrent/subscriptions/{id}), pero GET /v2/events no listaba nada durante más de un día. La causa, confirmada por el soporte de Culqi: los webhooks están separados por modo de llave. El Panel de Producción solo dispara para cobros con sk_live_; para probar con sk_test_ hay que configurar el webhook aparte en el panel de pruebas, y esa configuración no tiene endpoint de API para leerse.

Al pasar de sk_test_ a sk_live_ hay que borrar además el caché propio de planes: los pln_test_… no existen en Live, son espacios de nombres separados.

El cron que no confía en el aviso

Los dos avisos pueden fallar o llegar tarde, así que en Arca y SortealoPE hay un cron de respaldo. Pregunta a la pasarela y usa el contador de cobros como clave de idempotencia:

  • Izipay: Subscription/Get devuelve pastPaymentsNumber, las cuotas ya cobradas. En Arca la referencia del pago es izp_sub_<id>_p<n>; si ya existe, no se toca nada. En SortealoPE el cron corre cada 15 minutos.
  • Culqi: el equivalente es current_period, y la referencia queda como sxn_<id>_p<periodo>.

Si llegan el aviso y el cron, el segundo encuentra la fila y no hace nada: es la tabla de pagos procesados de Cobrar con Izipay.

Qué pasa cuando el cobro falla

  • SortealoPE: si un cobro falla, el plan expira. Sin reintentos ni correos de recuperación. Hay 36 horas de gracia para quien tiene recurrencia viva, porque Izipay cobra el día del vencimiento y su aviso puede tardar. Pasada la gracia se baja el plan y se cancela la recurrencia en Izipay, para que no reintente un cargo sobre un plan ya dado de baja.
  • wapi: el cron de vencimientos suspende con 2 días de gracia, y una renovación rechazada manda un aviso por correo, como máximo uno cada 3 días aunque la pasarela reintente varias veces.
  • Arca (Culqi): el cargo fallido avisa al usuario. Cancelar no corta el acceso: ya pagó hasta su próxima fecha y la baja ocurre al vencer.

Pruebas: las suscripciones de test siguen vivas

Una suscripción creada en modo prueba sigue cobrando cuotas por su cuenta. Nos pasó con Izipay: llegaban correos "ERROR en la llamada de su URL de notificación" cada unos 15 minutos. Cada cuota de test disparaba el IPN contra producción, que solo tiene la clave de producción: la firma no validaba, respondía 400 y la pasarela reintentaba. Al terminar pruebas de recurrencia, cancela siempre las suscripciones de test.

La prueba que de verdad cuenta es un cobro real de S/ 1, anulado el mismo día: así lo hicimos.

Lo que debe decidir siempre el servidor

  • El precio. El navegador solo manda el plan; el monto sale del catálogo del backend. Al crear la recurrencia se usa el total mensual del plan, no lo que se acaba de cobrar: un prorrateo no puede volverse la cuota mensual.
  • La vigencia. Se extiende cuando llega el cobro confirmado, no cuando el navegador dice que pagó. Un pago de ciclo suma sobre lo que quede, para que renovar antes de vencer no recorte días.
  • El modo. Una transacción de prueba nunca acredita servicio real. Con llaves sk_test_ de Culqi, solo correos autorizados pueden completar un pago: la tarjeta de prueba es pública y cualquiera se activaría un plan gratis.
  • La idempotencia. Una renovación notificada dos veces extiende el plan una sola vez.

¿Cuál elegir?

Ninguna es mejor en todo. Culqi expone cada paso y da más control, con más puntos donde algo puede quedar a medias. Izipay junta el primer cobro y la tarjeta, y a cambio exige las reglas de notificación bien puestas y cuidado con las fechas. En comisiones y plazos de aprobación no entramos: cambian por comercio y dependen de lo que negocies, así que pídelos a cada pasarela por escrito. Lo que no cambia es la disciplina: monto en el servidor, confirmación firmada, idempotencia, un cron que no confíe en el aviso y un cobro real antes de abrir la venta.

Si quieres ver el resultado en producción: wapi cobra su suscripción mensual con Izipay y SortealoPE renueva su plan Pro de la misma forma.