API Documentation

Tours is a Next.js 16 (App Router) application. There is no separate API server — every endpoint is a route handler (route.ts) living under src/app/api/. They run on the same host as the storefront and admin panel, so the base URL is simply your site origin.

Base URL (local): http://localhost:3000 Base URL (production): https://your-domain.com

The public/mobile surface is versioned under /api/v1/*. Endpoints are grouped by the audience they serve, each authenticated differently:

Family Path prefix Auth
Public / storefront /api/v1/tours, /api/v1/destinations, … Open (no auth)
Traveler (customer) portal /api/v1/portal/*, /api/v1/customer/* Customer session cookie (JWT) — getCustomerId()
Operator portal /api/v1/operator/* Operator session — getOperatorId()
Influencer portal /api/v1/influencer/* Influencer session
Admin /api/v1/admin/* admin_token cookie (JWT) + RBAC — getSession()
Install wizard /api/install/* First-run only, gated by the installer
Public REST API add-on /api/v1/ext/api/v1/* Bearer API key + scope

Route handler shape

Every route follows the same skeleton: guard auth first, validate + coerce the input server-side, scope every query to the owner, then return a response helper. Never trust a client-supplied id, price or bound.

TS
import { NextRequest } from "next/server";
import sql from "@/lib/db";
import { ok, err, serverErr } from "@/lib/api-helpers";
import { getCustomerId } from "@/lib/customer-auth"; // or getOperatorId / getSession

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  try {
    const me = await getCustomerId();
    if (!me) return err("unauthorized", 401);          // 1) AUTH FIRST
    const b = await req.json();

    const name = String(b.name ?? "").trim().slice(0, 160); // 2) VALIDATE + COERCE
    if (!name) return err("A name is required.");

    const [row] = await sql`
      UPDATE listings SET name = ${name}
      WHERE id = ${Number(b.id)} AND owner_id = ${me}   -- 3) SCOPE TO OWNER (no IDOR)
      RETURNING *`;
    if (!row) return err("Not found", 404);
    return ok(row);
  } catch (e) { return serverErr(e); }
}

Response helpers (@/lib/api-helpers)

Helper Emits Use for
ok(data) 200 with the JSON body Any success (ok(data, status) to change)
err(msg, code) { "error": msg } with status (default 400) Expected failures — surface a message
serverErr(e) { "error": "Server error" } with 500 (+ logs) Unexpected exceptions

Auth guards

The first line of every non-public route reads its session. The guard also tells the OpenAPI generator which audience an endpoint belongs to:

Guard Audience Session
getCustomerId() Customer Customer/traveler session cookie
getOperatorId() Operator Operator session
getSession() Admin admin_token cookie + RBAC

Domain vs. DB naming. The product vocabulary is tour / operator / traveler / departure / inclusion / booking, but the underlying tables keep their original names — a tour is a listings row, an operator a hosts row, a departure a units row. Tour vocabulary appears in URLs and JSON; the real table/column names appear only in SQL.


Conventions

Pagination

Paginated list endpoints return a { data, meta } envelope; detail endpoints return { data }.

JSON
{
  "data": [ /* rows */ ],
  "meta": {
    "total": 128,
    "page": 1,
    "per_page": 12,
    "total_pages": 11,
    "sort": "popular"
  }
}

per_page is clamped server-side (e.g. tours cap at 50, default 12). Common query params on GET /api/v1/tours: page, per_page, sort, q, city, category, inclusions, group_size, min_rating, instant, price_min, price_max, lang.

Money

All monetary values are integer minor units (cents) in the database, in transit, and in API bodies. Format for display with Intl.NumberFormat using the record's currency — never render raw cents. The client may choose options (e.g. deposit vs. full); it may never send the amount — totals are recomputed server-side.

Errors

Expected failures return err(message, status); unexpected ones return serverErr(e). Both use the shape { "error": "…" }.

Code Meaning
400 Bad request / validation error
401 Not signed in (missing/invalid session or API key)
403 Forbidden — missing RBAC permission, missing API-key scope, or unlicensed add-on
404 Resource not found
500 Server error

