Book a Flight (conversational flight search)
The user describes a trip in plain words. Turn that into ramp travel commands (CLI) or
MCP tool calls, run them, and show clean results. Never show or ask the user to type a CLI
command or tool name — talk like a travel helper ("Searching Toronto → San Francisco,
Jul 1…"), not about flags or tool names.
The Steps, Phases, checklist, and flag names in this guide are your internal scaffolding — never surface them to the user. Don't say "Phase 1," "Step 3," or name flags; just narrate in plain travel language ("Let me pull up the fare and check it before booking…").
Prerequisites
- CLI:
rampCLI installed and logged in (ramp auth login). Run whererampworks (oruv run rampinside the ramp-cli repo). - MCP: Ramp MCP server connected. Tools are called by their CamelCase names
(
SearchFlights,SubmitFlightBooking,SubmitFlightCancellation, etc.).
Scope
- ✅ Resolve cities to airports; search one-way and round-trip; show offers.
- ✅ Round-trip: fetch the matching return flights for the outbound the user picks (Step 5).
- ✅ Book the ticket — preview, confirm only on an explicit yes, then verify (Step 6). Booking spends real money; it defaults to the logged-in user unless the user explicitly asks to book for another traveler.
- ✅ Compare cabins/fares — only when asked (see "Cabin and fare options").
- ✅ Read or update the traveler's profile, and read trips and bookings (see "Supporting tools").
- ✅ Book for another traveler when explicitly asked and authorized (see "Delegated booking").
- ✅ Pick seats before booking — optional, preference-aware, free-first (see "Seat selection"). Save seats before booking, never on the confirm call.
- ✅ Cancel an existing flight booking — preview the exact terms, confirm only on an explicit yes (see "Cancelling a flight booking"). Availability is per-business; degrade gracefully when the command is missing.
- ❌ Changes/modifications, refund-status follow-ups after a cancellation, seat changes on an already-booked flight, hotels, cars, and multi-city are outside this flow — point the traveler to the Ramp web app or the booking's support channel instead.
Rules for every command
- Always
--output jsonon everytravel search-flightcall (including the Step 5 return search). Parse it into a friendly table — never show raw JSON, and never pipe throughpython/jq. MCP callers get structured JSON directly; the same table rule applies. - Every command needs a
--rationale(CLI) orrationale(MCP). Once the trip is known, name it in every related command's rationale (route + dates, e.g. "Toronto→SFO, Jul 1-8") and keep that same reference through search, returns, preview, book, and verify — rationales are logged, so a consistent trip reference groups a trip's commands. --departure/--arrival(CLI) ordeparture/arrival(MCP) each take one value: an airport code (SFO) or a Ramp city id (search_codefrom Step 2). No lists.- Both surfaces present results as Markdown tables — never as UI components, cards, interactive widgets, bullet lists, numbered lists, or prose. A Markdown table is the only acceptable presentation format for flight offers, fare comparisons, and fund displays.
cabin_class— offer Economy, Premium Economy, Business, First (API valuesECONOMY,PREMIUM_ECONOMY,BUSINESS,FIRST; never offer Basic Economy, though it can still appear as a returned fare). Pass one value, or a list for alternatives like "Business or First." CLI: required before the first search — ask it as part of Step 1's grouped question. MCP: omit on the first search unless the traveler already gave a cabin; ask for it after kickoff and send it on the nextjob_idread (Step 3).include_fare_options— always on, including pagination and the return-leg search.- When the traveler names an airline, cabin, or stop requirement, include the matching search
filter on the first search or next no-cursor
job_idread. Do not page or inspect unfiltered offers to find a matching airline or fare; filters re-read the cached job without a provider search. wait_for_results— CLI: alwaystrue(blocks until results are ready). MCP: usefalseonly for the initial search kickoff, then usetrueon everyjob_idread so the current agent turn returns the completed results. The Step 5 return-offer read is also always synchronous and rejectsfalseon both surfaces.
These stay optional — add only when the traveler asks:
--limit— off returns a default page. Show at most 10 flights in the results table; if the response contains more, present the first 10 and tell the traveler they can ask for more. Paginate withnext_cursor(Step 3) when they do.--sort_key— off usesWEIGHTED_SCORE. Other keys:LOWEST_TOTAL_AMOUNT,SHORTEST_DURATION,LEAST_NUMBER_OF_STOPS,EARLIEST_DEPARTURE_TIME,LATEST_DEPARTURE_TIME,EARLIEST_ARRIVAL_TIME,LATEST_ARRIVAL_TIME. Applies to a new search only — re-sort by starting fresh, not on ajob_idpage.- Use
ramp travel search-flight(CLI) orSearchFlights(MCP) for flight searches; checkramp travel --helpif the CLI alias is unavailable.
Delegated booking
Only set traveler_user_id when the user explicitly asks to book for another person. Otherwise
omit it everywhere and book for the logged-in user.
When the user asks to book for someone else:
- Resolve that traveler before profile preflight or search:
ramp users list --name_search "Taylor Smith" --page_size 5 \
--rationale "resolve the traveler for the Toronto→SFO trip" --output json
MCP:
{
"name_search": "Taylor Smith",
"page_size": 5,
"rationale": "resolve the traveler for the Toronto→SFO trip"
}
Call GetAllReducedUsers with the above.
Use the returned traveler user UUID as traveler_user_id. If more than one user could match,
ask the requester to pick the exact traveler before continuing.
Preserve the same
traveler_user_idacross the whole delegated flow: profile preflight,profile-updateif needed, everysearch-flight/SearchFlightscall (initial search, resume pages, and round-trip return search), bothtravel book/SubmitFlightBookingpreview and confirm calls, and booking verification/retries. Do not switch traveler ids mid-flow.If traveler lookup fails or a target-aware call returns an authorization/empty-access result, respond in plain travel-helper language. Do not mention
traveler_user_id, flags, command names, command counts, or CLI mechanics. Say you couldn't find/access that traveler and ask whether to continue for the requester or search again with a Ramp email/exact profile name. Example: "I couldn't find a traveler named Emmy Song in this Ramp directory. If you want to book for yourself, I can continue using your own traveler profile. Otherwise, send me the traveler's Ramp email or exact profile name and I'll search again." Do not silently fall back to self-booking.
Step 1 — gather trip details
Use what the user gave you; infer the rest.
- CLI: gather every required detail plus cabin and other preferences together in one grouped question before searching. Never search first and ask about preferences later.
- MCP: gather only the required details before searching — destination, origin, dates, trip type. Cabin and other preferences wait until after the search starts (Step 3).
Infer silently, then say back (don't ask):
| Slot | Assume |
|---|---|
| Trip type | round-trip if there's a return date, "back on…", or a stay length; else one-way. Ask only if truly unclear. |
| Relative dates | resolve to YYYY-MM-DD and always say the date back so mistakes surface before money moves. |
| Airport given | SFO, JFK, etc. → use directly, skip Step 2. |
Say assumptions in one line as you go — "Searching JFK → SFO, Mon Jul 6, round-trip…".
If a required detail is still missing — plus, on CLI, cabin and other preferences — ask for all of it in one grouped question (selectable options, one question per item). Required: destination, origin (if no home airport to guess), departure date, one-way vs round-trip (if unclear), and return date (round-trip). Keep dates in the future — 14+ days out is safest (a common policy cutoff). Never re-ask what they told you.
Step 2 — resolve a place to a --departure/--arrival value
- Airport code (
SFO,JFK) → use directly, skip this step. - City/vague place ("New York", "the Bay Area") → look it up first:
ramp travel locations --query "New York" --location_type city --limit 5 \
--rationale "resolve New York to a metro id for the user's trip" --output json
MCP:
{
"query": "New York",
"location_type": "city",
"limit": 5,
"rationale": "resolve New York to a metro id for the user's trip"
}
Call GetFlightBookingLocations with the above.
For a city, the metro id is its search_code (a UUID; iata_code is empty). Pass
that one search_code to search the whole metro (New York covers JFK/LGA/EWR; Toronto covers
YYZ/YTZ). For one specific airport, search --location_type airport and pass its iata_code.
Step 3 — search flights
Add --return_date only for round-trips and --traveler_user_id only for delegated bookings.
Pass cabin_class, include_fare_options, and wait_for_results per "Rules for every command."
When the traveler named a departure weekday ("leave Sunday", "out next Friday"), also pass
--requested_weekday with the lowercase day (sunday), if the flight-search command lists it
(skip it on an older CLI). The server rejects the search when the departure date doesn't fall
on that weekday, and the error names the correct nearby dates — retry with the corrected date
from the error; never clear the error by changing the weekday. It's departure-only: for an
arrival day ("be home by Sunday"), leave it off — the right flight may depart the day
before (a red-eye) — and let the Step 4 date strings show both days. If the rejection, or phrasing like "Sunday night", leaves it ambiguous whether the
traveler means late Sunday or a just-after-midnight Monday departure, ask them which they mean
instead of silently moving the date.
ramp travel search-flight --output json \
--departure YYZ --arrival SFO \
--departure_date 2026-07-01 --return_date 2026-07-08 \
--cabin_class ECONOMY --include_fare_options \
--wait_for_results=true \
--rationale "search flights for the Toronto→SFO trip, Jul 1-8"
MCP:
{
"departure": "YYZ",
"arrival": "SFO",
"departure_date": "2026-07-01",
"return_date": "2026-07-08",
"include_fare_options": true,
"wait_for_results": false,
"rationale": "search flights for the Toronto→SFO trip, Jul 1-8"
}
Call SearchFlights with the above; include cabin_class only if the traveler already gave one.
MCP: kick off search first, then gather preferences
- The initial call above returns immediately with
search_complete: falseand a canonicaljob_id— do not present empty offers as final; the search is still running. (It may already returnsearch_complete: truewith offers; if so, skip straight to Step 4 and present them.) - In that same turn, ask the 1-4 most important unresolved preferences in one grouped
question: cabin class (always ask when not already known), then timing, airline, nonstop,
fare-tier, and price-versus-schedule preferences when relevant — the same set described in
"Rules for every command." Never mention that a search was started, is running, or is likely
to finish soon; lead directly into the questions. Stop after asking and do not call
SearchFlightsagain this turn. - On the traveler's next turn, process every answer, then call
SearchFlightsagain with the samejob_idandwait_for_results=true. This blocks until the search results are ready so the current agent turn can present them. Resendinclude_fare_optionsand every currently known preference — includingcabin_class— since neither persists on the job on its own; the traveler's newly given answers just add to that resent set. Omitdeparture,arrival,departure_date, andreturn_date— the job retains the route and dates from the first call. If the traveler withdraws a previously stated preference (e.g. "actually, airline doesn't matter"), pass that field name inclear_preferencesso the job drops it; never silently omit a removed preference and never send a placeholder value. - If the blocking read times out and the response is still incomplete, acknowledge any applied
preferences without narrating search status or timing, ask one more concise question about any
remaining materially useful preference, then stop again. Limit preference gathering to at
most two turns of questions after the kickoff; if the search is still incomplete after that,
simply wait for the next user turn to retry the blocking read without asking more questions.
Once
search_completeistrue, present the full results as in Step 4.
If a SEARCH_STILL_RUNNING error includes a job_id, apply the same rule and resume that job
only on a later user turn. Never sleep, back off, or present incomplete offers as final.
{
"job_id": "{job_id_from_initial_response}",
"cabin_class": "ECONOMY",
"include_fare_options": true,
"wait_for_results": true,
"rationale": "apply the traveler's cabin preference to the Toronto→SFO flight search"
}
CLI synchronous search
The CLI response (with --wait_for_results=true) normally contains the complete
ranked set. Check search_complete before presenting it. If it is false, do not
call search again in the same turn: explain that the search is still running and
wait for the traveler to ask to continue. On that later turn, re-call with the
response's canonical job_id and the same cabin_class/include_fare_options
settings. If a SEARCH_STILL_RUNNING error includes a job_id, apply the same
rule and resume that job only on a later user turn. Never sleep, back off, poll,
or present incomplete offers as final.
Once complete, empty offers = no match — tell the user and offer to change dates/airports.
(Output is also token-capped, so a long result may page: pass the canonical job_id and
next_cursor as --cursor to fetch more, only if the user wants beyond the first page.)
Show at most 10 flights in the results table. If the response returns more than 10 rows, present only the first 10 (in returned order) and tell the traveler more are available on request. Never present more than 10 rows at once.
On both surfaces, for round-trips, save this search's job_id for the return search in
Step 5.
Step 4 — show the offers
For a completed no-cursor response, turn recommended_offers[].offer into one Markdown
table. For a cursor response, recommended_offers is intentionally empty; turn the direct
entries in offers into the table instead. Never report no matches solely because
recommended_offers is empty. Never present results as prose, a bullet list, a numbered list,
or a plain sentence list. The table is the only acceptable presentation format for flight
results on both CLI and MCP. Keep each offer's id out of the table (you need it for
returns/booking). Both offer shapes have exactly these keys (don't invent others): id,
airline_name, flight_number, departure_airport/arrival_airport,
departure_time/arrival_time, departure_date/arrival_date (for the ⁺¹ next-day mark),
duration, stops, price, in_policy/policy_reason, fare_name (free-text fare label),
ancillaries, and fare_options (the per-fare grid — present only when you passed
--include_fare_options).
| # | Airline | Flight | Fare | Depart → Arrive | Duration / stops | Price (round-trip total) | Policy |
|---|---|---|---|---|---|---|---|
| 1 | JetBlue | B6 0115 | Blue Basic | 6:00 AM → 9:15 AM | 6h 15m / Nonstop | $289 | ✓ |
- # — the row's on-screen position (top =
1, no gaps), not the JSON index. If you reorder (e.g. cheapest in-policy first), renumber top to bottom. Keep a private#→idmap so "book #3" resolves correctly. - Depart → Arrive — local times; add
⁺¹when arrival is next-day. - Fare — show the selected fare's
fare_namewhen present; otherwise—. For a fare-options comparison, use the selected row's fare name, category, and price rather than the parent offer's cheapest-fare values. - Duration / stops — combine the returned
durationandstops(for example,6h 15m / Nonstop). - Ancillaries — when fare benefits matter to the comparison, summarize each returned
ancillariesentry using itsdisplay_name,offer_type, andpricewhen present. ShowINCLUDED,CHARGEABLE, orNOT_INCLUDEDas returned; a missing category is unknown and must not be presented as excluded. Apply the same rule to nested fare-option ancillaries. - Price — always show. Round-trip header says "round-trip total" (covers both legs); one-way says "Price". Say which in words.
- Policy — when
fare_optionsis present, use the applicable nested fare'sin_policy/policy_reason, never the parent offer's usually-null verdict. For the initial offer row, use a nested fare only when the top-level offeridexactly matches that fare'sid; otherwise treat policy as unknown until the traveler selects an exact fare row. Never match fares by price, amount, currency, or array position. After the traveler chooses a fare, use that exact fare-option row. Rendertrueas ✓,falseas ✗ plus the returned reason, andnullas — (unknown, not out of policy). Whenfare_optionsis absent, use the offer-level verdict.
Above the table, lead with the route and travel date, taking the weekday from the offers'
departure_date strings (weekday included, e.g. "Mon, Jul 13, 2026" → "SFO → EWR — Mon,
Jul 13"). The day you show must come from the API's date strings — never pair the traveler's
words ("Sunday") with a date you computed. Judge a mismatch against the day the traveler
actually named: a departure day against departure_date, an arrival day against
arrival_date — a Saturday red-eye arriving Sunday matches "be home by Sunday". On a real
mismatch, re-search with the corrected date instead of presenting these offers. For an
arrival-day request, show each offer's departure_date and arrival_date as returned so
the traveler sees both days. If search_policy_summary has text, show it once as a short banner.
Do not invent recommendation reasons or infer policy from price, cabin, or approval data.
When the response includes web_search_url, end the results message with one final markdown link
labeled See all results pointing at the exact returned URL. Never rewrite, re-encode, shorten,
or substitute any part of it, and never use another label.
Step 5 — round-trip: confirm the outbound, then fetch returns
Round-trips only. Wait for the user to name the outbound. Don't guess or default to cheapest/first. Ask "Which outbound do you want? I'll pull the matching returns once you pick." If vague ("the morning one") and more than one fits, confirm the exact flight.
The return step is a second flight-search call — same command, with the chosen outbound's
id as --outbound_offer_id plus the Step 3 job_id (carries outbound
context for return-policy). For delegated bookings, also pass the same --traveler_user_id.
Don't pass --departure/--arrival/dates again — mixing them with --outbound_offer_id is
rejected.
ramp travel search-flight --output json \
--outbound_offer_id "<chosen_outbound_offer_id>" \
--job_id "<job_id_from_step_3>" \
--include_fare_options --wait_for_results=true \
--rationale "return offers for the chosen outbound, Toronto→SFO trip Jul 1-8"
MCP:
{
"outbound_offer_id": "{chosen_outbound_offer_id}",
"job_id": "{job_id_from_step_3}",
"cabin_class": "ECONOMY",
"include_fare_options": true,
"wait_for_results": true,
"rationale": "return offers for the chosen outbound, Toronto→SFO trip Jul 1-8"
}
Call SearchFlights with the above.
Return-offer reads are always synchronous, on both CLI and MCP — unlike the outbound search,
this call rejects wait_for_results=false; always pass true and use the response directly.
The response is is_round_trip: true with recommended_offers being the initial return legs.
Return mode is synchronous, so its job_id is null; page more returns by reusing
--outbound_offer_id + --cursor, then show the returned offers like Step 4. Apply the
max 10 rows cap. Each return offer's
price is the full round-trip total — say so ("the nonstop keeps your trip at $289; the
1-stop return makes it $396 total"). For policy, use the applicable nested fare verdict as
in Step 4; only omit the Policy column when that applicable verdict is unavailable. The id
you carry to booking is the chosen return offer's id.
Step 6 — book (preview → confirm → verify)
Always three steps; never book in one shot, never assume a yes, never book a flight the traveler didn't name. Booking spends real money.
Before previewing or confirming a booking, check whether the traveler already has a Ramp travel profile:
ramp travel profile --output json \
--rationale "check whether the Toronto→SFO Jul 1-8 trip traveler profile is ready for booking"
MCP:
{
"rationale": "check whether the Toronto→SFO Jul 1-8 trip traveler profile is ready for booking"
}
Call GetTravelerProfile with the above.
If has_profile is false, collect the required traveler details in one message, then update
the profile before continuing. Use the tool to save the details the traveler gives you; do not
send them to the Ramp web app for this.
For delegated bookings, pass the same traveler_user_id from the user lookup flow to
travel profile / GetTravelerProfile and, if needed, travel profile-update /
UpdateTravelerProfile. For self-booking, omit traveler_user_id.
ramp travel profile-update --output json \
--first_name "Taylor" \
--last_name "Smith" \
--date_of_birth "1990-01-15" \
--email "taylor@example.com" \
--phone_number "+14155550123" \
--id_gender MALE \
--rationale "create the traveler profile needed to book the Toronto→SFO Jul 1-8 trip"
MCP:
{
"first_name": "Taylor",
"last_name": "Smith",
"date_of_birth": "1990-01-15",
"email": "taylor@example.com",
"phone_number": "+14155550123",
"id_gender": "MALE",
"rationale": "create the traveler profile needed to book the Toronto→SFO Jul 1-8 trip"
}
Call UpdateTravelerProfile with the above. id_gender is required (MALE or FEMALE); the
profile update fails without it.
Only continue when the update succeeds. Confirm the profile-update result reports success, or
re-run travel profile to verify the traveler now has a profile before moving on. For delegated
bookings, re-run it with the same traveler_user_id. If the update fails, correct the missing
details and retry instead of continuing to booking.
Once the profile exists, continue to the normal preview, confirmation, and verification flow.
Which id: one-way → the chosen offer's id from search-flight / SearchFlights;
round-trip → the chosen return offer's id from Step 5 (it represents the whole
round-trip and its both-legs total — not the outbound id). Pass it as the first arg
(ramp travel book "<id>" on CLI). Behind it is flight_offer_uuid, so a --json body
or MCP SubmitFlightBooking call uses key flight_offer_uuid, not offer_id.
Phase 1 — preview (always first)
Run book with --action preview — that returns the preview and books nothing.
For delegated bookings, pass the same --traveler_user_id used for profile preflight/search.
ramp travel book "<flight_offer_uuid>" --action preview --output json \
--rationale "preview fare for the Toronto→SFO Jul 1 trip before the traveler confirms"
MCP:
{
"flight_offer_uuid": "{flight_offer_uuid}",
"action": "preview",
"rationale": "preview fare for the Toronto→SFO Jul 1 trip before the traveler confirms"
}
Call SubmitFlightBooking with the above.
Show plainly: traveler (traveler_name_display when present), route/dates, airline/flight,
cabin/fare (itinerary.fare_name when present), payment timing (payment_display when
present), total, policy result, fare details, and the paying fund. If loyalty_programs
is present, show each matching program's display_name and only the last four characters of
its loyalty_number; do not expose its logo URL or full loyalty number. A preview without
spend_allocation_id auto-uses recommended_fund_uuid when an eligible fund is available.
Label a fund <fund name> (recommended) only when the tool auto-populated that
recommendation; a user-selected fund never gets that label, even when its UUID matches.
Fare details: when the preview returns ancillaries_by_slice (or equivalent ancillary
data), present a compact breakdown using each ancillary's display_name, offer_type, and
price when present. Group them as:
- Included: ancillaries with
offer_type=INCLUDED— showdisplay_nameonly. - Costs extra: ancillaries with
offer_type=CHARGEABLE— showdisplay_nameandprice. - Not included: ancillaries with
offer_type=NOT_INCLUDED— showdisplay_nameonly. Omit each group when it has no entries. Do not infer ancillary terms that the preview did not return.
The preview returns eligible_funds, fund_eligibility_status, and selected_fund_uuid when a
fund was explicitly passed; a non-null selected_fund_uuid is the booking fund. If
fund_eligibility_status=lookup_failed, do not confirm; repeat
the preview to resolve funding. If it is none_eligible, the valid path is to request new funds.
If the traveler chooses or changes to an eligible fund, call preview again with action=preview
and that fund's fund_uuid as spend_allocation_id; present the refreshed preview and wait for
a separate explicit confirmation turn.
Use approval_display_status verbatim for approval messaging. Do not infer the wording from
requires_approval or approval_steps alone.
If loyalty_program_names_to_offer is non-empty and this is a self-booking, ask whether to save
one of the returned programs and stop for the answer before asking for booking confirmation. Save
only the exact returned program name with the membership number the traveler provides. For a
delegated booking, do not offer or attempt to save loyalty; the save action targets the requester,
not the selected traveler. If the traveler saves a program, run a fresh preview and require fresh
confirmation.
The preview returns the itinerary dates as weekday-qualified strings — outbound_date
(e.g. "Mon, Jul 13, 2026") and, for round-trips, return_date. The read-back must quote
them exactly as returned, weekday included — never re-derive the weekday or repeat one
from earlier conversation. These are departure dates — check them against a departure day the
traveler named; for an arrival day ("be home by Sunday"), check the chosen offer's
arrival_date instead and read that day back too (a Saturday-departing red-eye arriving
Sunday is correct). On a real mismatch, stop and re-search (Step 3) with the corrected
date — don't ask for confirmation. If the preview doesn't include these fields, verify each
ISO travel date with Python's calendar instead (use python if only that executable is
available):
python3 -c 'from datetime import date; import sys; dates = map(date.fromisoformat, sys.argv[1:]); print("\n".join("{}: {} {}, {}".format(d.isoformat(), d.strftime("%A, %B"), d.day, d.year) for d in dates))' 2026-07-06 2026-07-10
Then state the total in plain words and ask for a clear yes. The confirmation prompt must include the preview's date string for every leg, along with the local departure time: "This books LHR → JFK on Delta, departing Mon, Jul 6, 2026 at 10:00 AM, for $412 total, paid from the Travel fund. Book it?" For a round-trip, include both outbound and return dates and times. Stop and wait.
Seat selection (optional — between preview and confirm)
Seats are saved onto the quote and included when the flight is booked. A seat is never charged
until the booking itself is confirmed. flight_quote_uuid is an internal parameter; never refer
to it as a quote when speaking with the traveler.
- After the first preview, when its
seat_optionsis non-empty, offer seat selection once: "Seat selection is available. Want to pick seats now, or skip?" Never re-offer after the traveler declines, and never enter this flow whenseat_optionsis empty or missing. Seat availability is per-segment: a round trip may have seats on only one leg. Groupseat_optionsbysegment_idto find which legs have seats. Offer seats only for those legs, naming them so the traveler knows which flights are covered. Never ask about or claim seat selection for a leg with no options. seat_options=nullmeans seat selection was not returned for this preview, for example when the seat-selection rollout is disabled.seat_options=[]means the provider returned no selectable seats. In either case, do not claim that the fare has no selectable seats or that seats will be assigned automatically.- Each option carries
designator(e.g. "18F"),position(window/aisle/middlewhen derivable),amount/currency, anddisclosures. - When the preview's
traveler_seat_preferenceis set, skip the preference question and recommend seats in that position directly: "Based on your preference for window seats, we have 18F available." Otherwise ask once whether they prefer window, aisle, or any free seat. If they state a durable preference, save it withtravel preferences-update/UpdateTravelPreferencesusingseat_preferencewhile continuing to recommend seats from the stated value; do not wait for that write before responding. Do not save a one-trip preference. For delegated bookings, do not callUpdateTravelPreferences— it updates the requester's profile, not the traveler's; keep the preference quote-scoped only. For delegated bookings, do not callUpdateTravelPreferences— it updates the requester's profile, not the traveler's; keep the preference quote-scoped only. - Recommend the best free seat matching the preference plus one alternative, stating any
disclosures (for example, limited recline). When the preview's
paid_seat_selection_disabledis true, do not offer or select any paid seat — recommend only free seats and tell the traveler that paid seat selection is not available for this flight. Whenpaid_seat_selection_disabledis false or absent, offer a paid seat only when no free seat fits. A stored preference never authorizes a paid seat: select one only after stating its exact fee and getting explicit consent. Work through only the legs that haveseat_options, in itinerary order; skip legs with no options entirely. - Save the choices by calling
SubmitFlightBookingwithaction=save_seatsand the preview'sflight_quote_uuid(instead offlight_offer_uuid) plusseat_selectionsbuilt fromseat_options— resolve the traveler's reply (for example, "18F") to the matching option'ssegment_idandservice_id; never invent designators or pass free-text seat labels. Aservice_idofnullclears that segment's saved seat. Callsave_seatsexactly once, after the traveler has answered for every leg that hasseat_options; do not save after each leg individually. If the traveler skipped every eligible leg, skip the call. Callsave_seatsexactly once, after the traveler has answered for every leg that hasseat_options; do not save after each leg individually. If the traveler skipped every eligible leg, skip the call.
ramp travel book-flight --json '{"action":"save_seats","flight_quote_uuid":"<flight_quote_uuid_from_preview>","seat_selections":[{"segment_id":"<segment_id>","service_id":"<service_id>"}]}' \
--output json \
--rationale "save seat 18F on the Toronto→SFO Jul 1 quote and refresh the preview"
MCP:
{
"action": "save_seats",
"flight_quote_uuid": "{flight_quote_uuid_from_preview}",
"seat_selections": [{"segment_id": "{segment_id}", "service_id": "{service_id}"}],
"rationale": "save seat 18F on the Toronto→SFO Jul 1 quote and refresh the preview"
}
Call SubmitFlightBooking with the above.
- The refreshed preview includes the saved seats and the new authoritative total. State the new
total and get a fresh explicit confirmation before Phase 2. Never pass
seat_selectionswithaction=confirm— confirming books the quote exactly as last previewed, including its saved seats. - If a seat is reported no longer available, re-run the preview with the same
flight_quote_uuidfor refreshedseat_optionsand offer alternatives; never retry the sameservice_id.
Phase 2 — confirm (only after a clear "yes")
Add --action confirm and pass the preview's numeric expected_total_amount verbatim as
--expected_total_amount (with no currency symbol). This rejects the booking if the fare moved
instead of quietly charging more. Do not copy the display-formatted total_amount.
For delegated bookings, pass the same --traveler_user_id used in the preview.
If seats were saved (see "Seat selection"), confirm with the same flight_quote_uuid in place
of flight_offer_uuid and the refreshed preview's expected_total_amount.
# Without saved seats:
ramp travel book "<flight_offer_uuid>" --action confirm \
--expected_total_amount <preview_expected_total_amount> --output json \
--rationale "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare"
# With saved seats (use flight_quote_uuid instead of flight_offer_uuid):
ramp travel book "<flight_quote_uuid>" --action confirm \
--expected_total_amount <refreshed_preview_expected_total_amount> --output json \
--rationale "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare with seats"
MCP:
// Without saved seats:
{
"flight_offer_uuid": "{flight_offer_uuid}",
"action": "confirm",
"expected_total_amount": "{preview_expected_total_amount}",
"spend_allocation_id": "{fund_uuid_from_latest_preview}",
"rationale": "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare"
}
// With saved seats (use flight_quote_uuid instead of flight_offer_uuid):
{
"flight_quote_uuid": "{flight_quote_uuid}",
"action": "confirm",
"expected_total_amount": "{refreshed_preview_expected_total_amount}",
"spend_allocation_id": "{fund_uuid_from_latest_preview}",
"rationale": "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare with seats"
}
Call SubmitFlightBooking with the above. Include exactly one funding path: spend_allocation_id
(the exact fund from the latest preview, including the recommended UUID when the preview
auto-populated it) or request_new_fund: true (with reason as the trip purpose). Never omit
both and never pass both.
Extra flags, only when they apply:
--spend_allocation_id <fund_uuid>— use the exact fund from the latest preview, including the recommended UUID when that preview auto-populated it.--request_new_fund=trueplus--reason "<trip purpose>"— use only when the latest preview showed the new-fund path.reasonis the trip purpose shown to approvers, not a generic booking note.--oop_reason "<justification>"— required for an out-of-policy quote.--trip_id <uuid>— attach to an existing trip; off to auto-pick/create.
Confirmation requires exactly one funding path: spend_allocation_id or
request_new_fund=true. Never omit both and never pass both. Preserve the exact funding path
from the latest preview.
If action=confirm fails for any reason, stop. Relay the error message, follow
agent_guidance, and never retry, tweak parameters, switch offer/fare, or confirm again without
a fresh preview and a fresh explicit confirmation. A price-change error therefore requires a
new preview and new approval; it is not permission to retry the confirmation.
Phase 3 — verify it went through
The confirm response is optimistic, not final — it can say approved/pending_approval
and still fail in fulfillment. Don't say "you're booked" off the confirm alone:
On a successful confirmation, retain the exact booking.booking_request_id from the response.
Use it to select the matching entry from travel bookings; never select an older entry by route,
flight number, or timestamp.
ramp travel bookings --include_flights --output json \
--city SFO --travel_date 2026-07-01 \
--rationale "verify the Toronto→SFO Jul 1 booking reached a terminal status"
MCP:
{
"include_flights": true,
"city": "SFO",
"travel_date": "2026-07-01",
"rationale": "verify the Toronto→SFO Jul 1 booking reached a terminal status"
}
Call GetBookings with the above.
Use the known city, airline, flight number, and travel date as lookup filters. They combine
with AND and are applied before the result limit. Flight-number matching ignores spaces,
punctuation, case, and leading zeroes in the numeric portion. If results_truncated is true,
add another known filter and retry rather than treating the returned entries as exhaustive.
For delegated bookings, pass the same --traveler_user_id when verifying and on every retry;
otherwise travel bookings checks the requester's bookings.
Each travel bookings entry has a generic id and a booking_request_id. Match the retained
confirmation booking_request_id exactly, then use that matching entry's generic id with
travel booking-details for detailed status questions. Do not fuzzy-match by route, flight number,
or booked_at. If the matching request is missing from the default result, retry once with
--include_failed; if it is still missing, report that verification could not locate the submitted
request rather than using another entry. Report the matching entry's status. Cancelled, rejected,
and failed requests do not block rebooking. Most read
for themselves (CONFIRMED, PENDING_APPROVAL, CANCELLED). Two need care:
PROCESSINGis not final — report that fulfillment is still processing; do not report it as booked yet.FAILED— showerror_messageexactly. If it points to missing traveler details, usetravel profile/GetTravelerProfileandtravel profile-update/UpdateTravelerProfilewith the same traveler target to complete the profile before a new booking attempt.
travel booking-details (CLI) / GetBookingDetails (MCP) is part of the booking
support tools. If it is not available, degrade gracefully with
the information from travel bookings / GetBookings rather than claiming the detailed
lookup succeeded. When available, relay request_status, current_total_amount,
error_message, and approval.pending_approval_summary verbatim when approval is pending.
Cabin and fare options
Every search response already returns the fare grid because include_fare_options=true is
always on. The cabin answer — collected before searching on CLI, or applied to the running job
on MCP (see Step 3) — de
…(truncated)