Billete SDK
SDK oficial de TypeScript para la API de Billete.
Cobros con Pago Móvil, precios en bolívares o en divisas a tasa BCV y suscripciones, desde tu servidor y tu frontend.
- Tipado de punta a punta: cada recurso, entrada y evento tiene su tipo.
- Sin dependencias: solo
fetchy Web Crypto; funciona en Node.js 20+, Bun, Deno y runtimes edge (Cloudflare Workers, Vercel). El cliente de navegador también funciona en React Native y Expo. - Seguro ante reintentos: cada mutación lleva una clave de idempotencia, así un reintento nunca cobra dos veces.
Contenido
- Instalación
- Inicio rápido
- Configuración
- Recursos
- Montos
- Paginación
- Errores
- Idempotencia y reintentos
- Navegador
- Webhooks
- Licencia
Instalación
npm install @boelabs/billete-sdk
Cada versión del SDK habla una sola versión de la API, la que pide su opción apiVersion: una
versión mayor nueva del SDK es una versión nueva de la API. Las versiones beta de la API tienen su
SDK en @boelabs/billete-sdk@beta.
El paquete tiene tres puntos de entrada:
| Import | Dónde se usa | Para qué |
|---|---|---|
@boelabs/billete-sdk | Servidor | Toda la API, con tu clave secreta |
@boelabs/billete-sdk/client | Navegador | Checkout y estado de un cliente, sin clave secreta |
@boelabs/billete-sdk/webhooks | Servidor | Verificar la firma de los webhooks |
Inicio rápido
Crea una clave secreta en el dashboard (Desarrolladores → Claves API) y guárdala en
BILLETE_API_KEY.
import { createBillete } from "@boelabs/billete-sdk";
const billete = createBillete({ apiVersion: "2026-11-10", environment: "test" });
const checkout = await billete.checkouts.create({
plan: "plan_…",
months: 1,
customer: { email: "[email protected]", externalId: "user_123" },
// {CHECKOUT_ID} se reemplaza por el ID del checkout.
successUrl: "https://tuapp.com/gracias?checkout={CHECKOUT_ID}",
cancelUrl: "https://tuapp.com/planes",
});
// Redirige a tu cliente a checkout.url para que pague.
Cuando el pago se confirma, Billete te avisa con el webhook checkout.completed (ver
Webhooks).
Los productos físicos habilitan sus formas de entrega con fulfillmentMethods, por ejemplo
["pickup", "shipping"]; la lista está vacía por defecto. Cada método también debe estar activo
en la configuración del negocio. Desactivarlo conserva la selección de los productos. El checkout
ofrece «Acordar con el negocio» cuando los productos físicos tienen métodos habilitados y activos
en el negocio. Sin entrega habilitada, no exige datos para entregarla; respeta los que el enlace
solicite expresamente. En carritos, solo ofrece los demás métodos que todos los productos físicos admitan.
Configuración
const billete = createBillete({
apiVersion: "2026-11-10",
environment: "production",
apiKey: process.env.BILLETE_API_KEY,
timeoutMs: 30_000,
maxRetries: 2,
});
| Opción | Por defecto | Descripción |
|---|---|---|
environment | — (obligatoria) | "test" o "production". Debe coincidir con la clave. |
apiKey | BILLETE_API_KEY | Clave secreta: sk_test_… o sk_prod_…. |
baseUrl | BILLETE_BASE_URL, o https://api.billete.run | URL de la API. |
timeoutMs | 30000 | Tiempo máximo por intento. |
maxRetries | 2 | Reintentos ante errores de red, 429 y 5xx. |
fetch | globalThis.fetch | Implementación de fetch propia (proxies, tests). |
Si environment no coincide con el prefijo de la clave, createBillete lanza un error: una
clave de prueba nunca opera en producción ni al revés. Por la misma razón, el cliente de servidor
se niega a crearse en un navegador.
[!WARNING] La clave secreta da acceso completo a tu negocio. Úsala solo en el servidor y nunca la incluyas en código que llegue al navegador.
Recursos
Todos los métodos devuelven promesas con el recurso tipado. Los recursos con listado tienen
create, retrieve, update, list e iterate, además de sus acciones propias:
| Recurso | Acciones propias |
|---|---|
customers | — |
customerSessions | create |
portalSessions | create (enlace al portal del cliente, válido 60 minutos) |
products | delete, deleteVariant |
plans | delete |
features | delete, check, isEnabled, listForCustomer, consume, release, set |
coupons | delete |
checkouts | expire |
paymentLinks | delete |
invoices | void (cobros: renovaciones y cobros sueltos, con página de pago en url) |
subscriptions | cancel, resume |
payments | refund, listRefunds |
fulfillments | deliver, submitTracking (entregas de productos físicos; también retrieve, list e iterate) |
webhookEndpoints | delete |
events | retrieve, list, iterate |
exchangeRates | list |
currencies | list |
Ejemplo: plan con cuotas
await billete.features.create({
key: "tokens",
name: "Tokens",
type: "metered",
unit: "tokens",
reset: { every: "billing" },
});
const plan = await billete.plans.create({
name: "Pro",
currency: "USD",
prices: [
{ months: 1, amount: 1_000 },
{ months: 12, amount: 10_000 },
],
features: { tokens: 100_000 },
});
// `customer` acepta el ID de Billete (cus_…) o tu propio externalId.
const { allowed } = await billete.features.consume("user_123", "tokens", 850);
consume es atómico: si el consumo supera el límite, no se descuenta nada y allowed es
false.
Montos
Los montos son siempre enteros en unidad menor (céntimos), nunca decimales: 1_000 en USD
son 10,00 USD y 1_000 en VES son 10,00 Bs. Las tasas de cambio llegan como texto decimal para
no perder precisión.
Paginación
list devuelve una página ({ data, nextCursor }); iterate recorre todas por ti:
const page = await billete.payments.list({ limit: 50 });
const next = await billete.payments.list({ limit: 50, cursor: page.nextCursor ?? undefined });
for await (const payment of billete.payments.iterate()) {
console.log(payment.id, payment.amount);
}
Toda lista acepta un rango de creación (createdFrom, createdTo, ISO 8601, inclusivos) y, donde
tiene sentido, search: los pagos y los cobros por su cliente, los productos y planes por nombre,
los cupones por código.
const october = await billete.payments.list({
status: "succeeded",
search: "[email protected]",
createdFrom: "2026-10-01T04:00:00Z",
createdTo: "2026-11-01T03:59:59Z",
});
Errores
Toda respuesta fallida lanza un BilleteError:
import { BilleteError } from "@boelabs/billete-sdk";
try {
await billete.payments.refund("pay_…");
} catch (error) {
if (error instanceof BilleteError && error.code === "payment_not_refundable") {
// …
}
throw error;
}
| Propiedad | Descripción |
|---|---|
type | Categoría: invalid_request, authentication, permission, not_found, conflict, validation, rate_limit, provider, internal o network. |
code | Código estable, apto para decidir en tu código: uno del catálogo (BilleteErrorCode), o network_error / invalid_response del SDK. |
message | Descripción legible del error. |
status | Código HTTP (0 si no hubo respuesta). |
param | Campo que causó el error, cuando aplica. |
requestId | Identificador de la petición; inclúyelo al pedir soporte. |
Idempotencia y reintentos
Cada mutación del cliente de servidor envía una Idempotency-Key generada automáticamente, que se mantiene entre
reintentos. Si quieres que un reintento tuyo (por ejemplo, de un job) también sea seguro, pasa
una clave estable:
await billete.payments.refund("pay_…", {}, { idempotencyKey: `refund-${orderId}` });
Todos los métodos aceptan como último argumento { idempotencyKey?, signal? }; signal permite
cancelar la petición con un AbortController.
Navegador
El cliente de navegador nunca usa tu clave secreta. Tu servidor crea una sesión de cliente de corta duración y se la entrega al frontend:
// Servidor
const session = await billete.customerSessions.create({ customer: "user_123" });
return { token: session.token };
// Navegador
import { createClient } from "@boelabs/billete-sdk/client";
const billete = createClient({ apiVersion: "2026-11-10", customerSession: token });
const { data: subscriptions } = await billete.customer.subscriptions();
const tokens = await billete.customer.feature("tokens");
Para no construir esa pantalla, envía a tu cliente al portal alojado:
const portal = await billete.portalSessions.create({
customer: "user_123",
returnUrl: "https://tuapp.com/cuenta",
});
// Redirige a portal.url: allí ve sus suscripciones y cobros, cambia su ciclo o cancela.
customerSession también acepta una función, útil cuando el token se renueva. Sin sesión, el
cliente puede leer y pagar checkouts, cobros y links de pago, y consultar tasas y monedas.
El cliente de navegador solo necesita fetch, AbortController y setTimeout, así que funciona
igual en React Native y Expo, sin polyfills. No envía Idempotency-Key: cada mutación de
/v1/client ya rechaza su repetición por el estado de lo que cambia (un checkout con un pago en
curso responde payment_in_progress), así que reintentar es seguro.
Para saber cuándo se confirma un pago, espera el siguiente cambio en vez de consultar en bucle:
waitForChange responde en cuanto el checkout cambia, o el mismo checkout si en 25 segundos no
pasó nada.
const next = await billete.checkouts.waitForChange(checkout);
Webhooks
Billete firma los webhooks con Standard Webhooks. Verifica la firma con el cuerpo exactamente como llegó, antes de parsearlo:
import { verifyWebhook, WebhookVerificationError } from "@boelabs/billete-sdk/webhooks";
export async function POST(request: Request) {
try {
const event = await verifyWebhook({
payload: await request.text(),
headers: request.headers,
secret: process.env.BILLETE_WEBHOOK_SECRET!,
});
if (event.type === "checkout.completed") {
// event.data es un Checkout
}
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
throw error;
}
}
event es una unión discriminada por type, así que event.data tiene el tipo correcto en cada
rama. Se rechazan las firmas con más de 5 minutos (toleranceSeconds).