Modo 3A - Tarjetas guardadas (pagos tokenizados)
Cobra a una tarjeta que el cliente ya guardó, sin que vuelva a ingresar los datos. Esto es lo que hace posibles las suscripciones y las recompras con un clic.
23.1 El flujo
1. Customer pays once through mode 1 or mode 2
2. Customer explicitly ticks "save my card for next time"
3. ROKI stores the processor's own token - never the card number
4. Your backend receives the payment_method.saved webhook with an opaque pm_*
(or looks it up later with GET /payment-methods)
5. Later, your backend charges that pm_* with sk_*
6. ROKI verifies the method belongs to you, in the right environment, and is
still usable, then charges the stored token
7. You get the same payment.approved / payment.failed webhooks as any other payment
La tarjeta se guarda solo cuando el cliente acepta. No hay forma de guardar una en silencio.
23.1.0 Primero hay que autorizar las tarjetas guardadas en la cuenta
Antes de escribir cualquier código para este modo, confirma que la cuenta del comercio lo tenga habilitado. La tarjeta guardada no viene activada por defecto: ROKI la autoriza comercio por comercio, a mano. Eso es a propósito - guardar una credencial que se puede cobrar sin el cliente presente es un asunto de fraude y contracargos, no una bandera de funcionalidad - y otras pasarelas lo controlan igual.
Lo que importa para una integración es cómo llega la negativa, porque no es un error:
| Situación | Qué obtenés |
|---|---|
| Sandbox sin aprovisionar | 422 sandbox_terminal_unavailable, que lo dice sin rodeos (16.3) |
| Guardado de tarjeta no autorizado | Nada. El pago devuelve 201/202, el cobro se aprueba y no se guarda ninguna tarjeta |
saveCard: true fue aceptado, el SDK lo llevó
hasta el iframe como save_card=1, el pago se aprobó, GET /payment-methods siguió vacío y no llegó
ningún evento payment_method.saved. Nada reportó una negativa en ningún momento.
Sé preciso con lo que eso demuestra: nunca se confirmó de forma independiente que la cuenta tuviera
habilitada la tarjeta guardada, así que "no autorizado" es la explicación más probable del resultado
nulo, no una causa comprobada. Ojo también con que en POST /confirm no se manda ninguna intención
de guardado - solo el iframe la lleva - así que si el flujo también exige algo ahí, esta prueba se
vería idéntica.
Así que no te pongas a depurar tu código. Revisa primero:
# Pay once with saveCard, then immediately:
curl "https://aura.roki.systems/api/connect/v1/payment-methods?customer[identity_number]=0801199000000" \
-H "Authorization: Bearer sk_test_..."
Un data vacío después de un intento de guardado exitoso significa que la cuenta no está autorizada,
no que tu petición estuviera mal. Pedile a ROKI que habilite la tarjeta guardada para el comercio, y
recién entonces construí el modo 3A.
23.1.1 Cómo se habilita ese acuerdo en la práctica - verificado 2026-08-14
El flujo de arriba dice "el cliente marca una casilla" sin decir de dónde sale la casilla. Eso importa, porque los dos modos difieren y hoy solo uno funciona.
Modo 2 (componentes embebidos): lo habilita el comercio. El SDK acepta una opción saveCard y la
traduce a save_card=1 en la URL del iframe. La casilla que ve el cliente es tuya; ROKI solo
recibe la intención:
const payment = roki.createPaymentComponent({
customer: { identity_number: '0801199000000', email: 'buyer@example.com' },
saveCard: true, // el comercio habilita el guardado; la casilla la dibujas vos
});
Manda customer.identity_number: esa es la llave por la que GET /payment-methods busca la tarjeta.
Modo 1 (checkout alojado): nada de lo que mandes lo activa. En una cuenta sin el módulo
autorizado, la página de checkout no dibuja ninguna opción de guardado. Se probaron siete campos
plausibles en la petición de creación - save_card (booleano y entero), allow_save_card,
tokenize, save_payment_method, reusable, y ninguno - y ninguno hizo aparecer la opción. La hoja
de estilos de la página sí contiene una regla .rp-co2-save, así que el checkout sabe cómo dibujar
esa fila cuando la cuenta tiene derecho a ella. Es una autorización, no un parámetro (23.1.0).
Qué significa esto para una integración: si necesitas tarjetas guardadas, originalas desde el modo 2. No le prometas a un cliente que pagar por un enlace de pago va a guardar su tarjeta - en las cuentas verificadas acá, no lo hace.
23.2 Cómo conseguir el pm_*
Desde el webhook:
{
"id": "evt_01HPM01",
"type": "payment_method.saved",
"created_at": "2026-08-12 12:05:00",
"data": {
"id": "pm_7k2n9xqf31ab",
"card_brand": "Visa",
"last_four": "4242",
"exp_month": 11,
"exp_year": 2027,
"is_default": true
}
}
O bajo demanda:
curl -G https://aura.roki.systems/api/connect/v1/payment-methods \
-H "Authorization: Bearer sk_test_..." \
--data-urlencode "customer[identity_number]=0801199012345"
Identifica al cliente con customer[identity_number] (preferido) o customer[email] - los mismos
identificadores que mandas al crear pagos. Un cliente sin tarjetas guardadas es un 200 normal con
{"data": []}, no un 404.
23.3 Cobrar
POST /api/connect/v1/payment-methods/pm_7k2n9xqf31ab/charge
Authorization: Bearer sk_test_...
Idempotency-Key: order-2002-charge
{ "amount": 500.00, "currency_code": "HNL", "external_reference": "order-2002" }
Acá Idempotency-Key es obligatorio, no apenas recomendado como en la creación de pagos. Si lo
omitís devuelve 422. Usa una llave única por cada intento de cobro para que un reintento de red
nunca cobre dos veces.
Ojo que en este endpoint currency_code lleva el código alfabético ("HNL"), mientras que la
creación de pagos lleva el numérico ("340").
POST /payments/token-charge es un alias equivalente que recibe la tarjeta guardada en el cuerpo como
payment_token, lo que le queda bien a las renovaciones de suscripción:
{ "payment_token": "pm_7k2n9xqf31ab", "amount": 100.00,
"currency_code": "HNL", "external_reference": "sub-aug-2026" }
Nunca llames a ninguno de los dos endpoints desde un navegador. Los dos necesitan sk_*.
23.4 Reversar y revocar
La respuesta del cobro devuelve un id - ese es el UUID de la transacción. Usalo con los
endpoints normales /payments/{transaction_id}/void y /refund de la sección 13. No hay una ruta de
reverso aparte para los cobros con token.
Para eliminar una tarjeta guardada:
curl -X DELETE https://aura.roki.systems/api/connect/v1/payment-methods/pm_7k2n9xqf31ab \
-H "Authorization: Bearer sk_test_..."
Un método revocado hace fallar los cobros posteriores con un error propio, en vez de aprobarlos en silencio.
23.5 Qué pensar antes de habilitarlo
Una tarjeta guardada que se cobra sin el cliente presente tiene un perfil de riesgo distinto al de un checkout que el cliente acaba de completar. Antes de sacar a producción la facturación recurrente: definí qué pasa cuando una tarjeta vence o se reemite, decí cuántas veces reintentas una renovación rechazada y con qué códigos de rechazo dejas de reintentar, y dale al cliente una forma de ver y eliminar sus tarjetas guardadas. Nada de eso lo obliga la API.
