TripStack API Reference
REST surface for the two booking journeys. Machine-readable contract: /openapi.yaml (OpenAPI 3) — your RestAssured / contract source of truth.
Getting started
Every request except search and login needs a bearer token. Log in, then send
Authorization: Bearer <token> (15-minute TTL).
snake_case; booking objects are
camelCase. PNR = TS-<empId>-<seq>. The error envelope is
not uniform — each endpoint documents the exact shape it returns.text/plain or omits Content-Type
(RestAssured's .body(json) without .contentType(JSON)). Setting
application/json is still good practice — but a missing header will not fail you.Database (read-only) — for your JDBC assertions
This is the same database the live site writes to. After your API call creates a booking, you can assert the real row with JDBC — that is the DB pillar.
| You can read | bookings, seats, flights, buses, payments, pnr_sequences, airports, airlines, operators, and employees (minus password_hash) |
|---|---|
| You cannot | write anything (INSERT/UPDATE/DELETE are denied), read employees.password_hash, or read flags |
Example — assert the booking your API just created
SELECT pnr, emp_id, state, seat_ids, amount_paise FROM bookings WHERE pnr = 'TS-1004-0007'; -- your own namespace: emp_id must equal your empId
Auth
POST/api/auth/loginget a bearer token
Request body
{
"email": "dave@tripstack.test",
"password": "Password@123", // secret — request-only, never echoed
"ttlSeconds": 2 // OPTIONAL 1–900: short-lived token (see below)
}
"ttlSeconds": 2 (1–900) to mint a
deliberately short-lived token, wait ~3 s, then call any authenticated endpoint →
401 unauthorized. That makes the expired-token negative testable in
seconds instead of waiting out the full window. Omit it for a normal 15-minute token.200 response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // secret (JWT, 15 min)
"empId": "1004",
"role": "traveller",
"displayName": "Dave Dutta"
}
Errors
400 invalid_request malformed body ·
401 invalid_credentials unknown email or wrong password (same code — no user enumeration).
GET/api/auth/mecurrent identity
200 response
{ "empId": "1004", "role": "traveller",
"email": "dave@tripstack.test", "displayName": "Dave Dutta" }
Errors
401 unauthorized missing / invalid / expired token.
GET/api/auth/admin-pingadmin-only probe
Drives the privilege-escalation negative: a viewer/traveller token gets 403.
200 response
{ "ok": true, "empId": "1001", "role": "admin" }
Errors
401 unauthorized · 403 forbidden
→ { "error":"forbidden", "required":["admin"], "role":"traveller" }
Flights
GET/api/flightssearch flights
Query parameters
| name | req? | notes |
|---|---|---|
from | yes | origin code, e.g. DEL |
to | yes | destination code, e.g. BLR |
date | no | echoed back (YYYY-MM-DD) |
pax | no | passenger count (default 1) |
class | no | economy|business (also accepts cls) |
sort | no | price|airline|departure |
airline | no | filter by airline code |
200 response
{
"from": "DEL", "to": "BLR", "date": "2026-08-01", "pax": 1,
"class": "economy", "count": 48,
"flights": [
{
"id": "FL-DELBLR-51",
"airline_code": "UK", "airline_name": "Vistara", "flight_no": "UK-483",
"origin": "DEL", "dest": "BLR", "dep_time": "05:57", "arr_time": "08:18",
"base_fare_paise": 619700, "tax_paise": 111546, "total_paise": 731246,
"fare_display": "₹7,312" // display only — assert on total_paise
}
]
}
Errors
400 → { "error":"from and to are required" }
GET/api/flights/{id}/seatscabin seat map
Path id = flight id (e.g. FL-DELBLR-51). Optional ?class=. Row/column cabin grid — a locator surface.
occupied: true means the seat is genuinely unavailable —
pre-occupied, already booked, or currently held by anyone. A seat shown
occupied: false is holdable; so if you pick an available seat you will not get a
surprise 409 SEAT_CONFLICT. Re-fetch the seat map after someone books to see it update.200 response
{
"flight_id": "FL-DELBLR-51",
"layout": "cabin",
"cols": ["A","B","C","D","E","F"],
"total": 156, "available": 141,
"rows": [
{
"row": 12, "exit_row": false,
"seats": [
{ "seat_id": "12A", "row": 12, "col": "A",
"window": true, "aisle": false, "middle": false,
"exit_row": false, "occupied": false }
]
}
]
}
Errors
404 → { "error":"flight not found" }
Buses
GET/api/busessearch buses
Query: from, to, date (all optional; echoed back).
200 response
{
"from": "BLR", "to": "HYD", "date": "2026-08-01", "count": 6,
"buses": [
{
"id": "BUS-BLRHYD-3",
"operator": "VRL", "operatorName": "VRL Travels", "kind": "sleeper",
"origin": "BLR", "dest": "HYD", "depTime": "20:30", "arrTime": "07:45",
"baseFarePaise": 129000, "taxPaise": 6450, "farePaise": 135450,
"seatsLeft": 22
}
]
}
GET/api/buses/{id}/seatsdeck seat map
Path id = bus id. Two-deck layout — structurally different from the flight grid.
200 response
{
"busId": "BUS-BLRHYD-3", "operator": "VRL", "layout": "deck",
"decks": {
"lower": [ { "seatId": "L3", "deck": "lower", "kind": "sleeper", "state": "available" } ],
"upper": [ { "seatId": "U5", "deck": "upper", "kind": "sleeper", "state": "occupied" } ]
}
}
Errors
404 → { "error":"bus_not_found" }
Bookings
POST /bookings (HELD) → /{id}/pay
(PAYMENT_PENDING) → /{id}/confirm (CONFIRMED + PNR) → /{id}/cancel
(CANCELLED→REFUNDED). All namespace-scoped by your empId.{id} accepts either the booking id or a PNR.
/pay, /confirm and /cancel resolve either the
opaque booking id (UUID) or a TS-<empId>-<seq> PNR. That's how you
run the “cancel another employee's booking” negative: call
POST /api/bookings/TS-<someone-else>-0001/cancel → 403 CROSS_NAMESPACE
(an unknown PNR gives 404 NOT_FOUND, so use a real one).POST/api/bookingsplace a seat hold
Request body
{
"journeyType": "flight", // "flight" | "bus"
"inventoryId": "FL-DELBLR-51", // flights.id or buses.id
"seatIds": ["12A", "12B"], // 1..6 seats
"refundable": true, // optional
"holdTtlSec": 120 // optional (testing hook for the expiry negative)
}
201 response — the Booking object
{
"id": "adec25fe-dfd4-4b71-9d3b-d4b98637c0fc", // opaque id (used in /pay,/confirm,/cancel)
"pnr": null, // null until CONFIRMED
"empId": "1004",
"journeyType": "flight", "inventoryId": "FL-DELBLR-51",
"state": "HELD",
"seatIds": ["12A","12B"],
"amountPaise": 1462492,
"refundable": true,
"holdExpiresAt": "2026-07-17T05:15:35.869Z"
}
Errors
400 BAD_REQUEST (+ issues) ·
400 INVALID_SEATS empty or >6 seats · 401 unauthorized ·
409 SEAT_CONFLICT seat already taken.
POST/api/bookings/{id}/paycharge (HELD → PAYMENT_PENDING)
{}.
If you post card data to practise masking, treat it as secret.200 response
{ ...Booking..., "state": "PAYMENT_PENDING", "pnr": null }
Errors (fault-driven — diagnose which one)
| fault flag | HTTP | error |
|---|---|---|
| payment_402 | 402 | GATEWAY_DECLINE |
| payment_500 | 502 | GATEWAY_ERROR |
| conn_reset | 502 | GATEWAY_UNAVAILABLE |
| payment_timeout | 504 | GATEWAY_TIMEOUT |
| (breaker tripped) | 503 | BREAKER_OPEN |
Also 401 · 403 CROSS_NAMESPACE · 404 NOT_FOUND · 409 HOLD_EXPIRED.
POST/api/bookings/{id}/confirmmint PNR (→ CONFIRMED)
No body.
200 response
{ ...Booking..., "state": "CONFIRMED", "pnr": "TS-1004-0022" }
Errors
401 · 403 CROSS_NAMESPACE · 404 NOT_FOUND ·
409 INVALID_STATE (not PAYMENT_PENDING) or HOLD_EXPIRED.
POST/api/bookings/{id}/cancelcancel + refund
No body. Refundable → CANCELLED then REFUNDED.
200 response
{ ...Booking..., "state": "REFUNDED" }
Errors
401 · 403 CROSS_NAMESPACE · 404 NOT_FOUND ·
409 CANCEL_CANCELLED (cancel-twice) · 422 NON_REFUNDABLE.
GET/api/bookingslist your bookings
Returns a bare JSON array (no wrapper) of only your namespace's bookings — the primary isolation assertion.
200 response
[
{ "id": "...", "pnr": "TS-1004-0022", "empId": "1004", "journeyType": "flight",
"inventoryId": "FL-DELBLR-51", "state": "CONFIRMED", "seatIds": ["12A"],
"amountPaise": 731246, "refundable": true, "holdExpiresAt": "..." }
]
Errors
401 unauthorized
GET/api/bookings/{pnr}read one by PNR (BOLA)
Returns the booking for pnr only if it is yours. Another employee's
PNR → 403 CROSS_NAMESPACE (the cross-namespace read negative).
200 response
{ "id":"...", "pnr":"TS-1004-0022", "empId":"1004", "state":"CONFIRMED", ... }
Errors
401 · 403 CROSS_NAMESPACE → { "error":"CROSS_NAMESPACE", "message":"CROSS_NAMESPACE" } · 404 NOT_FOUND
Ops
POST/api/resetreset your namespace
Clears your bookings and releases held seats so you can re-run cleanly. Defaults to your
own empId; ?emp=<id> targets another (admin only).
200 response
{ "emp": "1004", "purged": 3 }
Errors
401 · 403 CROSS_NAMESPACE (non-admin targeting another emp).
Error envelopes
The shape depends on where the error is raised — assert accordingly:
| source | shape |
|---|---|
| simple guards / auth | { "error": "unauthorized" } — a code, or on flight/bus 400/404 a short sentence |
| role check (403) | { "error":"forbidden", "required":["admin"], "role":"traveller" } |
| booking state machine | { "error":"HOLD_EXPIRED", "message":"...", "state":"HELD" } |
| hold body invalid | { "error":"BAD_REQUEST", "issues":[...] } |
Booking codes:
BAD_REQUESTINVALID_SEATSSEAT_CONFLICTHOLD_EXPIREDINVALID_STATENOT_FOUNDCROSS_NAMESPACEGATEWAY_DECLINEGATEWAY_ERRORGATEWAY_TIMEOUTGATEWAY_UNAVAILABLEBREAKER_OPENCANCEL_CANCELLEDNON_REFUNDABLE
Full machine-readable contract: /openapi.yaml.