Sell a digital product
Beluga knows what a download is. It does not yet hand one over. This tutorial
sets up a digital product correctly, shows what the flag changes, and closes the
gap with the tool that exists for it: an order.paid webhook into a small
fulfilment script.
1. The product
Section titled “1. The product”Admin → Products → New product, and set Type to Digital.
Three things follow, and the editor enforces them:
- Stock is always unlimited. The API refuses a finite count on a digital
variant. This is not tidiness: the inventory decrement would count it down, reach
zero, and start flagging paid orders
oversoldfor a file that cannot run out. - Weight is irrelevant and the field is hidden.
- Tax code. Digital goods are taxed differently from tangible ones in many
places. If you use Stripe Tax, set a code for it; the store default
txcd_99999999is general tangible goods.
Publish it.
2. What changes in the cart
Section titled “2. What changes in the cart”A cart holding only downloads never asks for a destination country and reaches Stripe with no address collection and no shipping options. A mixed cart still collects an address, priced on its physical lines only.
Digital lines are excluded from the parcel, not zeroed. The difference matters: a zero-gram line still participates, so a cart of nothing but PDFs would report a 0 g parcel, match the store’s lightest weight band, and charge postage on a parcel that does not exist. The same rule applies to subtotal bands: a $40 download does not push a $10 box over a “free over $50” threshold, and on an upper bound it is worse, because it could push the cart past every band, match nothing, and ship free in silence.
physicalLines, requiresShipping and parcelFor in shared/shipping.ts are the
API for this, and they are what checkout uses too, from the catalogue rather than
the request, so a buyer cannot declare their order digital to skip the address.
3. Delivering the file
Section titled “3. Delivering the file”There is no file upload for products, no entitlement record, and no download route.
That is a known gap, and the approach has not been
chosen, so do not build it into the statically served assets directory: anything
under public/assets/ is downloadable by whoever guesses the path.
The supported route today is a webhook:
- Store the files somewhere private: object storage with signed URLs is the usual answer.
- Follow Connect a fulfilment webhook and
subscribe to
order.paid. - In the receiver, look at each item’s
productIdandvariantId, mint a time-limited signed URL for the matching file, and email it todata.email. - Deduplicate on the event id, because at-least-once delivery would otherwise send the link twice.
The payload snapshots productName and variantLabel at purchase, so the email can
name what was bought even if the product is renamed later.
4. Fulfilment status
Section titled “4. Fulfilment status”The order status vocabulary is paid, processing, shipped, cancelled,
refunded. None of those means “delivered”. Mark a download shipped once the
link has gone out if you want the admin to show it as done, and know that the
Shipped email talks about carriers. Adding a delivered status is one of the two
collisions the gap document records, along with what a refund should do to an
entitlement.
5. Refunds
Section titled “5. Refunds”A refund of a download restocks nothing, since there is no stock. It should revoke
access, which is your receiver’s job: subscribe to order.refunded and expire the
signed URL, or mark the entitlement revoked in whatever you built in step 1.