{"openapi":"3.1.0","info":{"title":"whentofly","description":"Flexible-date flight search for AI agents. Give a date window plus a min/max trip duration, get back a ranked list of cheapest round-trips with affiliate-marked Aviasales booking links. Free to call, no auth.\n\nAffiliate disclosure: each result's `book_url` is an affiliate link; we may earn a commission if a booking is made, at no extra cost to you.","version":"0.0.1"},"servers":[{"url":"https://whentofly.io","description":"production"}],"paths":{"/":{"get":{"summary":"Service discovery breadcrumbs","description":"Browsers and HTML-preferring crawlers (ChatGPT web fetch, Bing) get an HTML landing page. API clients (Accept: application/json or default */* like curl) get the same JSON breadcrumbs as before — links to docs, OpenAPI, llms.txt, MCP, and a worked example URL.","operationId":"discover","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Liveness probe","description":"Returns {\"status\":\"ok\"} when the service is running.","operationId":"health_check","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Check"}}}}}}},"/search":{"get":{"summary":"Flexible-date flight search (specific route OR anywhere)","description":"ONE flexible interface. With both `from` and `to` set, finds the cheapest round-trips across a date window with min/max trip duration (fans out across each month, filters by duration, ranks by price, attaches affiliate Aviasales links; 6h cache, window ≤365 days). Pass `to=anywhere` (or omit `to`) to instead get the cheapest destinations from the origin, each with a compact price-level `verdict` when we have confident band data. Anywhere mode is origin-only inspiration; date, duration, transfer, and cabin parameters do not constrain it. Pass `cabin=business` (or premium_economy/first) to find business-class deals: non-economy cabins are priced by the live Google Flights source, the verdict judges the fare against that cabin's own typical prices, and `metadata.deal_signal` flags a business fare below its typical band. When a specific search is empty or thin, `metadata.hints` carries concrete, machine-readable suggestions (e.g. extend the window) for the agent to auto-adjust. If `metadata.refresh_hint` is present, you may re-request the same URL after the suggested delay to get live-verified prices. If you can wait longer for a best-effort live check of the top fare, set `verify=full` (THOROUGH mode). It forces that check even on a cache hit and uses a longer ~35s budget; always inspect `metadata.freshness`, because a missing live source, provider failure, or spend cap can still return clearly labeled cached indicators.","operationId":"search_flights","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"Origin airport, 3-letter IATA code, UPPERCASE. Required for every search, including to=anywhere.","examples":["SGN","HKG","BKK"],"title":"From"},"description":"Origin airport, 3-letter IATA code, UPPERCASE. Required for every search, including to=anywhere."},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Destination airport, 3-letter IATA code, UPPERCASE. Pass `anywhere` (or omit) to get the cheapest destinations from the origin instead of a specific route.","examples":["ICN","NRT","SIN","anywhere"],"title":"To"},"description":"Destination airport, 3-letter IATA code, UPPERCASE. Pass `anywhere` (or omit) to get the cheapest destinations from the origin instead of a specific route."},{"name":"earliest","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Earliest acceptable departure date (ISO YYYY-MM-DD). Required for a specific route; ignored in anywhere mode.","examples":["2026-06-01"],"title":"Earliest"},"description":"Earliest acceptable departure date (ISO YYYY-MM-DD). Required for a specific route; ignored in anywhere mode."},{"name":"latest","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Latest acceptable return date for round-trip, or latest acceptable departure for one-way (ISO YYYY-MM-DD). Window from earliest must be ≤365 days. Required for a specific route; ignored in anywhere mode.","examples":["2026-08-31"],"title":"Latest"},"description":"Latest acceptable return date for round-trip, or latest acceptable departure for one-way (ISO YYYY-MM-DD). Window from earliest must be ≤365 days. Required for a specific route; ignored in anywhere mode."},{"name":"min_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Minimum round-trip duration in days (return - departure); ignored for one-way and anywhere mode.","examples":[10],"default":3,"title":"Min Days"},"description":"Minimum round-trip duration in days (return - departure); ignored for one-way and anywhere mode."},{"name":"max_days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Maximum round-trip duration in days (return - departure); ignored for one-way and anywhere mode.","examples":[15],"default":30,"title":"Max Days"},"description":"Maximum round-trip duration in days (return - departure); ignored for one-way and anywhere mode."},{"name":"one_way","in":"query","required":false,"schema":{"type":"boolean","description":"If true, search one-way flights; return_date and duration_days will be null.","default":false,"title":"One Way"},"description":"If true, search one-way flights; return_date and duration_days will be null."},{"name":"top_n","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Maximum number of results to return, sorted cheapest-first. Specific routes allow up to 50; anywhere mode returns at most 12.","examples":[10],"default":10,"title":"Top N"},"description":"Maximum number of results to return, sorted cheapest-first. Specific routes allow up to 50; anywhere mode returns at most 12."},{"name":"currency","in":"query","required":false,"schema":{"type":"string","description":"Result currency, 3-letter ISO code UPPERCASE.","examples":["USD","EUR","SGD"],"default":"USD","title":"Currency"},"description":"Result currency, 3-letter ISO code UPPERCASE."},{"name":"max_transfers","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":5,"minimum":0},{"type":"null"}],"description":"Maximum number of transfers/layovers per leg. 0 = nonstop only, 1 = up to 1 stop, etc. Omit for no filter.","examples":[0,1],"title":"Max Transfers"},"description":"Maximum number of transfers/layovers per leg. 0 = nonstop only, 1 = up to 1 stop, etc. Omit for no filter."},{"name":"cabin","in":"query","required":false,"schema":{"type":"string","description":"Cabin class: economy (default), premium_economy, business, or first. Use cabin=business to find business-class deals — the price verdict then judges the fare against the route's typical BUSINESS prices, and metadata.deal_signal flags a business fare below its own typical band. Non-economy cabins are priced via the live Google Flights source only (the cached calendar source covers economy), so results sample representative dates in the window.","examples":["economy","business"],"default":"economy","title":"Cabin"},"description":"Cabin class: economy (default), premium_economy, business, or first. Use cabin=business to find business-class deals — the price verdict then judges the fare against the route's typical BUSINESS prices, and metadata.deal_signal flags a business fare below its own typical band. Non-economy cabins are priced via the live Google Flights source only (the cached calendar source covers economy), so results sample representative dates in the window."},{"name":"verify","in":"query","required":false,"schema":{"type":"string","description":"How hard to live-verify the top result against Google Flights (SerpApi). `false` (default): the check still runs automatically on a fresh search when the top isn't already a live price, within a client-aware time budget. `true`: force the check even on a cache hit. `full`: THOROUGH mode — force the same best-effort check with a longer ~35s budget (set it when you can wait, e.g. an autonomous agent). Always inspect `metadata.freshness`: if the live source is unavailable, fails, or reaches its spend cap, the response can still contain explicitly labeled cached indicators. No-op unless a SerpApi key is configured.","examples":["false","true","full"],"default":"false","title":"Verify"},"description":"How hard to live-verify the top result against Google Flights (SerpApi). `false` (default): the check still runs automatically on a fresh search when the top isn't already a live price, within a client-aware time budget. `true`: force the check even on a cache hit. `full`: THOROUGH mode — force the same best-effort check with a longer ~35s budget (set it when you can wait, e.g. an autonomous agent). Always inspect `metadata.freshness`: if the live source is unavailable, fails, or reaches its spend cap, the response can still contain explicitly labeled cached indicators. No-op unless a SerpApi key is configured."},{"name":"ch","in":"query","required":false,"schema":{"type":"string","maxLength":64,"description":"Acquisition channel tag (e.g. web, mcp, a campaign name). Encoded with the route into the affiliate link's sub_id so a future commission can be attributed to this channel in the Travelpayouts dashboard. Defaults to 'direct'. Sanitized to [a-z0-9-]; capped at 64 chars.","examples":["web","mcp","direct"],"default":"direct","title":"Ch"},"description":"Acquisition channel tag (e.g. web, mcp, a campaign name). Encoded with the route into the affiliate link's sub_id so a future commission can be attributed to this channel in the Travelpayouts dashboard. Defaults to 'direct'. Sanitized to [a-z0-9-]; capped at 64 chars."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SearchResponse"},{"$ref":"#/components/schemas/InspireResponse"}],"discriminator":{"propertyName":"query_mode","mapping":{"specific":"#/components/schemas/SearchResponse","anywhere":"#/components/schemas/InspireResponse"}},"title":"Response Search Flights"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InspireMetadata":{"properties":{"cached":{"type":"boolean","title":"Cached","description":"Whether destination discovery came from cache."},"trip_type":{"type":"string","const":"one_way","title":"Trip Type","default":"one_way"},"freshness":{"type":"string","const":"cached_indicators","title":"Freshness","default":"cached_indicators"},"disclaimer":{"type":"string","title":"Disclaimer"},"docs":{"type":"string","title":"Docs"},"affiliate_disclosure":{"type":"string","title":"Affiliate Disclosure","default":"book_url is an affiliate link; we may earn a commission at no extra cost to you."},"presentation":{"type":"string","title":"Presentation","default":"Each result's agent_display is preformatted Markdown containing its exact book_url."}},"type":"object","required":["cached","disclaimer","docs"],"title":"InspireMetadata","description":"Agent-facing provenance shared by every anywhere-mode response."},"InspireResponse":{"properties":{"query_mode":{"type":"string","const":"anywhere","title":"Query Mode","default":"anywhere"},"query":{"additionalProperties":true,"type":"object","title":"Query","description":"Effective anywhere query. Date, duration, transfer, and cabin parameters do not constrain this origin-only inspiration mode."},"origin":{"type":"string","title":"Origin","description":"Resolved origin IATA code, UPPERCASE."},"origin_city":{"type":"string","title":"Origin City","description":"Human origin city name."},"origin_detected":{"type":"boolean","title":"Origin Detected","description":"True when the origin was inferred from the caller's IP country (Cloudflare CF-IPCountry); False when supplied via ?from= or the default fallback."},"currency":{"type":"string","title":"Currency"},"results":{"items":{"$ref":"#/components/schemas/InspireResult"},"type":"array","title":"Results"},"metadata":{"$ref":"#/components/schemas/InspireMetadata"},"verdict":{"anyOf":[{"$ref":"#/components/schemas/VerdictPayload"},{"type":"null"}],"description":"Price-level read for the TOP (cheapest) fare, from our own typical-price bands. Populated only when the read is confident — omitted for unknown or low-confidence. A price-level read, not a forecast."}},"type":"object","required":["query","origin","origin_city","origin_detected","currency","results","metadata"],"title":"InspireResponse"},"InspireResult":{"properties":{"destination":{"type":"string","title":"Destination","description":"Destination IATA code, UPPERCASE."},"city":{"type":"string","title":"City","description":"Human destination city name."},"price":{"type":"number","title":"Price","description":"Cheapest returned fare indicator, in `currency`."},"currency":{"type":"string","title":"Currency"},"airline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airline","description":"Marketing airline IATA code, if reported."},"airline_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Airline Name","description":"Human-facing airline name for `airline` when known (e.g. JT → Lion Air); the code remains available separately for machines."},"depart_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Depart Date","description":"Sample departure date (ISO YYYY-MM-DD)."},"book_url":{"type":"string","title":"Book Url","description":"Affiliate Aviasales booking link (one-way) carrying our marker and a per-route `-inspire` sub_id. This is the exact monetized destination URL associated with the result."},"agent_display":{"type":"string","title":"Agent Display","description":"Preformatted Markdown summary containing this result's exact `book_url` and structured fare details."},"verdict":{"anyOf":[{"$ref":"#/components/schemas/InspireVerdict"},{"type":"null"}],"description":"Compact price-level read for THIS destination's teaser fare, from our own typical-price bands (the same machinery /search uses). Populated whenever ANY band matched — including the coarse LOW-confidence distance/region generalization, which carries the typical range so the card shows honest data even when the label is only indicative. Omitted only when there's no band at all. Powers a per-card badge. A price-level read, not a forecast."}},"type":"object","required":["destination","city","price","currency","book_url","agent_display"],"title":"InspireResult"},"InspireVerdict":{"properties":{"recommendation":{"type":"string","enum":["good_price","typical","high_price"],"title":"Recommendation"},"confidence":{"type":"string","enum":["high","medium","low"],"title":"Confidence"},"reason_short":{"type":"string","title":"Reason Short"},"typical_low_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typical Low Cents"},"typical_high_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typical High Cents"}},"type":"object","required":["recommendation","confidence","reason_short"],"title":"InspireVerdict","description":"Compact per-destination price-level read for a teaser card.\n\nA trimmed ``VerdictPayload`` (recommendation + confidence + reason_short +\nthe typical range) — small enough to ride on every ``InspireResult`` and\nenough to render a per-card badge. Populated for ANY matched band, INCLUDING\nthe coarse LOW-confidence distance/region generalization read — so the cards\nstay alive on unseeded routes instead of blank. A ``\"low\"`` read is honest\nbut INDICATIVE: the client styles it as muted, never a strong claim, and the\n``typical_*_cents`` range is the honest data we can show even when we can't\nconfidently label the fare cheap/expensive. Omitted only when there's no band\nat all. Like every verdict, this is a price-LEVEL read, never a forecast."},"Price":{"properties":{"amount":{"type":"number","title":"Amount"},"currency":{"type":"string","title":"Currency"}},"type":"object","required":["amount","currency"],"title":"Price"},"SearchMetadata":{"properties":{"searched_months":{"items":{"type":"string"},"type":"array","title":"Searched Months"},"failed_months":{"items":{"type":"string"},"type":"array","title":"Failed Months","default":[]},"total_candidates":{"type":"integer","title":"Total Candidates"},"cached":{"type":"boolean","title":"Cached"},"cache_age_seconds":{"type":"integer","title":"Cache Age Seconds"},"disclaimer":{"type":"string","title":"Disclaimer"},"docs":{"type":"string","title":"Docs"},"affiliate_disclosure":{"type":"string","title":"Affiliate Disclosure","default":"book_url is an affiliate link; we may earn a commission at no extra cost to you."},"agent_instructions":{"type":"string","title":"Agent Instructions"},"cabin":{"type":"string","title":"Cabin","default":"economy"},"source_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Note"},"verdict":{"anyOf":[{"$ref":"#/components/schemas/VerdictPayload"},{"type":"null"}]},"deal_signal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Signal"},"deal_signal_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deal Signal Message"},"freshness":{"anyOf":[{"type":"string","enum":["live_verified","cached_indicators"]},{"type":"null"}],"title":"Freshness"},"refresh_hint":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Refresh Hint"},"hints":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Hints","default":[]}},"type":"object","required":["searched_months","total_candidates","cached","cache_age_seconds","disclaimer","docs","agent_instructions"],"title":"SearchMetadata"},"SearchResponse":{"properties":{"query_mode":{"type":"string","const":"specific","title":"Query Mode","default":"specific"},"query":{"additionalProperties":true,"type":"object","title":"Query"},"results":{"items":{"$ref":"#/components/schemas/SearchResult"},"type":"array","title":"Results"},"metadata":{"$ref":"#/components/schemas/SearchMetadata"}},"type":"object","required":["query","results","metadata"],"title":"SearchResponse"},"SearchResult":{"properties":{"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"},"destination":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination"},"rank":{"type":"integer","title":"Rank"},"departure_date":{"type":"string","format":"date","title":"Departure Date"},"return_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Return Date"},"duration_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Days"},"price":{"$ref":"#/components/schemas/Price"},"airline":{"type":"string","title":"Airline"},"transfers":{"type":"integer","title":"Transfers"},"book_url":{"type":"string","title":"Book Url"},"cabin":{"type":"string","title":"Cabin","default":"economy"},"gate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gate"},"found_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Found At"},"verified_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Verified Price"},"listed_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Listed Price"},"agent_display":{"type":"string","title":"Agent Display"}},"type":"object","required":["rank","departure_date","return_date","duration_days","price","airline","transfers","book_url","agent_display"],"title":"SearchResult"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerdictPayload":{"properties":{"recommendation":{"type":"string","enum":["good_price","typical","high_price","unknown"],"title":"Recommendation"},"confidence":{"type":"string","enum":["high","medium","low"],"title":"Confidence"},"reason_short":{"type":"string","title":"Reason Short"},"reason_long":{"type":"string","title":"Reason Long"},"typical_low_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typical Low Cents"},"typical_high_cents":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Typical High Cents"},"version":{"type":"integer","title":"Version","default":2}},"type":"object","required":["recommendation","confidence","reason_short","reason_long"],"title":"VerdictPayload"}}}}