Eventos y entradas
Tipos de entrada con su precio y su aforo, entradas emitidas por tu aplicación al pagarse y devoluciones si el evento se cancela.
Para un negocio que vende entradas: conciertos, talleres, conferencias. El ejemplo es un concierto con entradas General y VIP, cada una con su aforo.
El modelo
| En tu aplicación | En Billete |
|---|---|
| El concierto del 15 de noviembre | Un producto con category: "event" |
| General y VIP | Variantes, cada una con su precio |
| El aforo de cada zona | El stock de cada variante |
| Un pedido de 3 entradas | Un checkout con quantity: 3 |
| Cada entrada con su código | Algo que emite tu aplicación al completarse el checkout |
eventes una etiqueta del catálogo. Ordena tus productos y no cambia el cobro: no pide datos de entrega. Billete tampoco emite entradas, no guarda la fecha del evento ni controla el acceso: tu aplicación genera los códigos y los valida en la puerta.- El aforo es el stock. Las unidades se apartan cuando el cliente inicia el pago, se descuentan al confirmarse y vuelven si falla, así que no se venden más entradas de las que hay, aunque muchos paguen a la vez. Abrir el checkout no aparta nada.
- Un checkout por pedido. Todas las entradas de una compra salen de un mismo checkout, y su ID es la clave para emitirlas una sola vez.
- Cada evento es su propio producto. Una segunda función o una nueva fecha es otro producto, con su aforo; así se cierra o se cancela uno sin tocar los demás.
1. Crea el evento
const concierto = await billete.products.create({
name: "Concierto · 15 de noviembre",
lookupKey: "concierto-2026-11-15",
category: "event",
currency: "USD",
variants: [
{ name: "General", amount: 2000, stock: 400, isDefault: true },
{ name: "VIP", amount: 6000, stock: 50 },
],
}, { idempotencyKey: "evento-concierto-2026-11-15" });El stock que se ve en cada variante es lo que queda por vender; reserved son las entradas de pagos que se están verificando. Para abrir más cupos, actualiza el producto con sus variantes (cada una con su id) y el stock nuevo; para cerrar la venta, active: false en el producto.
2. Vende
Sin código, con un enlace de pago del producto: adjustableQuantity para elegir cuántas, submitLabel: "book" para que el botón diga "Reservar" y customFields para pedir, por ejemplo, la cédula de quien compra. Las respuestas llegan en customFieldValues del checkout.
Desde tu sitio, con un checkout:
const checkout = await billete.checkouts.create({
items: [{ product: EVENTS.concierto, variant: order.variantId, quantity: order.quantity }],
customer: { email: order.email },
successUrl: "https://tuevento.com/entradas?checkout={CHECKOUT_ID}",
metadata: { orderId: order.id },
}, { idempotencyKey: `pedido-${order.id}` });
return redirect(checkout.url);Si quedan menos entradas que las pedidas, responde insufficient_stock. Las preguntas propias (customFields) son de los enlaces y se responden una vez por compra, no por entrada. Si cada entrada lleva el nombre de quien asiste, pídelos en tu sitio antes de crear el checkout y guárdalos con tu pedido.
3. Emite las entradas al confirmarse
if (event.type === "checkout.completed") {
const checkout = event.data;
await db.transaction(async (tx) => {
// checkoutId es único: si el evento se repite, no se emite nada otra vez.
const order = await tx.orders.insertIfAbsent({ checkoutId: checkout.id, email: checkout.email });
if (!order) return;
// Una entrada por unidad: 3 VIP son 3 códigos.
const tickets = checkout.items.flatMap((item) =>
Array.from({ length: item.quantity }, () => ({
orderId: order.id,
variantId: item.variantId,
code: newTicketCode(),
})),
);
await tx.tickets.createMany(tickets);
});
await sendTickets(checkout.email);
}La redirección a successUrl no confirma el pago: muestra las entradas en esa página consultando tu base, no la URL.
4. Si el evento se cancela
Cierra la venta y devuelve cada pago. Billete no sabe qué pagos son de un evento, así que recórrelos desde tus pedidos:
await billete.products.update(EVENTS.concierto, { active: false });
for (const order of await db.orders.forEvent(concierto.id)) {
const checkout = await billete.checkouts.retrieve(order.checkoutId);
if (!checkout.paymentId) continue; // entradas gratuitas: nada que devolver
await billete.payments.refund(
checkout.paymentId,
{ reason: "Evento cancelado" },
{ idempotencyKey: `reembolso-${checkout.paymentId}` },
);
}Sin amount, se devuelve lo que queda del pago. Si el método de pago devuelve automáticamente, lo hace el proveedor; si no (mode: "manual"), registrar el reembolso dice que tu negocio ya devolvió el dinero, así que devuélvelo antes. Un reembolso no repone el stock: si una sola persona devuelve sus entradas y quieres revenderlas, súmalas tú. Los detalles están en reembolsos y reclamos.
Eventos
| Evento | Qué pasó | Qué hace tu aplicación |
|---|---|---|
checkout.completed | Se confirmó el pago del pedido. | Emite las entradas, una sola vez por checkout. |
payment.refunded | Devolviste todo o parte del pago. | Anula las entradas del pedido con ese checkoutId (todas si su status es refunded). |
refund.failed | El proveedor no pudo devolver el dinero. | Avisa al cliente y resuelve la devolución. |
Referencia de productos · Referencia de checkouts · Referencia de reembolsos