Suscripciones recurrentes en Perú: Culqi frente a Izipay
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/customersexige 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/cardsconvalidate: 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, mandamosvalidate: false. - El plan lleva el intervalo adentro.
interval_unit_timevale 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
PATCHde una suscripción solo aceptacard_idometadata. 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
effectDateno admite milisegundos. EltoISOString()de JavaScript los incluye siempre y Izipay respondeINT_069con "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:32Zdevolvió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/CanceldevuelveresponseCode0 (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/GetdevuelvepastPaymentsNumber, las cuotas ya cobradas. En Arca la referencia del pago esizp_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 comosxn_<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.