{"openapi":"3.1.0","info":{"title":"GetMyHotels REST API","version":"1.0.0","description":"REST surface for the native GetMyHotels apps. Every route is a thin transport over the shared tRPC routers — inputs are validated with the same Zod schemas the web client uses, and responses are returned verbatim. Authentication uses Better Auth bearer session tokens issued by the /api/auth/* endpoints (not part of this spec). Native clients send X-GMH-App-Version and X-GMH-Platform on every request; versions below the configured floor receive a typed 426 UpgradeRequiredError."},"servers":[{"url":"https://api.getmyhotels.com","description":"Production API"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Better Auth session token. Obtain it from the native /api/auth/* endpoints (Better Auth bearer plugin — out of scope for this spec, see #922)."}},"schemas":{"AppStatus":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]}},"required":["status"]},"UpgradeRequiredError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["UPGRADE_REQUIRED"]},"message":{"type":"string"},"minVersion":{"type":"string","example":"1.2.0"},"platform":{"type":["string","null"]}},"required":["code","message","minVersion","platform"]}},"required":["error"]},"ApiError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"NOT_FOUND"},"message":{"type":"string","example":"Booking not found"},"domainCode":{"type":"string","example":"NON_REFUNDABLE_CONSENT_REQUIRED"},"issues":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"]}}},"required":["code","message"]}},"required":["error"]}},"parameters":{}},"paths":{"/v1/app-status":{"get":{"tags":["app"],"summary":"Check whether this native app build is supported","responses":{"200":{"description":"This app build is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppStatus"}}}},"426":{"description":"Reported X-GMH-App-Version is below the platform's supported minimum","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}}}}},"/v1/admin/overview":{"get":{"tags":["admin"],"summary":"Read the iOS admin operations overview","description":"Runs admin.bookings.getStats, support.getAdminStats and admin.supplierHealth.getAllSuppliersHealth. Each procedure checks the admin role. The response contains aggregate counts and supplier health only, with no guest PII.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin operations overview` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/bookings":{"get":{"tags":["admin"],"summary":"List bookings for iOS admin triage","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["pending_payment","pending_manual","confirmed","cancelled","completed","refunded","failed"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.bookings.list` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/bookings/{publicId}":{"get":{"tags":["admin"],"summary":"Read booking details for iOS admin operations","description":"Runs admin.bookings.getById and returns a limited mobile projection. Guest documents, financial margin, internal notes and bearer credentials are excluded.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.bookings.getById` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/bookings/{publicId}/resend-confirmation":{"post":{"tags":["admin"],"summary":"Resend a booking confirmation","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.bookings.resendConfirmation` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/bookings/{publicId}/status":{"post":{"tags":["admin"],"summary":"Change a hotel booking status","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"description":"New booking status and audit reason","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"newStatus":{"type":"string","enum":["pending_payment","confirmed","cancelled","completed","failed"]},"reason":{"type":"string","minLength":1,"maxLength":1000}},"required":["newStatus","reason"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.bookings.updateStatus` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/support/tickets/{publicId}/status":{"post":{"tags":["admin"],"summary":"Change a support ticket status","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"description":"New ticket status","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","in_progress","resolved","closed"]}},"required":["status"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.updateStatus` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/support/tickets/{publicId}/assignment":{"post":{"tags":["admin"],"summary":"Assign or unassign a support ticket","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"description":"Assignee","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"assignee":{"type":"string","enum":["me","none"]}},"required":["assignee"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.assignTicket` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/hotels/search":{"post":{"tags":["hotels"],"summary":"Search hotels for a resolved place","description":"Runs `hotel.searchByPlace`. The client resolves the destination to a placeId + coordinates first (same contract as the web autosuggest flow). Response fields the native client decodes: `hotels[]` (each with `ref`, `name`, `starRating`, `userRating?`, `reviewCount?`, `address`, `city`, `country`, `latitude` and `longitude` (always numbers, for the map), `distanceKm?`, `images`, `amenities`, and `rooms` holding at most the cheapest `{ price, currency, available, cancellationPolicy, mealPlan? }`, a stay total); `total`, `page`, `limit`; `priceStats` and `filteredPriceStats` (null or `{ min, max, currency, buckets[] }`, the stay-total distribution before and after the price filter); `guestScoreCounts` `{ scored, min7, min8, min9 }`; `relaxed` (null, or `{ field }` with `field` one of `guestScore`, `stars`, `price`, `amenities`, naming the one filter dropped because nothing matched them all — only when `relaxFilters` is sent); `amenityFilter` (null, or `{ requested, probed, matched, unchecked }`, where `unchecked` hotels had no amenity data and were left out); `radiusExpanded` (null or `{ requestedKm, nearestKm }`); the effective `checkIn`/`checkOut`/`rooms`/`adults`/`children`; and `diagnostics`/`health`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"placeId":{"type":"string","minLength":1},"destination":{"type":"string","maxLength":255},"lat":{"type":"number"},"lng":{"type":"number"},"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"rooms":{"type":"integer","minimum":1,"maximum":6,"default":1},"adults":{"type":"integer","minimum":1,"maximum":6,"default":2},"children":{"type":"integer","minimum":0,"maximum":4,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":1,"maximum":18},"maxItems":4,"default":[]},"guestNationality":{"type":"string","minLength":2,"maxLength":2},"page":{"type":"integer","minimum":1,"default":1},"limit":{"type":"integer","minimum":10,"maximum":100,"default":20},"hotelName":{"type":"string","maxLength":200},"minStars":{"type":"integer","minimum":1,"maximum":5},"minPrice":{"type":"number","minimum":0},"maxPrice":{"type":"number","minimum":0},"priceCurrency":{"type":"string","enum":["USD","GBP","EUR","INR"]},"minGuestScore":{"type":"number","minimum":0,"maximum":10},"relaxFilters":{"type":"boolean"},"propertyType":{"type":"string","maxLength":30},"sortBy":{"type":"string","enum":["best","price","rating","distance"],"default":"best"},"sortOrder":{"type":"string","enum":["asc","desc"],"default":"asc"},"currency":{"type":"string","enum":["USD","GBP","EUR","INR"]},"maxDistanceKm":{"type":"number","minimum":0.5,"maximum":200},"supplierCodes":{"type":"array","items":{"type":"string","minLength":1},"maxItems":8},"previewOnly":{"type":"boolean"},"amenities":{"type":"array","items":{"type":"string","enum":["wifi","pool","breakfast","parking","gym","spa","pet-friendly"]}},"preferPlaceCity":{"type":"boolean"},"seenHotels":{"type":"array","items":{"type":"string","pattern":"^[0-9a-f]{12}$"}}},"required":["placeId","lat","lng","checkIn","checkOut"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `hotel.searchByPlace` tRPC procedure, WITHOUT the `bySupplier` field(s) — internal-only data stripped from this surface; clients must never depend on them. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/hotels/autosuggest":{"get":{"tags":["hotels"],"summary":"Resolve a destination query to place suggestions","description":"Runs `hotel.autosuggest`. Returns supplier place suggestions (cities, regions, POIs, hotels) for a free-text destination — the id/lat/lng of a pick is what `POST /v1/hotels/search` expects as placeId + coordinates (#1062). Results are ranked server-side; the first entry is the resolver's best match.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"example":"Dubai"},"required":true,"name":"query","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `hotel.autosuggest` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/composer/analyze":{"get":{"tags":["chat"],"summary":"Suggest date, destination and guest chips for a half-typed chat message","description":"Runs `composer.analyze` (#2318). Returns the travel intent plus parsed dates, destination options and party size, each with UTF-16 `spans` into `text` so the client can rewrite that phrase when the user picks a value. `missing` flags the chips to offer as 'Add dates' / 'Add guests'. Call on a ~350ms typing debounce; discard a response whose `text` no longer matches the composer.","parameters":[{"schema":{"type":"string","maxLength":500,"example":"hotel in goa from 12 to 15 dec for 2"},"required":true,"name":"text","in":"query"},{"schema":{"type":"string","maxLength":35,"example":"en-IN"},"required":true,"name":"locale","in":"query"},{"schema":{"type":"string","example":"2026-09-30"},"required":true,"name":"today","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `composer.analyze` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/hotels/{ref}":{"get":{"tags":["hotels"],"summary":"Supplier hotel detail","description":"Runs `hotel.getSupplierDetails` for an opaque supplier hotel ref (`sh_…`). Carries `latitude`/`longitude` and, when the supplier lists them, `attractions` (nearby landmarks as display strings) — the supplier detail has no structured nearby list with distances.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"ref","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `hotel.getSupplierDetails` tRPC procedure, WITHOUT the `raw` field(s) — internal-only data stripped from this surface; clients must never depend on them. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/hotels/{ref}/rooms":{"post":{"tags":["hotels"],"summary":"Live room availability and pricing","description":"Runs `hotel.getSupplierRooms` for the hotel ref with the stay in the body.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"ref","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"adults":{"type":"integer","minimum":1,"default":2},"children":{"type":"integer","minimum":0,"default":0},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"currency":{"type":"string","enum":["USD","GBP","EUR","INR"]}},"required":["checkIn","checkOut"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `hotel.getSupplierRooms` tRPC procedure, WITHOUT the `rawRooms` field(s) — internal-only data stripped from this surface; clients must never depend on them. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/flights/search":{"post":{"tags":["flights"],"summary":"Search flights (retired)","description":"Flights are no longer offered. Kept so iOS and Android builds already in the stores keep decoding: always returns 200 with an empty result set, `health.flights = \"skipped\"` and an informational diagnostic, and never calls a supplier.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"origin":{"type":"string","minLength":1,"maxLength":10},"destination":{"type":"string","minLength":1,"maxLength":10},"departureDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"returnDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"routeType":{"type":"string","enum":["oneway","return"],"default":"oneway"},"cabinType":{"type":"string","enum":["economy","business","first"],"default":"economy"},"adults":{"type":"integer","minimum":1,"maximum":9,"default":1},"children":{"type":"integer","minimum":0,"maximum":9,"default":0},"infants":{"type":"integer","minimum":0,"maximum":9,"default":0},"page":{"type":"integer","minimum":1,"default":1},"limit":{"type":"integer","minimum":1,"maximum":50,"default":20},"sortBy":{"type":"string","enum":["price","duration"]},"airline":{"type":"string","minLength":1,"maxLength":100},"stops":{"type":"string","enum":["non_stop","one_stop","two_plus"]},"nonstop":{"type":"boolean"}},"required":["origin","destination","departureDate"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `flight.searchFlights` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payments/hotel-checkout-quote":{"post":{"tags":["payments"],"summary":"Price a hotel checkout before payment","description":"Runs `payment.getHotelCheckoutQuote`. Public by design — it is the price-on-load half of the guest-capable payment-first checkout (critical decision #4), and it is the same call the web checkout makes when its form mounts. Pure read: no PaymentIntent, no payment row, no booking. Call it as the checkout screen loads so the real supplier tax/fee split, add-ons, promo outcome and `rateValidUntil` are on screen BEFORE the customer commits to paying, then send the same pricing fields to `POST /v1/payments/hotel-checkout-intent`. POST rather than GET because the input carries a supplier room id up to 8 KB. Signed-in callers also get their saved cards; staff accounts get `simulated: true`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"offerRef":{"type":"string","minLength":1,"maxLength":20000},"supplierCode":{"type":"string","minLength":1,"maxLength":50},"supplierHotelId":{"type":"string","minLength":1,"maxLength":255},"supplierRoomId":{"type":"string","minLength":1,"maxLength":8192},"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"totalPrice":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"promoCode":{"type":"string","maxLength":50},"addonIds":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":10,"default":[]}},"required":["checkIn","checkOut","totalPrice","currency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `payment.getHotelCheckoutQuote` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payments/hotel-checkout-intent":{"post":{"tags":["payments"],"summary":"Create a payment-first hotel checkout intent","description":"Runs `payment.createHotelCheckoutIntent`. Public by design — guests can complete a payment-first checkout without an account (critical decision #4). Guest calls must include a contact email on the lead guest.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"offerRef":{"type":"string","minLength":1,"maxLength":20000},"supplierCode":{"type":"string","minLength":1,"maxLength":50},"supplierHotelId":{"type":"string","minLength":1,"maxLength":255},"supplierRoomId":{"type":"string","minLength":1,"maxLength":8192},"hotelName":{"type":"string","minLength":1,"maxLength":255},"hotelStarRating":{"type":"number","minimum":0,"maximum":5},"hotelCity":{"type":"string","maxLength":255},"hotelCountry":{"type":["string","null"],"minLength":3,"maxLength":3},"roomName":{"type":"string","minLength":1,"maxLength":255},"roomMaxOccupancy":{"type":["number","null"],"minimum":1,"maximum":20},"roomBedType":{"type":"string","maxLength":50},"images":{"type":"array","items":{}},"address":{"type":"string"},"property":{"type":"object","description":"Rich supplier-mapped property snapshot; invalid values are dropped, never rejected."},"checkIn":{"type":["string","null"]},"checkOut":{"type":["string","null"]},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"totalPrice":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"baseRate":{"type":"number","minimum":0},"taxes":{"type":["number","null"],"minimum":0},"fees":{"type":["number","null"],"minimum":0},"expectedTotal":{"type":"object","properties":{"amountMinor":{"type":"integer","minimum":0,"maximum":9007199254740991},"currency":{"type":"string","minLength":3,"maxLength":3}},"required":["amountMinor","currency"]},"guests":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string","minLength":2,"maxLength":100},"lastName":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","maxLength":255,"format":"email"},"phone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"isLead":{"type":"boolean","default":false},"nationality":{"type":"string","minLength":2,"maxLength":3},"dateOfBirth":{"type":"string","maxLength":10},"gender":{"type":"string","maxLength":20}},"required":["firstName","lastName"]},"minItems":1,"maxItems":10},"specialRequests":{"type":"string","maxLength":2000},"arrivalTime":{"type":"string","maxLength":5},"departureFlightNumber":{"type":"string","maxLength":20},"promoCode":{"type":"string","maxLength":50},"language":{"type":"string","enum":["en","es","fr","de","zh","hi","pt","it","ja","te","gu","ko","bn","ru"]},"addonIds":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":10,"default":[]},"cancellationSummary":{"type":"string","maxLength":500},"refundable":{"type":"boolean"},"freeCancellationUntil":{"type":["string","null"],"maxLength":64},"allowNonRefundable":{"type":"boolean","default":false},"termsAccepted":{"type":"boolean","default":false},"termsVersion":{"type":"string","minLength":1,"maxLength":20},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255},"savedPaymentMethodPublicId":{"type":"string","minLength":1,"maxLength":36},"autoRebook":{"type":"boolean","default":false},"turnstileToken":{"type":"string","maxLength":2048},"buyerCompanyName":{"type":"string","minLength":1,"maxLength":255},"buyerTaxIdCountry":{"type":"string","enum":["IN","GB","AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK"]},"buyerTaxId":{"type":"string","minLength":1,"maxLength":50}},"required":["hotelName","roomName","checkIn","checkOut","totalPrice","currency","guests"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `payment.createHotelCheckoutIntent` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/{publicId}":{"get":{"tags":["admin"],"summary":"Resume an admin-created card checkout","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.getCardCheckout` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/cash/recover":{"post":{"tags":["admin"],"summary":"Recover an interrupted cash booking without a second reservation","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"idempotencyKey":{"type":"string","minLength":8,"maxLength":255}},"required":["idempotencyKey"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.recoverCashBooking` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/card/recover":{"post":{"tags":["admin"],"summary":"Recover an interrupted card checkout by its original request key","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"idempotencyKey":{"type":"string","minLength":1,"maxLength":255}},"required":["idempotencyKey"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.recoverCardIntent` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/quote":{"post":{"tags":["admin"],"summary":"Price an admin-created customer checkout","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"offerRef":{"type":"string","minLength":1,"maxLength":20000},"supplierCode":{"type":"string","minLength":1,"maxLength":50},"supplierHotelId":{"type":"string","minLength":1,"maxLength":255},"supplierRoomId":{"type":"string","minLength":1,"maxLength":8192},"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"totalPrice":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"promoCode":{"type":"string","maxLength":50},"addonIds":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":10,"default":[]}},"required":["checkIn","checkOut","totalPrice","currency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.quote` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/card-intent":{"post":{"tags":["admin"],"summary":"Create a guest-owned card checkout as staff","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"offerRef":{"type":"string","minLength":1,"maxLength":20000},"supplierCode":{"type":"string","minLength":1,"maxLength":50},"supplierHotelId":{"type":"string","minLength":1,"maxLength":255},"supplierRoomId":{"type":"string","minLength":1,"maxLength":8192},"hotelName":{"type":"string","minLength":1,"maxLength":255},"hotelStarRating":{"type":"number","minimum":0,"maximum":5},"hotelCity":{"type":"string","maxLength":255},"hotelCountry":{"type":["string","null"],"minLength":3,"maxLength":3},"roomName":{"type":"string","minLength":1,"maxLength":255},"roomMaxOccupancy":{"type":["number","null"],"minimum":1,"maximum":20},"roomBedType":{"type":"string","maxLength":50},"images":{"type":"array","items":{}},"address":{"type":"string"},"property":{"type":"object","description":"Rich supplier-mapped property snapshot; invalid values are dropped, never rejected."},"checkIn":{"type":["string","null"]},"checkOut":{"type":["string","null"]},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"totalPrice":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"baseRate":{"type":"number","minimum":0},"taxes":{"type":["number","null"],"minimum":0},"fees":{"type":["number","null"],"minimum":0},"expectedTotal":{"type":"object","properties":{"amountMinor":{"type":"integer","minimum":0,"maximum":9007199254740991},"currency":{"type":"string","minLength":3,"maxLength":3}},"required":["amountMinor","currency"]},"guests":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string","minLength":2,"maxLength":100},"lastName":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","maxLength":255,"format":"email"},"phone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"isLead":{"type":"boolean","default":false},"nationality":{"type":"string","minLength":2,"maxLength":3},"dateOfBirth":{"type":"string","maxLength":10},"gender":{"type":"string","maxLength":20}},"required":["firstName","lastName"]},"minItems":1,"maxItems":10},"specialRequests":{"type":"string","maxLength":2000},"arrivalTime":{"type":"string","maxLength":5},"departureFlightNumber":{"type":"string","maxLength":20},"promoCode":{"type":"string","maxLength":50},"language":{"type":"string","enum":["en","es","fr","de","zh","hi","pt","it","ja","te","gu","ko","bn","ru"]},"addonIds":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":10,"default":[]},"cancellationSummary":{"type":"string","maxLength":500},"refundable":{"type":"boolean"},"freeCancellationUntil":{"type":["string","null"],"maxLength":64},"allowNonRefundable":{"type":"boolean","default":false},"termsAccepted":{"type":"boolean","default":false},"termsVersion":{"type":"string","minLength":1,"maxLength":20},"idempotencyKey":{"type":"string","minLength":8,"maxLength":255},"savedPaymentMethodPublicId":{"type":"string","minLength":1,"maxLength":36},"autoRebook":{"type":"boolean","default":false},"turnstileToken":{"type":"string","maxLength":2048},"buyerCompanyName":{"type":"string","minLength":1,"maxLength":255},"buyerTaxIdCountry":{"type":"string","enum":["IN","GB","AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK"]},"buyerTaxId":{"type":"string","minLength":1,"maxLength":50}},"required":["hotelName","roomName","checkIn","checkOut","totalPrice","currency","guests","idempotencyKey"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.createCardIntent` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/cash":{"post":{"tags":["admin"],"summary":"Record collected cash and create a real supplier booking","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"offerRef":{"type":"string","minLength":1,"maxLength":20000},"supplierCode":{"type":"string","minLength":1,"maxLength":50},"supplierHotelId":{"type":"string","minLength":1,"maxLength":255},"supplierRoomId":{"type":"string","minLength":1,"maxLength":8192},"hotelName":{"type":"string","minLength":1,"maxLength":255},"hotelStarRating":{"type":"number","minimum":0,"maximum":5},"hotelCity":{"type":"string","maxLength":255},"hotelCountry":{"type":["string","null"],"minLength":3,"maxLength":3},"roomName":{"type":"string","minLength":1,"maxLength":255},"roomMaxOccupancy":{"type":["number","null"],"minimum":1,"maximum":20},"roomBedType":{"type":"string","maxLength":50},"images":{"type":"array","items":{}},"address":{"type":"string"},"property":{"type":"object","description":"Rich supplier-mapped property snapshot; invalid values are dropped, never rejected."},"checkIn":{"type":["string","null"]},"checkOut":{"type":["string","null"]},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"totalPrice":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"baseRate":{"type":"number","minimum":0},"taxes":{"type":["number","null"],"minimum":0},"fees":{"type":["number","null"],"minimum":0},"expectedTotal":{"type":"object","properties":{"amountMinor":{"type":"integer","minimum":0,"maximum":9007199254740991},"currency":{"type":"string","minLength":3,"maxLength":3}},"required":["amountMinor","currency"]},"guests":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string","minLength":2,"maxLength":100},"lastName":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","maxLength":255,"format":"email"},"phone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"isLead":{"type":"boolean","default":false},"nationality":{"type":"string","minLength":2,"maxLength":3},"dateOfBirth":{"type":"string","maxLength":10},"gender":{"type":"string","maxLength":20}},"required":["firstName","lastName"]},"minItems":1,"maxItems":10},"specialRequests":{"type":"string","maxLength":2000},"arrivalTime":{"type":"string","maxLength":5},"departureFlightNumber":{"type":"string","maxLength":20},"promoCode":{"type":"string","maxLength":50},"language":{"type":"string","enum":["en","es","fr","de","zh","hi","pt","it","ja","te","gu","ko","bn","ru"]},"addonIds":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":10,"default":[]},"cancellationSummary":{"type":"string","maxLength":500},"refundable":{"type":"boolean"},"freeCancellationUntil":{"type":["string","null"],"maxLength":64},"allowNonRefundable":{"type":"boolean","default":false},"termsAccepted":{"type":"boolean","default":false},"termsVersion":{"type":"string","minLength":1,"maxLength":20},"idempotencyKey":{"type":"string","minLength":8,"maxLength":255},"savedPaymentMethodPublicId":{"type":"string","minLength":1,"maxLength":36},"autoRebook":{"type":"boolean","default":false},"turnstileToken":{"type":"string","maxLength":2048},"buyerCompanyName":{"type":"string","minLength":1,"maxLength":255},"buyerTaxIdCountry":{"type":"string","enum":["IN","GB","AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK"]},"buyerTaxId":{"type":"string","minLength":1,"maxLength":50},"confirmedCollected":{"type":"boolean","enum":[true]},"confirmRealBooking":{"type":"boolean","enum":[true]},"confirmedAmount":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$"},"confirmedCurrency":{"type":"string","minLength":3,"maxLength":3}},"required":["hotelName","roomName","checkIn","checkOut","totalPrice","currency","guests","idempotencyKey","confirmedCollected","confirmRealBooking","confirmedAmount","confirmedCurrency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.createCashBooking` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/payment-link":{"post":{"tags":["admin"],"summary":"Create a shareable customer checkout link","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hotelPublicId":{"type":"string","minLength":1,"maxLength":512},"hotelName":{"type":"string","minLength":1,"maxLength":300},"roomName":{"type":"string","maxLength":300},"checkIn":{"type":"string"},"checkOut":{"type":"string"},"price":{"type":"number"},"stayTotal":{"type":"number"},"nightlyPrice":{"type":"number"},"pricingBasis":{"type":"string","enum":["stay_total","nightly"]},"availableRooms":{"type":"number"},"currency":{"type":"string","maxLength":8},"rooms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","maxLength":512},"name":{"type":"string","maxLength":300},"bedType":{"type":"string","maxLength":120},"maxOccupancy":{"type":"number"},"amenities":{"type":"array","items":{"type":"string","maxLength":120},"maxItems":200},"price":{"type":"number"},"stayTotal":{"type":"number"},"nightlyPrice":{"type":"number"},"pricingBasis":{"type":"string","enum":["stay_total","nightly"]},"availableRooms":{"type":"number"}},"required":["id","name","bedType","maxOccupancy","amenities","price"]},"maxItems":40},"offerRef":{"type":"string","maxLength":16384},"requiresLatinGuestNames":{"type":"boolean"},"supplierCode":{"type":"string","maxLength":64},"supplierHotelId":{"type":"string","maxLength":2048},"supplierRoomId":{"type":"string","maxLength":8192},"hotelStarRating":{"type":"number"},"hotelCity":{"type":"string","maxLength":200},"hotelCountry":{"type":"string","maxLength":200},"images":{"type":"array","items":{}},"address":{"type":"string"},"reviewScore":{"type":"number"},"reviewCount":{"type":"number"},"property":{"type":"object","description":"Supplier property display snapshot."},"roomMaxOccupancy":{"type":"number"},"roomBedType":{"type":"string","maxLength":120},"bookingRooms":{"type":"number"},"adults":{"type":"number"},"childGuests":{"type":"number"},"childrenAges":{"type":"array","items":{"type":"number"},"maxItems":12},"infants":{"type":"number"},"cancellationSummary":{"type":"string","maxLength":4000},"refundable":{"type":"boolean"},"freeCancellationUntil":{"type":["string","null"]},"mealPlan":{"type":"string","maxLength":120},"supplements":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"description":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"}},"required":["type","description","price","currency"]},"maxItems":20},"rateConditions":{"type":"array","items":{"type":"string"},"maxItems":40},"roomPromotions":{"type":"array","items":{"type":"string"},"maxItems":20},"amenities":{"type":"array","items":{"type":"string"},"maxItems":200},"withTransfers":{"type":"boolean"},"perNightRates":{"type":"array","items":{"type":"number"},"maxItems":60},"savedPaymentMethodPublicId":{"type":"string","maxLength":36}},"required":["hotelPublicId","hotelName","roomName","checkIn","checkOut","price","currency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.createPaymentLink` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/admin/checkouts/confirm":{"post":{"tags":["admin"],"summary":"Finalize an admin-created card checkout","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"publicId":{"type":"string","minLength":1,"maxLength":36},"providerTransactionId":{"type":"string","minLength":1,"maxLength":255}},"required":["publicId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `admin.checkout.confirmCardPayment` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The account does not have admin access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payments/{publicId}/confirm":{"post":{"tags":["payments"],"summary":"Finalize a hotel checkout after payment","description":"Runs `payment.confirm` for the payment transaction created by `POST /v1/payments/hotel-checkout-intent`. Public by design — it is the second half of the guest-capable payment-first checkout (critical decision #4), and ownership is enforced by the procedure against the transaction's own userId. Call it after the Stripe PaymentSheet reports success, or immediately when the intent came back with `testMode: true` (staff simulate path). Send `{}` when there is no provider transaction id to pass through. The booking row, supplier reservation and confirmation email are all created here, so the response carries the booking reference the confirmation screen shows.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"providerTransactionId":{"type":"string","minLength":1,"maxLength":255}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `payment.confirm` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payments/bookings/{bookingPublicId}/complete":{"post":{"tags":["payments"],"summary":"Finish a paid booking whose supplier call never landed","description":"Runs `payment.completePendingBooking`. Call it from the booking screen when a hotel booking reads `pending_payment`: on the payment-first flow the row is only written after the card is charged, so that status is not a customer who failed to pay — it is a customer whose money we took and whose supplier call died in flight. Charge-free by construction: it refuses unless a completed payment transaction already exists for the booking, and it never calls Stripe. Never send one of these bookings to `POST /v1/payments/hotel-checkout-intent` instead — that charges the card a second time for one stay. Public by design, matching the rest of the guest-capable checkout (critical decision #4): the CUID is the access key for guest rows, and a booking owned by an account must match the caller. Idempotent — a booking already confirmed answers 200 with `alreadyDone: true`, and the underlying supplier book is idempotent on the supplier reservation id, so a double tap cannot double-book. A supplier that still refuses is NOT an error here: the response is 200 with `status: \"pending_manual\"`, ops are paged, and the charge stands — show the customer that their booking is being settled by hand rather than a failure. 400 means the booking is unpaid or in a status nobody is waiting on us to finish.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"bookingPublicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `payment.completePendingBooking` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payments/recovery/{token}":{"get":{"tags":["payments"],"summary":"Resolve a 3DS payment-recovery link","description":"Runs `payment.resolveCheckoutRecovery`. Public by design — the caller is a guest or signed-in customer who followed the `requires_action` email link with no other credential (critical decision #4). Never throws for an unknown, expired, or wrong-state token: every outcome, including one that doesn't resolve to anything, is a distinct `kind` in the 200 body, so a probing caller and a legitimate one get the same shape of answer either way. When `kind` is `resumable`, the response carries a freshly-fetched Stripe client secret — re-fetched from Stripe on every call, never read from a stored column, and never logged — for the SAME PaymentIntent the original checkout created. Rate-limited like the rest of the write-tier payment surface even though this route never charges anything, because it calls out to Stripe on every resolve.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"required":true,"name":"token","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `payment.resolveCheckoutRecovery` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings":{"post":{"tags":["bookings"],"summary":"Create a booking","description":"Runs `booking.create` for the signed-in user.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hotelPublicId":{"type":"string","minLength":1,"maxLength":36},"roomTypePublicId":{"type":"string","minLength":1,"maxLength":36},"checkIn":{"type":["string","null"]},"checkOut":{"type":["string","null"]},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":10,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"guests":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"email":{"type":"string","maxLength":255,"format":"email"},"phone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"isLead":{"type":"boolean","default":false}},"required":["firstName","lastName"]},"minItems":1},"specialRequests":{"type":"string","maxLength":2000},"promoCode":{"type":"string","maxLength":50},"supplierRoomId":{"type":"string","maxLength":8192}},"required":["hotelPublicId","roomTypePublicId","checkIn","checkOut","guests"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.create` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["bookings"],"summary":"List my bookings","description":"Runs `booking.list` for the signed-in user.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.list` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/claim-guest":{"post":{"tags":["bookings"],"summary":"Attach this account's guest bookings to it","description":"Runs `booking.claimGuestBookings`. Call it once right after sign-in (and after sign-up), the same moment the web bookings page does. A guest checkout is complete and legitimate without an account (critical decision #4), so the booking row carries only `guestEmail`; this claims every unclaimed booking whose guest email matches the session's email — case-insensitively — and clears its booking token. Until it runs, `GET /v1/bookings` filters on `userId` and cannot see them, so a guest who booked in the app and then signed in on that same device loses their booking history (#1234). Idempotent: it returns `{ claimed: 0 }` when there is nothing left to claim, so it is safe to call on every sign-in. Takes no input, so this route accepts no request body.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.claimGuestBookings` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}":{"get":{"tags":["bookings"],"summary":"Booking detail","description":"Runs `booking.getById`; owner-only. The hotel's IANA time zone is `hotel.location.timezone` (e.g. `Asia/Dubai`). `hotel.location` or its `timezone` is null when we hold no zone for the property, which includes every supplier snapshot booking, so render check-in/out dates as calendar dates rather than converting them through a zone.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.getById` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/receipt":{"get":{"tags":["bookings"],"summary":"Booking tax invoice / receipt","description":"Runs `booking.receiptDetails`; owner-only. Returns the seller entity, the sequential invoice number and issue date once one has been issued, the stored card's last 4 digits, and the buyer's company name/tax ID — the invoice's own immutable snapshot once an invoice exists, or the booking's still-editable fields before that (#2229). Native clients build the invoice PDF client-side from this payload, the same way the web booking detail page does.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.receiptDetails` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/buyer-tax-details":{"put":{"tags":["bookings"],"summary":"Set the buyer company name and tax ID for a booking's invoice","description":"Runs `booking.updateBuyerTaxDetails`; owner-only. REPLACES all three fields: an omitted field is cleared, so send `{}` to remove the company details entirely. `buyerTaxId` needs `buyerTaxIdCountry` and is validated for that country (GSTIN for IN, VAT for EU/UK). Refused with 400 once a non-void invoice exists for the booking — the issued invoice keeps its own snapshot. Read the current values from `buyer` on `GET /v1/bookings/{publicId}/receipt`, whose `editable` flag says whether this call will be accepted.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"buyerCompanyName":{"type":"string","minLength":1,"maxLength":255},"buyerTaxIdCountry":{"type":"string","enum":["IN","GB","AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK"]},"buyerTaxId":{"type":"string","minLength":1,"maxLength":50}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.updateBuyerTaxDetails` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/supplier-reservation":{"get":{"tags":["bookings"],"summary":"Live supplier reservation details for a booking","description":"Runs `booking.getSupplierReservation`; owner-only. Fetched live from the supplier on every call, so render it as its own section rather than blocking the booking screen. Never fails on a supplier problem: `available: false` (with diagnostics) covers an unconfirmed booking, a supplier without reservation lookup, an outage, and the first two minutes after booking, when the supplier forbids the lookup — refetch later. `available: true` adds `voucherIssued`, `invoiceNumber`, `roomCount`, `supplierConfirmationNumber` and `bookingStatus`, each nullable.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.getSupplierReservation` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/resend-confirmation":{"post":{"tags":["bookings"],"summary":"Email the booking confirmation again","description":"Runs `booking.resendConfirmation`; owner-only. Sends to the booking's guest email unless `email` is given, which sends a copy there without changing the booking. Send `{}` for the default. 400 when the booking is cancelled, 412 while it is not yet confirmed, 429 after 3 resends of one booking in 10 minutes (whichever inbox they went to). Returns `{ success, sentTo, usedOverride }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":255,"format":"email"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.resendConfirmation` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"412":{"description":"The booking is not confirmed yet, so there is no confirmation to send","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"429":{"description":"Rate limit reached for this caller. Show the message and let the customer retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/review":{"post":{"tags":["bookings"],"summary":"Review a stay","description":"Runs `review.create` for the booking in the path. Returns `{ publicId, status: \"pending\" }` — reviews are moderated before they are shown. 403 when the booking belongs to someone else. A second review of the same booking is a 409 whose `domainCode` is `\"REVIEW_ALREADY_EXISTS\"`; branch on that, not the message.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hotelId":{"type":"string","minLength":1,"maxLength":36},"overallRating":{"type":"integer","minimum":1,"maximum":5},"title":{"type":"string","maxLength":255},"content":{"type":"string","maxLength":5000},"dimensions":{"type":"array","items":{"type":"object","properties":{"dimension":{"type":"string","minLength":1,"maxLength":50},"rating":{"type":"integer","minimum":1,"maximum":5}},"required":["dimension","rating"]}}},"required":["overallRating"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `review.create` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The booking belongs to another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/cancel":{"post":{"tags":["bookings"],"summary":"Cancel a booking","description":"Runs `booking.cancel`; owner-only. Send `{}` when no reason or verification code applies.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":1000},"verificationCode":{"type":"string","pattern":"^\\d{6}$"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.cancel` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/share":{"post":{"tags":["bookings"],"summary":"Create a booking share link","description":"Runs `booking.createShareLink`; owner-only. Returns a `shareUrl` for the system share sheet — without it the app can only share plain text (#926). Calling again ROTATES the token, so every previously shared link dies immediately. Both PII opt-ins default to false: `includeGuests` adds the guest names and `includeRequests` the special requests to the public page, so only send them when the customer asked for it. Send `{}` to share the stay without either.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"includeGuests":{"type":"boolean"},"includeRequests":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.createShareLink` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["bookings"],"summary":"Revoke a booking share link","description":"Runs `booking.revokeShareLink`; owner-only. Clears the token and both PII opt-ins, so the previously shared URL starts answering 404. Idempotent: revoking a booking that has no live link succeeds. Until this existed the app could mint a link it had no way to take back.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.revokeShareLink` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/auto-rebook":{"patch":{"tags":["bookings"],"summary":"Turn auto-rebook on or off for a booking","description":"Runs `booking.setAutoRebook`; owner-only. Standing permission, for this one stay, to cancel and rebook it at a lower rate when the guest comes out ahead after any cancellation fee. Arming rejects with 400 unless the booking is `confirmed`, its check-in is still in the future, and it is not itself the replacement for an earlier booking. DISARMING is never rejected — withdrawing consent must always work.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.setAutoRebook` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/share/booking/{shareToken}":{"get":{"tags":["bookings"],"summary":"Read a shared booking","description":"Runs `booking.getByShareToken`. Public read: no session, because the recipient of a share link does not have one. The response is sanitized server-side — payment fields, supplier ids and contact details never reach the wire, and guest names or special requests appear only where the owner opted in. A revoked or unknown token answers the same opaque 404 either way, so this cannot be used to probe which tokens exist.","parameters":[{"schema":{"type":"string","minLength":64,"maxLength":64},"required":true,"name":"shareToken","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.getByShareToken` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/bookings/{publicId}/modify":{"post":{"tags":["bookings"],"summary":"Modify a booking (date change or special requests)","description":"Runs `booking.modify`; owner-only — a booking the bearer session does not own answers 404, exactly like a booking that does not exist. The signed-in owner may modify from the booking screen without `verificationCode`, matching the web booking page. `verificationCode` is the second half of the OTP flow the `modifyBooking` chat tool starts: when sent it must be the emailed 6-digit code (single-use, 10-minute expiry), and a wrong or expired code is a 401 with nothing changed. At least one of `checkIn`, `checkOut`, `specialRequests` must be sent (the procedure rejects an empty change with 400). A date change on a supplier-linked booking is a real supplier cancel-and-rebook: expect 409 when the new dates have no availability or the supplier can't amend in place, and 502 when the rebook failed after the original was released. Show the 409/502 message verbatim — it states what happened to the reservation and what to do next.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"specialRequests":{"type":"string","maxLength":2000},"verificationCode":{"type":"string","pattern":"^\\d{6}$"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `booking.modify` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"A supplier call failed. The message is written by the procedure for the customer (it says what happened to their booking and what to do) — show it verbatim.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips":{"get":{"tags":["trips"],"summary":"List my trips","description":"Runs `itinerary.list` for the signed-in user.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.list` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["trips"],"summary":"Create a trip","description":"Runs `itinerary.create`. Saving a trip requires sign-in (critical decision #4).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":4000},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"destinationId":{"type":"integer","exclusiveMinimum":0},"budget":{"type":"number","minimum":0,"maximum":10000000},"budgetCurrency":{"type":"string","minLength":3,"maxLength":3},"coverImageUrl":{"type":"string","maxLength":2048,"format":"uri"},"aiScore":{"type":"integer","minimum":1,"maximum":10},"aiVerdict":{"type":"string","enum":["ready","improve","reject"]},"aiSummary":{"type":"string","maxLength":400},"aiIssues":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["blocker","warning","nit"]},"where":{"type":"string","maxLength":60},"problem":{"type":"string","maxLength":300},"fix":{"type":"string","maxLength":300}},"required":["severity","where","problem","fix"]},"maxItems":6}},"required":["title","startDate","endDate"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.create` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}":{"get":{"tags":["trips"],"summary":"Trip detail","description":"Runs `itinerary.getByPublicId`. Public read: published trips and share-token links resolve without a session.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"},{"schema":{"type":"string","maxLength":50},"required":false,"name":"shareToken","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.getByPublicId` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["trips"],"summary":"Delete a trip","description":"Runs `itinerary.remove`; owner-only (collaborators cannot delete). Permanent: the trip and every item go. A trip the caller does not own answers 404.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.remove` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/publish":{"post":{"tags":["trips"],"summary":"Publish a trip","description":"Runs `itinerary.publish`; owner-only. Makes the trip publicly readable and returns `{ shareToken }`. Publishing again keeps the same token, so links already shared keep working. The web share link is `https://www.getmyhotels.com/trips/{publicId}?token={shareToken}`; `GET /v1/trips/{publicId}` takes the same value as `shareToken`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.publish` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/unpublish":{"post":{"tags":["trips"],"summary":"Unpublish a trip","description":"Runs `itinerary.unpublish`; owner-only. Makes the trip private again and clears the share token, so every previously shared link stops working.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.unpublish` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/items":{"post":{"tags":["trips"],"summary":"Add an item to a trip","description":"Runs `itinerary.addItem`. The owner and `editor` collaborators may add; anyone else gets 403. `day` is 1-based and `sortOrder` orders items within a day. Returns the new item's numeric `{ id }`, which `DELETE /v1/trips/{publicId}/items/{itemId}` takes.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"day":{"type":"integer","minimum":1,"maximum":365},"sortOrder":{"type":"integer","minimum":0,"default":0},"type":{"type":"string","enum":["hotel","flight","experience","transfer","activity","meal","note"]},"title":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000},"referenceId":{"type":"string","minLength":1,"maxLength":36},"bookingId":{"type":"integer","exclusiveMinimum":0},"startTime":{"type":"string","maxLength":10},"endTime":{"type":"string","maxLength":10},"location":{"type":"string","maxLength":500},"address":{"type":"string","maxLength":500},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"notes":{"type":"string","maxLength":2000},"estimatedCost":{"type":"number","minimum":0,"maximum":1000000},"currency":{"type":"string","minLength":3,"maxLength":3},"imageUrl":{"type":"string","maxLength":2048,"format":"uri"}},"required":["day","type","title"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.addItem` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The caller may not edit this trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/items/{itemId}":{"delete":{"tags":["trips"],"summary":"Remove an item from a trip","description":"Runs `itinerary.removeItem`. The owner and `editor` collaborators may remove; anyone else gets 403. Idempotent: an item id that is not on this trip succeeds and changes nothing.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"},{"schema":{"type":"integer","example":42},"required":true,"name":"itemId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.removeItem` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The caller may not edit this trip","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/collaborators":{"get":{"tags":["trips"],"summary":"List a trip's collaborators","description":"Runs `itinerary.listCollaborators`. Readable by the owner and every collaborator; anyone else gets 403. Returns `{ collaborators: [{ userId, email, role, addedAt }] }`; `role` is `editor` today.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.listCollaborators` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"The trip is private to its owner and collaborators","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["trips"],"summary":"Invite a collaborator to a trip","description":"Runs `itinerary.addCollaborator`; owner-only. The invitee must already have an account (404 with a sign-up hint otherwise); inviting yourself is a 400 and inviting an existing collaborator a 409. `role` defaults to `editor`. Returns `{ collaborator }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email"},"role":{"type":"string","enum":["editor"],"default":"editor"}},"required":["email"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.addCollaborator` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/trips/{publicId}/collaborators/{userId}":{"delete":{"tags":["trips"],"summary":"Remove a collaborator from a trip","description":"Runs `itinerary.removeCollaborator`; owner-only. `userId` is the collaborator's `userId` from the list. 404 when the trip is not the caller's or that user is not a collaborator.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"userId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `itinerary.removeCollaborator` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/support/tickets":{"post":{"tags":["support"],"summary":"Create a support ticket","description":"Runs `support.createTicket` for the signed-in user. `priority` is optional and defaults to `medium`; customer clients should omit it. `message` becomes the first reply on the thread.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"maxLength":500},"message":{"type":"string","minLength":1,"maxLength":5000},"bookingType":{"type":"string","enum":["hotel","flight"]},"bookingPublicId":{"type":"string","minLength":1,"maxLength":36},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium"},"contactEmail":{"type":"string","maxLength":255,"format":"email"},"contactPhone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"channel":{"type":"string","enum":["web","mobile","email","phone","chat","social"]},"category":{"type":"string","enum":["booking","payment","refund","account","technical","complaint","suggestion","other"]}},"required":["subject"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.createTicket` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["support"],"summary":"List my support tickets","description":"Runs `support.listTickets` for the signed-in user.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","enum":["open","in_progress","resolved","closed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["all","low","medium","high","urgent"],"default":"all"},"required":false,"name":"priority","in":"query"},{"schema":{"type":"string","maxLength":255},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","description":"Comma-separated ticket statuses. A client tab is a SET (Open = open,in_progress; Resolved = resolved,closed), which the single-valued `status` cannot express.","example":"open,in_progress"},"required":false,"name":"statuses","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.listTickets` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/support/guest-tickets":{"post":{"tags":["support"],"summary":"Contact support without an account","description":"Runs `support.guestCreateTicket` (public). For a signed-out guest — typically one who booked without an account (critical decision #4). Stores the ticket against `email` and returns only `{ publicId }`: the link to read and reply to the thread is emailed to that address, so opening it proves the guest owns the inbox. The response is the same whether or not an account uses that email. Signed-in callers get 400 and must use `POST /v1/support/tickets`. Rate limited to 5 tickets per 10 minutes per IP and 3 per email address per hour (429). Native callers send no Turnstile token. `locale` picks the email language and falls back to English.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":255,"format":"email"},"subject":{"type":"string","minLength":1,"maxLength":500},"message":{"type":"string","minLength":1,"maxLength":5000},"category":{"type":"string","enum":["booking","payment","refund","account","technical","complaint","suggestion","other"]},"locale":{"type":"string","maxLength":10}},"required":["email","subject","message"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.guestCreateTicket` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"429":{"description":"Rate limit reached for this caller. Show the message and let the customer retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/support/tickets/{publicId}":{"get":{"tags":["support"],"summary":"Support ticket detail","description":"Runs `support.getTicket`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.getTicket` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/support/tickets/{publicId}/comments":{"post":{"tags":["support"],"summary":"Reply to a support ticket","description":"Runs `support.addComment`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","minLength":1,"maxLength":5000}},"required":["content"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `support.addComment` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches":{"post":{"tags":["price-watches"],"summary":"Create a price watch","description":"Runs `priceWatch.create` for the signed-in user.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ref":{"type":"string","minLength":1},"checkIn":{"type":["string","null"]},"checkOut":{"type":["string","null"]},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":9,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"childrenAges":{"type":"array","items":{"type":"integer","minimum":0,"maximum":18},"maxItems":6,"default":[]},"currency":{"type":"string","minLength":3,"maxLength":3},"currentPrice":{"type":"number","exclusiveMinimum":0},"targetPrice":{"type":"number","exclusiveMinimum":0},"viewedRoomName":{"type":"string","maxLength":255},"viewedBoard":{"type":"string","maxLength":100},"viewedRoomPrice":{"type":"number","exclusiveMinimum":0},"hotelName":{"type":"string","maxLength":255},"hotelImageUrl":{"type":"string","maxLength":2000,"format":"uri"},"city":{"type":"string","maxLength":255},"starRating":{"type":"number","minimum":0,"maximum":5},"source":{"type":"string","enum":["chat","hotel_page","search_card"],"default":"hotel_page"}},"required":["ref","checkIn","checkOut","currency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.create` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["price-watches"],"summary":"List my price watches","description":"Runs `priceWatch.list` for the signed-in user.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["active","paused","expired","cancelled"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.list` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/{publicId}":{"get":{"tags":["price-watches"],"summary":"Price watch detail","description":"Runs `priceWatch.detail`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.detail` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["price-watches"],"summary":"Cancel a price watch","description":"Runs `priceWatch.remove`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.remove` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/{publicId}/pause":{"post":{"tags":["price-watches"],"summary":"Pause a price watch","description":"Runs `priceWatch.pause`; owner-only. Idempotent — pausing a watch that is not active returns its current status unchanged.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.pause` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/{publicId}/resume":{"post":{"tags":["price-watches"],"summary":"Resume a paused price watch","description":"Runs `priceWatch.resume`; owner-only. Rejects with 400 when the stay's check-in has already passed — that watch can only be replaced, not revived.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.resume` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/{publicId}/target":{"patch":{"tags":["price-watches"],"summary":"Set or clear a price watch's target","description":"Runs `priceWatch.updateTarget`; owner-only. A null `targetPrice` clears the target. Rejects with 400 when the target is not below the baseline the watch started at.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"targetPrice":{"type":["number","null"],"exclusiveMinimum":0}},"required":["targetPrice"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.updateTarget` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/destinations":{"post":{"tags":["price-watches"],"summary":"Watch the cheapest price for a whole destination search","description":"Runs `priceWatch.createSearchWatch`. Tracks the cheapest hotel for `destination` and the stay, rather than one hotel. Send `currentPrice` (the cheapest price on screen, a stay total) when creating from live results so it becomes the baseline; `targetPrice` must be below it. `source` defaults to `saved_page`; native search results should send `search_results`. 400 for past or reversed dates, or when the account already tracks the maximum number of destinations; 409 when this destination and stay are already tracked. Returns `{ watch }`, the same shape the destination entries of `GET /v1/price-watches` carry.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination":{"type":"string","minLength":2,"maxLength":255},"checkIn":{"type":"string","format":"date"},"checkOut":{"type":"string","format":"date"},"rooms":{"type":"integer","minimum":1,"maximum":9,"default":1},"adults":{"type":"integer","minimum":1,"maximum":9,"default":2},"children":{"type":"integer","minimum":0,"maximum":6,"default":0},"currency":{"type":"string","minLength":3,"maxLength":3},"currentPrice":{"type":"number","exclusiveMinimum":0},"targetPrice":{"type":"number","exclusiveMinimum":0},"source":{"type":"string","enum":["chat","search_results","saved_page"],"default":"saved_page"}},"required":["destination","checkIn","checkOut","currency"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.createSearchWatch` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"The request conflicts with the resource's current state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/price-watches/destinations/{publicId}":{"delete":{"tags":["price-watches"],"summary":"Cancel a destination price watch","description":"Runs `priceWatch.removeSearchWatch`; owner-only. Idempotent — cancelling an already-cancelled watch succeeds.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.removeSearchWatch` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"tags":["price-watches"],"summary":"Destination price watch detail","description":"Runs `priceWatch.searchWatchDetail`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `priceWatch.searchWatchDetail` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/live-activities":{"post":{"tags":["live-activities"],"summary":"Register a Live Activity push token","description":"Runs `liveActivity.register`. Idempotent per token — a device re-registers on every launch and after every token rotation, so this upserts rather than inserts.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["start","update"]},"pushToken":{"type":"string","minLength":32,"maxLength":200,"pattern":"^[0-9a-fA-F]+$"},"bookingPublicId":{"type":["string","null"],"minLength":1,"maxLength":36,"default":null},"activityId":{"type":["string","null"],"minLength":1,"maxLength":64,"default":null},"environment":{"type":"string","enum":["production","development"],"default":"production"}},"required":["kind","pushToken"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `liveActivity.register` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["live-activities"],"summary":"Retire every Live Activity token for the signed-in user","description":"Runs `liveActivity.unregisterAll`. Called on sign-out.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `liveActivity.unregisterAll` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/live-activities/{pushToken}":{"delete":{"tags":["live-activities"],"summary":"Retire one Live Activity push token","description":"Runs `liveActivity.unregister`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"pushToken","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `liveActivity.unregister` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/devices":{"post":{"tags":["devices"],"summary":"Register this device for push notifications","description":"Runs `push.register`. Idempotent per token — the app calls this on every launch, which is what keeps `lastSeenAt` meaningful. A bearer token is honored but NOT required, and sending one is what claims a guest's device row into their account: the row is upserted on the token, so the first launch after signing in re-points an existing guest registration at the user. There is no separate claim call.\n\n`platform` selects the push service and, with it, how `token` is validated. It defaults to `ios` for the clients that shipped before it existed.\n\n**iOS (APNs).** Send the token as lowercase hex with no separators — `Data.description` prints `<a1b2 c3d4 …>`, which is not that, and a non-hex token is rejected here rather than becoming a row that can never receive a push. `environment` picks the APNs host and the two are not interchangeable. `bundleId` is the APNs topic, stored per device so an extension cannot inherit the app's.\n\n**Android (FCM).** Send the registration token exactly as `FirebaseMessaging.getToken()` returned it — base64url with a colon, longer than an APNs token and never hex. `environment` is always `production`: FCM has one endpoint and one credential for every build. `bundleId` is the applicationId (`com.gmh.mobile`, or `com.gmh.mobile.dev` for a debug build); FCM routes on the token alone, so it is a record of which build registered rather than an address.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":32,"maxLength":255},"platform":{"type":"string","enum":["ios","android"],"default":"ios"},"environment":{"type":"string","enum":["production","development"],"default":"production"},"bundleId":{"type":"string","minLength":1,"maxLength":128},"locale":{"type":["string","null"],"minLength":2,"maxLength":16,"default":null}},"required":["token","bundleId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `push.register` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["devices"],"summary":"Retire every push token for the signed-in account","description":"Runs `push.unregisterAll`. Called on sign-out. Protected — \"all of mine\" needs an owner to mean anything — and enforced by the procedure rather than by the path list, since `POST /v1/devices` on the same path must stay guest-reachable.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `push.unregisterAll` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/devices/{token}":{"delete":{"tags":["devices"],"summary":"Retire one device push token","description":"Runs `push.unregister`. Called when the customer withdraws notification permission or the token rotates. Public for the same reason registration is — a guest device that registered without a session must be able to retire itself. Takes either an APNs or an FCM token; the row is found by the token alone, so the caller does not repeat its platform.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"token","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `push.unregister` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions":{"get":{"tags":["chat"],"summary":"List my chat sessions","description":"Runs `chat.listSessions` for the signed-in user. Pinned sessions come first, then the rest by last message. `query` filters on title and summary (case-insensitive substring). `archived=true` lists ONLY archived sessions, the restore half of `POST /v1/chat/sessions/{sessionPublicId}/archive`; omitted or `false` lists only the unarchived ones, as before.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":100,"example":"Lisbon"},"required":false,"name":"query","in":"query"},{"schema":{"type":"boolean"},"required":false,"name":"archived","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.listSessions` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions/{sessionPublicId}/messages":{"get":{"tags":["chat"],"summary":"Chat session history","description":"Runs `chat.getHistory`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"},{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"boolean"},"required":false,"name":"includeVariants","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.getHistory` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions/{sessionPublicId}":{"delete":{"tags":["chat"],"summary":"Delete a chat session","description":"Runs `chat.deleteSession`; owner-only. Permanent — the session, its messages, attachments, tool state and handoffs are all removed. There is no soft-delete and no restore; a client offering undo must hold the call, not expect the server to reverse it.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.deleteSession` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"tags":["chat"],"summary":"Rename a chat session","description":"Runs `chat.updateTitle`; owner-only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":200}},"required":["title"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.updateTitle` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions/{sessionPublicId}/pin":{"post":{"tags":["chat"],"summary":"Pin or unpin a chat session","description":"Runs `chat.togglePin`; owner-only. `pinned` is the desired end state, not a toggle, so a double tap cannot race itself into the wrong one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pinned":{"type":"boolean"}},"required":["pinned"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.togglePin` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions/{sessionPublicId}/archive":{"post":{"tags":["chat"],"summary":"Archive or unarchive a chat session","description":"Runs `chat.toggleArchive`; owner-only. Soft-hides the session from `GET /v1/chat/sessions` without deleting anything, and clears any pin so a hidden chat does not reappear pinned. `GET /v1/chat/sessions?archived=true` lists the archived ones; send `archived: false` here to restore one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"archived":{"type":"boolean"}},"required":["archived"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.toggleArchive` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/sessions/{sessionPublicId}/share":{"post":{"tags":["chat"],"summary":"Create a chat share link","description":"Runs `chat.createShareLink`; owner-only. Send `{}` to accept the default share options.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"sessionPublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"includeTools":{"type":"boolean"},"includeReasoning":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.createShareLink` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/guest-session":{"post":{"tags":["chat"],"summary":"Mint a chat session for streaming","description":"Runs `chat.createGuestSession` (public — guests chat without an account, critical decision #4). Returns `{ publicId, sessionToken }`; the id + token pair rides the `POST /api/chat` stream body, and the one-time token later authorizes `POST /v1/chat/claim-guest-session` after sign-in. Signed-in callers get `sessionToken: null` — the minted session is already theirs. Lazy server-side: no row is persisted until the first message lands. The procedure takes no input, so this route accepts no request body.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.createGuestSession` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/claim-guest-session":{"post":{"tags":["chat"],"summary":"Claim a guest chat session after sign-in","description":"Runs `chat.claimGuestSession`. Transfers ownership of the guest conversation to the now-signed-in account, authorized by the one-time sessionToken minted alongside the guest session (critical decision #4). Re-claiming a session the caller already owns is an idempotent success.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionPublicId":{"type":"string","minLength":1,"maxLength":36},"sessionToken":{"type":"string","minLength":1,"maxLength":64}},"required":["sessionPublicId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.claimGuestSession` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/messages/{messagePublicId}/feedback":{"post":{"tags":["chat"],"summary":"Rate an assistant message","description":"Runs `chat.setMessageFeedback` (public — guests vote too, authorized by the guest `sessionToken`). Assistant messages only. `type` is the vote; `category` and `reason` carry the thumbs-down detail and are optional, so a bare vote is a valid body. Re-posting a different `type` replaces the previous vote rather than stacking.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"messagePublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionToken":{"type":"string","maxLength":64},"type":{"type":"string","enum":["thumbs_up","thumbs_down"]},"category":{"type":"string","enum":["incorrect","unhelpful","offensive","refused","style","other"]},"reason":{"type":"string","maxLength":2000}},"required":["type"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.setMessageFeedback` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["chat"],"summary":"Withdraw a rating","description":"Runs `chat.clearMessageFeedback` (public, same guest rules as the POST). Clearing a message that was never rated is an idempotent success. The body carries only the guest `sessionToken`, so signed-in callers may send `{}`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"messagePublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionToken":{"type":"string","maxLength":64}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.clearMessageFeedback` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/messages/{messagePublicId}/edit":{"post":{"tags":["chat"],"summary":"Edit one of my earlier prompts","description":"Runs `chat.editMessage`; owner-only, user messages only. Writes a new row carrying `newText` and supersedes the original along with everything after it, so the conversation reads linearly from the edited prompt onward. Returns the new `newPublicId`, which the client sends as `parentMessagePublicId` on the turn it starts next so the fresh answer links to the edited prompt.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"messagePublicId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"newText":{"type":"string","minLength":1,"maxLength":10000}},"required":["newText"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.editMessage` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/messages/{messagePublicId}/regenerate":{"post":{"tags":["chat"],"summary":"Ask for a different answer","description":"Runs `chat.regenerateMessage`; owner-only, assistant messages only. This records the intent and resolves the prompt the answer came from — it does NOT stream. The client drops the old answer and opens an ordinary turn, passing the returned `parentUserMessagePublicId` as `parentMessagePublicId` so both answers hang off the same prompt.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"messagePublicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.regenerateMessage` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/messages/{messagePublicId}/variant":{"post":{"tags":["chat"],"summary":"Select a conversation branch","description":"Runs `chat.selectMessageVariant`; owner-only. Pass an edit sibling to persist that branch as the selected transcript. Non-edit messages are an idempotent no-op, matching the web client's local-only regenerate navigation.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"messagePublicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `chat.selectMessageVariant` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/phone/request-otp":{"post":{"tags":["phone"],"summary":"Request a phone verification code","description":"Runs `phone.requestOtp` (public — the post-signup verify-phone flow). Sends a 6-digit SMS code to the E.164 phone and returns `{ verificationPublicId }`. Rate-limited and suppressed numbers get the SAME response shape by design, so callers cannot enumerate which numbers are blocked.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"phone":{"type":"string","maxLength":80},"locale":{"type":"string","enum":["en","es","fr","de","zh","hi","pt","it","ja","te","gu","ko","bn","ru"],"default":"en"},"turnstileToken":{"type":"string","maxLength":2048}},"required":["phone"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `phone.requestOtp` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/phone/verify-otp":{"post":{"tags":["phone"],"summary":"Verify a phone OTP code","description":"Runs `phone.verifyOtp` (public). Checks the 6-digit code for `verificationPublicId`; the verified phone is bound to a profile only when the same bearer session was present for both the OTP request and verification. Codes expire after 10 minutes and lock after 5 failed attempts.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"verificationPublicId":{"type":"string","minLength":1},"code":{"type":"string"}},"required":["verificationPublicId","code"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `phone.verifyOtp` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/consents":{"post":{"tags":["consents"],"summary":"Record a batch of consent decisions","description":"Runs `compliance.recordConsentBatch` (public — signup and cookie-banner capture). Anonymous callers MUST send a `cookie:<cuid>` subjectId and a `cookie_banner` or `signup_form` source; signed-in callers are bound to their own account automatically and the subjectId is ignored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subjectId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^(cookie:[a-z0-9]{20,32}|[a-zA-Z0-9_-]{1,64})$"},"decisions":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string","enum":["cookies.analytics","cookies.marketing","cookies.preferences","marketing.email","marketing.sms","marketing.push","ai.training","ccpa.do_not_sell","terms","privacy","age_confirmation","auto_rebook"]},"granted":{"type":"boolean"}},"required":["purpose","granted"]},"minItems":1,"maxItems":20},"policyVersion":{"type":"string","maxLength":16},"source":{"type":"string","enum":["cookie_banner","signup_form","settings","stop_keyword","unsubscribe_link","admin"]}},"required":["decisions","source"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `compliance.recordConsentBatch` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/account/set-password":{"post":{"tags":["account"],"summary":"Legacy password setup route; email verification required","description":"Retained for older clients. Returns PRECONDITION_FAILED: a signed-in session alone cannot establish a password. Use POST /api/auth/request-password-reset and complete the emailed link through POST /api/auth/reset-password to set the first password or replace a forgotten one. Password reset revokes existing sessions.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"newPassword":{"type":"string","minLength":6,"maxLength":128}},"required":["newPassword"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `auth.setPassword` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/account/delete":{"post":{"tags":["account"],"summary":"Delete the signed-in user's own account","description":"Runs `user.deleteMyAccount`. IRREVERSIBLE. Anonymizes the Better Auth `user` row (name/email/image blanked to `deleted-<id>@anon.local`) and BANS it so sign-in is impossible, hard-deletes the owned personal-data tables (profile, addresses, saved payment methods, travel companions, recently viewed, saved searches, price alerts and watches, notification preferences, notifications, wishlists), revokes every active session, then writes a `gdpr_request` deletion record and a `user.self_deleted` audit entry. Bookings, reviews, support tickets and the audit log are deliberately KEPT for legal and accounting retention — they now point at the anonymized row. Requires `{ \"confirm\": \"DELETE MY ACCOUNT\" }` verbatim; anything else is a 400. Refuses non-`customer` roles with 403 FORBIDDEN — staff and admin accounts go through admin offboarding so access can be handed over first.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"string","enum":["DELETE MY ACCOUNT"]}},"required":["confirm"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.deleteMyAccount` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/account/profile":{"get":{"tags":["account"],"summary":"Read the signed-in user's profile","description":"Runs `user.getProfile`. Returns `{ user, profile }`: the Better Auth `user` row (id, name, email, emailVerified, image, role, timestamps) plus the `user_profile` row, which is `null` until the customer has saved something. The bearer session payload carries the user half only, so this is the sole way to read the stored phone, date of birth, nationality, preferred currency/language/country and address that the settings screen and the checkout guest defaults render. Read `user.name` from here rather than from the session: this is our copy, so it is populated once `PATCH /v1/account/profile` has run even when the identity provider never gave us one.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.getProfile` tRPC procedure, WITHOUT the `profile.travelPreferences` field(s) — internal-only data stripped from this surface; clients must never depend on them. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"patch":{"tags":["account"],"summary":"Update the signed-in user's profile","description":"Runs `user.updateProfile`. PARTIAL update: send only the fields being changed. An omitted field is left untouched — it is not cleared — and at least one field must be present or the call is a 400. There is no clear-a-field verb: no field accepts JSON `null` (that is a 400 too), so a value can only be replaced, never erased. `name` is the field this route exists for. It writes `user.name`, the name the Settings account row and the checkout guest form both render, and it is the only writer of that column outside admin tooling — Sign in with Apple hands back a name on the first consent only, so for most native accounts it starts empty and stays that way until this call runs. An all-whitespace name is ignored rather than written (and an empty string is a 400), so a stray save cannot wipe a name the customer already set. CHANGING `phone` DELIBERATELY RESETS that number's verified state — `phoneVerifiedAt` goes back to null, so booking-confirmation SMS stops being delivered until the customer re-runs `POST /v1/phone/request-otp` then `POST /v1/phone/verify-otp`. Any screen with a phone field must say so and send them back through that flow; the reset is an audit fix, because without it a profile update could point a stay's confirmation SMS at another number while inheriting the old verification. `dateOfBirth` is an ISO-8601 date string (`YYYY-MM-DD`), not a Unix timestamp. `nationality` and `country` are ISO 3166-1 alpha-3; `preferredCountry` is alpha-2, and the three preference fields are enum-validated against the supported currency/language/country lists, so an unsupported value is a 400 rather than a silently stored string. `avatarUrl` must be HTTPS (or an empty string). Returns `{ success: true }` only — it does not echo the saved row, so refetch `GET /v1/account/profile` when the screen needs to show what was stored.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"phone":{"anyOf":[{"type":"string","maxLength":80},{"type":"string","minLength":0,"maxLength":0}]},"dateOfBirth":{"type":"string","format":"date"},"nationality":{"type":"string","minLength":3,"maxLength":3},"preferredCurrency":{"type":"string","enum":["USD","GBP","EUR","INR"]},"preferredLanguage":{"type":"string","enum":["en","es","fr","de","zh","hi","pt","it","ja","te","gu","ko","bn","ru"]},"preferredCountry":{"type":["string","null"],"enum":["US","CA","DE","FR","IT","ES","NL","BE","AT","IE","PT","FI","GR","LU","SI","SK","EE","LV","LT","MT","CY","HR","SE","DK","PL","CZ","HU","RO","BG","NO","IS","LI","CH","GB","GG","JE","IM","IN"]},"address":{"type":"string","maxLength":500},"city":{"type":"string","maxLength":255},"state":{"type":"string","maxLength":255},"country":{"type":["string","null"],"minLength":3,"maxLength":3},"postalCode":{"type":"string","maxLength":20},"avatarUrl":{"anyOf":[{"type":"string","maxLength":2048,"format":"uri"},{"type":"string","enum":[""]}]}},"description":"Partial update. Every field is optional and an omitted field is left unchanged — this never clears anything. No field accepts null, so a stored value can be replaced but not erased. At least one field must be present.","minProperties":1}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.updateProfile` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/account/export":{"post":{"tags":["account"],"summary":"Export everything we hold about the signed-in user","description":"Runs `user.exportMyData`, the same GDPR/DSAR export the web profile downloads as `gmh-data-export-<date>.json`. Returns one JSON document (profile, addresses, saved cards without provider tokens, bookings, reviews, chats, support tickets, notifications, consents and more); hand it to the share sheet as a file rather than rendering it. Each call is recorded as a data-access request. Takes no input, so this route accepts no request body.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.exportMyData` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/account/memory":{"get":{"tags":["account"],"summary":"Read what the assistant remembers about the user","description":"Runs `user.getMemoryNote`. Returns `{ note }`: the free-form note the assistant reads on every chat turn, plus any preferences it learned on its own, as one editable string. An empty string means nothing is remembered.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.getMemoryNote` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"put":{"tags":["account"],"summary":"Replace what the assistant remembers about the user","description":"Runs `user.setMemoryNote`. Replaces the whole note (up to 4000 characters); learned preferences shown by `GET /v1/account/memory` are folded into it, so send back the edited text, not a delta. An empty or whitespace-only `note` clears the memory. Returns `{ success: true }`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":4000}},"required":["note"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.setMemoryNote` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/notifications":{"get":{"tags":["notifications"],"summary":"List my in-app notifications","description":"Runs `notification.list`, newest first. `unreadOnly=true` limits it to unread ones. Returns `{ notifications, page, limit }`; each notification is `{ publicId, type, category, priority, title, body, data, actionUrl, imageUrl, readAt, createdAt, expiresAt }`. `type` includes `price_drop`, `rebook_alert` and `booking_auto_rebooked` among others — render unknown types with a generic icon. `data` is a JSON-encoded string or null; `actionUrl` is a site-relative or absolute link to open on tap.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"default":1,"example":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"boolean"},"required":false,"name":"unreadOnly","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.list` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/notifications/unread-count":{"get":{"tags":["notifications"],"summary":"Count my unread notifications","description":"Runs `notification.unreadCount`. Returns `{ count }` for the inbox badge; it counts exactly what `GET /v1/notifications?unreadOnly=true` lists.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.unreadCount` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/notifications/read-all":{"post":{"tags":["notifications"],"summary":"Mark every notification read","description":"Runs `notification.markAllRead`. Returns `{ count }`, how many were unread. Takes no input, so this route accepts no request body.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.markAllRead` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/notifications/{publicId}/read":{"post":{"tags":["notifications"],"summary":"Mark one notification read","description":"Runs `notification.markRead`; owner-only. Idempotent — returns `{ success: true, alreadyRead }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.markRead` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/notifications/preferences":{"get":{"tags":["notifications"],"summary":"Read my notification preferences","description":"Runs `notification.listEmailPreferences` and `notification.listInAppPreferences`. Returns `{ email: [{ code, enabled }], inApp: [{ code, enabled }] }`. Email codes are `onboarding`, `reminder`, `win_back`, `partner`, `price_alert` and `rebook_alert`; the in-app list carries `price_alert`. Everything defaults to enabled. Booking, account and refund emails always send and are not listed.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.listEmailPreferences` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"put":{"tags":["notifications"],"summary":"Update my notification preferences","description":"Runs `notification.updatePreferences`. Upserts each `{ channel, type, enabled }` and leaves unlisted ones alone, so send only the toggles that changed. Use `channel: \"email\"` with an email code from `GET /v1/notifications/preferences` and `channel: \"in_app\"` with an in-app code. Turning `rebook_alert` off on any channel stops rebook watches for new bookings. Returns `{ success: true, count }`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"array","items":{"type":"object","properties":{"channel":{"type":"string","enum":["email","push","sms","in_app"]},"type":{"type":"string","minLength":1,"maxLength":50},"enabled":{"type":"boolean"}},"required":["channel","type","enabled"]},"minItems":1}},"required":["preferences"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `notification.updatePreferences` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/favorites":{"get":{"tags":["favorites"],"summary":"List my favorites","description":"Runs `user.listFavorites` for one `entityType` (default `hotel`), newest first, one entry per hotel. Returns `{ entityIds, items }`: `items` are `{ entityType, entityId, hotelKey, ref, name, imageUrl, city, country, starRating }` where `entityId` equals `hotelKey` for a supplier hotel. `ref` is freshly minted on every response and opens `GET /v1/hotels/{ref}`; it is null when the row cannot mint one. The display fields are the snapshot the client sent when it saved the row, and are null for rows saved before snapshots existed (render a placeholder; never fail). Each item also has `savedAt`. `entityIds` lists the same entries' ids, in order.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["hotel","flight","car"],"default":"hotel"},"required":false,"name":"entityType","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.listFavorites` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["favorites"],"summary":"Favorite a hotel","description":"Runs `user.setFavorite` with `favorited: true`. Send the hotel's `hotelKey` (`hk_…`, from search or detail) as `entityId`, plus its `ref` and the display fields `name`, `imageUrl` (https or site-relative), `city`, `country`, `starRating` (0-5) so the list can open and render the hotel on any device. A `ref` naming a different hotel than `entityId` is a 400. An `sh_` ref sent as `entityId` is accepted and filed under its hotel key. Idempotent: favoriting something already favorited leaves one entry and refreshes its snapshot (unlike the web toggle). Returns `{ favorited: true, entityId }`, `entityId` being the key the entry is stored under.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["hotel","flight","car"]},"entityId":{"type":"string","minLength":1,"maxLength":255},"ref":{"type":["string","null"],"minLength":1,"maxLength":255},"name":{"type":["string","null"],"minLength":1,"maxLength":255},"imageUrl":{"type":["string","null"],"maxLength":2048},"city":{"type":["string","null"],"maxLength":255},"country":{"type":["string","null"],"maxLength":100},"starRating":{"type":["number","null"],"minimum":0,"maximum":5},"note":{"type":"string","maxLength":500}},"required":["entityType","entityId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.setFavorite` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"delete":{"tags":["favorites"],"summary":"Unfavorite a hotel","description":"Runs `user.setFavorite` with `favorited: false`. `entityId` is the `hotelKey` (or an `sh_` ref of the same hotel); this also removes entries saved under a ref before hotel keys existed. Idempotent: removing something that is not a favorite succeeds. Returns `{ favorited: false, entityId }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["hotel","flight","car"]},"required":true,"name":"entityType","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":255},"required":true,"name":"entityId","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.setFavorite` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/recent-views":{"get":{"tags":["favorites"],"summary":"List what I viewed recently","description":"Runs `user.getRecentlyViewed`, newest first, one entry per hotel, from the last 50 views; filter with `entityType`. Returns `{ items }`: each is `{ entityType, entityId, hotelKey, ref, name, imageUrl, city, country, starRating }` where `entityId` equals `hotelKey` for a supplier hotel. `ref` is freshly minted on every response and opens `GET /v1/hotels/{ref}`; it is null when the row cannot mint one. The display fields are the snapshot the client sent when it saved the row, and are null for rows saved before snapshots existed (render a placeholder; never fail). Each item also has `viewedAt`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["hotel","flight","car"]},"required":false,"name":"entityType","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":50,"default":20,"example":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.getRecentlyViewed` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["favorites"],"summary":"Record a view","description":"Runs `user.trackView`. Call when the customer opens a hotel. Send the hotel's `hotelKey` (`hk_…`, from search or detail) as `entityId`, plus its `ref` and the display fields `name`, `imageUrl` (https or site-relative), `city`, `country`, `starRating` (0-5) so the list can open and render the hotel on any device. A `ref` naming a different hotel than `entityId` is a 400. An `sh_` ref sent as `entityId` is accepted and filed under its hotel key. A repeat view of the same hotel moves it to the top and refreshes its snapshot instead of adding an entry. Returns `{ success: true }`. Best-effort on the client: a failure here must never block the screen.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["hotel","flight","car"]},"entityId":{"type":"string","minLength":1,"maxLength":255},"ref":{"type":["string","null"],"minLength":1,"maxLength":255},"name":{"type":["string","null"],"minLength":1,"maxLength":255},"imageUrl":{"type":["string","null"],"maxLength":2048},"city":{"type":["string","null"],"maxLength":255},"country":{"type":["string","null"],"maxLength":100},"starRating":{"type":["number","null"],"minimum":0,"maximum":5}},"required":["entityType","entityId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.trackView` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payment-methods":{"get":{"tags":["payment-methods"],"summary":"List my saved cards","description":"Runs `user.listPaymentMethods`. Returns `{ paymentMethods: [{ publicId, type, brand, last4, expiryMonth, expiryYear, isDefault, holderName }] }` for active cards; no provider token ever leaves the server. `brand` is Stripe's lowercase name (`visa`, `mastercard`, `amex`, ...). The billing address is the profile address on `GET`/`PATCH /v1/account/profile`.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.listPaymentMethods` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"tags":["payment-methods"],"summary":"Save a card confirmed through a SetupIntent","description":"Runs `user.attachStripePaymentMethod`. Send the `pm_` id Stripe returned after confirming the SetupIntent from `POST /v1/payment-methods/setup-intent`. Brand, last 4 and expiry are read from Stripe, not the client. Card payment methods only (400 otherwise). The card must be unattached (it is then attached to the user's Stripe customer) or already on that customer; a card on any other customer is a 400. Saving the same card twice returns the existing row with `alreadySaved: true`. `isDefault` (default false) makes it the default card. Returns `{ publicId, alreadySaved }`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"stripePaymentMethodId":{"type":"string","pattern":"^pm_"},"isDefault":{"type":"boolean","default":false}},"required":["stripePaymentMethodId"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.attachStripePaymentMethod` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payment-methods/setup-intent":{"post":{"tags":["payment-methods"],"summary":"Start saving a card","description":"Runs `user.createSetupIntent`. Returns `{ clientSecret }` for a Stripe SetupIntent on the user's Stripe customer (created on first use). Confirm it with the Stripe SDK — the card number never touches our servers — then send the resulting `pm_` id to `POST /v1/payment-methods`. Redirect-based methods are excluded, so no return URL is needed. Takes no input, so this route accepts no request body.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.createSetupIntent` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/payment-methods/{publicId}":{"delete":{"tags":["payment-methods"],"summary":"Remove a saved card","description":"Runs `user.removePaymentMethod`; owner-only. Detaches the card from Stripe and hides it from checkout. A card the caller does not own answers 404. Returns `{ success: true }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":36},"required":true,"name":"publicId","in":"path"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"description":"Verbatim JSON output of the `user.removePaymentMethod` tRPC procedure. Response shapes are intentionally not modeled in this spec; the native client types them by hand."}}}},"400":{"description":"Request validation failed or the router rejected the input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid bearer session token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"426":{"description":"Reported X-GMH-App-Version is below the supported minimum — the app must update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpgradeRequiredError"}}}},"500":{"description":"Unexpected server error (opaque; details stay in logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/chat":{"post":{"tags":["chat"],"summary":"AI chat streaming (SSE)","description":"Streams the AI chat turn as server-sent events. Served from the WEB origin (https://www.getmyhotels.com/api/chat), NOT from api.getmyhotels.com. Treat the response as an opaque `text/event-stream` byte stream; event payloads are intentionally not modeled here and are hand-typed in the native client (#923).","responses":{"200":{"description":"Opaque server-sent event byte stream (`text/event-stream`).","content":{"text/event-stream":{"schema":{"type":"string","description":"Opaque SSE byte stream."}}}}}}}},"webhooks":{}}