Checkout en el navegador

Construye tu propio checkout con el cliente público, con sus métodos de pago, estados, códigos de verificación, errores y escenarios de prueba.

Checkout propio

El checkout alojado resuelve los estados de pago por ti. Si construyes uno propio:

  1. Consulta client.checkouts.retrieve(id).
  2. Obtén client.checkouts.paymentMethods(id) y muestra lo que cada método pide. Si no trae ninguno, unavailable dice por qué: muéstralo y no pidas datos.
  3. Envía client.checkouts.pay(id, input) con el amount del método que mostraste.
  4. Si el pago queda pending, atiende su nextAction y espera el resultado con waitForChange.
  5. Si falla, muestra el motivo y deja que el cliente pague de nuevo.
Consultar métodos disponibles
import { createClient } from "@boelabs/billete-sdk/client";

const client = createClient({ apiVersion: "2026-10-24" });
const checkout = await client.checkouts.retrieve("chk_ejemplo");
const methods = await client.checkouts.paymentMethods(checkout.id);

Cuando no hay con qué pagar

Si el checkout no se puede pagar ahora, paymentMethods devuelve data vacío y unavailable con el motivo; si hay métodos, unavailable es null. Muestra su message antes de pedirle datos al cliente:

unavailable.codeQué pasaQué haces
account_suspendedEl negocio no está recibiendo pagos: está pausado o suspendido, o tiene un reembolso vencido.Dile que no puede pagar ahora. Volver a intentar no cambia nada.
payment_method_unavailableEl negocio no ofrece ningún método para este pago: los desactivó o ninguno cobra esa moneda.Dile que no puede pagar ahora y que hable con el negocio.
payments_unavailableNada puede cobrar ahora: el proveedor no responde o el método está en mantenimiento.Ofrece reintentar en unos minutos.
Antes de pedir datos
const { data: methods, unavailable } = await client.checkouts.paymentMethods(checkout.id);
if (unavailable) showNotice(unavailable.message, { retry: unavailable.code === "payments_unavailable" });

Lo que pide cada método

Un mismo método puede atenderlo un proveedor u otro, y cada uno pide cosas distintas. Construye la pantalla con lo que devuelve paymentMethods, nunca con campos fijos en tu código:

CampoQué muestras
amount, currencyLo que el cliente debe pagar, ya convertido a la moneda del método.
instructionsA dónde envía el dinero: beneficiario, cédula o RIF, teléfono y banco, o un QR (qrImage) para escanear con la app del banco. null si no hay que enviar nada antes.
fieldsLos datos que van en pay dentro de fields. Cada uno trae key, label, type (text, tel, date, select o document) y, si aplica, options. Puede venir vacío.
verificationtrue si después de pay el cliente escribe un código en un paso propio.

De ahí salen dos recorridos:

  • Paga primero y luego confirma. Con instructions y fields, el cliente envía el dinero, vuelve y escribe cómo pagó (por ejemplo, teléfono, banco y fecha de su Pago Móvil). Esos fields van en pay.
  • Paga y luego escribe un código. Con verification: true, pay devuelve el pago pending con nextAction.type === "code". Si además hay instructions, el cliente envía el dinero antes de pay y escribe la referencia en el paso del código. Si no, el código llega por SMS después de pay.

Así se ven hoy los métodos de Billete:

MétodoAntes de payPaso del código
pago_movilMuestra instructions para el Pago Móvil. Según el proveedor, fields pide teléfono, banco y fecha, o viene vacío.Si verification es true, la referencia del Pago Móvil (6 dígitos).
pago_movil_qrIgual que pago_movil, con el QR en instructions.qrImage.Igual que pago_movil.
debito_inmediatoSin instructions. fields pide cédula, banco y teléfono.La clave que el banco envía por SMS (8 dígitos, se puede reenviar).
criptoPaga en la billetera cripto de tu negocio, si la conectaste (en prueba, siempre). Sin instructions. fields pide la moneda estable y la red (network: usdt_trc20, usdt_bep20, usdc_polygon, usdc_base); solo trae las redes en las que tu billetera tiene dirección. pay devuelve el pago pending con nextAction.type === "display": la dirección y, en exactAmount, el monto exacto a enviar con su moneda ({ value: "10.000173", unit: "USDC" }). Sus últimos decimales identifican el pago: muéstralo completo.No tiene: el pago se confirma al llegar el depósito a la cadena.

Tasa congelada

Un precio en dólares o euros se cobra en bolívares a tasa BCV. Con cripto, un precio en bolívares, dólares o euros se cobra en USDT: el dólar a la par y lo demás a la tasa BCV del dólar. La tasa queda congelada para el checkout o cobro durante 30 minutos desde que se consultan sus métodos: hasta expiresAt de cada método, paymentMethods devuelve el mismo amount y el pago se cobra a esa tasa. Pasado ese tiempo, la siguiente consulta usa la tasa vigente y la congela de nuevo.

