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ónEn Billete
El concierto del 15 de noviembreUn producto con category: "event"
General y VIPVariantes, cada una con su precio
El aforo de cada zonaEl stock de cada variante
Un pedido de 3 entradasUn checkout con quantity: 3
Cada entrada con su códigoAlgo que emite tu aplicación al completarse el checkout
  • event es 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

EventoQué pasóQué hace tu aplicación
checkout.completedSe confirmó el pago del pedido.Emite las entradas, una sola vez por checkout.
payment.refundedDevolviste todo o parte del pago.Anula las entradas del pedido con ese checkoutId (todas si su status es refunded).
refund.failedEl proveedor no pudo devolver el dinero.Avisa al cliente y resuelve la devolución.

Referencia de productos · Referencia de checkouts · Referencia de reembolsos