The licence checkout
How the website sells its two licences: the checkout, Stripe's and PayPal's own pages, the test checkout, the webhook, the receipt and its licence key, and how to change the offers or sell through an outside store.
For the Full Stack package
From a licence to a receipt
- On the pricing page the buyer presses Get single project or Get Studio. The Purchase summary shows the licence, what it covers and the total, and Continue to checkout opens
/checkout?license=singleor?license=studio. - The checkout asks for an email, a full name and a payment method, Card or PayPal, and the agreement to the licence terms. Continue to payment sends them to the API, which writes the order (a reference such as
NS-7KQ2-M9XD) at its own price: the browser never sends one. - The buyer pays on Stripe's or PayPal's own page, or on the site's test checkout when that method has no keys. Card details are never typed on your site.
- The page the buyer comes back to,
/checkout/success, asks the API to check the payment with the provider, then again every three seconds, up to five times, while it is still processing. Once the API has verified it, the order is paid, the receipt shows the licence key (such asNSK-4F7Q-2M9X-HB3T-8KRD) with Copy the license key, and the receipt is emailed to the buyer.
The website never decides that an order is paid. An order is paid only by Stripe's signed webhook, by the Stripe session the API reads back when the buyer returns, or by the PayPal capture the API makes on that return. Exactly one of them issues the key and the email, however many arrive. A buyer who leaves the payment page lands on /checkout/cancelled, which says no money was taken and offers Try again.
Payment methods
| Method | Set in the API's .env | How it works |
|---|---|---|
| Card, on Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Stripe Checkout in payment mode. The price travels with each session, so nothing has to be created in Stripe first. Stripe returns the buyer to /checkout/success or /checkout/cancelled. |
| PayPal | PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET (a PayPal REST app), PAYPAL_ENV (sandbox by default, or live) | PayPal's own page. The API captures the approved payment when the buyer comes back to /checkout/success. |
| Test checkout | nothing: it takes any method that has no keys | The site's page /checkout/test, with Pay and Cancel payment. It charges nobody. |
- Each method goes to its provider once that provider's keys are set, and to the test checkout until then, so a fresh install can complete a purchase at once.
- While no method moves real money (no keys, a
sk_test_Stripe key, or the PayPal sandbox), the checkout says it is in test mode. With a Stripe test key it names the test card:4242 4242 4242 4242, any future date and any CVC. - Use test keys first: they move no money. When the API starts it logs, for each provider, whether it takes test payments or real ones.
- Stripe's keys are the same ones the dashboard's subscription billing uses.
The Stripe webhook
Licence orders and workspace subscriptions share one Stripe webhook. Before going live, register it in Stripe on your API's public address and put its signing secret in STRIPE_WEBHOOK_SECRET:
https://<your-api>/api/billing/webhooks/stripeFor licences the API acts on checkout.session.completed (the order is paid) and checkout.session.expired (the order failed). It tells a licence session from a subscription by the session's kind: license metadata. PayPal needs no webhook: the capture happens when the buyer returns.
With STRIPE_SECRET_KEY set and STRIPE_WEBHOOK_SECRET empty, the API's log says so when it starts and every delivery is refused with a 400 naming the variable. A card order is then paid only when the buyer comes back to the receipt page.
On your own computer Stripe cannot reach the API, and a test payment still completes: the return page asks the API, which reads the session back from Stripe. Set WEBSITE_URL to the website's address, because every return link the API gives Stripe and PayPal is built from it, in the buyer's language.
Order statuses
| Status | Meaning | What the receipt page says |
|---|---|---|
pending | Written, not paid yet. | "Checking your payment.", then "Your payment is still processing." with Check again. |
paid | Verified with the provider; the licence key is issued. | "Thank you. Studio is yours." (with the licence bought), the order's details and the key. |
failed | Stripe's session expired, PayPal refused the capture, or the provider's page could not be opened. | "The payment did not go through." with Try again. |
cancelled | The buyer left the payment page. | "This order was cancelled." |
The receipt shows the licence key only for a paid order, and marks a test order with "Test payment: no money moved." If Stripe's webhook later reports a payment for a cancelled or failed order (a buyer who went back and paid in another tab), that order becomes paid.
Limits and safety
- An order is read only by its id, 32 random characters that only the buyer's return links and receipt email carry. There is no list of orders for a visitor to browse.
- Each submission carries its own token, so pressing Continue to payment twice opens the same order and the same payment page, never a second charge.
- Each address may start ten checkouts in an hour; the next one says "Too many attempts from this address. Try again in an hour." Behind a reverse proxy, set
TRUST_PROXY=1in the API's.env, or every buyer shares the proxy's allowance. - No tax is added. Set up tax for your country before taking real payments; Stripe's and PayPal's own fees are separate.
The receipt email
The paid order's email gives the order reference, the licence, the amount, the licence key and a link back to the receipt. It is sent through SMTP_HOST and the other SMTP_* settings; without them it is written to the API's log. The email's wording is English in both languages, in back-end/src/modules/licenses/services/license-checkout.service.ts; the receipt link opens in the language the buyer checked out in.
Change the licences and their prices
The price is kept in two places, and they must agree:
| File | What it decides |
|---|---|
back-end/src/modules/licenses/constants/license-offers.ts | What the API charges, in cents (19900 and 39900, US dollars), and the name on the provider's page and the receipt. |
website/src/config/licenses.ts | What the website prints, how many end products each licence covers, which card is recommended, and the lines each card lists (their words are under PricingPage.licenses in website/messages/pricing/en.json and ar.json). |
A price changed in the website alone changes only what the page shows: the buyer still pays the API's amount. The two licence ids, single and studio, are written into both files, the API's checkout request and the website's messages, so a third licence is a change in each of them.
Sell through an outside store
Set NEXT_PUBLIC_PURCHASE_URL_SINGLE and NEXT_PUBLIC_PURCHASE_URL_STUDIO to the store's checkout for each licence and build the website again (with Docker Compose, PURCHASE_URL_SINGLE and PURCHASE_URL_STUDIO in the .env beside docker-compose.yml). Continue to checkout then opens that address instead of /checkout. A licence left empty keeps the site's own checkout.
Without the API
When the website runs on its built-in sample data, the checkout is checked and answered in the browser: an order opens the site's own test checkout, Pay issues a licence key on the receipt, and the orders are kept in that browser only. Nothing is charged or sent.
In demo mode
Included with your purchase. Sign in to read, or open it in your download.
How the checkout behaves on a public demo.
The routes behind it
| Route | What it does |
|---|---|
GET /api/licenses/checkout/options | The methods on offer, the provider that takes each (stripe, paypal or simulator) and testMode. |
POST /api/licenses/checkout | license (single or studio), email, name, method (card or paypal), locale (en or ar) and clientToken. Answers the order's id and the page to pay on; 429 past the limit, 503 payment_unavailable when the provider's page could not be opened. |
GET /api/licenses/orders/:id | The order, with its licence key once paid. |
POST /api/licenses/orders/:id/confirm | Asks the provider whether the payment went through. |
POST /api/licenses/orders/:id/cancel | The buyer left the payment page. |
POST /api/licenses/orders/:id/simulate | The test checkout's pay or cancel, for an order the test checkout took. |
All of them are public: an order's id is what proves the reader is its buyer. An id that is not 32 hexadecimal characters, or that matches no order, answers 404 order_not_found.