---
title: "Save Room"
method: POST
path: "/trips/{trip_id}/itinerary/rooms/save"
tags: ["itinerary"]
---

# Save Room

`POST /trips/{trip_id}/itinerary/rooms/save`

Save a room to the trip, putting its hotel there first if it is not.

Two applies rather than one: the room action needs the stay's id, which only
exists once the add has resolved. The add dedupes on `source_reference`, so
a hotel already on the trip is reused rather than duplicated, and re-saving
is idempotent on both halves.

## Path parameters

- `trip_id` string, required

## Headers

- `x-client-id` string, nullable

## Request body

- SaveRoomRequest — Choose a room from a search result, wherever the hotel currently lives. The reference is the hotel's search-result card. A room hangs off a stay, so saving one from a chat card has to put the hotel on the trip first — this endpoint owns that ordering rather than leaving the client to sequence two calls and eventually get it wrong. That ordering is also load-bearing beyond convenience: once choosing a room is what creates the stay, the stay always predates any booking, which is what lets the booking path find it instead of minting a second one.
  - `tool_call_id` string, required
  - `result_index` integer, required
  - `conversation_id` string, required
  - `room` StayRoom, required — One room the traveller chose on a stay. **`StayRoom` is to `Stay` what `Stay` is to `canonical_places`** — the traveller's instance of a room, not the room itself. See docs/HOTEL_ROOM_MODEL_ASSESSMENT.md §5. **One StayRoom is exactly one LiteAPI offer**, and therefore one prebook and one booking: an offer is one room type priced for N occupancies, so N rooms of the SAME type are N entries in `occupancies`, never N StayRooms. Two DIFFERENT room types cannot share an offer, so those are two StayRooms and two bookings. Identity is `hotel_id` + `mapped_room_id` and nothing else. Everything else — how many travellers and their ages, board, refundability, the price — is a choice about that room and may change without making it a different room.
    - `id` string, required
    - `source` 'liteapi'
    - `hotel_id` string, required
    - `mapped_room_id` integer, nullable
    - `canonical_room_id` string, nullable
    - `room_name` string, required
    - `occupancies` RoomOccupancy[]
      - `adults` integer, required
      - `children` integer[]
    - `board_type` string, nullable
    - `prefer_refundable` boolean, nullable
    - `chosen_rate` SeenRate — A price we OBSERVED for a room, and when. Never a held price. LiteAPI publishes no TTL on an offer — expiry surfaces only as error 4040 at prebook — so the honest shape is an observation with a timestamp rather than a quote that pretends to still be live. `offer_id` lives HERE rather than on `StayRoom` deliberately: an offer id is part of *a price seen at a time*, not part of *which room this is*. Pairing them means a stale rate is self-describing, and a surface can date the number instead of presenting it as current. Also the seed for price-change notification on a saved room: two `SeenRate`s for one `StayRoom` are a price movement.
      - `offer_id` string, required
      - `all_in_total` number, required
      - `price_per_night` number, nullable
      - `currency` string
      - `board_type` string, nullable
      - `board_name` string, nullable
      - `refundable` boolean
      - `cancel_until` string, nullable
      - `seen_at` string, date-time, required
    - `latest_seen_rate` SeenRate — A price we OBSERVED for a room, and when. Never a held price. LiteAPI publishes no TTL on an offer — expiry surfaces only as error 4040 at prebook — so the honest shape is an observation with a timestamp rather than a quote that pretends to still be live. `offer_id` lives HERE rather than on `StayRoom` deliberately: an offer id is part of *a price seen at a time*, not part of *which room this is*. Pairing them means a stale rate is self-describing, and a surface can date the number instead of presenting it as current. Also the seed for price-change notification on a saved room: two `SeenRate`s for one `StayRoom` are a price movement.
      - `offer_id` string, required
      - `all_in_total` number, required
      - `price_per_night` number, nullable
      - `currency` string
      - `board_type` string, nullable
      - `board_name` string, nullable
      - `refundable` boolean
      - `cancel_until` string, nullable
      - `seen_at` string, date-time, required
    - `booking_id` string, uuid, nullable

## Response `200`

Successful Response

- SaveRoomResponse
  - `stay_id` string, required
  - `rooms` StayRoom[], required
    - `id` string, required
    - `source` 'liteapi'
    - `hotel_id` string, required
    - `mapped_room_id` integer, nullable
    - `canonical_room_id` string, nullable
    - `room_name` string, required
    - `occupancies` RoomOccupancy[]
      - `adults` integer, required
      - `children` integer[]
    - `board_type` string, nullable
    - `prefer_refundable` boolean, nullable
    - `chosen_rate` SeenRate — A price we OBSERVED for a room, and when. Never a held price. LiteAPI publishes no TTL on an offer — expiry surfaces only as error 4040 at prebook — so the honest shape is an observation with a timestamp rather than a quote that pretends to still be live. `offer_id` lives HERE rather than on `StayRoom` deliberately: an offer id is part of *a price seen at a time*, not part of *which room this is*. Pairing them means a stale rate is self-describing, and a surface can date the number instead of presenting it as current. Also the seed for price-change notification on a saved room: two `SeenRate`s for one `StayRoom` are a price movement.
      - `offer_id` string, required
      - `all_in_total` number, required
      - `price_per_night` number, nullable
      - `currency` string
      - `board_type` string, nullable
      - `board_name` string, nullable
      - `refundable` boolean
      - `cancel_until` string, nullable
      - `seen_at` string, date-time, required
    - `latest_seen_rate` SeenRate — A price we OBSERVED for a room, and when. Never a held price. LiteAPI publishes no TTL on an offer — expiry surfaces only as error 4040 at prebook — so the honest shape is an observation with a timestamp rather than a quote that pretends to still be live. `offer_id` lives HERE rather than on `StayRoom` deliberately: an offer id is part of *a price seen at a time*, not part of *which room this is*. Pairing them means a stale rate is self-describing, and a surface can date the number instead of presenting it as current. Also the seed for price-change notification on a saved room: two `SeenRate`s for one `StayRoom` are a price movement.
      - `offer_id` string, required
      - `all_in_total` number, required
      - `price_per_night` number, nullable
      - `currency` string
      - `board_type` string, nullable
      - `board_name` string, nullable
      - `refundable` boolean
      - `cancel_until` string, nullable
      - `seen_at` string, date-time, required
    - `booking_id` string, uuid, nullable
  - `itinerary` object, required

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/stardrift/apis/fastapi.md) · [All operations](https://skmtc.dev/stardrift/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc.dev/stardrift/apis/fastapi/revisions/5acadde833b2?raw)
