# Build a storefront on FrontDesk โ€” instructions for AI app builders You are building an online store whose backend is FrontDesk. FrontDesk already holds the merchant's products, events, availability, checkout, payments, orders and webhooks. Your job is the frontend and a thin server layer. Do not build a database of products, a cart-to-payment pipeline, or anything that touches cards. Full API reference: https://api.frontdesk.africa/v1/store/llms.txt (agents) ยท https://api.frontdesk.africa/v1/store/openapi.json (codegen) Typed client for Node: npm i @frontdesk-africa/store-js ## The five rules 1. TWO KEYS, ONE BOUNDARY. A publishable key (fd_pk_...) reads the catalogue and registers guests for FREE events, is origin-locked, and may ship in the browser. A secret key (fd_sk_...) opens checkouts (paid tickets, products) and reads orders: it lives in a SERVER environment variable and must never appear in client code, client bundles, or repository files. If the user pastes a secret key into the chat, use it only as a server env var and say so. 2. NEVER BUILD A PAYMENT UI. No card inputs, no bank pickers. Your server opens a hosted checkout (POST /v1/store/checkouts with an Idempotency-Key header) and you redirect the buyer to the hostedUrl it returns. FrontDesk's page takes the payment on its own origin and sends the buyer back to your returnUrl with ?checkout= appended. 3. CONFIRM SERVER-SIDE, ALWAYS. The return redirect is a browser navigation: it can be lost, faked or replayed, so it is a UX signal only. Fulfil nothing from it. Confirm with GET /v1/store/checkouts/:ref from your server, or from the checkout.completed webhook. 4. START IN TEST MODE. Build against a test key (fd_sk_test_...). Checkouts run the real rails against the provider sandbox: pay with a Paystack test card, get a real order marked test, real webhooks, and not a unit of currency moves. Going live is swapping the key. 5. VERIFY WEBHOOKS PROPERLY. Raw request body + X-FD-Signature (HMAC-SHA256, that destination's secret), constant-time comparison, reject stale X-FD-Timestamp, and store the event id with a unique constraint so a retried delivery is processed once. Payloads carry refs only: re-fetch through the API rather than trusting figures inside the event. ## The minimal shape (Next.js App Router terms; adapt to your framework) - Server env: FRONTDESK_SECRET_KEY, FRONTDESK_WEBHOOK_SECRET. Client env: none required. - Catalogue pages: server-render from GET /v1/store/products (and /events for tickets). - Free event RSVP page: POST /v1/store/events/{slug}/register straight from the browser with the publishable key (tickets, contact, responses keyed by each tier's formFields[].key). Only a PAID tier needs the server checkout route; a paid tier here answers 402 TIER_REQUIRES_PAYMENT. - POST /api/checkout (server route): body = cart lines; opens the hosted checkout with crypto.randomUUID() as the Idempotency-Key; responds with hostedUrl; the page redirects to it. - /thanks page: reads ?checkout=, asks YOUR server, which asks GET /v1/store/checkouts/:ref and renders paid/open/expired from that answer alone. - POST /api/frontdesk-webhook: verifies per rule 5, then reacts to checkout.completed. ## If the stack is Supabase The merchant can install FrontDesk's Edge Functions from their portal (Developers โ†’ Supabase). Then the frontend calls those functions instead of this API, no FrontDesk key exists in the browser at all, and the frontdesk schema keeps realtime mirrors: checkout rows, the product catalogue, and the merchant's own orders (synced via the delta reads, deletions via events). Prefer that path when a Supabase project is already in play, and query the mirrors instead of building your own product/order tables. ## Do not build Card or bank-detail forms. Wallets, payouts, refunds, or any money movement (merchant-side, not exposed here). Admin/fulfilment screens beyond what the documented endpoints offer. A hand-rolled database of products or orders: FrontDesk stays the source of truth โ€” re-fetch it, or keep a LOCAL COPY the honest way with the delta reads (GET /v1/store/products?updatedSince=, GET /v1/store/orders) plus the product.deleted / order.deleted webhooks, and let FrontDesk win every conflict.