{
  "openapi": "3.0.3",
  "info": {
    "title": "JetWize Partner API",
    "version": "1.0.0",
    "description": "B2B flight Partner API. Authenticate every request with the `X-API-Key` header (`jwz_test_…` for sandbox, `jwz_live_…` for production). Sandbox returns deterministic mock inventory and simulates pay/issue/refund — no real money or supplier calls. Import this spec into Postman/Insomnia, set the `apiKey` variable, and run search → book → pay → ticket."
  },
  "servers": [
    { "url": "https://api.jetwize.com/api/v1/partner/v1", "description": "Production base (use jwz_live_ key; sandbox keys are isolated)" }
  ],
  "security": [ { "ApiKey": [] } ],
  "components": {
    "securitySchemes": {
      "ApiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "jwz_test_… (sandbox) or jwz_live_… (production)" }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "object", "properties": {
          "code": { "type": "string", "example": "INVALID_API_KEY" },
          "message": { "type": "string" },
          "traceId": { "type": "string" }
        } } }
      },
      "FlightSegment": {
        "type": "object",
        "description": "A single flight leg within an offer (direct flight = 1 segment, 1-stop = 2 segments).",
        "required": ["flightNumber", "carrier", "carrierName", "origin", "destination", "departureTime", "arrivalTime", "duration", "aircraft", "cabinClass", "baggageAllowance"],
        "properties": {
          "flightNumber": { "type": "string", "description": "IATA flight number, e.g. JT-123", "example": "JT-123" },
          "carrier": { "type": "string", "description": "IATA 2-letter marketing carrier code", "example": "JT" },
          "carrierName": { "type": "string", "description": "Full airline name", "example": "Lion Air" },
          "origin": { "type": "string", "description": "Departure airport IATA code", "example": "CGK" },
          "destination": { "type": "string", "description": "Arrival airport IATA code", "example": "DPS" },
          "departureTime": { "type": "string", "format": "date-time", "description": "UTC ISO 8601 departure time", "example": "2026-07-15T06:00:00.000Z" },
          "arrivalTime": { "type": "string", "format": "date-time", "description": "UTC ISO 8601 arrival time", "example": "2026-07-15T07:20:00.000Z" },
          "duration": { "type": "integer", "description": "Flight duration in minutes", "example": 80 },
          "aircraft": { "type": "string", "description": "Aircraft type / ICAO designator", "example": "B738" },
          "terminal": { "type": "string", "description": "Departure terminal (if known)", "example": "2D" },
          "gate": { "type": "string", "description": "Departure gate (if known)", "example": "G5" },
          "cabinClass": { "type": "string", "enum": ["ECONOMY", "PREMIUM_ECONOMY", "BUSINESS", "FIRST"], "description": "Cabin class for this segment" },
          "baggageAllowance": { "type": "string", "description": "Human-readable checked-baggage allowance, e.g. '20kg' or '1 pcs / 23kg'", "example": "20kg" },
          "mealService": { "type": "boolean", "description": "True if in-flight meal is included" },
          "departureTz": { "type": "string", "description": "IANA timezone of departure airport, e.g. 'Asia/Jakarta'", "example": "Asia/Jakarta" },
          "arrivalTz": { "type": "string", "description": "IANA timezone of arrival airport", "example": "Asia/Makassar" }
        }
      },
      "FlightOffer": {
        "type": "object",
        "description": "A priced flight offer returned by the search endpoint. Use `id` as the `offerId` when creating a booking or calling price-check.",
        "required": ["id", "segments", "totalDuration", "stops", "basePrice", "tax", "totalPrice", "currency", "seatsAvailable", "expiresAt"],
        "properties": {
          "id": { "type": "string", "description": "Opaque offer token — treat as an opaque string and send it back verbatim on booking / price-check. Do not parse it.", "example": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6" },
          "segments": {
            "type": "array",
            "description": "Ordered list of flight legs. Direct = 1 item; 1-stop = 2 items.",
            "items": { "$ref": "#/components/schemas/FlightSegment" }
          },
          "totalDuration": { "type": "integer", "description": "Total journey duration in minutes (including layover time)", "example": 80 },
          "stops": { "type": "integer", "description": "Number of stops (0 = direct)", "example": 0 },
          "basePrice": { "type": "number", "description": "Net base fare (NTA) before tax, per booking (all pax combined)", "example": 850000 },
          "tax": { "type": "number", "description": "Total taxes and surcharges (all pax combined)", "example": 150000 },
          "totalPrice": { "type": "number", "description": "NTA total (basePrice + tax). The sell price shown to the end passenger = totalPrice + platformMarkup + bookingFee + issuanceFee.", "example": 1000000 },
          "currency": { "type": "string", "description": "ISO 4217 currency code", "example": "IDR" },
          "seatsAvailable": { "type": "integer", "description": "Seats left at this fare (may be capped at 9)", "example": 7 },
          "expiresAt": { "type": "string", "format": "date-time", "description": "When this cached offer expires — re-search if stale", "example": "2026-07-04T09:05:00.000Z" },
          "fareBrand": { "type": "string", "description": "Fare family / branded fare name, e.g. 'Economy Basic', 'Flexi'. Present when a carrier returns multiple brands for the same flight.", "example": "Economy Basic" },
          "refundable": { "type": "boolean", "description": "Whether this fare permits a refund. Absent = unknown." },
          "changeable": { "type": "boolean", "description": "Whether this fare permits a date change / reschedule. Absent = unknown." },
          "platformMarkup": { "type": "number", "description": "Platform markup amount added on top of NTA (all-pax, IDR). Included in the displayed sell price.", "example": 50000 },
          "bookingFee": { "type": "number", "description": "Per-booking platform fee for the searched passenger count (IDR).", "example": 10000 },
          "issuanceFee": { "type": "number", "description": "Per-supplier ticket-issuance fee charged on top of the booking fee (IDR, all-pax).", "example": 5000 }
        }
      },
      "FlightSearchResponse": {
        "type": "object",
        "description": "Response envelope for POST /flights/search.",
        "required": ["results", "count", "sandbox"],
        "properties": {
          "results": {
            "type": "array",
            "description": "List of matching flight offers, sorted by totalPrice ascending by default.",
            "items": { "$ref": "#/components/schemas/FlightOffer" }
          },
          "count": { "type": "integer", "description": "Total number of offers returned", "example": 12 },
          "sandbox": { "type": "boolean", "description": "True when the request was authenticated with a sandbox (jwz_test_) key", "example": true }
        }
      },
      "PriceCheckResult": {
        "type": "object",
        "description": "Result of POST /flights/price-check. Confirms whether the offer price is still valid before creating a booking.",
        "required": ["offerId", "priceChanged", "currentPrice", "currency", "stillAvailable"],
        "properties": {
          "offerId": { "type": "string", "description": "The offer ID that was checked", "example": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6" },
          "priceChanged": { "type": "boolean", "description": "True if the current price differs from the cached search price", "example": false },
          "previousPrice": { "type": "number", "description": "The cached price from search (present only when priceChanged=true)", "example": 1000000 },
          "currentPrice": { "type": "number", "description": "Live price confirmed by the supplier at this moment", "example": 1000000 },
          "currency": { "type": "string", "description": "ISO 4217 currency code", "example": "IDR" },
          "stillAvailable": { "type": "boolean", "description": "False if the fare is sold out or no longer bookable", "example": true },
          "baggageAllowance": { "type": "string", "description": "Checked-baggage allowance resolved at verify time (some suppliers only surface this here, not at search). E.g. '1 pcs / 15kg'.", "example": "20kg" }
        }
      },
      "Passenger": {
        "type": "object",
        "description": "A passenger on a booking. `ticketNumber` is populated after the booking reaches ISSUED status.",
        "required": ["title", "firstName", "lastName", "dateOfBirth", "nationality", "type"],
        "properties": {
          "id": { "type": "string", "description": "Passenger record ID", "example": "clxpax1abcdef" },
          "title": { "type": "string", "enum": ["MR", "MRS", "MS", "MSTR"], "description": "Salutation" },
          "firstName": { "type": "string", "example": "Budi" },
          "lastName": { "type": "string", "description": "Family name. May be empty for single-name (mononym) passengers.", "example": "Santoso" },
          "dateOfBirth": { "type": "string", "format": "date", "description": "Date of birth (YYYY-MM-DD)", "example": "1990-01-15" },
          "nationality": { "type": "string", "description": "ISO 3166-1 alpha-2 country code", "example": "ID" },
          "passportNo": { "type": "string", "description": "Passport number (PII — encrypted at rest)" },
          "passportExpiry": { "type": "string", "format": "date", "description": "Passport expiry date (YYYY-MM-DD)" },
          "type": { "type": "string", "enum": ["ADULT", "CHILD", "INFANT"], "description": "Passenger type" },
          "seatNumber": { "type": "string", "description": "Assigned seat number (if seat selected)", "example": "12A" },
          "ticketNumber": { "type": "string", "description": "Airline ticket number — populated once the booking is ISSUED", "example": "997-1234567890" }
        }
      },
      "Booking": {
        "type": "object",
        "description": "A flight booking record. `status` follows the state machine: DRAFT → PENDING_PAYMENT → PAID → TICKETING → ISSUED. Terminal states: FAILED, CANCELLED, REFUNDED, HOLD_EXPIRED, HOLD_LOST.",
        "required": ["id", "bookingCode", "status", "totalAmount", "currency", "expiresAt", "passengers"],
        "properties": {
          "id": { "type": "string", "description": "Internal booking ID (CUID)", "example": "clxbkg1abcdef" },
          "bookingCode": { "type": "string", "description": "Human-readable booking reference shown to the agent and passenger", "example": "JWZ-20260704-ABC123" },
          "pnr": { "type": "string", "description": "Supplier order reference used for operations (issue, cancel, refund). Distinct from the airline record locator shown to the passenger.", "example": "FT12345K001824" },
          "supplierPnr": { "type": "string", "description": "GDS / airline record locator shown to the passenger at check-in, e.g. F08EJF", "example": "F08EJF" },
          "airlinePnr": { "type": "string", "description": "Airline record locator (set by admin after manual coordination with the airline ops desk, if different from supplierPnr)", "example": "F08EJF" },
          "status": {
            "type": "string",
            "enum": ["DRAFT", "PENDING_PAYMENT", "PAID", "TICKETING", "ISSUED", "TICKETING_FAILED", "FAILED", "CANCELLED", "REFUNDED", "HOLD_EXPIRED", "HOLD_LOST"],
            "description": "Current booking status",
            "example": "PENDING_PAYMENT"
          },
          "totalAmount": { "type": "number", "description": "Total amount charged to the partner, in the booking's currency (includes base fare + tax + platform markup + fees)", "example": 1065000 },
          "currency": { "type": "string", "description": "ISO 4217 currency code", "example": "IDR" },
          "markup": { "type": "number", "description": "Platform markup amount included in totalAmount", "example": 50000 },
          "agentMarkup": { "type": "number", "description": "Agent's own markup (PERCENT or FIXED) applied at booking time", "example": 0 },
          "bookingFee": { "type": "number", "description": "Platform per-booking fee", "example": 10000 },
          "issuanceFee": { "type": "number", "description": "Per-supplier ticket-issuance fee", "example": 5000 },
          "commission": { "type": "number", "description": "Commission credited to the agency on this booking", "example": 0 },
          "flightData": { "type": "object", "description": "Immutable snapshot of the flight offer at booking time (segments, schedule, fare brand). Structure matches FlightOffer — no supplier-identifying fields are exposed." },
          "expiresAt": { "type": "string", "format": "date-time", "description": "Payment hold deadline. The booking auto-cancels if payment is not received by this time.", "example": "2026-07-04T10:30:00.000Z" },
          "issuedAt": { "type": "string", "format": "date-time", "description": "Timestamp when e-tickets were successfully issued by the supplier. Null until ISSUED status.", "example": "2026-07-04T09:45:00.000Z" },
          "passengers": {
            "type": "array",
            "description": "Passenger details for this booking.",
            "items": { "$ref": "#/components/schemas/Passenger" }
          },
          "createdAt": { "type": "string", "format": "date-time", "description": "Booking creation timestamp", "example": "2026-07-04T09:00:00.000Z" },
          "updatedAt": { "type": "string", "format": "date-time", "description": "Last update timestamp", "example": "2026-07-04T09:45:00.000Z" }
        }
      }
    }
  },
  "paths": {
    "/me": {
      "get": { "summary": "Account & key info", "tags": ["Account"],
        "responses": { "200": { "description": "Account, environment, wallet/credit" }, "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }
    },
    "/wallet": {
      "get": { "summary": "Wallet balance & credit", "tags": ["Account"], "responses": { "200": { "description": "Balance, hold, currency, credit" } } }
    },
    "/airports": {
      "get": { "summary": "Airport / city autocomplete", "tags": ["Reference"],
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "example": "DPS" },
          { "name": "limit", "in": "query", "schema": { "type": "integer" }, "example": 10 }
        ], "responses": { "200": { "description": "Matching airports" } } }
    },
    "/flights/search": {
      "post": { "summary": "Search flights", "tags": ["Flights"],
        "requestBody": { "required": true, "content": { "application/json": { "example": {
          "origin": "CGK", "destination": "DPS", "departDate": "2026-07-15",
          "passengers": { "adult": 1, "child": 0, "infant": 0 }, "cabinClass": "ECONOMY"
        } } } },
        "responses": {
          "200": {
            "description": "Offer list (sandbox: deterministic mock). Each item has an `id` used as the `offerId` for booking and price-check.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FlightSearchResponse" },
                "example": {
                  "results": [
                    {
                      "id": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
                      "segments": [
                        {
                          "flightNumber": "JT-123",
                          "carrier": "JT",
                          "carrierName": "Lion Air",
                          "origin": "CGK",
                          "destination": "DPS",
                          "departureTime": "2026-07-15T06:00:00.000Z",
                          "arrivalTime": "2026-07-15T07:20:00.000Z",
                          "duration": 80,
                          "aircraft": "B738",
                          "cabinClass": "ECONOMY",
                          "baggageAllowance": "20kg",
                          "departureTz": "Asia/Jakarta",
                          "arrivalTz": "Asia/Makassar"
                        }
                      ],
                      "totalDuration": 80,
                      "stops": 0,
                      "basePrice": 850000,
                      "tax": 150000,
                      "totalPrice": 1000000,
                      "currency": "IDR",
                      "seatsAvailable": 7,
                      "expiresAt": "2026-07-04T09:05:00.000Z",
                      "refundable": false,
                      "changeable": false,
                      "platformMarkup": 50000,
                      "bookingFee": 10000,
                      "issuanceFee": 5000
                    }
                  ],
                  "count": 1,
                  "sandbox": true
                }
              }
            }
          }
        }
      }
    },
    "/flights/price-check": {
      "post": { "summary": "Confirm current price before booking", "tags": ["Flights"],
        "requestBody": { "required": true, "content": { "application/json": { "example": { "offerId": "PASTE_offerId_FROM_SEARCH" } } } },
        "responses": {
          "200": {
            "description": "Latest price + availability. If `priceChanged=true`, show the user the new price and ask for confirmation before proceeding.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PriceCheckResult" },
                "example": {
                  "offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
                  "priceChanged": false,
                  "currentPrice": 1000000,
                  "currency": "IDR",
                  "stillAvailable": true,
                  "baggageAllowance": "20kg"
                }
              }
            }
          }
        }
      }
    },
    "/flights/{offerId}/fare-rules": {
      "get": { "summary": "Fare rules & baggage for an offer", "tags": ["Flights"],
        "parameters": [ { "name": "offerId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Fare rules summary" } } }
    },
    "/bookings": {
      "get": { "summary": "List agency bookings", "tags": ["Bookings"],
        "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "integer" }, "example": 20 } ],
        "responses": {
          "200": {
            "description": "Paginated bookings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Booking" }
                    },
                    "nextCursor": { "type": "string", "description": "Pass as `cursor` query param to fetch the next page. Absent when no more pages." }
                  }
                }
              }
            }
          }
        }
      },
      "post": { "summary": "Create a booking (hold)", "tags": ["Bookings"],
        "parameters": [ { "name": "Idempotency-Key", "in": "header", "schema": { "type": "string" }, "description": "Recommended for money-mutating requests" } ],
        "requestBody": { "required": true, "content": { "application/json": {
          "schema": {
            "type": "object",
            "required": ["offerId", "passengers", "contactPhone"],
            "properties": {
              "offerId": { "type": "string", "description": "Offer ID from /flights/search" },
              "passengers": {
                "type": "array",
                "minItems": 1,
                "items": { "$ref": "#/components/schemas/Passenger" }
              },
              "contactPhone": { "type": "string", "description": "Primary contact phone number (required)", "example": "+628123456789" },
              "contactEmail": { "type": "string", "format": "email", "description": "Contact email (optional, for confirmation delivery)", "example": "partner@example.com" },
              "paymentGateway": { "type": "string", "description": "Preferred payment gateway (optional; defaults to WALLET for sandbox)" },
              "agentMarkup": {
                "type": "object",
                "description": "Agent's own profit margin applied on top of the platform price (optional)",
                "required": ["type", "value"],
                "properties": {
                  "type": { "type": "string", "enum": ["PERCENT", "FIXED"] },
                  "value": { "type": "number" },
                  "max": { "type": "number", "description": "Optional IDR cap for PERCENT markup" }
                }
              },
              "voucherCode": { "type": "string", "description": "Discount voucher code (optional)" }
            }
          },
          "example": {
            "offerId": "PASTE_offerId_FROM_SEARCH",
            "contactPhone": "+628123456789",
            "contactEmail": "partner@example.com",
            "passengers": [ { "title": "MR", "firstName": "Budi", "lastName": "Santoso", "dateOfBirth": "1990-01-15", "nationality": "ID", "type": "ADULT" } ]
          }
        } } },
        "responses": {
          "201": {
            "description": "Booking created (PENDING_PAYMENT). Pay within `expiresAt` to avoid auto-cancellation.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Booking" },
                "example": {
                  "id": "clxbkg1abcdef",
                  "bookingCode": "JWZ-20260704-ABC123",
                  "pnr": null,
                  "status": "PENDING_PAYMENT",
                  "totalAmount": 1065000,
                  "currency": "IDR",
                  "markup": 50000,
                  "agentMarkup": 0,
                  "bookingFee": 10000,
                  "issuanceFee": 5000,
                  "commission": 0,
                  "flightData": {},
                  "expiresAt": "2026-07-04T10:30:00.000Z",
                  "issuedAt": null,
                  "passengers": [
                    { "id": "clxpax1abcdef", "title": "MR", "firstName": "Budi", "lastName": "Santoso", "dateOfBirth": "1990-01-15", "nationality": "ID", "type": "ADULT", "ticketNumber": null }
                  ],
                  "createdAt": "2026-07-04T09:00:00.000Z",
                  "updatedAt": "2026-07-04T09:00:00.000Z"
                }
              }
            }
          },
          "200": { "description": "Booking created (idempotent replay of a prior identical request)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } }
        }
      }
    },
    "/bookings/{id}": {
      "get": { "summary": "Booking detail", "tags": ["Bookings"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": {
          "200": {
            "description": "Full booking record including passengers and current status.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Booking" },
                "example": {
                  "id": "clxbkg1abcdef",
                  "bookingCode": "JWZ-20260704-ABC123",
                  "pnr": "FT12345K001824",
                  "supplierPnr": "F08EJF",
                  "status": "ISSUED",
                  "totalAmount": 1065000,
                  "currency": "IDR",
                  "markup": 50000,
                  "agentMarkup": 0,
                  "bookingFee": 10000,
                  "issuanceFee": 5000,
                  "commission": 0,
                  "flightData": {},
                  "expiresAt": "2026-07-04T10:30:00.000Z",
                  "issuedAt": "2026-07-04T09:45:00.000Z",
                  "passengers": [
                    { "id": "clxpax1abcdef", "title": "MR", "firstName": "Budi", "lastName": "Santoso", "dateOfBirth": "1990-01-15", "nationality": "ID", "type": "ADULT", "ticketNumber": "997-1234567890" }
                  ],
                  "createdAt": "2026-07-04T09:00:00.000Z",
                  "updatedAt": "2026-07-04T09:45:00.000Z"
                }
              }
            }
          }
        }
      }
    },
    "/bookings/{id}/pay": {
      "post": { "summary": "Pay with wallet (sandbox: simulated + auto-issue)", "tags": ["Bookings"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "Idempotency-Key", "in": "header", "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "example": {} } } },
        "responses": { "200": { "description": "Paid (sandbox: ticket issued)" } } }
    },
    "/bookings/{id}/cancel": {
      "post": { "summary": "Cancel before issuance (DRAFT/PENDING/PAID)", "tags": ["Bookings"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Cancelled; hold released" } } }
    },
    "/bookings/{id}/ticket": {
      "get": { "summary": "E-ticket download URL", "tags": ["Bookings"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Signed e-ticket URL" } } }
    },
    "/bookings/{id}/refund-quote": {
      "get": { "summary": "Refund estimate", "tags": ["Refunds"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Estimated refund" } } }
    },
    "/bookings/{id}/refund": {
      "post": { "summary": "Request refund (issued)", "tags": ["Refunds"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "example": { "reason": "Pembatalan oleh penumpang" } } } },
        "responses": { "200": { "description": "Refund requested (async via webhook)" } } }
    },
    "/bookings/{id}/ancillaries/catalog": {
      "get": { "summary": "Post-booking ancillary catalog (baggage/meal/seat)", "tags": ["Ancillaries"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "kind", "in": "query", "required": false, "schema": { "type": "string", "enum": ["BAGGAGE", "MEAL", "SEAT"] }, "description": "Filter by ancillary kind; omit for all" } ],
        "responses": { "200": { "description": "Catalog { supported, airline, options[] }. supported:false (reason UNSUPPORTED|NOT_ISSUED) when the airline has no ancillary API. Seat options carry seatNumber + the optionId SEAT-<seatNumber>.", "content": { "application/json": { "example": { "supported": true, "airline": { "code": "JT", "name": "Lion Air" }, "options": [ { "id": "SEAT-12A", "kind": "SEAT", "label": "Seat 12A", "seatNumber": "12A", "price": 150000, "currency": "IDR", "scope": "PER_SEGMENT", "available": true } ] } } } } } }
    },
    "/bookings/{id}/ancillaries": {
      "get": { "summary": "List purchased ancillaries", "tags": ["Ancillaries"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Purchased ancillaries" } } },
      "post": { "summary": "Buy ancillary (baggage/meal/seat)", "tags": ["Ancillaries"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "Idempotency-Key", "in": "header", "schema": { "type": "string" } } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["items"], "properties": { "payment": { "type": "string", "enum": ["WALLET", "CREDIT"], "default": "WALLET" }, "items": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "type": "object", "required": ["optionId", "kind", "passengerId"], "properties": { "optionId": { "type": "string", "description": "Catalog id (BAG-20, MEAL-VGML) or seat: SEAT-<seatNumber> e.g. SEAT-12A" }, "kind": { "type": "string", "enum": ["BAGGAGE", "MEAL", "SEAT"] }, "passengerId": { "type": "string", "description": "Passenger id from booking detail (one SEAT per passenger)" }, "segmentIndex": { "type": "integer", "minimum": 0, "default": 0 } } } } } }, "example": { "payment": "WALLET", "items": [ { "optionId": "SEAT-12A", "kind": "SEAT", "passengerId": "clxpax1..." } ] } } } },
        "responses": { "200": { "description": "Purchased { purchaseId, total, currency, items[] } (sandbox: simulated). Seat errors: 409 SEAT_UNAVAILABLE, 400 SEAT_PER_PAX." } } }
    },
    "/bookings/{id}/seatmap": {
      "get": { "summary": "Seat map for seat selection", "tags": ["Ancillaries"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Seat map. Requires the booking to be ISSUED; supported:false (reason NOT_ISSUED|UNSUPPORTED) otherwise. Seats already held by this booking's passengers show as OCCUPIED. Build the purchase optionId as SEAT-<seatNumber>.", "content": { "application/json": { "schema": { "type": "object", "properties": { "supported": { "type": "boolean" }, "columns": { "type": "array", "items": { "type": "string" }, "description": "Column letters; '' marks an aisle gap, e.g. ['A','B','C','','D','E','F']" }, "rows": { "type": "array", "items": { "type": "object", "properties": { "row": { "type": "integer" }, "seats": { "type": "array", "items": { "type": "object", "properties": { "seatNumber": { "type": "string" }, "status": { "type": "string", "enum": ["AVAILABLE", "OCCUPIED"] }, "exitRow": { "type": "boolean" }, "price": { "type": "number" }, "currency": { "type": "string" } } } } } } } }, "example": { "supported": true, "columns": ["A", "B", "C", "", "D", "E", "F"], "rows": [ { "row": 12, "seats": [ { "seatNumber": "12A", "status": "AVAILABLE", "exitRow": false, "price": 150000, "currency": "IDR" }, { "seatNumber": "12B", "status": "OCCUPIED", "exitRow": false } ] } ] } } } } } } }
    },
    "/bookings/{id}/reschedule/options": {
      "get": { "summary": "Same-route reschedule options", "tags": ["Reschedule"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Options; supported:false / WINDOW_CLOSED if <24h" } } }
    },
    "/bookings/{id}/reschedule": {
      "post": { "summary": "Execute reschedule (void old, issue new PNR)", "tags": ["Reschedule"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "Idempotency-Key", "in": "header", "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "example": { "optionId": "PASTE_optionId", "payment": "WALLET" } } } },
        "responses": { "200": { "description": "Rescheduled (sandbox: simulated)" } } }
    },
    "/bookings/{id}/auto-rebook/enable": {
      "post": { "summary": "Enable auto-rebook on a held booking", "tags": ["Auto-Rebook"],
        "description": "Auto-renews the supplier hold before it expires (availability + price re-check, then cancel→rebook each cycle), up to a max window. Opt-in per booking; the booking must be PENDING_PAYMENT and its supplier must be gated on by the operator. Does NOT issue the ticket or take payment. Sandbox bookings run on the mock supplier.",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "maxHoldMinutes": { "type": "integer", "minimum": 60, "maximum": 2880, "default": 1440, "description": "Total hold window (1h–48h). Default 24h." }, "priceThresholdPct": { "type": "number", "minimum": 0, "maximum": 100, "description": "If a renewal price rises more than this %, the cycle PAUSES for approval. Omit = never pause." } } }, "example": { "maxHoldMinutes": 1440, "priceThresholdPct": 10 } } } },
        "responses": { "200": { "description": "{ status: 'ACTIVE', cycleId, nextAttemptAt }" }, "409": { "description": "AUTO_REBOOK_NOT_ELIGIBLE (not PENDING_PAYMENT) | AUTO_REBOOK_ALREADY_ACTIVE | AUTO_REBOOK_SUPPLIER_DISABLED" } } }
    },
    "/bookings/{id}/auto-rebook/disable": {
      "post": { "summary": "Disable the auto-rebook cycle (keeps the current hold)", "tags": ["Auto-Rebook"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "{ status: 'STOPPED', bookingId, lastAttemptAt }" }, "409": { "description": "AUTO_REBOOK_NOT_ACTIVE" } } }
    },
    "/bookings/{id}/auto-rebook/approve": {
      "post": { "summary": "Approve a paused cycle (price rose above threshold) and resume", "tags": ["Auto-Rebook"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "{ status: 'ACTIVE', nextAttemptAt }" }, "409": { "description": "AUTO_REBOOK_NOT_PAUSED" } } }
    },
    "/bookings/{id}/auto-rebook/history": {
      "get": { "summary": "Auto-rebook cycle status + attempt history (price snapshots)", "tags": ["Auto-Rebook"],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "{ cycle: { status, maxHoldMinutes, priceThresholdPct, expiresAt, nextAttemptAt, attemptCount, lastPrice } | null, attempts: [{ attemptNumber, attemptedAt, result (SUCCESS|SEAT_UNAVAILABLE|API_ERROR|PRICE_THRESHOLD_EXCEEDED|HOLD_LOST), prevBookingRef, newBookingRef, prevPrice, newPrice, priceDelta, priceDeltaPct }] }", "content": { "application/json": { "example": { "cycle": { "status": "ACTIVE", "maxHoldMinutes": 1440, "priceThresholdPct": 10, "expiresAt": "2026-06-16T04:00:00.000Z", "nextAttemptAt": "2026-06-15T05:25:00.000Z", "attemptCount": 1, "lastPrice": 1500000 }, "attempts": [ { "attemptNumber": 1, "attemptedAt": "2026-06-15T04:55:00.000Z", "result": "SUCCESS", "prevBookingRef": "BKG-AB12CD", "newBookingRef": "BKG-EF34GH", "prevPrice": 1500000, "newPrice": 1500000, "priceDelta": 0, "priceDeltaPct": 0 } ] } } } } } }
    }
  }
}
