Checkout propio

Cobra dentro de tu web o de tu app de React Native o Expo con tu propia pantalla de pago.

Para cuando el pago tiene que ocurrir en tu interfaz: una app móvil, una compra de varios pasos o un diseño que no encaja con la página de Billete. Si no tienes una razón así, redirige a checkout.url: el checkout alojado ya resuelve todo lo que explica esta receta.

El modelo

En tu aplicaciónEn Billete
El pedido o la compra de un planUn checkout, creado en tu servidor con la clave secreta
La pantalla de pagoEl cliente público (@boelabs/billete-sdk/client) con el ID del checkout
Entregar la compraEl webhook checkout.completed en tu servidor
  • El servidor decide qué se cobra; la pantalla solo paga. Productos, precio, cliente y cupón se fijan al crear el checkout. A la pantalla solo llega el ID (chk_…): no se puede adivinar y solo sirve para pagar ese checkout. Ninguna clave sk_ va en tu web ni dentro de tu app.
  • Pagar no necesita una sesión de cliente. La sesión de cliente sirve para leer las suscripciones y cuotas de un cliente desde el navegador (autoservicio); un checkout se paga solo con su ID.
  • La pantalla se arma con lo que responde Billete. Cada método de pago dice qué datos pedir y a dónde enviar el dinero. Si cambia el proveedor detrás de un método, tu pantalla sigue funcionando sin cambios.
  • El checkout alojado es tu salida de emergencia. checkout.url abre el mismo checkout en la página de Billete: úsalo para lo que tu pantalla no sepa resolver.

1. Crea el checkout en tu servidor

const checkout = await billete.checkouts.create({
  items: [{ product: "prod_camiseta", quantity: 2 }],
  customer: { externalId: user.id, email: user.email },
  metadata: { orderId: order.id },
}, { idempotencyKey: `checkout-${order.id}` });

await db.orders.update(order.id, { checkoutId: checkout.id });
return { checkoutId: checkout.id };

Para una suscripción, cambia items por plan y months (checkouts). successUrl solo hace falta si alguna vez envías al cliente al checkout alojado: en tu pantalla, el resultado lo esperas tú (paso 4). Si no envías el correo del cliente, va en pay (si falta, email_required), igual que lo que pida checkout.collect.

2. Muestra los métodos de pago

import { createClient, type PublicCheckout } from "@boelabs/billete-sdk/client";

const client = createClient({ apiVersion: "2026-10-24" });

const checkout = await client.checkouts.retrieve(checkoutId);
const { data: methods } = await client.checkouts.paymentMethods(checkout.id);

El mismo código funciona en el navegador y en React Native o Expo, sin polyfills. Cada método trae lo que muestras:

  • amount y currency: lo que el cliente paga, ya convertido a bolívares si el precio está en dólares o euros.
  • instructions: a dónde envía el dinero (beneficiario, cédula o RIF, teléfono, banco o un QR en qrImage).
  • fields: los datos que le pides, con su key, label y type. Puede venir vacío.
  • verification: si después de pagar escribe un código en un paso propio.

Si no hay ninguno, unavailable dice por qué (account_suspended, payment_method_unavailable o payments_unavailable, con un message para el cliente): muéstralo antes de pedir datos. Mira cuando no hay con qué pagar.

No escribas campos fijos por método: un Pago Móvil puede pedir teléfono, banco y fecha con un proveedor y nada con otro. La tabla completa está en lo que pide cada método.

La tasa queda congelada 30 minutos desde que consultas los métodos (hasta expiresAt de cada uno). Muestra ese amount y envíalo tal cual en pay: si la tasa venció, el pago no se cobra y responde exchange_rate_changed. Consulta los métodos otra vez y muestra el monto nuevo. Mira tasa congelada.

3. Envía el pago

const method = methods.find((option) => option.id === selectedMethod)!;

const result = 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,
});

pay espera hasta 25 segundos la respuesta del banco, así que muchos pagos vuelven ya succeeded o failed. Si el total es 0 (una prueba gratis o un cupón del 100 %), envía pay sin method.

4. Atiende el código y espera el resultado

Si el pago queda pending, su nextAction dice qué falta. Con type: "code", el cliente escribe la referencia de su Pago Móvil o la clave que su banco le envió por SMS; label, length y message dicen qué pedirle.

const next = await client.checkouts.submitPaymentCode(checkout.id, { code });

Un código incorrecto responde payment_code_invalid y el pago sigue esperando otro. Mientras la pantalla espera el código, llama a notePayerPresence cada 15 segundos y cuando el cliente vuelve a ella (en React Native, cuando AppState pasa a active): si deja de llegar, el pago se cancela como abandoned. cancelPayment deja volver atrás y elegir otro método. Los detalles están en el paso del código.

Sin nextAction, solo falta la respuesta del banco. Espera el siguiente cambio en vez de consultar cada pocos segundos:

async function settled(current: PublicCheckout): Promise<PublicCheckout> {
  const waiting = current.status === "open" && current.payment?.status === "pending"
    && current.payment.nextAction === null;
  return waiting ? settled(await client.checkouts.waitForChange(current)) : current;
}
Estado del pagoQué muestra tu pantalla
pending con nextActionEl paso que pide: escribir un código, enviar fondos o ir a la página del proveedor.
pending sin nextActionQue lo estás verificando. No permitas pagar otra vez (payment_in_progress).
succeededLa confirmación. El checkout pasa a completed.
failedfailure.message, y deja pagar de nuevo con el mismo método u otro.

Lo que ve la pantalla no entrega nada: tu servidor entrega el pedido al recibir checkout.completed (webhooks sin perder ni duplicar).

5. Maneja los errores

Todos llegan como BilleteError con un code estable y un message en español que puedes mostrar tal cual. Los que más verás:

CódigoQué haces
exchange_rate_changedConsulta los métodos y muestra el monto nuevo.
invalid_payment_detailsMarca el campo que dice param (por ejemplo, fields.phone).
payment_in_progressMuestra el pago en curso y espera su resultado.
checkout_expired, too_many_attemptsCrea otro checkout desde tu servidor.
payments_unavailableOfrece otro método o reintenta en unos minutos.
account_suspendedEl negocio no está recibiendo pagos: dile al cliente que no puede pagar ahora.
subscription_existsEl cliente ya tiene ese plan: llévalo a su suscripción en vez de cobrarle otra.

La lista completa está en errores frecuentes.

6. Pruébalo con los escenarios

En un checkout de prueba, cada método trae testScenarios: qué enviar en fields y en submitPaymentCode para llegar a un resultado conocido (pagado, fallido con un motivo, pendiente o código rechazado). Úsalos para recorrer cada rama de tu pantalla, incluida la de exchange_rate_changed: envía un amount distinto del que devolvió el método. Mira escenarios de prueba.

Checkout en el navegador · Cliente de navegador · Checkouts