Payments
How the checkout takes payment, how to connect Stripe or PayPal, and what happens to an order at each step.
For the Full Stack package
How the checkout takes payment
The customer pays on the provider's own page, Stripe Checkout or PayPal, so the website and the API never see a card number. The API prices the order itself, from its own menu, fees and discounts, never from the prices the browser sends.
| Method | Offered when | What happens |
|---|---|---|
| Pay by card (Stripe) | STRIPE_SECRET_KEY is set | The order is saved unpaid and the browser goes to Stripe's page. The order becomes paid once the API has asked Stripe what happened and the amount matches. |
| PayPal | PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are set | The same, on PayPal's page: the API captures the approved payment before the order counts as paid. |
| Demo payment | No provider is set | Marks the order paid without moving money, so the whole flow can be tried locally. It is never offered next to a configured provider. |
Guests can check out with their name and email; signed-in customers see the order in their account. A paid order moves to the kitchen's queue; an unpaid one cannot be confirmed by staff.
Prices are in US dollars. Delivery costs $2.99 and pickup is free in the demo settings, with a $10.00 delivery minimum. There is no separate tax line: the customer pays the menu prices plus the fee, less any discount. Staff change the fees in the dashboard, under Settings, Restaurant, which also lists the payment methods this build offers and whether each runs in test or live mode.
Connect Stripe
Add your secret key
From the Stripe dashboard's API keys page, put the secret key in the API's
.env. Use a test key (sk_test_…) until you are ready to take real money: it moves none.The API's .envSTRIPE_SECRET_KEY=sk_test_…Subscribe the webhook
In Stripe, add a webhook endpoint at your API's address followed by
/api/payments/webhooks/stripe, subscribed tocheckout.session.completed,checkout.session.expired,charge.refundedandcharge.dispute.created. Copy its signing secret into the API's.env.The API's .envSTRIPE_WEBHOOK_SECRET=whsec_…Check the return addresses
Stripe sends the browser back to
API_PUBLIC_URL, and the API sends it on to the website atSTOREFRONT_URL. Locally both defaults work. On your own domains, set both to the real addresses.Restart the API
Expected result: Checkout offers "Pay by card", and Settings, Restaurant, Payments lists it as offered, in test mode.
The publishable key is not needed: the payment happens on Stripe's own page. To test the webhook on your own machine, the Stripe CLI can forward events to http://localhost:8000/api/payments/webhooks/stripe and prints the signing secret to use while it runs.
Connect PayPal
Add your app's credentials
From the PayPal developer dashboard, copy your app's client id and secret into the API's
.env.PAYPAL_ENVissandboxby default, which moves no money;livetakes real payments.The API's .envPAYPAL_CLIENT_ID=your-paypal-sandbox-client-id PAYPAL_CLIENT_SECRET=your-paypal-sandbox-client-secret PAYPAL_ENV=sandboxSubscribe the webhook
Add a webhook at your API's address followed by
/api/payments/webhooks/paypal, with the payment capture events: completed, denied, refunded and reversed. Copy the webhook's id into the API's.env: the API verifies every call against it.The API's .envPAYPAL_WEBHOOK_ID=your-paypal-webhook-idRestart the API
Expected result: Checkout offers PayPal.
What happens to an order
| Event | The order |
|---|---|
| The customer places it | Saved as pending and unpaid. It holds its stock and any reward points the customer redeemed. |
| The provider confirms the payment | Paid, by the customer's return or by the webhook, whichever comes first. The browser's return alone never pays an order: the API asks the provider first. |
| The customer leaves the payment page | Checked with the provider after 35 minutes for Stripe and 3 hours for PayPal, then cancelled as a failed payment, with its stock and points given back. |
| The provider reports a full refund | Refunded, and the points it earned are taken back. A partial Stripe refund is logged and leaves the order as it is. |
A payment that arrives for an order already cancelled is logged as needing a refund and never brings the order back. Staff cannot set an order to refunded by hand: refunds are made in Stripe or PayPal, and the webhook updates the order.
The demo payment
- With no provider configured, checkout shows "Demo payment" and "Place demo order". The order is paid at once, without moving money, so the kitchen's queue and the tracking page can be tried end to end.
- As soon as Stripe or PayPal is configured, the demo payment disappears, and an order asking for it is refused.
- The demo payment's own webhook,
POST /api/payments/webhooks/demo, refuses every call unlessPAYMENT_WEBHOOK_SECRETis set and sent as thex-webhook-secretheader.
Taking real payments
- Swap in the live keys (
sk_live_…, or PayPal's live app withPAYPAL_ENV=live) and a webhook created in live mode, which has its own signing secret or id. - Set
API_PUBLIC_URLandSTOREFRONT_URLto your real addresses, and list the website inCORS_ORIGIN: a return address outside it is refused. - Settings, Restaurant, Payments shows each method as "Live, real money" once it runs on live credentials.
- Amounts are in US dollars throughout: the API charges in
CURRENCYfromback-end/src/common/constants/ordering.constants.ts, and both frontends show prices as dollars. Another currency is a code change in the API and the frontends, withNEXT_PUBLIC_ANALYTICS_CURRENCYset to match.