Use this skill when the user wants to search travel options or book a flight/stay through TravAI.
If this skill conflicts with backend behavior, check the live OpenAPI schema:
text
https://api.travai.tech/openapi.json
Follow the live schema over this doc.
search_id (not search_hash)method ( "CRYPTO" or "CARD") — not typeEXPIRED_OFFER, re-run the search and use fresh IDsoffer_id values from the new results; old offer IDs will not work with the new search_idtext
https://api.travai.tech
All endpoints (except sign-in and sign-up) require:
text
Authorization: Bearer {access_token}
Three ways to authenticate:
text
POST /auth/signin
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret"
}
Response (200):
json
{
"access_token": "eyJhbG...",
"token_type": "bearer"
}
text
POST /auth/signup
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret",
"first_name": "Jane",
"last_name": "Doe"
}
Response (200):
json
{
"message": "User created successfully",
"access_token": "eyJhbG...",
"token_type": "bearer"
}
text
GET /auth/me
Authorization: Bearer {access_token}
Response (200):
json
{
"user_id": 1,
"email": "user@example.com",
"role": "customer",
"is_active": true,
"created_at": "2025-01-01T00:00:00",
"currency": "USD"
}
Any failed request may return:
json
{ "detail": "Human-readable error description" }
Always check for detail in non-2xx responses and surface the message to the user.
text
POST /searches
Authorization: Bearer {access_token}
Content-Type: application/json
{
"type": "FLIGHT",
"origin_iata": "LON",
"destination_iata": "LIS",
"departure_date": "2025-10-10",
"return_date": "2025-10-17",
"customers": [
{ "type": "ADULT" },
{ "type": "CHILD", "age": 10 }
]
}
FieldRequiredNotestypeyes"FLIGHT"``origin_iatayes3-letter IATA code (uppercase, e.g. "LON")destination_iatayes3-letter IATA code (uppercase, e.g. "LIS")departure_dateyesYYYY-MM-DD``return_datenoomit for one-waycustomersyesarray of CustomerAge (see below)
CustomerAge object:
FieldRequiredNotestypeno (default: ADULT)ADULT, CHILD, SENIOR, HELD_INFANT``agerequired for CHILD and HELD_INFANTinteger
Customer type rules:
HELD_INFANT — under 2 years oldCHILD — between 2 and 18 years oldADULT — defaultSENIOR — senior travellerResponse (200):
json
{
"search_id": "abc123",
"total_offers": 42,
"best_offers": [ /* top 3 offers */ ]
}
FieldDescriptionsearch_idIdentifier for this search — use in all subsequent callstotal_offersTotal number of offers matching the searchbest_offersThe 3 best offers (sorted by price/quality)
Direct-flight filtering: To find direct flights, only keep offers where the total segment count across all slices equals the number of slices (i.e. 1 segment per slice = no stops).
text
POST /searches
Authorization: Bearer {access_token}
Content-Type: application/json
{
"type": "STAY",
"location_anchor": {
"value": "Abu Dhabi",
"type": "city"
},
"check_in_date": "2026-03-05",
"check_out_date": "2026-03-10",
"rooms": 1,
"guests": 1,
"radius_km": 50
}
FieldRequiredDefaultNotestypeyes—"STAY"``location_anchoryes—see StaysSearchAnchor belowcheck_in_dateyes—YYYY-MM-DD``check_out_dateyes—YYYY-MM-DD``roomsno11–8guestsno1number of adult guestsradius_kmno50search radius in km around the anchor
StaysSearchAnchor object:
FieldRequiredDescriptionvalueyesReference point text, e.g. "Milano", "London center", "LHR"``typeyesOne of: airport, city, city_center, region, address, place, others
Anchor type guide:
airport — 3-letter IATA code (e.g. "LHR")city — city name (e.g. "Abu Dhabi")city_center — city centre / downtown (e.g. "London center")region — neighbourhood / district (e.g. "Manhattan")address — street address (e.g. "Via G.B. Pioda 10")place — landmark / point of interest (e.g. "Parque Edgar Sampaio Fontes")others — anything elseResponse (200):
json
{
"search_id": "def456",
"total_offers": 15,
"best_offers": [ /* top 3 offers */ ]
}
FieldDescriptionsearch_idIdentifier for this search — use in all subsequent callstotal_offersTotal number of offers matching the searchbest_offersThe 3 best offers
text
GET /searches/{search_id}/offers?limit=20&offset=0
Authorization: Bearer {access_token}
ParameterRequiredDefaultDescriptionsearch_idyes (path)—search_id from POST /searches``limitno (query)20Max offers to return (1–200)offsetno (query)0Offset for pagination
Response (200):
json
{
"total_offers": 42,
"offers": [ /* paginated list of offer summaries */ ]
}
FieldDescriptiontotal_offersTotal number of offers for this searchoffersPaginated slice of offer summaries
text
GET /searches/{search_id}/offers/{offer_id}
Authorization: Bearer {access_token}
Response (200): a single offer summary object.
text
GET /searches/{search_id}/offers/{offer_id}/pricing
Authorization: Bearer {access_token}
Path parameterDescriptionsearch_id``search_id from POST /searches``offer_idOffer ID from best_offers or the paginated offers list
Response (200): a flat offer summary dict with full pricing details (final price, segments, airlines, etc.).
Error (410): returns { "detail": "EXPIRED_OFFER" } if the offer is no longer available. When this happens, inform the user and re-run the search.
text
GET /searches/{search_id}/offers/{offer_id}/rates
Authorization: Bearer {access_token}
Path parameterDescriptionsearch_id``search_id from POST /searches``offer_idHotel offer ID from best_offers or the paginated offers list
Response (200): a list of room objects, each containing room details (name, description, size, max occupancy, photos, beds) and nested rates array with pricing, payment type, and cancellation conditions.
text
GET /searches/{search_id}/offers/{offer_id}/rates/{rate_id}/pricing
Authorization: Bearer {access_token}
Path parameterDescriptionsearch_id``search_id from POST /searches``offer_idHotel offer IDrate_idRate ID from the rates list
Response (200): a stay summary with full pricing details (total amount, currency, accommodation info, room breakdown).
Error (410): returns { "detail": "EXPIRED_OFFER" } if the rate is no longer available. When this happens, inform the user and re-run the search.
text
GET /searches/{search_id}/offers/{offer_id}/photos
No authentication required. Returns an interactive HTML photo gallery for the hotel.
Path parameterDescriptionsearch_id``search_id from POST /searches``offer_idHotel offer ID
How to present to the user: Build the full gallery URL and share it as a clickable link:
text
https://api.travai.tech/searches/{search_id}/offers/{offer_id}/photos
Do not share individual photo URLs from the API response — they are CDN-signed and will not load in the browser. Always use the gallery link instead.
The gallery features keyboard navigation (arrow keys), click-to-advance, and a thumbnail strip.
Individual photos can also be accessed at /searches/{search_id}/offers/{offer_id}/photos/{index} (0-based) if needed for embedding.
text
GET /tokens
Authorization: Bearer {access_token}
Response (200): a flat list of supported crypto tokens. Each item represents one asset on one blockchain.
json
[
{ "assetId": "nep141:wrap.near", "blockchain": "NEAR", "symbol": "NEAR" },
{ "assetId": "nep141:usdc.near", "blockchain": "NEAR", "symbol": "USDC" },
{ "assetId": "eth:0xabc...", "blockchain": "Ethereum", "symbol": "USDC" }
]
FieldDescriptionassetIdToken identifier — use as origin_asset in a CRYPTO paymentblockchainBlockchain network the token lives onsymbolToken symbol (e.g. "NEAR", "USDC")
Token selection guidance:
blockchain when presenting to the usersymbol + blockchainassetId before proceedingAfter the user picks a token, ask for their wallet address on that token's blockchain to use as refund_to.
text
POST /payments
Authorization: Bearer {access_token}
Content-Type: application/json
Payment methods: CRYPTO or CARD. Use the method field (not type).
json
{
"method": "CRYPTO",
"origin_asset": "{assetId from GET /tokens}",
"refund_to": "{user wallet address}",
"offers": [ /* see offer object below */ ]
}
FieldRequiredDescriptionmethodyes"CRYPTO"``origin_assetyesassetId of the token selected by the user from GET /tokens``refund_toyesUser's wallet address on the selected token's network — refunds go here in the same currencyoffersyesSelected offers to pay for
Response (200):
json
{
"payment_id": 123,
"itinerary_id": 456,
"deposit_address": "travai.near",
"amount_in": "1000000",
"amount_in_formatted": "1.00",
"total_amount": 100,
"currency": "USD"
}
FieldDescriptionpayment_idPayment identifier (use to check status via GET /payments/{payment_id})itinerary_idItinerary identifier (use to get confirmations via GET /itineraries/{itinerary_id}/offers)deposit_addressAddress to send tokens toamount_in_formattedExact amount of tokens to send — must match exactly, do not roundamount_inRaw token amount (integer string)total_amountFiat equivalent in smallest unit (cents)currencyISO currency code (e.g. "USD")
json
{
"method": "CARD",
"success_url": "https://app.travai.tech/?payment=success",
"cancel_url": "https://app.travai.tech/?payment=cancel",
"offers": [ /* see offer object below */ ]
}
FieldRequiredDescriptionmethodyes"CARD"``success_urlyesAlways "https://app.travai.tech/?payment=success"``cancel_urlyesAlways "https://app.travai.tech/?payment=cancel"``offersyesSelected offers to pay for
Response (200):
json
{
"payment_id": 123,
"itinerary_id": 456,
"payment_url": "https://checkout.stripe.com/...",
"total_amount": 100,
"currency": "USD"
}
FieldDescriptionpayment_idPayment identifier (use to check status via GET /payments/{payment_id})itinerary_idItinerary identifier (use to get confirmations via GET /itineraries/{itinerary_id}/offers)payment_urlURL for the user to complete card paymenttotal_amountAmount in smallest currency unit (cents)currencyISO currency code (e.g. "USD")
text
GET /payments/{payment_id}
Authorization: Bearer {access_token}
Path parameterDescriptionpayment_idPayment ID from the POST /payments response
Response (200): full payment record including status, fiat_amount, fiat_currency, token_amount, token_asset, created_at, updated_at.
Payment statuses: PENDING, PENDING_DEPOSIT, INCOMPLETE_DEPOSIT, KNOWN_DEPOSIT_TX, PROCESSING, SUCCESS, REFUNDED, FAILED, ERROR.
text
GET /itineraries/{itinerary_id}/offers
Authorization: Bearer {access_token}
Path parameterDescriptionitinerary_idItinerary ID from the POST /payments response
Response (200): list of itinerary offer items, each with itinerary_offer_id, type, provider, price, currency, status, booking_summary, created_at.
offers array in payment request)#json
{
"type": "FLIGHT",
"search_id": "abc123",
"offer_id": "offer_xyz",
"customers": [
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1989-04-23",
"gender": "FEMALE",
"nationality": "UA",
"country_code": "380",
"phone_number": "632816433",
"title": "MS"
}
]
}
FieldRequiredDescriptiontypeyes"FLIGHT" or "STAY"``search_idyessearch_id from the search stepoffer_idyes (FLIGHT)Offer ID from the search resultsrate_idyes (STAY)Rate ID from the hotel rates stepcustomersyesTraveller details (see below)
customers array)#FieldRequiredExamplefirst_nameyesJane``last_nameyesDoe``titleyesMR, MRS, MS, DR``genderyesMALE or FEMALE``date_of_birthyes1989-04-23 (YYYY-MM-DD)nationalityyesUA, GB, US (ISO 3166-1 alpha-2)country_codeyes380 (phone country code, no +)phone_numberyes632816433 (without country code)emailyesjane@example.com
IMPORTANT: first_name, last_name, and date_of_birth must exactly match the traveller's international passport. Always remind the user of this before collecting their details.
POST /searches with type: "FLIGHT"GET /searches/{search_id}/offers/{offer_id}/pricingGET /tokens, present grouped by blockchain (prefer stablecoins), collect wallet address for refundPOST /payments with method fieldamount_in_formatted + token + deposit_address + refund walletpayment_urlGET /payments/{payment_id}POST /searches with type: "STAY"https://api.travai.tech/searches/{search_id}/offers/{offer_id}/photos — do NOT share raw photo URLsGET /searches/{search_id}/offers/{offer_id}/rates when user picks a hotelGET .../rates/{rate_id}/pricing when user picks a raterate_id in offer object, use method fieldGET /payments/{payment_id}Many cities are served by multiple airports. Always check this mapping before searching — run parallel searches for all relevant airports and compare results.
If a city is not listed below but the user mentions a country or region, use your knowledge of IATA codes to identify all relevant airports.
City / CountryAirports (IATA)London / UKLHR, LGW, STN, LTN, LCYManchester / UKMAN, LPL (Liverpool, 1h)Edinburgh / UKEDI, GLA (Glasgow, 1h)Paris / FranceCDG, ORY, BVANice / FranceNCELyon / FranceLYSAmsterdam / NetherlandsAMS, EIN (Eindhoven), RTM (Rotterdam)Brussels / BelgiumBRU, CRL (Charleroi)Berlin / GermanyBERFrankfurt / GermanyFRA, HHN (Hahn)Munich / GermanyMUC, NUE (Nuremberg, nearby)Düsseldorf / GermanyDUS, CGN (Cologne, 1h), DTM (Dortmund)Hamburg / GermanyHAMZurich / SwitzerlandZRH, BSL (Basel), BRN (Bern)Geneva / SwitzerlandGVAVienna / AustriaVIE, BTS (Bratislava, 80km)Dublin / IrelandDUB, SNN (Shannon), ORK (Cork)
City / CountryAirports (IATA)Milan / ItalyMXP, LIN, BGY (Bergamo)Rome / ItalyFCO, CIA (Ciampino)Naples / ItalyNAPVenice / ItalyVCE, TSF (Treviso)Florence / ItalyFLR, PSA (Pisa, 1h)Bologna / ItalyBLQCatania / SicilyCTA, PMO (Palermo)Madrid / SpainMADBarcelona / SpainBCN, GRO (Girona), REU (Reus)Malaga / SpainAGPValencia / SpainVLCSeville / SpainSVQAlicante / SpainALCPalma de Mallorca / SpainPMILisbon / PortugalLISPorto / PortugalOPOAthens / GreeceATHThessaloniki / GreeceSKGIstanbul / TurkeyIST, SAW (Sabiha Gökçen)Antalya / TurkeyAYT
City / CountryAirports (IATA)Stockholm / SwedenARN, BMA, NYO (Skavsta), VST (Västerås)Copenhagen / DenmarkCPH, BLL (Billund), AAR (Aarhus)Oslo / NorwayOSL, TRF (Torp), RYG (Rygge)Helsinki / FinlandHEL, TMP (Tampere)Reykjavik / IcelandKEF, RKV
City / CountryAirports (IATA)Prague / Czech RepublicPRGWarsaw / PolandWAW, WMI (Modlin)Krakow / PolandKRKGdansk / PolandGDNWroclaw / PolandWROBudapest / HungaryBUDBucharest / RomaniaOTP, BBU (Băneasa)Sofia / BulgariaSOFKyiv / UkraineKBP, IEV (Zhuliany)Belgrade / SerbiaBEGZagreb / CroatiaZAGSplit / CroatiaSPUDubrovnik / CroatiaDBVRiga / LatviaRIXVilnius / LithuaniaVNOTallinn / EstoniaTLLBratislava / SlovakiaBTS, VIE (Vienna, 60km)
City / CountryAirports (IATA)Dubai / UAEDXB, DWC (Al Maktoum)Abu Dhabi / UAEAUHDoha / QatarDOHRiyadh / Saudi ArabiaRUHJeddah / Saudi ArabiaJEDMuscat / OmanMCTBahrainBAHKuwaitKWIAmman / JordanAMMTel Aviv / IsraelTLVBeirut / LebanonBEYCairo / EgyptCAISharm El Sheikh / EgyptSSHHurghada / EgyptHRG
City / CountryAirports (IATA)Tokyo / JapanNRT (Narita), HND (Haneda)Osaka / JapanKIX (Kansai), ITM (Itami)Seoul / South KoreaICN (Incheon), GMP (Gimpo)Beijing / ChinaPEK (Capital), PKX (Daxing)Shanghai / ChinaPVG (Pudong), SHA (Hongqiao)Guangzhou / ChinaCANShenzhen / ChinaSZXHong KongHKGTaipei / TaiwanTPE (Taoyuan), TSA (Songshan)
City / CountryAirports (IATA)Bangkok / ThailandBKK (Suvarnabhumi), DMK (Don Mueang)Phuket / ThailandHKTChiang Mai / ThailandCNXSingaporeSINKuala Lumpur / MalaysiaKUL, SZB (Sultan Abdul Aziz Shah)Jakarta / IndonesiaCGK (Soekarno-Hatta)Bali / IndonesiaDPSManila / PhilippinesMNLHo Chi Minh City / VietnamSGNHanoi / VietnamHANPhnom Penh / CambodiaPNHSiem Reap / CambodiaREPYangon / MyanmarRGN
City / CountryAirports (IATA)Delhi / IndiaDELMumbai / IndiaBOMBangalore / IndiaBLRColombo / Sri LankaCMBMaldivesMLEKathmandu / NepalKTMAlmaty / KazakhstanALATbilisi / GeorgiaTBSYerevan / ArmeniaEVNBaku / AzerbaijanGYDTashkent / UzbekistanTAS
City / CountryAirports (IATA)Johannesburg / South AfricaJNB, HLA (Lanseria)Cape Town / South AfricaCPTNairobi / KenyaNBOAddis Ababa / EthiopiaADDCasablanca / MoroccoCMNMarrakech / MoroccoRAKTunis / TunisiaTUNLagos / NigeriaLOSAccra / GhanaACCDar es Salaam / TanzaniaDARZanzibar / TanzaniaZNZMauritiusMRU
City / CountryAirports (IATA)New York / USAJFK, EWR (Newark), LGA (LaGuardia)Los Angeles / USALAX, BUR (Burbank), LGB (Long Beach), ONT, SNA (Orange County)San Francisco / USASFO, OAK (Oakland), SJC (San Jose)Chicago / USAORD (O'Hare), MDW (Midway)Miami / USAMIA, FLL (Fort Lauderdale), PBI (West Palm Beach)Washington DC / USAIAD (Dulles), DCA (Reagan), BWI (Baltimore)Dallas / USADFW, DAL (Love Field)Houston / USAIAH, HOU (Hobby)Boston / USABOSSeattle / USASEAAtlanta / USAATLDenver / USADENLas Vegas / USALASHonolulu / USAHNLToronto / CanadaYYZ, YTZ (Billy Bishop)Montreal / CanadaYULVancouver / CanadaYVRMexico City / MexicoMEX, NLU (Felipe Ángeles)Cancun / MexicoCUN
City / CountryAirports (IATA)São Paulo / BrazilGRU (Guarulhos), CGH (Congonhas), VCP (Campinas)Rio de Janeiro / BrazilGIG (Galeão), SDU (Santos Dumont)Buenos Aires / ArgentinaEZE (Ezeiza), AEP (Aeroparque)Santiago / ChileSCLLima / PeruLIMBogota / ColombiaBOGMedellin / ColombiaMDE, EOH (Olaya Herrera)Cartagena / ColombiaCTG
City / CountryAirports (IATA)Sydney / AustraliaSYDMelbourne / AustraliaMEL, AVV (Avalon)Brisbane / AustraliaBNEPerth / AustraliaPERAuckland / New ZealandAKLQueenstown / New ZealandZQNFijiNAN
This is the core of intelligent search. Always follow this protocol.
Before searching, check the Nearby Airports Database for both origin and destination. If the city has multiple airports, plan parallel searches.
Example: User says "London to Milan" — search LHR->MXP, LHR->LIN, LHR->BGY, LGW->MXP, LGW->LIN, LGW->BGY, STN->MXP, STN->BGY in parallel. Surface cheapest overall.
Always launch multiple searches simultaneously using parallel tool calls:
Flexible date matrix example (+/-1 day, 1 origin, 1 destination = 3 searches):
If user says they are flexible or "around that date", go +/-2 days (5 searches).
After collecting all results:
If a search returns 0 offers:
After showing results, if prices are high (>$300 for short-haul, >$700 for medium-haul), proactively say:
"Prices on this date are on the higher side. Would you like me to check +/-1-2 days to find a cheaper option?"
Act like an experienced travel agent, not a search engine.
Always start with a header containing route, dates, passengers, and total offers found.
Template:
text
## [City] [IATA] -> [City] [IATA] | [Departure date] - [Return date] | [N] adults
Found [X] offers. Here are the best:
All results are divided into three groups in strict order:
Group 1 — Cheapest
Header: ### Cheapest
Group 2 — Fastest
Header: ### Fastest
Group 3 — Other options
Header: ### Other options
Round trip:
Route outDeparture -> Arrival (duration)ClassRoute backDeparture -> Arrival (duration)ClassPrice
One way:
RouteDeparture -> Arrival (duration)ClassPrice
Column rules:
Route (out / back):
Bangkok BKK -> Kuala Lumpur KUL -> Singapore SIN, Amsterdam AMS -> Barcelona BCN (direct)Departure -> Arrival (duration):
+1 after arrival time and include arrival date06/10 06:00 -> 11:25 (4h 25m)06/10 23:00 -> 06/11 03:30 +1 (4h 30m)Class: Economy / Business / First
Price:
After each table — one italic line with key details.
Key info to include per offer:
Caption template:
text
*[Airline] · baggage included · non-refundable · ...*
Next-day arrival — always use +1:
+1 goes right after arrival time in table cell05/11 23:25 -> 05/12 06:25 +1After all tables — a summary block with:
5.1 Cheapest
text
Cheapest: [Airline] — [price]. [pros]. But: [cons].
5.2 Best pick A reasoned recommendation — not always the cheapest. Consider: direct vs layover, refund, baggage, price difference.
text
Best pick: [Airline], [route] — [price] ([difference] more). [conditions]. [One-sentence rationale].
5.3 Warnings (if any) Each warning — separate line. Must mention:
5.4 Call to action Last line — always offer to proceed to booking:
text
Want to book? Name your preferred option and I'll request exact pricing and details.
Always respond in the language the user is writing in. The structure stays the same regardless of language.
ErrorCauseFix422 method field requiredSent type instead of method in payment requestUse "method": "CRYPTO" or "method": "CARD"``Search ... not foundSearch expired or invalid search_idRe-run POST /searches and use fresh search_id404 Offer ... not found in searchOffer ID from a different/old searchRe-run search and use offer_id values from the new results410 EXPIRED_OFFEROffer or rate is no longer availableRe-run search, pick a fresh offer, then retry422 on signupSent name instead of first_name/ last_nameUse separate first_name and last_name fieldsUnsupported tokenToken/chain mismatchCall GET /tokens and pick a valid assetIdRedirect loop on POSTTrailing-slash routing issueEnsure POST goes to /payments (no trailing slash)
Booking changes and cancellations cannot be done via the API. The user must contact support directly:
Response time: within 24 hours.
When a user asks to change or cancel a booking, do not attempt any API calls — instead, direct them to one of the support channels above.
For any technical issues, questions, or problems with the API:
We will reply within 24 hours.