Authentication

Admin session (admin_token)

Admins sign in at POST /api/v1/admin/auth/login; on success the server sets an HTTP-only admin_token cookie holding a signed JWT. Every /api/v1/admin/* route reads it via getSession(), and write routes additionally enforce a permission (RBAC) per route — there is no global middleware.

Endpoint Purpose
POST /api/v1/admin/auth/login Sign in → sets admin_token cookie
POST /api/v1/admin/auth/logout Clear the admin session
GET /api/v1/admin/auth/me Current admin + roles/permissions
POST /api/v1/admin/auth/change-password Change password (clears session)

Traveler (customer) session

Travelers register and verify via email OTP, then receive a customer session cookie (JWT). All /api/v1/portal/* and /api/v1/customer/* routes require it (getCustomerId()).

Endpoint Purpose
POST /api/v1/portal/auth/register Create an account + send email OTP
POST /api/v1/portal/auth/verify-otp Verify OTP → set session cookie
POST /api/v1/portal/auth/login Sign in → set session cookie
POST /api/v1/portal/auth/logout Clear the session
POST /api/v1/portal/auth/forgot · /reset Password reset via OTP
POST /api/v1/portal/auth/check-email Check whether an email is registered
GET /api/v1/portal/auth/social/:provider/start Begin a social sign-in flow
GET /api/v1/portal/auth/social/:provider/callback Complete a social sign-in flow
GET /api/v1/portal/me Current traveler profile

Auth and public write endpoints are rate-limited via @/lib/rate-limit.

Public REST API add-on (bearer keys)

The REST API & Keys add-on (src/product/addons/api/) exposes a read-only data API authenticated by an API key rather than a session. Keys are issued under Admin → API Keys, stored only as a SHA-256 hash (just the display prefix is retained), and carry scopes. Send the raw key either way:

HTTP
Authorization: Bearer <your-api-key>
X-API-Key:     <your-api-key>

Each endpoint requires a scope; a key missing the scope is rejected with 403. Available scopes: tours, customers, bookings, orders (or * for all).


Interactive API docs (OpenAPI + Scalar)

Tours ships an admin-gated Scalar reference whose OpenAPI spec is auto-generated from the route handlers — so a mobile-app dev always has an accurate, current API surface.

Resource URL Notes
Scalar reference /api/docs/v1 Scalar UI over the generated spec. Linked from the admin sidebar.
Raw OpenAPI JSON /api/openapi.json The generated spec the reference page consumes.

Both are admin-gated (getSession()): the reference page redirects to /admin/login and the spec returns 401 without an admin session, so the spec can't be scraped publicly. Both are dynamic = "force-dynamic" and sent noindex, nofollow.

Regenerate the spec after adding or renaming routes:

Terminal
npm run gen:openapi

The generator (scripts/gen-openapi.ts) scans every src/app/api/**/route.ts, derives each URL ([id]{id}, route groups dropped), the exported HTTP methods, and the audience from which auth guard the file calls (getSession → Admin, getCustomerId → Customer, getOperatorId → Operator). It groups endpoints into mobile-app surfaces, excludes internal areas (admin, install, license, plugins, ext, cron, chat, analytics and the docs routes themselves), and enriches high-traffic flows (tours browse, auth, bookings/checkout) with real params and request/response bodies via curated OVERRIDES / SCHEMAS.

Terminal
# Fetch the spec (needs an admin session cookie)
curl --cookie "admin_token=<your-session-jwt>" \
  http://localhost:3000/api/openapi.json -o tours-openapi.json

Endpoint reference

The tables below list real route handlers. List endpoints accept query params; :slug / :public_id / :id are path params.

Tours (public catalog)

Method Path Purpose
GET /api/v1/tours List published tours (paginate / sort / filter)
GET /api/v1/tours/availability Departure availability for a tour
GET /api/v1/tours/quote Price quote for a departure + party size
GET /api/v1/tours/:slug Tour detail (itinerary, inclusions, departures)
GET/POST /api/v1/tours/:slug/reviews List / submit tour reviews
GET /api/v1/destinations List destinations
GET /api/v1/inclusions List inclusion items
GET /api/v1/extras List bookable extras / add-ons
GET /api/v1/reviews Recent published reviews

