• English
  • API & Data

    This page documents the API client layer, all backend endpoints, error handling, the Proof-of-Work engine, client-side storage, and utility libraries. For i18n details, see Architecture.


    API Client

    Source: src/lib/api/client.ts

    The HTTP client is a thin wrapper over fetch. Three exported functions — apiGet, apiPost, apiDelete — delegate to a shared request<T>() helper:

    1. Constructs the full URL via apiUrl(path) (see config.ts).
    2. Sends the request with Content-Type: application/json.
    3. On non-2xx: throws ApiError with a typed code (never returns error bodies).
    4. On success: returns the parsed JSON body.

    Key detail: parseBody first reads the response as text, then attempts JSON.parse. If parsing fails, it returns undefined — this handles empty 204-style responses gracefully.


    API Endpoints

    Source: src/lib/api/booking.ts

    All paths are relative to /v1/book. The single URL construction point is src/lib/config.ts (apiUrl()).

    Booking Flow

    FunctionMethodPathPurpose
    getServices(slug)GET/{slug}/servicesLoad service catalog
    getProfile(slug)GET/{slug}/profileSalon profile: timezone, currency, hours, OTP methods, contact info
    getAvailability(slug, serviceIds, date)GET/{slug}/availability?serviceIds=...&date=...Available slots for a given date
    getPow()POST/powRequest a PoW challenge
    createHold(slug, body)POST/{slug}/holdsReserve a slot before identity verification
    releaseHold(slug, holdId)DELETE/{slug}/holds/{holdId}Explicitly release a hold
    releaseHoldBeacon(slug, holdId)DELETE/{slug}/holds/{holdId}Best-effort release via fetch({ keepalive: true }) on page hide
    requestOtp(slug, body)POST/{slug}/otp/requestSend OTP via email or SMS
    verifyOtp(slug, body)POST/{slug}/otp/verifyVerify OTP code, returns consumerToken
    createBooking(slug, body)POST/{slug}/bookingsFinalize booking with consumer token

    Manage Flow

    FunctionMethodPathPurpose
    getManage(token)GET/manage/{token}Load booking details for management
    getManageAvailability(token, date)GET/manage/{token}/availability?date=...Available slots for rescheduling
    cancelBooking(token)POST/manage/{token}/cancelCancel the booking
    rescheduleBooking(token, newStart)POST/manage/{token}/rescheduleMove booking to a new time

    Key Response Types

    • ProfileResponsetimezone, currency (fallback USD), hoursJson, otpMethods (['sms','email']), logoUrl, name, address, phone, email.
    • AvailabilityResponsedate, slots[] (UTC ISO strings), timezone.
    • ManageViewstatus, start, end, services[], timezone, currency, canCancel, canReschedule.
    • CreateBookingResponsebookingId, status: 'pending', holdExpiresAt, manageToken.
    • PowChallengechallenge, signature, difficultyBits.

    Error Handling

    Source: src/lib/api/errors.ts

    All API errors are thrown as ApiError instances with a typed code property. Always branch on ApiError.code, not the HTTP status.

    Error Code Categories

    Body-driven codes (read from the response body's error field, snake_case normalized):

    CodeTrigger
    slot_unavailableChosen time was taken
    out_of_horizonTime is outside bookable window
    not_actionableBooking can't be modified (cancel/reschedule)
    hold_expiredHold TTL elapsed
    too_many_holdsRate limit on concurrent holds
    invalid_proof_of_workPoW solution rejected
    services_requiredNo services selected
    too_many_servicesExceeded service cap
    duplicate_serviceSame service picked twice
    invalid_service_idSelected service no longer exists

    Status-driven codes (inferred from HTTP status when no body error matches):

    StatusCode
    429rate_limited
    404not_bookable
    401unauthorized
    503send_budget_exhausted
    400bad_request
    Otherunknown

    The KNOWN_BODY_CODES set in errors.ts is the authoritative list of which error strings the backend sends in the body. Adding a new backend error code requires updating both the ApiErrorCode union and KNOWN_BODY_CODES.


    Proof-of-Work Engine

    Source: src/lib/pow/

    PoW protects the booking API from abuse by requiring clients to solve a hashcash challenge before calling sensitive endpoints (createHold, requestOtp).

    Pipeline

    1. PowProvider (PowProvider.tsx) — React context that wraps the booking wizard. Caches solved tokens for 240s (server TTL is ~300s). Deduplicates concurrent callers with a single in-flight Promise.
    2. getPow() — Fetches a { challenge, signature, difficultyBits } triple from POST /pow.
    3. solvePowAuto (solveAuto.ts) — Dispatches solving to a Web Worker if available, falling back to the synchronous solvePow on the main thread.
    4. solver.worker.ts — Web Worker entry point. Receives { challenge, difficultyBits }, runs solvePow, posts back { nonce }.
    5. solvePow (solve.ts) — Brute-force solver: iterates nonce integers, computes SHA-256(challenge|nonce), checks leading zero bits against difficultyBits.
    6. leadingZeroBits (leadingZeroBits.ts) — Bit-counting utility for the hash output.

    Usage

    Components call usePowToken().getFreshPow(force?) to get a { challenge, signature, nonce } triple. The wizard pre-warms a token during pickTime so that holding and verifyContact don't block on solving. On invalid_proof_of_work or bad_request rejection, callers retry once with getFreshPow(force: true).


    Client-Side Storage

    Source: src/lib/storage.ts

    A single localStorage key (salon.bookings) stores a JSON array of SavedBooking objects:

    { bookingId, manageToken, slug, savedAt }
    • saveBooking(b) — appends to the array.
    • listBookings() — reads the array; returns [] on parse failure or missing key.

    Saved bookings are used on the HomePage to show a "My bookings" list with links to /manage/:token. This is purely a convenience feature — the manage token is the sole credential; localStorage is never used for auth.


    Contact Validation

    Source: src/lib/contact/validate.ts

    Supports two contact channels:

    • Email — basic regex validation (isValidEmail).
    • Phone — US/CA NANP only. Functions:
      • normalizeNanp(s) — strip to 10-digit national number, dropping leading country-code 1.
      • isValidNanpPhone(s) — validates area code and exchange first digits are 2–9 (NXX-NXX-XXXX).
      • formatNanpInput(raw) — progressive (555) 223-4567 input mask.
      • formatNanpDisplay(s) — display format for stored numbers.

    Time Utilities

    Calendar (src/lib/time/calendar.ts)

    Pure functions operating on YYYY-MM-DD date strings (never UTC instants, never browser-local zone):

    • formatYMD, parseYMD — convert between Date and YYYY-MM-DD.
    • addDays(ymd, n) — arithmetic on calendar dates.
    • upcomingDays(startYmd, count) — generate a list of consecutive dates.
    • monthGrid(year, month0) — 42-cell Monday-first calendar grid.

    Business Hours (src/lib/time/hours.ts)

    Parses hoursJson (JSON object keyed by ISO weekday 1=Mon..7=Sun, mapping to { open, close } in minutes since midnight):

    • parseHours(json) — parse and validate the JSON string.
    • weeklyHours(parsed) — normalize into a 7-element DayHours[] array.
    • formatMinutes(min) — format minutes-since-midnight as 12-hour time.

    Timezone Display (src/lib/time/tz.ts)

    Converts UTC ISO instants to the salon's IANA timezone for display. All output uses US conventions:

    • Times: 12-hour format (9:00 AM).
    • Dates: Jun 21, 2026.
    • formatSlotTime(utcIso, timezone) — time only.
    • formatInTz(utcIso, timezone, opts) — time with optional date prefix (e.g., Jun 21, 2026 · 9:00 AM).

    Service Grouping

    Source: src/lib/services/group.ts

    Groups a flat ServiceItem[] list by categoryId. Unclassified services (categoryId: null) appear first as "Featured." Remaining groups are sorted by categorySortOrder (ascending, nulls last). This is used by PickService to render the categorized service list with a <ChipBar> filter.


    Notes for Future Agents

    • Adding a new API endpoint: Add a typed function in src/lib/api/booking.ts. Use apiGet/apiPost/apiDelete from client.ts. Do not construct URLs manually — always use apiUrl(path).
    • Adding a new error code: Add the string to ApiErrorCode in errors.ts. If the backend sends it in the response body, also add it to KNOWN_BODY_CODES.
    • Changing the API base URL: Only src/lib/config.ts constructs URLs. No other file should reference import.meta.env.VITE_BOOKING_BASE_URL directly.
    • PoW solving changes: The solver intentionally runs on the main thread as a fallback. If changing the Web Worker, test both worker and non-worker paths (typeof Worker === 'undefined' in environments that lack it).
    • LocalStorage: The salon.bookings key is the only data stored client-side. It's never used for authentication — the manage token from the URL is the sole credential.