Use case:
Create a complete booking system using MarketBox APIs.
Problem this solves:
Many platforms want full control over booking UX but don’t want to build complex scheduling and routing logic from scratch.
How Smart Clusters fits in: Smart Clusters integrates with MarketBox’s broader API set (services, providers, schedules, orders), enabling teams to build a fully custom, white-labeled booking flow while offloading scheduling intelligence.
This guide implements the booking flow in Next.js App Router using server-only code (Route Handlers), and renders a React UI with shadcn/ui + Tailwind.
This guide assumes no payment collection inside the booking flow. If you collect payment separately (Stripe, etc.), you can store that info in your system and optionally pass it into paymentDetails later—but it is not required for this flow.
Sequence
List services (GET /resources/services)
Collect service address (Google Places Autocomplete in browser)
Find best provider + time slots (POST /smart-clusters/who-is-available) — server
Reserve the chosen slot (POST /resources/reservedslots) — server
Create order (confirm booking) (POST /resources/orders) — server
(Recommended) Cleanup on abandon (DELETE /resources/reservedslots/{reservedSlotId}) — server
MarketBox Base URL:https://api.gomarketbox.com/v1
Security model (non-negotiable)
Put your API key in an env var: MARKETBOX_API_KEY.
Only access it server-side (Next.js Route Handlers / server-only modules).
Never send x-api-key to the browser.
Calls from the browser should hit your Next.js endpoints, not MarketBox directly.
billingId is a UUID you choose to track your end-customer inside your own SaaS. If you are not a SaaS, you can still generate a stable UUID per customer/account in your system and reuse it in calls.
Timezone rule (exact)
Timezone is derived from the service address geolocation (lat/lng). Use a geocoding/timezone provider (commonly Google) to compute an IANA timezone string (e.g., America/Toronto) and pass it into reserved-slot creation.
Idempotency keys (required)
For these calls:
POST /resources/reservedslots
POST /resources/orders
You must send:
Header: idempotency-key: <uuid>
Generate a UUID per “create” attempt, and reuse the same key when retrying the same request.
Project structure (App Router)
Code
app/ booking/ page.tsx # step container (UI) components/ ServicePicker.tsx AddressAutocomplete.tsx SlotPicker.tsx CheckoutForm.tsx api/ marketbox/ services/route.ts # server proxy to GET /resources/services who-is-available/route.ts # server proxy to POST /smart-clusters/who-is-available reservedslots/route.ts # server proxy to POST /resources/reservedslots orders/route.ts # server proxy to POST /resources/orders reservedslots/[id]/route.ts # server proxy to DELETE reserved slotlib/ marketbox/ client.ts # fetch wrapper (server-only)
Server-only MarketBox fetch wrapper
Create lib/marketbox/client.ts:
Code
import "server-only";const BASE_URL = process.env.MARKETBOX_BASE_URL ?? "https://api.gomarketbox.com/v1";const API_KEY = process.env.MARKETBOX_API_KEY;if (!API_KEY) throw new Error("Missing MARKETBOX_API_KEY");export async function mbFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); headers.set("x-api-key", API_KEY); // JSON convenience if (!headers.has("content-type") && init.body) { headers.set("content-type", "application/json"); } const res = await fetch(`${BASE_URL}${path}`, { ...init, headers, cache: "no-store", // booking availability should not be cached }); const text = await res.text(); const data = text ? JSON.parse(text) : null; if (!res.ok) { const error = new Error(`MarketBox ${res.status} ${res.statusText}`); (error as any).details = data; throw error; } return data;}
Step 1 — List services (server)
MarketBox endpoint
GET /resources/services
Next.js route handler
app/api/marketbox/services/route.ts:
Code
import { NextResponse } from "next/server";import { mbFetch } from "@/lib/marketbox/client";export async function GET(req: Request) { const { searchParams } = new URL(req.url); const limit = searchParams.get("limit") ?? "20"; const nextCursor = searchParams.get("nextCursor"); const qs = new URLSearchParams({ limit }); if (nextCursor) qs.set("nextCursor", nextCursor); const data = await mbFetch(`/resources/services?${qs.toString()}`, { method: "GET" }); return NextResponse.json(data);}
Client-side usage (booking page)
Code
const res = await fetch("/api/marketbox/services?limit=50");const services = await res.json();
Build a Next.js App Router booking flow UI using shadcn/ui + Tailwind.Do NOT call MarketBox APIs from the browser. MarketBox uses API-key auth:send header x-api-key: your-api-key from server-side code only (env var MARKETBOX_API_KEY).MarketBox API Open API 3.0 spec: https://docs.gomarketbox.com/api-documentation/1.0.0/mbapispec.jsonAssumptions:- billingId is a UUID the developer uses to track their own customer (SaaS end-customer). It is not a MarketBox-issued identifier.- timezone is derived from the service address geo location (lat/lng) and passed as an IANA timezone string.- No payment collection in this flow (omit paymentDetails).Implement these server routes (Route Handlers) that proxy to MarketBox (base URL https://api.gomarketbox.com/v1):- GET /api/marketbox/services -> GET /resources/services (support limit, nextCursor)- POST /api/marketbox/who-is-available -> POST /smart-clusters/who-is-available?sortBy=leastTravelTime- POST /api/marketbox/reservedslots -> POST /resources/reservedslots (requires idempotency-key header UUID)- POST /api/marketbox/orders -> POST /resources/orders (requires idempotency-key header UUID)- DELETE /api/marketbox/reservedslots/[id] -> DELETE /resources/reservedslots/{reservedSlotId}Client-side booking page (/app/booking/page.tsx):1) Fetch services from /api/marketbox/services and show Card list.2) Address step: Google Places Autocomplete returns street/city/state/country/postalCode + latitude/longitude. Use geo->timezone lookup to get IANA timezone string (e.g., America/Toronto).3) Call /api/marketbox/who-is-available with body fields: billingId, targetGeoLocation [lat,lng], targetServiceId, plus optional startDateAndTime/endDateAndTime/daysOfWeek. Render provider cards + time slots from response timeSlots[].4) On slot select: Call /api/marketbox/reservedslots with body: providerId, serviceId, serviceLocation.address{street,city,state,country,postalCode,latitude,longitude}, timezone, startDateAndTime, reservationTimeoutMins. Send idempotency-key header from the browser to your server route (server forwards it). Save reservedSlotId from response.5) Checkout form (shadcn Input): Collect client{firstName,lastName,email,phone}. On confirm: Call /api/marketbox/orders with body reservedSlotId, serviceId, client (omit paymentDetails). Send idempotency-key header UUID.6) If user cancels/abandons after reserving, call DELETE /api/marketbox/reservedslots/[reservedSlotId].Deliver:- file structure- server-only fetch wrapper using env vars (x-api-key: your-api-key)- React components using shadcn + Tailwind- clear response->next-request variable wiring