Skip to the article
Aniq-UI

Food StudioPayments

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.

MethodOffered whenWhat happens
Pay by card (Stripe)STRIPE_SECRET_KEY is setThe 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.
PayPalPAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are setThe same, on PayPal's page: the API captures the approved payment before the order counts as paid.
Demo paymentNo provider is setMarks 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

  1. 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 .env
    STRIPE_SECRET_KEY=sk_test_…
  2. Subscribe the webhook

    In Stripe, add a webhook endpoint at your API's address followed by /api/payments/webhooks/stripe, subscribed to checkout.session.completed, checkout.session.expired, charge.refunded and charge.dispute.created. Copy its signing secret into the API's .env.

    The API's .env
    STRIPE_WEBHOOK_SECRET=whsec_…
  3. Check the return addresses

    Stripe sends the browser back to API_PUBLIC_URL, and the API sends it on to the website at STOREFRONT_URL. Locally both defaults work. On your own domains, set both to the real addresses.

  4. 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

  1. Add your app's credentials

    From the PayPal developer dashboard, copy your app's client id and secret into the API's .env. PAYPAL_ENV is sandbox by default, which moves no money; live takes real payments.

    The API's .env
    PAYPAL_CLIENT_ID=your-paypal-sandbox-client-id
    PAYPAL_CLIENT_SECRET=your-paypal-sandbox-client-secret
    PAYPAL_ENV=sandbox
  2. Subscribe 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 .env
    PAYPAL_WEBHOOK_ID=your-paypal-webhook-id
  3. Restart the API

    Expected result: Checkout offers PayPal.

What happens to an order

EventThe order
The customer places itSaved as pending and unpaid. It holds its stock and any reward points the customer redeemed.
The provider confirms the paymentPaid, 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 pageChecked 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 refundRefunded, 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 unless PAYMENT_WEBHOOK_SECRET is set and sent as the x-webhook-secret header.

Taking real payments

  • Swap in the live keys (sk_live_…, or PayPal's live app with PAYPAL_ENV=live) and a webhook created in live mode, which has its own signing secret or id.
  • Set API_PUBLIC_URL and STOREFRONT_URL to your real addresses, and list the website in CORS_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 CURRENCY from back-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, with NEXT_PUBLIC_ANALYTICS_CURRENCY set to match.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.