Envía en pay el amount que viste. Si ya no es el que se cobra (la tasa venció y el monto cambió, o cambió el total), el pago no se cobra y responde exchange_rate_changed (409, param: "amount"). Consulta de nuevo los métodos, muestra el nuevo monto y vuelve a enviar el pago. Sin amount, el pago se cobra a la tasa vigente al pagar.

Para probar esa respuesta sin esperar a que cambie la tasa, envía un amount distinto del que devolvió el método (por ejemplo, method.amount + 1). Billete lo rechaza igual que a un monto vencido, en prueba y en producción, y no cobra nada.

Pagar el monto mostrado
const method = methods.data.find((option) => option.id === "pago_movil")!;
await client.checkouts.pay(checkout.id, {
  method: method.id,
  amount: method.amount,
  // Lo que escribió el cliente en cada uno de `method.fields`, por su `key`.
  fields: values,
});

Al aplicar un cupón con client.checkouts.update(id, { coupon }) (en un cobro, client.invoices.update(id, { coupon })), el total cambia y también los montos convertidos: vuelve a pedir paymentMethods antes de pagar. allowCoupons dice si tu negocio acepta cupones de sus clientes.

Estados del pago

pay responde el checkout con su payment. Antes del primer intento, payment es null.

EstadoQué significaQué haces
pending con nextActionEl pago espera algo del cliente.Muestra el paso: escribir un código (code), enviar fondos a lo que se muestra (display) o ir a la página del proveedor y volver (redirect).
pending sin nextActionSolo falta la respuesta del banco o del proveedor.Muestra que lo estás verificando y consulta el checkout. No permitas pagar de nuevo: un segundo pay responde payment_in_progress.
succeededEl pago se confirmó. El checkout pasa a completed.Muestra la confirmación y sigue afterCompletion.
failedNo se cobró. failure.code dice por qué y failure.message lo explica al cliente.Muestra el mensaje y deja pagar de nuevo, con el mismo método u otro.

Los motivos de failure.code son insufficient_funds, payment_not_found, amount_mismatch, invalid_details, declined, provider_unavailable, expired (no terminó a tiempo), canceled (volvió atrás) y abandoned (salió de la página mientras se esperaba su código). Un checkout admite hasta 10 intentos de pago.

pay espera hasta 25 segundos la respuesta del proveedor antes de responder pending, así que muchos pagos llegan ya resueltos.

El paso del código

Enviar el código
const action = checkout.payment?.status === "pending" ? checkout.payment.nextAction : null;
if (action?.type === "code") {
  const next = await client.checkouts.submitPaymentCode(checkout.id, { code });
}
  • label, length y message dicen qué pedir: "Referencia del pago", 6 dígitos.
  • Un código incorrecto responde payment_code_invalid (422, param: "code") y el pago sigue esperando. Al quinto intento fallido, el pago pasa a failed.
  • Con resendable: true, resendPaymentCode envía otro código. Pasado expiresAt, el código ya no sirve.
  • cancelPayment vuelve atrás: el pago pasa a failed con canceled, nada se cobra y el cliente puede corregir sus datos o elegir otro método.
  • Mientras la página espera el código, llama a notePayerPresence cada 15 segundos y al volver a la página. Después de la primera llamada, un minuto sin noticias cancela el pago como abandoned. Si el pago ya no espera un código, responde payment_code_not_expected: deja de enviarla.

Esperar el resultado

Mientras el pago esté pending, espera el siguiente cambio con waitForChange en vez de consultar cada pocos segundos: la API responde en cuanto el checkout cambia (el banco confirmó, el pago falló, llegó un código) o, si en 25 segundos no cambió nada, devuelve el mismo checkout y vuelves a esperar.

Esperar el resultado del pago
async function settled(checkout: PublicCheckout): Promise<PublicCheckout> {
  const waiting = checkout.status === "open" && checkout.payment?.status === "pending";
  return waiting ? settled(await client.checkouts.waitForChange(checkout)) : checkout;
}

const result = await settled(checkout);

waitForChange envía la revision del checkout que tienes (If-None-Match) y pide esperar (Prefer: wait=25); por HTTP, la respuesta es el checkout nuevo o 304 sin cambios. Los cobros tienen lo mismo con client.invoices.waitForChange. Si tu timeoutMs es menor de 30 segundos, la espera se acorta para terminar antes.

El resultado también llega a tu servidor por el webhook checkout.completed: es la fuente de verdad para entregar la compra. Si tu checkout no puede seguir (un método que no soportas, un error inesperado), envía al cliente a checkout.url, la página alojada del mismo checkout.

Errores frecuentes

Todos llegan como BilleteError con code estable y un message en español que puedes mostrar al cliente. Estos son los que puede ver el pagador; el catálogo completo dice qué significa cada uno, y en TypeScript code tiene ese tipo.

