Payments
Let people pay for what they order. Paying offline (at pickup, on delivery) always works, so an app is never blocked on a payment provider. Connect Stripe and checkout also offers card, through Stripe's own payment page. The server sets the amount and asks Stripe whether it was paid; the browser never decides.
When to use it
- Shops, menus and marketplaces with a basket and orders.
- Anything priced per item, where the total comes from catalogue prices.
For subscriptions, deposits or refunds, write a server function that calls the Stripe API with ctx.secrets.get("STRIPE_SECRET_KEY").
Step by step
- Declare a
basketin the contract: the order table, the line table, the line's ref to the item, its quantity field and the item's price field. - Apps built from blocks get the basket bar and checkout automatically. In your own component, create the order and its lines, then call
app.pay.checkout. - To take cards, connect Stripe in the builder's integrations. Until then
app.pay.methods()is just["offline"]. - When the shopper comes back from Stripe, call
app.pay.returned()andapp.pay.confirm().
Code
"coordinates": { "primaryVerb": "order", "inventoryShape": "shelf", "moneySurface": "cart" },
"basket": {
"orderEntity": "order", "lineEntity": "order_item",
"itemRef": "product_id", "qtyField": "quantity", "priceField": "price"
}// Place the order
const order = await app.create("order", { shopper_id: app.me("shopper") });
for (const l of lines) {
await app.create("order_item", { order_id: order.id, product_id: l.id, quantity: l.qty });
}
const methods = await app.pay.methods(); // ["offline"] or ["offline", "card"]
const res = await app.pay.checkout("order", String(order.id), methods.includes("card") ? "card" : "offline");
// card: the browser goes to Stripe Checkout
// offline: { method: "offline", status: "due" }
// card that couldn't start: { fallback: true, note: "…" }, recorded as offline
// Back on the page after Stripe
useEffect(() => {
const back = app.pay.returned(); // { status, orderId } or null
if (back?.status === "success") void app.pay.confirm("order", back.orderId).then(setStatus); // "paid"
}, [app]);Where payment state lives
Each checkout is an entry in a platform ledger, visible to you under Backend, Logs, Payments. app.pay.status("order", id) returns the latest entry to whoever can read the order. Nothing writes "paid" onto your order row: give the order a status enum (placed, paid, ready) that staff move, or a workflow moves.
Gotchas
- A component never sends an amount. The server totals quantity times the item's catalogue price, in USD, over the order's lines.
- The caller must be able to read the order, or checkout answers
ORDER_NOT_FOUND. - Stripe's minimum is $0.50.
- Orders are never lost. If Stripe can't be reached, the order stands as an offline payment with a
noteto show the shopper. - Always confirm. A return to your page with
successis not proof of payment;confirmasks Stripe.