Payments
How the checkout takes payment, how to connect Stripe, and what happens to an order at each step.
For the Full Stack package
How the checkout takes payment
The storefront offers two ways to pay: a card through Stripe, and cash on delivery. There is no PayPal option in the checkout. Both create the order on the API first, priced by the API from its own catalogue, never from the prices the browser sends.
| Method | What happens to the order |
|---|---|
| Card (Stripe) | Saved as pending and unpaid. The API asks Stripe for a payment and the storefront confirms the card on the same page. When Stripe reports the payment through the webhook, the order becomes paid and confirmed; a declined card marks it failed. |
| Cash on delivery | Saved as confirmed and unpaid straight away. You collect the money on delivery and move the order on in the dashboard. |
Cash on delivery is selected by default. Guests can check out with their email; signed-in customers see the order in their account.
Shipping is free from $75 and costs $9.99 below it, and there is no tax. The cart, the checkout summary and the API apply the same rule, so the total a shopper sees is the total charged. The storefront keeps the rule in storefront/src/config/shipping.ts and the API in back-end/src/modules/orders/orders.service.ts: change both together. Stripe charges in US dollars.
Connect Stripe
Add your API keys
From the Stripe dashboard's API keys page, put the secret key in
back-end/.env. Use the test keys (sk_test_…) until you are ready to take real money.back-end/.envSTRIPE_SECRET_KEY=sk_test_…Subscribe the webhook
In Stripe, add a webhook endpoint at your API's address followed by
/api/payments/webhook, subscribed topayment_intent.succeededandpayment_intent.payment_failed. Copy its signing secret intoback-end/.env.back-end/.envSTRIPE_WEBHOOK_SECRET=whsec_…Give the storefront its publishable key
Set the publishable key in the storefront's
.env, then rebuild it: the key is compiled in. With Docker Compose, put it in the.envnext todocker-compose.ymland rundocker compose up --build.storefront/.envNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_…Restart the API
Expected result: The API's log says "Stripe initialized", and the checkout shows the card form.
The webhook marks the order paid
An order is marked paid when Stripe calls the webhook, not when the shopper returns from the payment. Without the subscription and its signing secret, card orders stay pending and unpaid.
To test the webhook on your own machine, the Stripe CLI can forward events to http://localhost:8000/api/payments/webhook and prints the signing secret to use while it runs.
Without Stripe
- Without
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, or with anything that is not a realpk_test_…orpk_live_…key, the card option at checkout says "Card payments are unavailable right now. Please choose cash on delivery." and hides the card field. - Without
STRIPE_SECRET_KEY, or with the.env.exampleplaceholder, the API takes no card payment: an order placed with the card method is saved as pending and unpaid. - Cash on delivery works either way.
- On the storefront's sample store, with no API at all, checkout completes without calling Stripe and nothing is charged.
Taking real payments
- Swap in the live keys (
sk_live_…andpk_live_…) and a webhook created in live mode, which has its own signing secret. - Set
NEXT_PUBLIC_DEMO_CHECKOUT=false, so the checkout is not prefilled with a generated buyer. - Prices are charged in US dollars. To sell in another currency, change the currency the API sends to Stripe in
back-end/src/modules/payments/payments.service.ts, and setNEXT_PUBLIC_ANALYTICS_CURRENCYto match.