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).

curl -X POST https://tripstack.doomple.com/api/auth/login \ -H 'content-type: application/json' \ -d '{"email":"dave@tripstack.test","password":"Password@123"}'
Conventions. Money is integer paise (÷100 = INR). Flight/bus reference data is snake_case; booking objects are camelCase. PNR = TS-<empId>-<seq>. The error envelope is not uniform — each endpoint documents the exact shape it returns.
Content-Type is forgiving. The JSON API parses your body even if the client sends 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.

jdbc:postgresql://34.180.8.149:5432/tripstack user : tripstack_ro password : TripStackRO@2026
You can readbookings, seats, flights, buses, payments, pnr_sequences, airports, airlines, operators, and employees (minus password_hash)
You cannotwrite 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
The credentials are shared and read-only — treat the password as public test data, not a secret. Your own login password and bearer token are the secrets.
AuthFlightsBuses BookingsOpsDatabaseError codes

Auth

POST/api/auth/loginget a bearer token
public

Request body

{
  "email": "dave@tripstack.test",
  "password": "Password@123",     // secret — request-only, never echoed
  "ttlSeconds": 2                 // OPTIONAL 1–900: short-lived token (see below)
}
Testing the expired-token 401. Tokens live 15 minutes by default. Pass "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
bearer

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
bearer role: admin

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
public

Query parameters

namereq?notes
fromyesorigin code, e.g. DEL
toyesdestination code, e.g. BLR
datenoechoed back (YYYY-MM-DD)
paxnopassenger count (default 1)
classnoeconomy|business (also accepts cls)
sortnoprice|airline|departure
airlinenofilter 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
public

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
public

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
public

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

Lifecycle: 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/cancel403 CROSS_NAMESPACE (an unknown PNR gives 404 NOT_FOUND, so use a real one).
POST/api/bookingsplace a seat hold
bearer

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)
bearer
This endpoint ignores the request body — the gateway outcome is driven by your per-employee fault flag, not the payload. Send {}. 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 flagHTTPerror
payment_402402GATEWAY_DECLINE
payment_500502GATEWAY_ERROR
conn_reset502GATEWAY_UNAVAILABLE
payment_timeout504GATEWAY_TIMEOUT
(breaker tripped)503BREAKER_OPEN

Also 401 · 403 CROSS_NAMESPACE · 404 NOT_FOUND · 409 HOLD_EXPIRED.

POST/api/bookings/{id}/confirmmint PNR (→ CONFIRMED)
bearer

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
bearer

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
bearer

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)
bearer

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
bearer

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:

sourceshape
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.