Productos digitales y servicios
Cursos, plantillas, licencias o asesorías que tu aplicación entrega al confirmarse el pago, una sola vez por compra.
Para un negocio que vende algo que no se envía: un curso, una plantilla, una licencia, una asesoría. El ejemplo es una plataforma de cursos donde el usuario user_42 compra el curso de fotografía y lo ve en tu aplicación al confirmarse el pago.
El modelo
| En tu aplicación | En Billete |
|---|---|
| El curso, la plantilla, la licencia | Un producto con category: "digital" |
| La asesoría, la clase en vivo | Un producto con category: "service" |
El usuario user_42 | Un cliente con externalId: "user_42" |
| La compra | Un checkout; su ID es la clave de tu entrega |
- La categoría ordena tu catálogo; no cambia el cobro. Un producto
digitaloserviceno pide datos de entrega: eso solo pasa con los productosphysicalque tienen formas de entrega. - Se entrega con
checkout.completed, no conpayment.succeeded. El checkout dice qué se compró (items), quién (customerId,metadata) y llega también cuando no hubo pago que confirmar, como con un cupón del 100 %.payment.succeededllega por cualquier pago, incluidas renovaciones y Cobros, y no trae los productos. - Una entrega por checkout. Las entregas del webhook pueden repetirse; guarda el ID del checkout con una restricción única y una segunda entrega no hace nada.
- La página de gracias no confirma nada. Cualquiera puede abrir esa URL. Sirve para mostrar el resultado, consultando el checkout desde tu servidor.
1. Crea el producto
const curso = await billete.products.create({
name: "Curso de fotografía",
lookupKey: "curso-fotografia",
category: "digital",
currency: "USD",
amount: 2500,
}, { idempotencyKey: "producto-curso-fotografia" });Sin stock, las unidades son ilimitadas. Para vender un número fijo de licencias o de cupos de una asesoría, ponle stock: las unidades se apartan al iniciar el pago y vuelven si falla. Encuéntralo luego con billete.products.list({ lookupKey: "curso-fotografia" }).
2. Cobra
const checkout = await billete.checkouts.create({
items: [{ product: PRODUCTS.cursoFotografia }],
customer: { externalId: user.id, email: user.email },
successUrl: "https://tuapp.com/compras/gracias?checkout={CHECKOUT_ID}",
cancelUrl: "https://tuapp.com/cursos/fotografia",
metadata: { userId: user.id },
});
return redirect(checkout.url);customer como objeto crea el cliente o reutiliza el que tiene ese externalId, así todas las compras del usuario quedan juntas. metadata lleva tus referencias de vuelta en el evento sin tener que consultar el cliente.
Un checkout creado por la API no admite preguntas propias (customFields). Si para una asesoría necesitas la fecha que prefiere, pregúntala en tu aplicación antes de crear el checkout y guárdala con tu pedido, o véndela con un enlace de pago, que sí las admite.
3. Entrega al confirmarse
Una sola función entrega la compra, y no hace nada si ya la entregó:
import type { Checkout } from "@boelabs/billete-sdk";
async function deliver(checkout: Checkout) {
if (checkout.status !== "completed" || checkout.purpose !== "product") return;
await db.transaction(async (tx) => {
// checkoutId tiene una restricción única: la segunda vez no inserta nada.
const purchase = await tx.purchases.insertIfAbsent({
checkoutId: checkout.id,
userId: checkout.metadata.userId,
paymentId: checkout.paymentId,
});
if (!purchase) return;
for (const item of checkout.items) {
await tx.access.grant(checkout.metadata.userId, item.productId);
}
});
}
// En tu webhook, ya verificado:
if (event.type === "checkout.completed") await deliver(event.data);Para un servicio, "entregar" es reservarle la cita o avisarle a quien lo presta; la regla es la misma.
4. Muestra el resultado
Billete redirige a successUrl solo después de completar el checkout, con su ID en lugar de {CHECKOUT_ID}. Consulta el checkout en tu servidor, comprueba que es de quien tiene la sesión y llama a la misma función: si el webhook llegó primero, no hace nada; si todavía no llega, el usuario no espera.
const checkout = await billete.checkouts.retrieve(searchParams.checkout);
if (checkout.metadata.userId !== session.userId) return notFound();
await deliver(checkout);5. Recibos
Si el negocio tiene activado Enviar recibos por correo a tus clientes (Configuración › Cobros), Billete le envía el recibo al pagar. Para reenviarlo, usa el pago del checkout:
await billete.payments.sendReceipt(checkout.paymentId);Una compra gratuita (paymentId: null) no tiene recibo. El recibo es un comprobante de pago, no una factura fiscal. Mira recibos.
Eventos
| Evento | Qué pasó | Qué hace tu aplicación |
|---|---|---|
checkout.completed | Se confirmó el pago, o la compra no tenía nada que pagar. | Entrega la compra, una sola vez por checkout. |
checkout.expired | Venció sin pagarse. | Nada que entregar. Puedes recordarle que su compra quedó pendiente. |
payment.refunded | Devolviste todo o parte del pago. | Si su status es refunded, retira el acceso de la compra con ese checkoutId. |
Cómo y cuándo devolver una compra está en reembolsos y reclamos.
Referencia de checkouts · Referencia de productos · Verificar webhooks