One-off purchases

Products people buy once, signed in or not, the purchase email, and showing what you sell.

One-off purchases

Plans are for subscriptions. To sell a single thing (a beat, an ebook, a course, a licence), add products on the same page and choose how each is sold:

  • Stripe. A price, with Stripe connected; Gemmein makes the Payment Link. g.payments.buy navigates to Stripe only for a product sold this way.
  • Relay. Any provider whose webhook you map (GoCardless, Lemon Squeezy, Paddle, bank transfer) fulfils the product with the fulfil_product relay action. The relay binds by name: renaming or deleting it stops fulfilment until a relay with that name exists again, and the product card shows this.
  • Not yet. The product is defined and its grants and credits are known, but no way of selling it is connected.

g.payments.buy on a product sold via a relay or not yet answers 409 product_not_sellable.

Buying signed out

A visitor who is not signed in can buy a product. g.payments.buy works either way, so a product's buy button needs no sign-in in front of it. Signed out, Stripe Checkout asks for the buyer's email, and the purchase is theirs the first time they sign in with that email. A product bought with an item note needs a signed-in buyer (401 authentication_required). Plans always need sign-in, because a plan is tied to an account.

The purchase email

After each paid product purchase, Stripe or relay-sold, Gemmein emails the buyer once for that payment: Your <product> is ready, from your domain's account address, with a link to your Library on your app's verified web domain. It is on by default. On this page, under Products, Email buyers when they buy turns it off and Library path sets where the link opens (/ until you set one). It is sent only from your own verified domain; until one is verified it is not sent, and Logs shows one row a day saying so. See Email.

What it is. A product: a named thing you sell once, such as a download, a licence or a credit pack.

Does. Grants its access key and its credits on purchase. Writes the buyer's receipt. A full refund takes both back.

Does not. Does not sell subscriptions (those are plans). Sold through a relay, it does not set a price; the provider does.

Needs something else when. You sell through a provider without a Payment Link: use a relay with fulfil_product. You meter by usage: use credits spent per AI tool.

Example. "Starter pack", 100 credits, sold through a GoCardless relay.

For one product that covers many items, name the item. The item is display text on the receipt; the price is always the Payment Link's or the relay's. g.purchases.mine() returns the person's payment history from Gemmein's own record:

await g.payments.buy("premium license", { item: "beat_37" })   // redirects

const purchases = await g.purchases.mine()   // the array itself
// [{ item, amountMinor, currency, refundedMinor, status, grants, paidAt }]

To fulfil, tick the product in the collection's Unlocked by row on its page in the dashboard's Data. The server then refuses people who haven't bought it, with no code from you. Never grant access when the redirect comes back: redirects can be faked, and access comes from Stripe's signed webhook.

The 14-day waiver at checkout

Buyers in the UK and EU have 14 days to cancel an online purchase. For digital content, that right ends when delivery starts, but only if the buyer agreed to it before paying. One switch per environment, off by default, adds that agreement to the Stripe checkout of every one-off product: a file, a link, or access alone. Set it in gemmein/payments.json, with the link to your Terms of Service:

{ "consent": { "digitalWaiver": true, "termsUrl": "https://example.com/terms" }, "products": [ … ] }

The buyer must tick this box before paying. "the seller's terms" links to your termsUrl:

"I agree to the seller's terms, and I ask for this digital content to be delivered straight away. I understand that once delivery starts, I lose my right to cancel within 14 days."

What it is. One switch per environment for one-off products: a file, a link, or access alone. Go-live and promote carry it to Live.

Does. Gives each such product a new Payment Link at the same price, whose checkout requires the box above; the old link goes off. Every Logs row of a purchase made with it records the consent and the words the buyer ticked.

Does not. Does not touch plans or credit packs: they are services used over time. The wording is one neutral default and cannot be edited. It is not legal advice: whether it fits your business is your decision.

Needs something else when. Turning it on needs termsUrl: https, a public domain name (no IP address, localhost or private name), no username or password, at most 500 characters. Without it, the save is refused with consent_terms_url_required. Stripe also needs the Terms of Service link in your account: add it under Settings → Business → Public details. Until then, turning it on is refused with stripe_terms_url_missing.

Example. A GBP ebook shop turns it on; the ebook's checkout shows the box, and the purchase's Logs rows read consent: accepted with the words.

Your AI recommends it when your Stripe account is in the UK or EU, or a price is in GBP or EUR. npx gemmein payments --cloud shows whether it is on, and when it is recommended.

Show what you sell

The products and plans on the Payments page are your app's catalog. A storefront lists them with g.payments.products() and a pricing page with g.subscriptions.plans(), in the order the page shows them. No sign-in is needed; the app key is enough. Each item's name is what buy and checkout take, and your app supplies its own words and images for each one in code, keyed by that name:

const products = await g.payments.products()
// [{ name, price: { amountMinor, currency }, delivers, credits, unlocks }]
const plans = await g.subscriptions.plans()
// [{ name, free, price: { amountMinor, currency, period }, credits, unlocks }]

What it is. One read of what your app sells: the products and plans on the Payments page, for each environment.

Does. Lists every product and plan that can be bought right now, plus the free plan at price 0. Gives each one's name, price (in minor units: 1500 is 15.00), what the buyer gets (a file, a link or nothing to download; credits; access) and, for a plan, how often it bills. A price changed on the Payments page shows within a minute.

Does not. Does not list a product or plan that is not sold yet, sold through a relay, or waiting for Stripe to be connected. Does not carry Payment Links, Stripe ids, files or delivery links. Does not carry descriptions or images; those are your app's design.

Needs something else when. Someone buys: g.payments.buy(name) or g.subscriptions.checkout(name). You need what a person owns: g.purchases.mine() and g.subscriptions.mine().

Example. A shop page renders each of g.payments.products() with its words and image from the app's own copy for that name, and a Buy button that calls g.payments.buy(p.name).

Do not make a collection to list what you sell. The Payments page is the one list; a second one has to be filled and kept in step by hand.

Was this page helpful?

Last updated