Book a Hotel
The user describes a stay in plain words. Turn that into ramp travel commands (CLI only) 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 hotels near Lower Manhattan, Aug 10-13...), not about tools or flags.
The steps, phases, checklists, field names, and command names in this guide are your internal scaffolding; never surface them to the user. Reason through them silently. Never mention this skill, its instructions or specification, whether you are following or complying with it, or how an internal tool name maps to a CLI command. Do not narrate command-name or alias reconciliation. Give only concise, user-relevant action and result updates.
Scope and safety
- Use
ramp travel search-hotel(CLI) /SearchHotels(MCP) to search,ramp travel hotel-rates(CLI) /GetHotelRates(MCP) to fetch the selected hotel's rates, andramp travel book-hotel(CLI) /SubmitHotelBooking(MCP) to preview and book. - Always use
--output json(CLI); MCP callers receive structured JSON directly. Build a readable comparison instead of showing raw JSON. - Always include a rationale that consistently names the destination and stay dates.
- 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 hotel comparisons, room/rate lists, fund displays, and the booking preview.
- Keep each tool step to one direct command/tool call. Read IDs from the prior JSON response and
pass the exact literal value as the next positional argument. Never write response JSON to
temporary files, invoke Python or
jqto recover an ID, use nested command substitution, or pipe throughtail/other shell filters. Keep the ID in context and call the next command directly. - Omit flags whose documented defaults match the request, such as
--num_adults 1; do not make commands longer by restating defaults. - Always follow this order:
search-hotel→ traveler selects a hotel →hotel-rates→ traveler selects a room/rate →book-hotelpreview → explicit approval →book-hotel --confirm. - Search result rate summaries are not selectable booking inventory. Pass the selected hotel's
idtohotel-rates, then book only a literal selectedall_rates[].rates[].idit returns. - A hotel booking spends real money. Always preview first and wait for explicit confirmation.
- Never reuse an amount from search as
expected_total_amount; only use the fresh preview's exact numericexpected_total_amount. - A selected fund is part of the approved preview state. Never add or change
--spend_allocation_idonly at confirmation: preview again with the exact selectedfund_uuid, get explicit approval, then confirm with that same fund UUID. Any fund change requires another preview. - Treat relevant
external_agent_messagesfrom search, rates, preview, and confirm as service guidance and surface them plainly. - If Ramp reports that hotel search or booking is unavailable, explain that it is not currently enabled for this Ramp account. Do not fall back to legacy hotel tools or claim that no inventory exists.
Resolve the traveler
For self-booking, omit traveler_user_id. Before searching, check the traveler's profile:
CLI:
ramp travel profile --output json \
--rationale "check the traveler profile for the Lower Manhattan hotel stay, Aug 10-13"
MCP:
{
"rationale": "check the traveler profile for the Lower Manhattan hotel stay, Aug 10-13"
}
Call GetTravelerProfile with the above.
If has_profile is false, collect the required identity and contact details together and call
ramp travel profile-update (CLI) / UpdateTravelerProfile (MCP). Continue only after the
profile update succeeds. This profile preflight happens before search, not only at checkout.
Only book for another person when the requester explicitly asks. Resolve that person first:
CLI:
ramp users list --name_search "Taylor Smith" --page_size 5 --output json \
--rationale "resolve the traveler for the Lower Manhattan hotel stay, Aug 10-13"
MCP:
{
"name_search": "Taylor Smith",
"page_size": 5,
"rationale": "resolve the traveler for the Lower Manhattan hotel stay, Aug 10-13"
}
Call GetAllReducedUsers with the above.
If multiple people match, ask the requester to choose. Pass the selected user UUID to travel profile / GetTravelerProfile, the fresh travel search-hotel / SearchHotels call,
travel hotel-rates / GetHotelRates, and both travel book-hotel / SubmitHotelBooking
calls. Cursor pages use the cached traveler from the original search, so do not resend or
change the traveler there. Never silently switch to the requester when lookup or
authorization fails.
Gather the stay
Required details are destination, check-in date, and check-out date. Use the traveler's words for
a city, neighborhood, address, or landmark. Resolve relative dates to
YYYY-MM-DD and repeat the dates back so mistakes surface before searching. Default to one adult;
only set another count when the traveler asks.
Ask for all genuinely missing required details in one message. Do not ask for preferences such as hotel chain, amenities, or refundability unless the traveler made them important.
Office and headquarters destinations
When the destination references a company office, HQ, or headquarters in any form, resolve the
office with ramp travel offices (CLI) / GetOfficeLocations (MCP) before searching. Never
pass an unresolved office phrase to hotel search: it rejects an office-keyword
location_query that arrives without resolved coordinates.
CLI:
ramp travel offices --output json \
--rationale "resolve the company office anchor for the hotel stay, Aug 10-13"
MCP:
{
"rationale": "resolve the company office anchor for the hotel stay, Aug 10-13"
}
Call GetOfficeLocations with the above.
Each office_locations[] entry carries display_name, latitude, and longitude. Match the requested
city or office name against this shape. If more than one office could match, ask the traveler to choose.
Do not try to infer an office. Once a coordinate-bearing office_locations entry is chosen, pass its
display_name as --location_query together with its exact --latitude and --longitude to
ramp travel search-hotel (CLI) / SearchHotels tool (MCP). Never expose the coordinates to the traveler.
display_name is nullable: when the chosen office has none, keep the traveler's own office phrase as
--location_query while still passing the exact coordinates. Refer to offices by display name — or by the
traveler's phrase when the office is unnamed.
If only company_address matches and no coordinate-bearing office entry is available, ask the traveler for
a specific neighborhood, landmark, or address instead. When the traveler gives only a bare city and the
company has an office there, offer the office as an anchor option instead of silently adopting it.
Search hotels
Use --location_query (CLI) / location_query (MCP) for the destination context (city, neighborhood,
landmark, or address). When the traveler asks for a specific hotel or property, pass its exact name
with --hotel_name (CLI) / hotel_name (MCP).
ramp travel search-hotel --output json \
--location_query "Lower Manhattan" --hotel_name "citizenM New York Bowery" \
--wait_for_results=true \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "search for citizenM New York Bowery for the Manhattan stay, Aug 10-13"
MCP:
{
"location_query": "Lower Manhattan",
"hotel_name": "citizenM New York Bowery",
"wait_for_results": true,
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"rationale": "search for citizenM New York Bowery for the Manhattan stay, Aug 10-13"
}
Call SearchHotels with the above when searching for a specific hotel or property.
Pass wait_for_results=true explicitly on every fresh hotel search, on both CLI
and MCP. Both block synchronously until results are ready.
Add --traveler_user_id only for delegated booking. Add --num_adults only when it differs from
one. The default page is ten hotels; --limit can request 1-10.
When the traveler explicitly requests supported filtering or ordering, send one complete --json
request because filters and sort are structured rather than standalone flags. Supported
filters are star_ratings, brand_ids, amenity_ids, and min_review_rating. Supported sort
keys are DISTANCE, LOWEST_PRICE, TIME, WEIGHTED, MOST_POPULAR, and STAR_RATING, with
ASC or DESC. Do not invent brand or amenity IDs; omit criteria that cannot be represented with
known literal values. Preserve destination, dates, traveler, adult count, limit, hotel name, and
rationale in the JSON body when applicable; omit adult count and limit when their defaults match.
A completed synchronous search returns recommendations, not a plain list: recommended holds
best_match plus alternates in ranked order — up to ten hotels total on this surface, capped by
--limit when it is smaller — and hotels is empty. Each recommendation is {hotel, reasons, tradeoffs}; read the hotel metadata from its hotel object. The rest of the ranked inventory
stays cached behind next_cursor, and total_count still counts the full inventory. Present
every returned recommendation in text; never call hotel-rates just to render options for hotels
the traveler has not selected. The response's assistant_note is a presentation reminder
addressed to you — follow it and never show it to the traveler.
next_cursor is opaque. If the traveler wants hotels beyond the recommendations, call search with
that value unchanged as --cursor (CLI) / cursor (MCP) and a rationale; omit the original search
fields because Ramp reads the cached result. For delegated bookings, re-pass the same
--traveler_user_id (CLI) / traveler_user_id (MCP) on every cursor call so the page is
reauthorized against the delegated traveler's current access. Preserve a non-default --limit when
consistent page size matters. Cursor calls on both CLI and MCP continue to use
wait_for_results=true. Append the new hotels; never re-run the search for more results and never
inspect, edit, synthesize, or reuse a cursor with a different search.
Use total_count and the number displayed so the traveler knows when more results are available.
No matching inventory means recommended is absent and hotels is empty. Offer to adjust the
location or dates. Surface policy_summary before the table when present; it is Ramp's
authoritative summary of what this traveler may book.
Present the hotel comparison
When applied_preferences_summary is present, lead with it once as a short sentence so the
traveler knows how preferences shaped the ranking. Then render the recommended hotels as
one Markdown comparison table — never as a bullet list, numbered list, prose, or a plain
sentence list. recommended.best_match first, then each alternates entry in returned
order. Search follows the Ramp web flow and returns zero or one selected/best room-rate for
each hotel inside its hotel.rates. It does not return every available room or rate. The
full column order is:
| # | Hotel | Rating | Chain | Nightly (pre-tax) | All-in total | Policy | Loyalty program | Notes |
|---|---|---|---|---|---|---|---|---|
| 1 | Four Seasons Chicago | 5-star | Four Seasons | $245 USD | $812 USD | In policy | Four Seasons Preferred Partner | Matches gym preference; 12 min walk; Company preferred |
Populate it only from returned metadata:
- Always show
#,Hotel,Rating,Nightly (pre-tax),All-in total, andPolicy. Chain,Loyalty program, andNotesare optional columns. Omit an optional column completely when every row on the displayed page would be-. If at least one row has a meaningful value, keep the column in the order above and use-for individual rows without a value.- Rating: use only
star_rating, formatted as4-star,5-star, etc. Never showreview_ratingvalues such as9.4. - Chain: show a real
chainname such asFour Seasons. Render missing, blank, or placeholder values such asDefault Chainas-. - Nightly (pre-tax): show
nightly_amountexactly as returned withcurrency. It is the base nightly amount before taxes and fees, matching web's/nightprice. - All-in total: show
total_amountexactly as returned. It is the full-stay total including taxes and fees; never derive it by multiplying the nightly amount or add taxes/fees yourself. Showcurrency. - Policy: keep it concise:
In policy,Out of policy: <short violation>, or-when no verdict is available. Trust the returned policy verdict and violations; do not infer policy from or reverse-engineer it from displayed prices. Usepolicy_summaryfor broader policy context. - Loyalty program: show
loyalty_programwhen present; otherwise show-. Do not replace a missing program with generic eligibility or points-earning prose. - Notes: start with the recommendation's
reasons, condensed to a few words each, and any materialtradeoffs(a policy caveat or missed preference the traveler should weigh); then combineoffice_travel_time_minuteswithoffice_travel_mode(for example,12 min walk),coworker_booking_countwhen nonzero, andCompany preferredwhenis_company_preferred=true, separated by;. If the combined cell gets long, move the reasons/tradeoffs to a compact one-line note under the row instead. Show-when none is present. Do not invent reasons beyond the returned ones and do not call travel time a distance. Use hoteladdressto disambiguate similar properties; mention relevanthotel_amenitiesonly when they match a stated preference. - If
rates=[], show-for price, policy, and loyalty cells and noteNo current summary rate. Never index the first rate unless it exists; the traveler can still select the hotel and fetch complete rates. - Keep a private display-number-to-hotel-
idmap. Never print hotel IDs unless asked. - Do not show a second room/rate table during this initial comparison. Do not show
room_name,refundability, orcancellation_policyyet; reserve them for the selected-hotel details.
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.
Wait for the traveler to select a hotel option. If their choice is ambiguous, confirm the hotel, nightly amount, and all-in total. Do not preview or book the search result's summary rate.
Fetch and present selected-hotel rates
Call hotel-rates with the literal hotel id from search and the same dates, adult count, and
traveler target:
CLI:
ramp travel hotel-rates "<selected_hotel_id>" --output json \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "fetch current rooms and rates for the selected Chicago hotel, Aug 10-13"
MCP:
{
"hotel_id": "{selected_hotel_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"rationale": "fetch current rooms and rates for the selected Chicago hotel, Aug 10-13"
}
Call GetHotelRates with the above.
For delegated booking, pass the same --traveler_user_id. Keep --num_adults consistent with
search.
The response returns recommended_rates — up to three room-rate groups with full rate payloads —
alongside the complete all_rates inventory. Lead with these recommended options: name each
group's room and rate and quote its recommendation_reason when populated. Reasons are generated
fail-open and may be null; a missing reason is not a signal, so present the rate without inventing
one. recommendation_reason appears only on rates inside recommended_rates (including each
group's best_rate) and is always null on all_rates rows. A nonempty unmet_preferences on a
recommended group marks a best-available fallback; tell the traveler which requested preferences
it does not meet. When every rate is out of policy, recommended_rates still holds the fallback
picks with their policy-violation reasons.
Then keep the full inventory visible: render one Markdown table row for every returned
all_rates[].rates[] option — never as a bullet list or prose:
| # | Room | Nightly (pre-tax) | All-in total | Payment | Refundability | Cancellation | Policy | Loyalty | Notes | |---|------|---------------------|----------------|--------------|---------|---------------|--------------|--------|---------|-------| | 1 | Deluxe King | $245 USD | $812 USD | Pay later | Refundable | Free until Aug 8 | In policy | Four Seasons | Recommended; Company preferred |
- Use the room group's
room_name,room_description, androom_amenities. Mark a group as recommended only when it also appears inrecommended_rates; do not infer recommendations from list order orbest_rate.best_rateis the lowest-total option within that room group, not a global recommendation. - Show each rate's
nightly_amount,total_amount,payment_type,refundability, andcancellation_policywhen present. Show amount strings exactly as returned and include the separatecurrencyonce; do not append it when the amount already includes it. - Keep policy concise in the table and surface full
policy_violationswhen the traveler compares or selects an out-of-policy rate. - For loyalty, show
loyalty_programandearns_loyalty_pointswhen relevant. Clearly flagloyalty_required=trueand stop before preview whenloyalty_eligible=falseuntil the membership is corrected. - Put
is_company_preferred,is_corporate_rate, and useful room amenities in Notes when present.has_corporate_ratesis context, not proof every rate is corporate. - Keep a private display-number-to-literal-rate-
idmap. Never use a room ID, hotel ID, or search summary rate ID for booking. - Use top-level
hotelmetadata to reconfirm the selected property.has_corporate_ratesis hotel-level context, while each rate'sis_corporate_rateidentifies the actual corporate rate.
These rates are current options, not quotes. Wait for the traveler to select one exact room/rate; never guess or default to the first or recommended row.
Preview the selected rate
Call book-hotel without --confirm. Its positional values are the literal selected hotel ID
from search and selected rate ID from hotel-rates. Pass the same stay dates used to fetch the
rate:
CLI:
ramp travel book-hotel "<selected_hotel_id>" "<selected_rate_id>" --output json \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "preview the selected Chicago hotel rate, Aug 10-13"
MCP:
{
"hotel_id": "{selected_hotel_id}",
"rate_id": "{selected_rate_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"confirm": false,
"rationale": "preview the selected Chicago hotel rate, Aug 10-13"
}
Call SubmitHotelBooking with the above.
For delegated booking, pass the same --traveler_user_id. If Ramp says the rate expired or is
missing from cache, start a fresh search-hotel; if the rate mismatches the selected hotel or stay
dates, call hotel-rates again for that hotel/current dates. Never retry an old rate or silently
choose another.
The preview is authoritative and may differ from the rates response. Present:
- hotel, room, and stay dates
- traveler (
traveler_name_displaywhen the preview card provides it) - the selected rate's
nightly_amountas the pre-tax nightly price; do not substitute an all-in nightly amount - payment timing from the selected rate's
payment_type(or preview cardpayment_display) - exact all-in
total_amountfrom the preview; do not derive it from a nightly amount in_policyand everypolicy_violationsentryrequires_approvaland eachapproval_stepsentry- every
eligible_fundsoption:fund_nameand available balance/spending limit when present; keep a private display-number-to-fund_uuidmap rather than printing UUIDs - matching
loyalty_programs: meaningfuldisplay_nameandloyalty_numbervalues that Ramp will submit on confirm
Present funds as a Markdown table without internal IDs — never as a bullet list or prose:
| # | Fund | Available balance | Spending limit |
|---|---|---|---|
| 1 | Client Travel | $1,500 USD | $5,000 USD |
Keep the display-number-to-fund_uuid map private. For loyalty memberships, show display_name
and only the last four characters of loyalty_number unless the traveler explicitly asks for the
full saved number; never display loyalty_program_id or logo as text.
Carry forward refundability, cancellation, and payment details from the selected rate. If that
rate requires loyalty but the traveler is not eligible, stop before preview and ask them to
correct the membership or choose another rate. Use preview loyalty_programs to verify which saved
matching membership Ramp will submit, rather than treating the rate's generic program name as
proof of enrollment.
If the preview is out of policy, show every returned violation and ask why the traveler needs that
rate. Keep their exact justification as oop_reason; Ramp requires --oop_reason on confirmation
for an out-of-policy quote and approvers see it. reason is a separate optional general booking
note unless request_new_fund=true, where it is the required trip purpose; include it only under
those conditions.
If no fund was explicitly selected, the preview auto-uses recommended_fund_uuid when an eligible
fund is available. Label <fund name> (recommended) only when the tool auto-populated that
recommendation; a user-selected fund never gets the label, even when its UUID matches. If the
preview echoes an explicitly passed fund as selected_fund_uuid; when non-null, that is the
booking fund. If the traveler chooses or changes to a fund, repeat the preview with confirm=false and its literal
fund_uuid as spend_allocation_id, present the refreshed result, and wait for a separate
explicit confirmation turn.
Distinguish fund_eligibility_status=none_eligible from lookup_failed: the former permits the
new-fund path. When using that path, collect the trip purpose before confirmation because it is
required as reason and shown to approvers. The latter requires a fresh preview and must never be confirmed. Use
approval_display_status verbatim; do not infer approval wording from requires_approval or
approval_steps alone.
If loyalty_program_names_to_offer is non-empty and this is a self-booking, offer to save one of
the returned programs. The hotel flow may ask that loyalty question together with the usual
confirmation question. 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.
If the traveler selected an existing trip, keep its literal UUID in context but do not send
--trip_id during preview: Ramp only resolves it during confirm. A missing, invalid, or other-user
trip is ignored and Ramp auto-selects or creates one. Verify the resulting trip_id/trip_name
after booking when attachment matters. Omitting --confirm is the preview flow.
Finish with an explicit confirmation question containing the hotel, room, dates, refundability, cancellation terms, all-in total, policy/approval state, matching loyalty membership, selected fund behavior, and OOP justification when applicable. Stop and wait for a clear yes.
Confirm only after explicit approval
Use the same hotel ID, rate ID, dates, traveler, and fund selection from the approved final preview.
Add --confirm and copy the preview's numeric expected_total_amount verbatim, with no currency
symbol:
CLI:
ramp travel book-hotel "<selected_hotel_id>" "<selected_rate_id>" --confirm \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--expected_total_amount <preview_expected_total_amount> --output json \
--rationale "book the selected Lower Manhattan hotel rate; traveler approved the preview"
MCP:
{
"hotel_id": "{selected_hotel_id}",
"rate_id": "{selected_rate_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"confirm": true,
"expected_total_amount": "{preview_expected_total_amount}",
"spend_allocation_id": "{fund_uuid_from_latest_preview}",
"rationale": "book the selected Lower Manhattan hotel rate; traveler approved the preview"
}
Call SubmitHotelBooking 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.
Add optional confirmation flags only when applicable:
--traveler_user_id '<traveler_uuid>': exact delegated traveler UUID used for fresh search, rates, and preview.--spend_allocation_id '<fund_uuid>': exact fund from the final preview, including the recommended UUID when the preview auto-populated it.--request_new_fund=trueplus--reason '<trip purpose>': use only when the latest preview showed the new-fund path. The trip purpose is shown to approvers.--trip_id '<trip_uuid>'(CLI) /trip_id(MCP): exact existing trip UUID explicitly selected by the traveler; omit to let Ramp auto-select or create a trip. This is the step where Ramp resolves it. Note: delegated trip lookup is not exposed on MCP;GetUserTripsreturns the caller's own trips only.--oop_reason '<traveler_justification>': exact justification collected after an out-of-policy preview; required only whenin_policy=false.--reason '<trip purpose>': required withrequest_new_fund=true; never repurpose it as the OOP justification.
Keep any collected OOP justification in context through a fund or price re-preview, but send it only on confirmation. If the new preview changes policy violations materially, reconfirm that the traveler's justification still applies.
Confirmation requires exactly one funding path: spend_allocation_id or
request_new_fund=true. Never omit both and never pass both. Never normalize, reformat, or
recalculate the numeric expected_total_amount.
If confirm=true fails for any reason, stop. Relay the error message, follow
agent_guidance, and never retry, tweak parameters, switch hotel/room/rate, or confirm again
without a fresh preview and a fresh explicit confirmation. A changed total, missing/expired cache
mapping, or hotel/date mismatch therefore requires the appropriate fresh preview/rates/search and
new approval; it is not permission to retry the confirmation.
On a successful confirmation, present booking.booking_request_id, lowercase booking.status,
booking.total_amount, and booking.next_steps before verification. Do not call the reservation
confirmed merely because booked=true.
Verify the booking request
The confirm response's booked=true means a booking request was created, not necessarily that the
hotel is confirmed. Show the returned booking status and approval state accurately. Verify with:
CLI:
ramp travel bookings --output json \
--hotel_name "citizenM New York Bowery" --travel_date 2026-08-10 \
--rationale "verify the Lower Manhattan hotel booking request, Aug 10-13"
MCP:
{
"hotel_name": "citizenM New York Bowery",
"travel_date": "2026-08-10",
"rationale": "verify the Lower Manhattan hotel booking request, Aug 10-13"
}
Call GetBookings with the above.
Use the known city, hotel name, and stay date as lookup filters. They combine with AND and are
applied before the result limit. If results_truncated is true, add another known filter and
retry rather than treating the returned entries as exhaustive.
For delegated booking, pass the same traveler UUID. Retain the exact booking.booking_request_id
from confirmation and match it to the same booking_request_id in the bookings response. The
default call includes current/upcoming hotels, flights, and cars. Each entry has a generic id;
use the matching entry's exact id with travel booking-details (CLI) / GetBookingDetails
(MCP) for detailed questions. Do not fuzzy-match by hotel name, dates, room type, or booking
time. Use returned trip_id and trip_name to verify trip attachment when needed. If the
matching request is missing from the default result, retry once with --include_failed (CLI) /
include_failed: true (MCP); if it is still missing, report that verification could not locate
the submitted request rather than using another entry. Cancelled, rejected, and failed requests
do not block rebooking.
The submit response's nested booking.status is lowercase (pending_approval, approved,
booked, or rejected). The bookings response uses uppercase request/reservation states:
CONFIRMED: report the reservation as confirmed.PENDING_APPROVAL: report that the request is awaiting approval, not booked.PROCESSING: report that fulfillment is still processing and check again later.FAILED: showerror_message; address the stated issue before a new booking attempt.CANCELLED: report that the request/reservation was cancelled.REJECTED: report that the request was rejected.
travel booking-details (CLI) / GetBookingDetails (MCP) is part of the booking
support tools. If it is not available, degrade gracefully to the
information from travel bookings / GetBookings. When available, use request_status,
current_total_amount, error_message, and approval.pending_approval_summary; relay the
pending approval summary verbatim.
Cancelling a hotel booking
Cancelling forfeits or spends real money, so it follows the same preview → explicit yes →
confirm discipline as booking. The command is ramp travel cancel-hotel (CLI) /
SubmitHotelCancellation (MCP); it is enabled per business, so it may be absent for some
accounts (see "If cancellation is unavailable"). It always cancels the entire hotel booking.
Identify the exact booking first
Never guess which booking to cancel. Resolve it from ramp travel bookings --output json
(CLI) / GetBookings (MCP) (same --traveler_user_id / traveler_user_id for delegated
travelers), passing every known city, hotel_name, and travel_date filter, and use the
exact entry id — the same exact ID this skill already uses with travel booking-details /
GetBookingDetails. If more than one booking could match ("cancel my New York hotel"), show
the likely matches and ask which one; never pick by hotel name, dates, or recency on your own.
If results_truncated is true, ask for another identifying detail and run a narrower lookup
before selecting a booking. If the traveler pasted an ID that this conversation's travel bookings / GetBookings never returned, look the booking up first instead of trusting the
pasted value.
Cancellation applies to a fulfilled booking. For an unfulfilled request (e.g.
PENDING_APPROVAL), the entry id is the request UUID, which this command will not find —
direct the traveler to the request in the Ramp web app instead.
Preview the terms (read-only)
Always call without --confirm first. This cancels nothing and returns the authoritative
terms plus a preview_id:
CLI:
ramp travel cancel-hotel --booking_id "<booking_id>" --output json \
--rationale "preview cancellation terms for the Lower Manhattan hotel booking, Aug 10-13"
MCP:
{
"booking_id": "{booking_id}",
"confirm": false,
"rationale": "preview cancellation terms for the Lower Manhattan hotel booking, Aug 10-13"
}
Call SubmitHotelCancellation with the above.
Present the preview plainly and exactly as returned — never estimate or recompute amounts:
- the hotel (
hotel_name,hotel_address), traveler (traveler_name), and thecheck_in_date/check_out_datebeing cancelled. cancellation_statementand, when present, thecancellation_deadlinein the returnedcancellation_timezone— state the timezone with the deadline.- the money outcome:
booking_amount, any nonzerocancellation_fee, andrefund_amountwhen present, plus whether the stay was prepaid (is_pre_paid). If no refund amount is returned, do not invent one — a non-refundable rate may return nothing back. - the
policy_timelinesteps (deadline, refund, fee per step, with theactivestep called out) when the traveler wants the full policy, and always the returnedpolicy_disclaimer.
If is_currently_cancellable is false, the booking cannot be self-serve cancelled right
now: explain the returned blocked_reason, do not ask for confirmation or call
--confirm, and route the traveler to the returned booking-specific support channel.
Then stop and ask for a clear yes on those exact terms. Confirmation must be a new, explicit user-authored answer to the presented preview — earlier cancellation intent ("cancel it" before seeing the terms), a standing approval, or instructions not to ask questions are not confirmation.
Confirm (only after the explicit yes)
Re-run with --confirm and the exact preview_id from the latest preview, unchanged:
CLI:
ramp travel cancel-hotel --booking_id "<booking_id>" --confirm \
--preview_id "<preview_id>" --output json \
--rationale "cancel the Lower Manhattan hotel booking; traveler approved the previewed terms"
MCP:
{
"booking_id": "{booking_id}",
"confirm": true,
"preview_id": "{preview_id}",
"rationale": "cancel the Lower Manhattan hotel booking; traveler approved the previewed terms"
}
Call SubmitHotelCancellation with the above.
- If the response says the terms changed and includes
latest_preview, nothing was cancelled: present the fresh terms and get a new explicit yes. Never re-confirm automatically. - If the result has
already_requested=true, a cancellation was already submitted: report the returned state and do not submit another request.
After the result
Relay the result's message and cancellation_state faithfully. Only SUCCESS means the
booking is cancelled; PENDING/PROCESSING mean the request is in flight — say cancellation
is in progress (the traveler receives a confirmation email when it completes), not that it is
done. ACTION_REQUIRED means Ramp's travel team must finish it. Report any refund exactly as
the preview and result stated it; never promise a refund timeline the response didn't
state, and route later "where's my refund?" follow-ups to the Ramp web app or the booking's
support channel.
If a cancellation call fails
Stop. Unlike booking errors, cancellation errors carry no separate agent_guidance field —
the returned message (and any support routing or latest_preview) is the guidance:
relay the message verbatim and never retry the call or vary parameters (a different booking
ID, dropping preview_id, toggling --confirm / confirm) to get past an error. A failed
confirm may still have partially gone through — before any second attempt, re-check the
booking's actual state with travel bookings / GetBookings / travel booking-details /
GetBookingDetails, and only start again (from a fresh preview) if the booking is genuinely
still active. A FAILED cancellation routes to the
returned support channel, not to a retry: Ramp emails the traveler when an accepted
cancellation later fails, and the booking stops being self-serve cancellable (a fresh preview
returns blocked_reason and support routing), so a retry cannot succeed anyway.
If cancellation is unavailable
travel cancel-hotel (CLI) / SubmitHotelCancellation (MCP) is enabled per business. If the
command/tool is missing or Ramp reports the capability is unavailable, do not say the booking
can't be cancelled — say self-serve cancellation isn't enabled here and direct the traveler to
the booking in the Ramp web app or the booking's support channel.
Stay changes — different dates, a different room, adding nights, or cancel-and-rebook — are not cancellations and stay outside this skill; send the traveler to the Ramp web app or the booking's support channel for those.