Sesiones de pago
Una sesión de pago representa una intención de cobro. La creas en tu servidor y obtienes un checkoutUrl para cobrar al comprador. Referencia pública: cs_…. Es de corta duración: 5 minutos por defecto, o lo que indiques en expiresInMinutes (ver Duración de la sesión). Créala justo antes de cobrar y usa expiresAt para el momento exacto de expiración.
Crear una sesión
POST /v1/sessionsAutenticación: clave secreta (Authorization: Bearer sk_…).
Cuerpo
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
amountMinor | entero | Sí | Monto en unidades menores (céntimos). Mínimo 1. |
currency | string | Sí | VES o USD. Hoy el cobro con tarjeta se liquida en VES. |
customerRef | string | No | Tu id de cliente. La tarjeta guardada quedará asociada a él. Máx. 120. |
cardToken | string | No | Token de una tarjeta ya guardada (tok_seif_…). El checkout pedirá solo el CVV. Ver Cobrar una tarjeta guardada. |
returnUrl | string (URL) | No | A dónde redirigir al comprador tras un cobro aprobado. |
description | string | No | Texto corto mostrado en el checkout. Máx. 140. |
maxFailures | entero | No | Cuántos intentos fallidos tolera la sesión antes de cerrarse sola. Omítelo para el comportamiento por defecto. Entre 0 y 10. Ver Limitar los intentos. |
expiresInMinutes | entero | No | Cuántos minutos dura la sesión. Por defecto 5. Entre 2 y 60. Ver Duración de la sesión. |
prefill | objeto | No | Datos que ya conoces del comprador para rellenar el formulario del checkout. Ver Rellenar el formulario. |
Ejemplo
curl -X POST https://api.seif.pagosripei.com/v1/sessions \
-H "Authorization: Bearer sk_test_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"amountMinor": 105000,
"currency": "VES",
"customerRef": "usuario_123",
"description": "Pedido #1042",
"returnUrl": "https://mitienda.com/gracias"
}'Respuesta 201
{
"id": "9f0c1f2a-...-e3",
"reference": "cs_8sKd2pQ1Aa",
"status": "created",
"amountMinor": 105000,
"currency": "VES",
"expiresAt": "2026-06-09T18:30:00.000Z",
"checkoutUrl": "https://checkout.seif.pagosripei.com/checkout?session=cs_8sKd2pQ1Aa",
"description": "Pedido #1042"
}| Campo | Descripción |
|---|---|
reference | Referencia pública de la sesión (cs_…). |
status | Estado de la sesión (ver Estados de una sesión). |
closedReason | Por qué se cerró: expired (por tiempo), max_failures_reached (agotó sus intentos) o merchant_request (la expiraste tú). null mientras no esté cerrada. Son los mismos valores que reason en el webhook session.expired. |
checkoutUrl | URL para abrir el checkout incrustado. |
expiresAt | Cuándo expira (ISO-8601). |
attemptsRemaining | Intentos que le quedan antes de cerrarse. null si no enviaste maxFailures (sin límite). |
prefill | Los datos de prellenado tal como quedaron guardados, o null. Es lo que enviaste ya normalizado, no lo que el checkout terminará usando — ver Lo ya guardado tiene prioridad. |
Duración de la sesión
Una sesión dura 5 minutos por defecto. Si tu flujo necesita otra cosa, envía
expiresInMinutes al crearla: entre 2 y 60 minutos, en minutos enteros.
{
"amountMinor": 105000,
"currency": "VES",
"expiresInMinutes": 15
}- Más tiempo sirve si tus compradores tardan en tener la tarjeta a mano.
- Menos tiempo sirve si necesitas liberar pronto lo que la sesión reserva (un cupo, una entrada).
La respuesta trae el expiresAt resultante, y todo lo demás sigue igual: la
sesión se cierra sola a esa hora y se emite session.expired. Un valor fuera de
rango se rechaza con 400; no se recorta.
Si tu pasarela le pide un código (OTP) al comprador, el código tiene que
confirmarse antes de expiresAt. Con una sesión muy corta, al comprador
puede no alcanzarle el tiempo para recibirlo y escribirlo.
Si lo que quieres es cerrar una sesión ya, antes de que se le acabe el tiempo, no hace falta acortarla: usa Expirar una sesión.
Rellenar el formulario (prefill)
Si ya tienes los datos del comprador en tu sistema, no hace falta que los
escriba otra vez. Envía prefill al crear la sesión y el checkout abre con esos
campos ya puestos.
{
"amountMinor": 105000,
"currency": "VES",
"prefill": {
"cardholderName": "María Rodríguez",
"documentType": "V",
"documentId": "12345678"
}
}Campos
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cardholderName | string | No | Nombre del titular. Máx. 80 caracteres. |
documentType | string | Solo si envías documentId | V, E, J, G o P (pasaporte). |
documentId | string | No | Número del documento. Dígitos para V/E/J/G; alfanumérico para P. Entre 5 y 12 caracteres. |
Puedes enviar solo documentType. El checkout deja preseleccionado el tipo
y el comprador únicamente escribe el número. Lo que no puedes es enviar un
número sin su tipo: es el tipo el que define cómo se valida el número.
Qué hacemos con lo que envías
Antes de guardarlo, normalizamos los valores. Es útil conocerlo porque la respuesta te devuelve el resultado, no lo que enviaste:
- Los separadores del documento se eliminan.
"12.345.678"se guarda como"12345678", y un pasaporte se guarda en mayúsculas. - Los espacios de más se colapsan y se recorta el nombre.
- Los caracteres invisibles y de control se eliminan (saltos de línea,
espacios de ancho cero, marcas de dirección de texto). Un nombre que quede
vacío tras esa limpieza se rechaza con
400. - El límite de 80 caracteres se aplica después de limpiar.
Es un prellenado, no una imposición
Esto es lo más importante de la funcionalidad:
- El comprador puede editar los tres campos. Nada queda bloqueado.
- El cobro se procesa con lo que el comprador envió, no con lo que tú prellenaste. Si lo corrige, vale su corrección.
- Si necesitas saber con qué documento se cobró finalmente, léelo de la transacción, no de la sesión.
Lo ya guardado tiene prioridad
Si la sesión usa una tarjeta guardada (cardToken) y esa tarjeta ya tiene
titular o documento, lo guardado gana y tu prellenado se ignora. No es un
error: la sesión se crea igual, simplemente ese campo no se prellena porque ni
siquiera se le pregunta al comprador.
La regla se aplica campo por campo:
| La tarjeta guardada tiene… | Qué pasa con tu prefill |
|---|---|
| Titular y documento | Se ignora completo. Al comprador solo se le pide el CVV. |
| Documento, pero no titular | Se ignoran documentType y documentId; el nombre sí se prellena. |
| Titular, pero no documento | Se ignora cardholderName; el documento sí se prellena. |
Nada (o no hay cardToken) | Se aplica completo. |
Por eso prefill rinde sobre todo en el primer cobro, cuando la tarjeta
todavía no existe en Seif. A partir del segundo, la tarjeta ya trae sus datos y
no hay nada que prellenar.
Esto es deliberado y no se puede desactivar: un prefill que pisara el
documento guardado sería una forma de cobrarle a alguien con un documento que
nunca vio. Para corregir los datos de una tarjeta guardada, el comprador
los edita en el checkout cuando se le piden; el prefill no sirve para eso.
El prellenado viaja al navegador del comprador, porque es donde se muestra.
Envía únicamente los datos de la persona a la que le pasaste ese enlace de
pago: no uses prefill para nada que esa persona no deba ver.
Errores propios de este flujo
| HTTP | Causa | Solución |
|---|---|---|
400 | prefill.documentId sin prefill.documentType. | Envía siempre el tipo junto con el número. |
400 | El documento no cumple el formato de su tipo. | Dígitos para V/E/J/G; alfanumérico para P. Entre 5 y 12 caracteres. |
400 | prefill.cardholderName vacío o de más de 80 caracteres. | Omite el campo en vez de mandarlo vacío. |
400 | Una clave desconocida dentro de prefill. | Solo se aceptan cardholderName, documentType y documentId. |
Cobrar una tarjeta guardada (solo CVV)
Cuando tu cliente ya tiene una tarjeta guardada, no vuelvas a pedirle el número. Crea la sesión con el cardToken de esa tarjeta y el checkout mostrará únicamente:
- la tarjeta con la que va a pagar (marca y últimos 4 dígitos),
- el campo CVV,
- su documento de identidad.
El número y el vencimiento salen de la bóveda del lado de Seif. Tu servidor nunca toca datos de tarjeta, así que el flujo es 100 % PCI para ti.
curl -X POST https://api.seif.pagosripei.com/v1/sessions \
-H "Authorization: Bearer sk_test_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"amountMinor": 105000,
"currency": "VES",
"cardToken": "tok_seif_Xa92...kQ",
"description": "Pedido #1043"
}'La respuesta es idéntica a la de cualquier sesión: abre el checkoutUrl (o móntalo con seif-js) y el comprador solo confirma su CVV.
Si no envías customerRef, la sesión hereda el de la tarjeta. Y si tu pasarela
pide OTP, el flujo del checkout es el mismo que en un pago normal.
Errores propios de este flujo
| HTTP | Causa | Solución |
|---|---|---|
400 | cardToken no empieza por tok_seif_. | Usa el seifToken tal cual lo devuelve GET /v1/cards. |
404 | La tarjeta no existe en tu comercio, o fue eliminada. | Vuelve a listar las tarjetas del cliente y usa una activa. |
409 | La tarjeta no admite este flujo (por ejemplo, quedó guardada en una pasarela que no pide CVV). | Escríbenos a soporte: esa tarjeta se cobra con un flujo servidor a servidor. |
El cardToken se valida al crear la sesión, no al pagar: un token inválido
falla en tu servidor, nunca delante del comprador.
Consultar una sesión
GET /v1/sessions/{reference}curl https://api.seif.pagosripei.com/v1/sessions/cs_8sKd2pQ1Aa \
-H "Authorization: Bearer sk_test_tu_clave"Devuelve el mismo objeto, con el status actualizado.
Expirar una sesión
POST /v1/sessions/{reference}/expireAutenticación: clave secreta (Authorization: Bearer sk_…). Sin cuerpo.
Cierra la sesión ya, antes de que se le acabe el tiempo o los intentos. Úsalo cuando el pedido deja de existir: se agotó, el cliente pagó por otra vía o lo anulaste. Desde ese momento la sesión ya no se puede pagar: si el comprador intenta pagar, el checkout le muestra la pantalla de sesión expirada.
curl -X POST https://api.seif.pagosripei.com/v1/sessions/cs_8sKd2pQ1Aa/expire \
-H "Authorization: Bearer sk_test_tu_clave"Respuesta 200
El mismo objeto de la sesión, ya cerrada:
{
"reference": "cs_8sKd2pQ1Aa",
"status": "expired",
"closedReason": "merchant_request",
"expiresAt": "2026-06-09T18:30:00.000Z",
"...": "…"
}statuspasa aexpiredyclosedReasonamerchant_request.expiresAtno cambia: sigue diciendo cuándo iba a vencer.- Se emite el webhook
session.expired, con el mismo formato de siempre yreason: "merchant_request". - Si la sesión ya había vencido por tiempo, pero todavía no se había marcado, se
cierra con
closedReason: "expired": es lo que de verdad le pasó.
Puedes repetir la llamada sin problema. Si la sesión ya estaba cerrada (por
tiempo, por intentos o por una llamada anterior), responde 200 con su estado
actual y no emite otro webhook.
Cuándo no se puede
| HTTP | Causa | Qué hacer |
|---|---|---|
404 | La sesión no existe en tu comercio. | Revisa la reference. |
409 | La sesión ya se pagó (completed). | No hay nada que expirar: si hace falta, reembolsa el cobro. |
409 | Hay un cobro en curso (processing, otp_requested, otp_confirmed). | El banco ya lo tiene. Espera a que se resuelva y vuelve a llamar: si se rechaza, la sesión se podrá expirar. |
409 | La sesión se creó con la otra clave (prueba o producción). | Usa una clave del mismo modo que la creó. |
503 | En una sesión de Stripe, no pudimos cancelar el pago en Stripe. | No se cambió nada; vuelve a intentarlo. |
Sesiones de Stripe (USD): el comprador paga directamente contra Stripe, así
que al expirar la sesión cancelamos también su pago pendiente en Stripe. Si
Stripe ya lo había cobrado, respondemos 409 en vez de dar la sesión por
expirada. Si el comprador ya había abierto el checkout, esa transacción pasa a
failed y recibes también charge.failed.
Listar sesiones
GET /v1/sessionsAutenticación: clave secreta (Authorization: Bearer sk_…).
Devuelve tus sesiones, de la más reciente a la más antigua, con cuántos intentos de cobro generó cada una. Sirve para revisar qué pasó con tus cobros: cuáles se pagaron, cuáles se rechazaron y cuáles nadie llegó a intentar.
Parámetros (query)
| Parámetro | Tipo | Descripción |
|---|---|---|
page | entero | Página, desde 1. Por defecto 1. |
pageSize | entero | Sesiones por página. Por defecto 10. |
status | string | Filtra por estado. Usa los mismos valores de Estados de una sesión. expired trae todas las cerradas sin pagarse, sea cual sea su closedReason. |
curl "https://api.seif.pagosripei.com/v1/sessions?status=expired&page=1&pageSize=20" \
-H "Authorization: Bearer sk_test_tu_clave"Respuesta 200
{
"data": [
{
"reference": "cs_8sKd2pQ1Aa",
"status": "expired",
"closedReason": "max_failures_reached",
"amountMinor": 105000,
"currency": "VES",
"description": "Pedido #1042",
"customerRef": "usuario_123",
"createdAt": "2026-06-09T18:25:00.000Z",
"expiresAt": "2026-06-09T18:30:00.000Z",
"attemptCount": 1,
"attemptsRemaining": 0,
"lastTransaction": { "reference": "txn_…", "status": "declined" }
}
],
"total": 1,
"page": 1,
"pageSize": 20
}| Campo | Descripción |
|---|---|
status | El mismo estado que devuelve GET /v1/sessions/{reference} para esa sesión. |
closedReason | Por qué se cerró, o null. Igual que en el detalle. |
attemptCount | Cuántos intentos de cobro generó. 0 significa que el comprador nunca llegó a intentar pagar. |
attemptsRemaining | Intentos que le quedan, o null si no tiene límite. |
lastTransaction | El último intento de cobro (el mismo objeto que en Transacciones), o null si no hubo ninguno. |
Los cobros con tarjeta guardada que haces servidor a servidor (POST /v1/charges)
no tienen sesión, así que no aparecen aquí. Consúltalos en
Transacciones.
Estados de una sesión
El listado y el detalle devuelven siempre el mismo estado para la misma sesión.
| Estado | Significado |
|---|---|
created | Lista para cobrar. |
otp_requested | El comprador está confirmando el pago con un código (pasarelas con segundo factor). |
otp_confirmed | El código se confirmó y el cobro se está enviando. |
processing | Hay un cobro en curso. |
completed | Pagada con éxito. |
failed | Un cobro falló o fue rechazado, pero la sesión sigue viva y se puede reintentar. |
expired | Cerrada sin pagarse: se alcanzó expiresAt, agotó sus intentos o la expiraste tú. closedReason dice cuál. |
Una sesión pasa a expired en cuanto llega expiresAt, sin que nadie tenga
que volver a abrirla. La única excepción es una sesión con un cobro en curso
(processing, otp_requested, otp_confirmed): vencer no corta un cobro que ya
salió hacia el banco. Si ese cobro se rechaza, la sesión se cierra entonces.
La misma regla aplica si la expiras tú: con un cobro en
curso, la llamada responde 409.
Limitar los intentos
Por defecto una sesión se puede reintentar libremente hasta que expire. Si un cobro es rechazado, el comprador puede corregir los datos o probar otra tarjeta. Eso funciona bien para la mayoría, pero no para todos: hay integraciones que necesitan un número fijo de intentos y una respuesta definitiva, para que su propio sistema deje de esperar.
Para eso está maxFailures.
{
"amountMinor": 105000,
"currency": "VES",
"maxFailures": 0
}Qué significa el número
maxFailures es cuántos fallos tolera la sesión, no cuántos intentos
concede. El intento que rompe el límite también cuenta, así que el total de
intentos es maxFailures + 1.
| Valor | Intentos totales | Comportamiento |
|---|---|---|
| (omitido) | Ilimitados | Reintentos libres hasta expiresAt. Es el comportamiento por defecto. |
0 | 1 | Al primer fallo la sesión se cierra. No espera a que expire. |
1 | 2 | Tolera un fallo; el segundo la cierra. |
2 | 3 | Tolera dos fallos; el tercero la cierra. |
0 no es lo mismo que omitir el campo. Omitirlo significa “sin límite”;
0 significa “cierra al primer fallo”. Si tu cliente HTTP descarta los ceros
al serializar, revísalo: la diferencia entre los dos es total.
Qué cuenta como fallo
Cualquier intento que termine en error. No distinguimos entre un rechazo del banco y un dato mal escrito: si el cobro no salió, gastó un intento.
- Rechazado por el banco emisor — cuenta.
- Fallido en el procesador — cuenta.
- Datos inválidos (
400: falta el documento, falta el nombre del titular) — cuenta.
No consumen intentos las peticiones que nunca llegaron a ser un intento:
- Una sesión ya cerrada, expirada o con un cobro en curso (
409/410). - Los OTP incorrectos, que tienen su propio límite independiente.
Cómo se cierra
Una sesión puede terminar sin pagarse de tres maneras: por tiempo
(expiresAt), por fallos (maxFailures) o porque la expiraste tú
(POST /v1/sessions/{reference}/expire). La que ocurra
primero.
Se cierre como se cierre, el resultado es el mismo:
- Su estado pasa a
expired, yclosedReasondice de qué manera murió. - El checkout muestra la pantalla de sesión expirada, sin botón de reintento.
- Un nuevo intento de cobro se rechaza:
410si venció o la expiraste tú,409si agotó sus intentos. - Se emite el webhook
session.expired, con el mismo formato en los tres casos. El camporeasondice de qué manera murió.
El webhook por tiempo sale al vencer la sesión, con unos segundos de margen, aunque el comprador haya cerrado la página y nadie la vuelva a abrir.
{
"event": "session.expired",
"data": {
"sessionReference": "cs_8sKd2pQ1Aa",
"reason": "max_failures_reached",
"failureCount": 1,
"maxFailures": 0
}
}reason | Significado |
|---|---|
expired | Se alcanzó expiresAt sin completarse. |
max_failures_reached | Se agotaron los intentos permitidos. |
merchant_request | La expiraste tú con POST /v1/sessions/{reference}/expire. |
Es un solo evento con un solo formato: si hoy manejas session.expired, no
tienes que cambiar nada para enterarte también de los otros cierres. reason
está ahí solo si quieres distinguirlos.
En el checkout
Al comprador se le avisa cuando le queda un solo intento, para que no lo gaste reintentando la misma tarjeta. Antes de eso no se le dice nada: mostrar un contador desde el principio agrega presión sin cambiar nada.
Cerrar y volver a cobrar
Una sesión cerrada no se reabre. Si quieres darle otra oportunidad al
comprador, crea una sesión nueva. Es deliberado: el sentido de maxFailures es
que la decisión de reintentar sea tuya y explícita, no del comprador.
Errores
| HTTP | Causa | Solución |
|---|---|---|
400 | Falta amountMinor/currency, o amountMinor < 1, o returnUrl no es una URL válida, o expiresInMinutes está fuera de 2–60. | Revisa el cuerpo según la tabla de campos. |
401 | Clave secreta inválida o ausente. | Verifica el header Authorization. |
404 | La sesión consultada no existe. | Revisa la reference. |
409 | El comercio aún no está aprovisionado. | Contacta a soporte para activar tu comercio. |
La sesión es de un solo uso. Una vez completed (o expired), genera una nueva
para un nuevo cobro.