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:
- Consulta
client.checkouts.retrieve(id). - Obtén
client.checkouts.paymentMethods(id)y muestra lo que cada método pide. Si no trae ninguno,unavailabledice por qué: muéstralo y no pidas datos. - Envía
client.checkouts.pay(id, input)con elamountdel método que mostraste. - Si el pago queda
pending, atiende sunextActiony espera el resultado conwaitForChange. - Si falla, muestra el motivo y deja que el cliente pague de nuevo.
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.code | Qué pasa | Qué haces |
|---|---|---|
account_suspended | El 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_unavailable | El 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_unavailable | Nada puede cobrar ahora: el proveedor no responde o el método está en mantenimiento. | Ofrece reintentar en unos minutos. |
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:
| Campo | Qué muestras |
|---|---|
amount, currency | Lo que el cliente debe pagar, ya convertido a la moneda del método. |
instructions | A 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. |
fields | Los 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. |
verification | true 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
instructionsyfields, el cliente envía el dinero, vuelve y escribe cómo pagó (por ejemplo, teléfono, banco y fecha de su Pago Móvil). Esosfieldsvan enpay. - Paga y luego escribe un código. Con
verification: true,paydevuelve el pagopendingconnextAction.type === "code". Si además hayinstructions, el cliente envía el dinero antes depayy escribe la referencia en el paso del código. Si no, el código llega por SMS después depay.
Así se ven hoy los métodos de Billete:
| Método | Antes de pay | Paso del código |
|---|---|---|
pago_movil | Muestra 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_qr | Igual que pago_movil, con el QR en instructions.qrImage. | Igual que pago_movil. |
debito_inmediato | Sin instructions. fields pide cédula, banco y teléfono. | La clave que el banco envía por SMS (8 dígitos, se puede reenviar). |
cripto | Paga 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.
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.
| Estado | Qué significa | Qué haces |
|---|---|---|
pending con nextAction | El 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 nextAction | Solo 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. |
succeeded | El pago se confirmó. El checkout pasa a completed. | Muestra la confirmación y sigue afterCompletion. |
failed | No 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
const action = checkout.payment?.status === "pending" ? checkout.payment.nextAction : null;
if (action?.type === "code") {
const next = await client.checkouts.submitPaymentCode(checkout.id, { code });
}label,lengthymessagedicen 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 afailed. - Con
resendable: true,resendPaymentCodeenvía otro código. PasadoexpiresAt, el código ya no sirve. cancelPaymentvuelve atrás: el pago pasa afailedconcanceled, 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
notePayerPresencecada 15 segundos y al volver a la página. Después de la primera llamada, un minuto sin noticias cancela el pago comoabandoned. Si el pago ya no espera un código, respondepayment_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.
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ódigo | Cuándo | Qué haces |
|---|---|---|
checkout_not_found | El ID no existe. | Muestra que el enlace no es válido. |
checkout_expired | El checkout venció. | Ofrece volver a la tienda o crear otro checkout. |
checkout_completed | Ya se pagó. | Muestra la confirmación. |
checkout_not_editable | update sobre un checkout que ya no está abierto, o un cambio que no admite. | Vuelve a consultar el checkout. |
payment_in_progress | Hay un pago pending. | Muestra el pago en curso y espera su resultado. |
exchange_rate_changed | Venció la tasa congelada. | Consulta los métodos y muestra el nuevo monto. |
invalid_payment_details | Un 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_invalid | Falta un dato que pide collect o customFields. | Marca el campo. |
payment_method_unavailable | El 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_unavailable | Nada puede cobrar ese método ahora: su proveedor no responde o está en mantenimiento. | Ofrece otro método o reintenta en unos minutos. |
account_suspended | El negocio no está recibiendo pagos. | Dile al cliente que no puede pagar ahora. |
too_many_attempts | Se alcanzaron los 10 intentos. | Pide crear otro checkout. |
payment_code_invalid | El código o la referencia no es correcto. | Deja escribirlo de nuevo. |
coupon_not_found | El 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_met | El cupón no aplica. | Muestra el mensaje junto al campo del cupón. Ver Cupones. |
coupon_already_used, coupon_first_purchase_only | Al pagar, el cliente no puede usar ese cupón: el checkout ya se recalculó sin él. | Muestra el nuevo total antes de pagar. |
subscription_exists | El 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_unavailable | No hay existencias suficientes. | Ajusta la cantidad o vuelve a la tienda. |
rate_limited | Demasiadas 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.
| Campo | Qué es |
|---|---|
fields | Lo que va en pay dentro de fields. Vacío en el escenario por defecto. |
code | Lo que va en submitPaymentCode después de pay, en los métodos con verification. null si el método no pide código. |
outcome | Có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étodo | Escenario |
|---|---|
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_inmediato | Clave 12345678: pago verificado. Teléfono 04140000002 con esa clave: débito rechazado. Otra clave: payment_code_invalid. |
cripto | Cualquier 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
| Recurso | Operaciones |
|---|---|
checkouts | retrieve, waitForChange, update, paymentMethods, pay, openClaim |
invoices | retrieve, waitForChange, paymentMethods, pay, openClaim |
paymentLinks | retrieve, createCheckout |
customer | retrieve, subscriptions, portal, features, feature, cancelSubscription, resumeSubscription, updateSubscription, changeSeats, stopAutoCharge, openClaim |
fulfillments | retrieve, confirmReceived, openClaim, reportProblem, setRefundDestination |
claims | retrieve, respond, withdraw (con el token del enlace del reclamo) |
refunds | retrieve, setDestination (con el token del enlace del reembolso; reglas) |
exchangeRates, currencies | list |
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.