CódigoCuándoQué haces
checkout_not_foundEl ID no existe.Muestra que el enlace no es válido.
checkout_expiredEl checkout venció.Ofrece volver a la tienda o crear otro checkout.
checkout_completedYa se pagó.Muestra la confirmación.
checkout_not_editableupdate sobre un checkout que ya no está abierto, o un cambio que no admite.Vuelve a consultar el checkout.
payment_in_progressHay un pago pending.Muestra el pago en curso y espera su resultado.
exchange_rate_changedVenció la tasa congelada.Consulta los métodos y muestra el nuevo monto.
invalid_payment_detailsUn dato de fields no es válido. param dice cuál (fields.phone).Marca el campo.
email_required, name_required, phone_required, address_required, custom_field_required, custom_field_invalidFalta un dato que pide collect o customFields.Marca el campo.
payment_method_unavailableEl negocio no ofrece ese método para este pago: no existe, no cobra en esa moneda, solo funciona en pruebas o el negocio no lo tiene.Ofrece otro de paymentMethods.
payments_unavailableNada puede cobrar ese método ahora: su proveedor no responde o está en mantenimiento.Ofrece otro método o reintenta en unos minutos.
account_suspendedEl negocio no está recibiendo pagos.Dile al cliente que no puede pagar ahora.
too_many_attemptsSe alcanzaron los 10 intentos.Pide crear otro checkout.
payment_code_invalidEl código o la referencia no es correcto.Deja escribirlo de nuevo.
coupon_not_foundEl cupón no existe.Marca el campo del cupón.
coupon_inactive, coupon_expired, coupon_exhausted, coupon_not_applicable, coupon_currency_mismatch, coupon_minimum_not_metEl cupón no aplica.Muestra el mensaje junto al campo del cupón. Ver Cupones.
coupon_already_used, coupon_first_purchase_onlyAl pagar, el cliente no puede usar ese cupón: el checkout ya se recalculó sin él.Muestra el nuevo total antes de pagar.
subscription_existsEl cliente que paga ya tiene una suscripción en curso a este plan. No se cobró nada.Llévalo a su suscripción o al portal para cambiarla.
insufficient_stock, product_unavailableNo hay existencias suficientes.Ajusta la cantidad o vuelve a la tienda.
rate_limitedDemasiadas peticiones seguidas.Espera unos segundos; el SDK ya reintenta.

Escenarios de prueba

Con un checkout de prueba, cada método trae testScenarios: lo que hay que enviar en cada paso para llegar a un resultado conocido. Sirven para rellenar tu formulario automáticamente mientras pruebas.

CampoQué es
fieldsLo que va en pay dentro de fields. Vacío en el escenario por defecto.
codeLo que va en submitPaymentCode después de pay, en los métodos con verification. null si el método no pide código.
outcomeCómo termina: succeeded, failed (con failureCode), pending (sigue esperando al proveedor) o code_invalid (el código se rechaza con payment_code_invalid y el pago sigue esperando otro).
MétodoEscenario
pago_movil, pago_movil_qr (con código)Referencia 000000: payment_code_invalid. Cualquier otra: pago verificado.
pago_movil, pago_movil_qr (sin código)Teléfono 04140000002, 04140000003 o 04140000004: falla como payment_not_found, amount_mismatch o provider_unavailable. Cualquier otro: pago verificado.
debito_inmediatoClave 12345678: pago verificado. Teléfono 04140000002 con esa clave: débito rechazado. Otra clave: payment_code_invalid.
criptoCualquier red: el pago queda pending con la dirección y el monto exacto, y se confirma solo a los 10 segundos. La dirección es de prueba y ninguna billetera la acepta.

Qué vende el checkout

Cada ítem de checkout.items trae planId o productId y, si le diste una a ese plan o producto, su lookupKey: tu propio nombre para él (plus, camiseta_negra). Úsalo para mostrar lo tuyo sin guardar IDs de Billete en tu app. A diferencia de metadata, que nunca llega al navegador, lookupKey es público.

Un ítem de producto trae también imageUrl: la foto de su variante o, si no tiene, la principal del producto (null en los planes). Y checkout.merchant.logoUrl es el logo de tu negocio, si subiste uno en Configuración › General. Ambas direcciones son públicas y se pueden guardar en caché sin vencimiento.

Métodos del cliente público

RecursoOperaciones
checkoutsretrieve, waitForChange, update, paymentMethods, pay, openClaim
invoicesretrieve, waitForChange, paymentMethods, pay, openClaim
paymentLinksretrieve, createCheckout
customerretrieve, subscriptions, portal, features, feature, cancelSubscription, resumeSubscription, updateSubscription, changeSeats, stopAutoCharge, openClaim
fulfillmentsretrieve, confirmReceived, openClaim, reportProblem, setRefundDestination
claimsretrieve, respond, withdraw (con el token del enlace del reclamo)
refundsretrieve, setDestination (con el token del enlace del reembolso; reglas)
exchangeRates, currencieslist

También existe carrierOffices(carrier) para consultar oficinas de envío. Checkouts y cobros incluyen submitPaymentCode, resendPaymentCode, notePayerPresence y cancelPayment, y un cobro se paga igual que un checkout.

Los permisos se validan en tu servidor

El navegador sirve para presentar la experiencia. Para entregar compras o autorizar acciones de tu aplicación, confirma el pago y las cuotas desde tu servidor.