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:3000Base 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.
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
listingsrow, an operator ahostsrow, a departure aunitsrow. 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 }.
{
"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:
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:
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.
# 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:
{ "data": [ /* rows */ ], "limit": 50, "offset": 0 }
Request example
curl "https://your-domain.com/api/v1/ext/api/v1/tours?limit=10" \
-H "Authorization: Bearer <your-api-key>"
{
"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:
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:
{ "error": "This key is missing the 'orders' scope." }
© CreativeCape Solutions · creative-cape.com · support@creative-cape.com