Bookings & checkout

Method Path Purpose
POST /api/v1/bookings Create a booking
POST /api/v1/bookings/checkout Start checkout for a booking
GET /api/v1/bookings/payment-methods Enabled payment methods
POST /api/v1/bookings/pay Pay / initiate a gateway charge
POST /api/v1/bookings/verify Verify a gateway payment
POST /api/v1/coupons/validate Validate a coupon code

Traveler portal (/api/v1/customer/*, /api/v1/portal/*)

Method Path Purpose
GET /api/v1/customer/dashboard Dashboard summary
GET /api/v1/customer/bookings The traveler's bookings
GET /api/v1/customer/bookings/:public_id A single booking
GET /api/v1/customer/payments Payment history / invoices
GET /api/v1/customer/wishlist Saved tours
GET /api/v1/customer/reviews The traveler's reviews
GET /api/v1/customer/conversations Messages with operators
GET /api/v1/customer/notifications Activity notifications
GET /api/v1/customer/support Support requests
GET/PUT /api/v1/portal/profile Read / update profile
POST /api/v1/portal/change-password Change password

Content & config (public)

Method Path Purpose
GET /api/v1/brand Storefront branding / settings
GET /api/v1/pages · /blogs · /faqs · /gallery · /testimonials CMS content
GET /api/v1/countries · /currencies · /languages · /locations · /i18n Geo / localization
POST /api/v1/forms Contact / enquiry / newsletter submissions

Admin (session + RBAC)

Method Path Purpose
GET /api/v1/admin/dashboard KPIs + revenue series
GET/POST /api/v1/admin/tours · /:id Manage tours (approval flow)
GET/POST /api/v1/admin/inclusions · /extras · /operators Catalog & operators
GET /api/v1/admin/customers · /:id List / fetch customers
GET/POST /api/v1/admin/bookings · /:id List / manage bookings
GET/POST /api/v1/admin/coupons Manage coupons
GET/POST /api/v1/admin/reviews Moderate reviews
GET/POST /api/v1/admin/pages · /blogs · /faqs · /gallery CMS content
GET/POST /api/v1/admin/settings/:section Read / write a settings section

Public REST API add-on (/api/v1/ext/api/v1/*)

The optional add-on is dispatched through the catch-all /api/v1/ext/[addon]/[...path] route into the add-on's handler map. Every endpoint is read-only and requires the scope shown.

Method Path Scope Purpose
GET /api/v1/ext/api/openapi — (public) The add-on's OpenAPI 3.1 spec
GET /api/v1/ext/api/v1/tours tours List published tours
GET /api/v1/ext/api/v1/tours/:slug tours Get a tour by slug
GET /api/v1/ext/api/v1/customers customers List members
GET /api/v1/ext/api/v1/bookings bookings List bookings
GET /api/v1/ext/api/v1/orders orders List orders (transactions)

List endpoints accept limit (≤ 100, default 50) and offset (default 0) and return an envelope:

JSON
{ "data": [ /* rows */ ], "limit": 50, "offset": 0 }

Request example

Terminal
curl "https://your-domain.com/api/v1/ext/api/v1/tours?limit=10" \
  -H "Authorization: Bearer <your-api-key>"
JSON
{
  "data": [
    {
      "id": 42,
      "slug": "amalfi-coast-day-trip",
      "title": "Amalfi Coast Day Trip",
      "duration_days": 1,
      "price_cents": 8900,
      "currency": "EUR",
      "status": "published",
      "rating_avg": 4.7,
      "review_count": 128
    }
  ],
  "limit": 10,
  "offset": 0
}

The same key also works via the X-API-Key header:

Terminal
curl "https://your-domain.com/api/v1/ext/api/v1/bookings" \
  -H "X-API-Key: <your-api-key>"

A key missing the endpoint's scope is rejected:

JSON
{ "error": "This key is missing the 'orders' scope." }

© CreativeCape Solutions · creative-cape.com · support@creative-cape.com