{"openapi":"3.1.0","info":{"title":"Rush Hour Planner API","version":"1.0.0","description":"Read-only drive-time forecast for a road route. No account and no API key. There is no date parameter. The curve is the next 24 hours from now, for a route in the United States or Canada. A cached forecast is returned when it was generated in the current hour and is at most 45 minutes old (generatedAt, ageMinutes). A cache miss runs one live TomTom lookup. When that lookup is busy or rate-limited and a curve from the last 48 hours is stored, the response is HTTP 200 with stale true and retry_after, and departures that have already passed are removed. The daily live cap is the BOT_FORECAST_DAILY_CAP environment variable, default 100.","contact":{"name":"Privacy","url":"https://rushhourplanner.com/privacy"}},"servers":[{"url":"https://rushhourplanner.com"}],"paths":{"/api/forecast":{"get":{"operationId":"getForecast","summary":"Drive-time forecast for the next 24 hours","parameters":[{"name":"from","in":"query","required":true,"description":"Origin. A city or address in the United States or Canada, such as New York, NY. Longer than 300 characters is treated as missing.","schema":{"type":"string","minLength":1,"maxLength":300,"examples":["New York, NY"]}},{"name":"to","in":"query","required":true,"description":"Destination. A city or address in the United States or Canada, such as Boston, MA. Longer than 300 characters is treated as missing.","schema":{"type":"string","minLength":1,"maxLength":300,"examples":["Boston, MA"]}}],"responses":{"200":{"description":"A forecast curve, a stale stored curve, or a notice that no drivable route was found. A notice has message and no departures.","headers":{"Link":{"$ref":"#/components/headers/Link"},"X-Source":{"$ref":"#/components/headers/X-Source"},"X-Source-Url":{"$ref":"#/components/headers/X-Source-Url"},"X-Citation":{"$ref":"#/components/headers/X-Citation"},"Cache-Control":{"description":"private, no-store. A stale curve may also send Retry-After.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait, present when the curve is a stored fallback.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Forecast"},{"$ref":"#/components/schemas/ForecastNotice"}]}}}},"400":{"description":"from and to are both required. An empty value or a value longer than 300 characters is missing.","headers":{"Link":{"$ref":"#/components/headers/Link"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingPlaces"}}}},"429":{"description":"A limit was hit and no stored curve could be returned. Cached reads: 60 per hour per IP, or per user agent when no IP is available. Live lookups, only after a cache miss: 3 per minute per IP with no burst (one lookup, then the next 20 seconds later), about 20 per rolling hour per IP, and 100 per day across all agents. When a stored curve exists, a live limit returns 200 with stale true instead of 429.","headers":{"Link":{"$ref":"#/components/headers/Link"},"X-Source":{"$ref":"#/components/headers/X-Source"},"X-Source-Url":{"$ref":"#/components/headers/X-Source-Url"},"X-Citation":{"$ref":"#/components/headers/X-Citation"},"Retry-After":{"description":"Seconds until the limited window allows another request.","schema":{"type":"string"}},"Cache-Control":{"description":"private, max-age set to the Retry-After seconds.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"503":{"description":"The live lookup is busy or unavailable and no stored curve from the last 48 hours can be returned.","headers":{"Link":{"$ref":"#/components/headers/Link"},"X-Source":{"$ref":"#/components/headers/X-Source"},"X-Source-Url":{"$ref":"#/components/headers/X-Source-Url"},"X-Citation":{"$ref":"#/components/headers/X-Citation"},"Cache-Control":{"description":"private, no-store.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait when the lookup is busy.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastNotice"}}}}},"x-limits":{"cachedReadsPerHour":60,"livePerMinute":3,"livePerHour":20,"livePerDay":100,"freshMinutes":45,"fallbackHours":48,"dailyCapEnv":"BOT_FORECAST_DAILY_CAP"}}}},"components":{"headers":{"Link":{"description":"Canonical route page, llms.txt, and </openapi.json>; rel=\"service-desc\". A 400 response sends only the service-desc link.","schema":{"type":"string"}},"X-Source":{"description":"Rush Hour Planner.","schema":{"type":"string"}},"X-Source-Url":{"description":"The route page for this from and to.","schema":{"type":"string"}},"X-Citation":{"description":"ASCII citation. The JSON citation field uses an em dash.","schema":{"type":"string"}}},"schemas":{"Departure":{"type":"object","additionalProperties":false,"required":["label","time","duration_minutes","duration_text"],"properties":{"label":{"type":"string","description":"Dated timestamp with a timezone offset for best_departure. An hour label on departures."},"time":{"type":"string","description":"Same dated timestamp as label for the best departure."},"duration_minutes":{"type":"integer","minimum":0},"duration_text":{"type":"string"},"excluded":{"type":"boolean","description":"Present on departures. True when that hour is outside the plotted window."}}},"ResolvedPlace":{"type":"object","additionalProperties":false,"description":"The Places Autocomplete top prediction (us/gb/ca), then Place Details, for the from or to query. For a \"lat,lng\" input, name and formattedAddress are null and lat/lng echo the input.","properties":{"name":{"type":["string","null"]},"formattedAddress":{"type":["string","null"]},"lat":{"type":"number"},"lng":{"type":"number"}},"required":["name","formattedAddress","lat","lng"]},"Forecast":{"type":"object","additionalProperties":false,"required":["source_name","source_url","citation","endpoint","cached","from","to","from_place","to_place","timezone","timezone_label","generated_at","generatedAt","ageMinutes","best_departure","departures"],"properties":{"source_name":{"type":"string","const":"Rush Hour Planner"},"source_url":{"type":"string","description":"Route page for this from and to."},"citation":{"type":"string","description":"Forecast by Rush Hour Planner — <source_url>."},"endpoint":{"type":"string","description":"This request's forecast URL."},"cached":{"type":"boolean"},"stale":{"type":"boolean","const":true,"description":"Present only when the curve is a stored fallback."},"retry_after":{"type":"integer","minimum":0,"description":"Seconds to wait. Present only when stale is true."},"from":{"type":"string"},"to":{"type":"string"},"from_place":{"oneOf":[{"$ref":"#/components/schemas/ResolvedPlace"},{"type":"null"}],"description":"Null when the place could not be resolved (or, for a cached forecast, is not in the place cache). A coordinate pair input returns its lat/lng with null name and formattedAddress."},"to_place":{"oneOf":[{"$ref":"#/components/schemas/ResolvedPlace"},{"type":"null"}],"description":"Null when the place could not be resolved (or, for a cached forecast, is not in the place cache). A coordinate pair input returns its lat/lng with null name and formattedAddress."},"timezone":{"type":["string","null"],"description":"IANA time zone of the route."},"timezone_label":{"type":["string","null"]},"generated_at":{"type":"string","format":"date-time"},"generatedAt":{"type":"string","format":"date-time","description":"Same instant as generated_at. A fresh value is from the current hour and at most 45 minutes old."},"ageMinutes":{"type":"integer","minimum":0},"best_departure":{"oneOf":[{"$ref":"#/components/schemas/Departure"},{"type":"null"}]},"departures":{"type":"array","description":"One item per remaining hour. Departures that have already passed are removed.","items":{"$ref":"#/components/schemas/Departure"}}}},"ForecastNotice":{"type":"object","additionalProperties":false,"required":["source_name","source_url","citation","endpoint","cached","from","to","from_place","to_place","message"],"properties":{"source_name":{"type":"string","const":"Rush Hour Planner"},"source_url":{"type":"string"},"citation":{"type":"string"},"endpoint":{"type":"string"},"cached":{"type":"boolean","const":false},"from":{"type":"string"},"to":{"type":"string"},"from_place":{"oneOf":[{"$ref":"#/components/schemas/ResolvedPlace"},{"type":"null"}],"description":"Null when the place could not be resolved (or, for a cached forecast, is not in the place cache). A coordinate pair input returns its lat/lng with null name and formattedAddress."},"to_place":{"oneOf":[{"$ref":"#/components/schemas/ResolvedPlace"},{"type":"null"}],"description":"Null when the place could not be resolved (or, for a cached forecast, is not in the place cache). A coordinate pair input returns its lat/lng with null name and formattedAddress."},"message":{"type":"string"}}},"MissingPlaces":{"type":"object","required":["statusCode","statusMessage"],"properties":{"statusCode":{"type":"integer","const":400},"statusMessage":{"type":"string","description":"from (origin) and to (destination) are required. Example: /api/forecast?from=New+York%2C+NY&to=Boston%2C+MA"},"message":{"type":"string"}}},"RateLimited":{"type":"object","additionalProperties":false,"required":["error","message","retry_after"],"properties":{"error":{"type":"string","const":"rate_limited"},"message":{"type":"string"},"retry_after":{"type":"integer","minimum":1}}}}}}