{"openapi":"3.1.0","info":{"title":"Terrace Weather API","description":"Tells restaurants whether their terrace is viable for a given hour or service window, based on the weather forecast. Weather data by Open-Meteo.com (CC BY 4.0). Limit: 60 requests/minute per IP.","version":"v1"},"servers":[{"url":"https://terrace.javiermateo.dev","description":"Generated server url"}],"tags":[{"name":"Terrace","description":"Terrace viability from the weather forecast. All date-times are local time at the restaurant (no 'Z', no UTC offset)."}],"paths":{"/api/v1/terrace/windows":{"post":{"tags":["Terrace"],"summary":"Verdicts for up to 25 service windows of the same location in one call","description":"Check every reservation of a day at once. Counts as a single request for the rate limit and a single weather lookup. Each window is evaluated independently: one with bad times returns an 'error' instead of failing the whole batch. Use 'id' to match results to your reservations (defaults to the position in the list).","operationId":"windows","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindowsRequest"}}},"required":true},"responses":{"400":{"description":"Invalid request; 'detail' names the offending field","content":{"application/problem+json":{}}},"429":{"description":"Rate limit (60 requests/minute per IP) exceeded; see the Retry-After header","content":{"application/problem+json":{}}},"502":{"description":"The weather provider is temporarily unavailable; retry with backoff","content":{"application/problem+json":{}}}}}},"/api/v1/terrace/window":{"get":{"tags":["Terrace"],"summary":"Single verdict and suggested terrace capacity for a service window","description":"Uses the worst hour of the window, e.g. lunch 13:00-16:00: one storm closes the terrace. `start` is inclusive and `end` exclusive. Times are local to the restaurant.","operationId":"window","parameters":[{"name":"lat","in":"query","description":"Latitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":90.0,"minimum":-90.0},"example":40.4168},{"name":"lon","in":"query","description":"Longitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":180.0,"minimum":-180.0},"example":-3.7038},{"name":"start","in":"query","description":"Start, local time at the restaurant (no 'Z' or offset)","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-09-22T13:00:00"},{"name":"end","in":"query","description":"End (exclusive), local time at the restaurant (no 'Z' or offset)","required":true,"schema":{"type":"string","format":"date-time"},"example":"2026-09-22T16:00:00"},{"name":"profile","in":"query","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","required":false,"schema":{"type":"string","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","enum":["TEMPERATE","TROPICAL","MEDITERRANEAN","NORDIC"],"example":"MEDITERRANEAN"},"example":"MEDITERRANEAN"},{"name":"maxRainProbability","in":"query","description":"Rain probability (%) above which points are lost. Overrides the profile.","required":false,"schema":{"type":"integer","format":"int32","description":"Rain probability (%) above which points are lost. Overrides the profile.","example":40,"maximum":100,"minimum":0},"example":40},{"name":"maxPrecipitationMm","in":"query","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","example":0.5,"minimum":0.0},"example":0.5},{"name":"maxGustsKmh","in":"query","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","example":35,"minimum":0.0},"example":35},{"name":"minApparentTempC","in":"query","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","example":14,"maximum":50.0,"minimum":-40.0},"example":14},{"name":"maxApparentTempC","in":"query","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","example":34,"maximum":60.0,"minimum":-40.0},"example":34}],"responses":{"400":{"description":"Invalid request; 'detail' names the offending field","content":{"application/problem+json":{}}},"429":{"description":"Rate limit (60 requests/minute per IP) exceeded; see the Retry-After header","content":{"application/problem+json":{}}},"502":{"description":"The weather provider is temporarily unavailable; retry with backoff","content":{"application/problem+json":{}}}}}},"/api/v1/terrace/profiles":{"get":{"tags":["Terrace"],"summary":"Climate profiles and the thresholds they apply","description":"Presets to use as the `profile` parameter. Explicit thresholds override them.","operationId":"profiles","responses":{"400":{"description":"Invalid request; 'detail' names the offending field","content":{"application/problem+json":{}}},"429":{"description":"Rate limit (60 requests/minute per IP) exceeded; see the Retry-After header","content":{"application/problem+json":{}}},"502":{"description":"The weather provider is temporarily unavailable; retry with backoff","content":{"application/problem+json":{}}}}}},"/api/v1/terrace/forecast":{"get":{"tags":["Terrace"],"summary":"Hour-by-hour terrace score for the next hours (max 72)","description":"Starts at the current hour at the location, so the first entry is 'now'. Each hour has a 0-100 score, a verdict (OPEN, CAUTION, CLOSED) and the reasons that cost points.","operationId":"forecast","parameters":[{"name":"lat","in":"query","description":"Latitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":90.0,"minimum":-90.0},"example":40.4168},{"name":"lon","in":"query","description":"Longitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":180.0,"minimum":-180.0},"example":-3.7038},{"name":"hours","in":"query","description":"How many hours to return (1-72)","required":false,"schema":{"type":"integer","format":"int32","default":24,"maximum":72,"minimum":1},"example":12},{"name":"profile","in":"query","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","required":false,"schema":{"type":"string","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","enum":["TEMPERATE","TROPICAL","MEDITERRANEAN","NORDIC"],"example":"MEDITERRANEAN"},"example":"MEDITERRANEAN"},{"name":"maxRainProbability","in":"query","description":"Rain probability (%) above which points are lost. Overrides the profile.","required":false,"schema":{"type":"integer","format":"int32","description":"Rain probability (%) above which points are lost. Overrides the profile.","example":40,"maximum":100,"minimum":0},"example":40},{"name":"maxPrecipitationMm","in":"query","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","example":0.5,"minimum":0.0},"example":0.5},{"name":"maxGustsKmh","in":"query","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","example":35,"minimum":0.0},"example":35},{"name":"minApparentTempC","in":"query","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","example":14,"maximum":50.0,"minimum":-40.0},"example":14},{"name":"maxApparentTempC","in":"query","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","example":34,"maximum":60.0,"minimum":-40.0},"example":34}],"responses":{"400":{"description":"Invalid request; 'detail' names the offending field","content":{"application/problem+json":{}}},"429":{"description":"Rate limit (60 requests/minute per IP) exceeded; see the Retry-After header","content":{"application/problem+json":{}}},"502":{"description":"The weather provider is temporarily unavailable; retry with backoff","content":{"application/problem+json":{}}}}}},"/api/v1/terrace/best-window":{"get":{"tags":["Terrace"],"summary":"Best window of a given length on a day, plus non-overlapping alternatives","description":"Searches every start hour between fromHour and toHour (the restaurant's service hours) and ranks windows by their worst hour, then average, then earliest start. Alternatives never overlap the best window and are never CLOSED. Useful to propose a time to a customer.","operationId":"bestWindow","parameters":[{"name":"lat","in":"query","description":"Latitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":90.0,"minimum":-90.0},"example":13.7563},{"name":"lon","in":"query","description":"Longitude of the restaurant (decimal degrees)","required":true,"schema":{"type":"number","format":"double","maximum":180.0,"minimum":-180.0},"example":100.5018},{"name":"date","in":"query","description":"Day to search, local date (the forecast covers the next 3 days)","required":true,"schema":{"type":"string","format":"date"},"example":"2026-09-22"},{"name":"durationHours","in":"query","description":"Length of the window in hours (1-12), e.g. how long a customer stays","required":false,"schema":{"type":"integer","format":"int32","default":2,"maximum":12,"minimum":1},"example":2},{"name":"fromHour","in":"query","description":"First hour the restaurant serves (0-23)","required":false,"schema":{"type":"integer","format":"int32","default":12,"maximum":23,"minimum":0},"example":12},{"name":"toHour","in":"query","description":"Hour service ends (1-24); windows must finish by then","required":false,"schema":{"type":"integer","format":"int32","default":23,"maximum":24,"minimum":1},"example":23},{"name":"profile","in":"query","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","required":false,"schema":{"type":"string","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","enum":["TEMPERATE","TROPICAL","MEDITERRANEAN","NORDIC"],"example":"MEDITERRANEAN"},"example":"MEDITERRANEAN"},{"name":"maxRainProbability","in":"query","description":"Rain probability (%) above which points are lost. Overrides the profile.","required":false,"schema":{"type":"integer","format":"int32","description":"Rain probability (%) above which points are lost. Overrides the profile.","example":40,"maximum":100,"minimum":0},"example":40},{"name":"maxPrecipitationMm","in":"query","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","example":0.5,"minimum":0.0},"example":0.5},{"name":"maxGustsKmh","in":"query","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","example":35,"minimum":0.0},"example":35},{"name":"minApparentTempC","in":"query","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","example":14,"maximum":50.0,"minimum":-40.0},"example":14},{"name":"maxApparentTempC","in":"query","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","required":false,"schema":{"type":"number","format":"double","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","example":34,"maximum":60.0,"minimum":-40.0},"example":34}],"responses":{"400":{"description":"Invalid request; 'detail' names the offending field","content":{"application/problem+json":{}}},"429":{"description":"Rate limit (60 requests/minute per IP) exceeded; see the Retry-After header","content":{"application/problem+json":{}}},"502":{"description":"The weather provider is temporarily unavailable; retry with backoff","content":{"application/problem+json":{}}}}}}},"components":{"schemas":{"Item":{"type":"object","properties":{"id":{"type":"string","description":"Your label for this window (e.g. the reservation id), echoed back in the result. Defaults to the position in the list.","example":"mesa-12","maxLength":64,"minLength":0},"start":{"type":"string","format":"date-time","description":"Start, local time at the restaurant, no 'Z' or UTC offset","example":"2026-09-22T13:30:00"},"end":{"type":"string","format":"date-time","description":"End (exclusive), local time at the restaurant, no 'Z' or UTC offset","example":"2026-09-22T15:30:00"}},"required":["end","start"]},"RulesQuery":{"type":"object","properties":{"profile":{"type":"string","description":"Climate preset used as the base for all thresholds. Default TEMPERATE. See GET /profiles for the values.","enum":["TEMPERATE","TROPICAL","MEDITERRANEAN","NORDIC"],"example":"MEDITERRANEAN"},"maxRainProbability":{"type":"integer","format":"int32","description":"Rain probability (%) above which points are lost. Overrides the profile.","example":40,"maximum":100,"minimum":0},"maxPrecipitationMm":{"type":"number","format":"double","description":"Expected precipitation (mm/hour) at which the terrace closes outright. Overrides the profile.","example":0.5,"minimum":0},"maxGustsKmh":{"type":"number","format":"double","description":"Wind gusts (km/h) above which points are lost; 1.5x closes outright. Overrides the profile.","example":35,"minimum":0},"minApparentTempC":{"type":"number","format":"double","description":"Feels-like temperature (C) below which it is too cold. Must be lower than maxApparentTempC. Overrides the profile.","example":14,"maximum":50,"minimum":-40},"maxApparentTempC":{"type":"number","format":"double","description":"Feels-like temperature (C) above which it is too hot. Overrides the profile.","example":34,"maximum":60,"minimum":-40}}},"WindowsRequest":{"type":"object","properties":{"lat":{"type":"number","format":"double","description":"Latitude of the restaurant (decimal degrees)","example":40.4168,"maximum":90,"minimum":-90},"lon":{"type":"number","format":"double","description":"Longitude of the restaurant (decimal degrees)","example":-3.7038,"maximum":180,"minimum":-180},"rules":{"$ref":"#/components/schemas/RulesQuery","description":"Optional tolerances: a climate profile and/or individual thresholds"},"windows":{"type":"array","description":"Service windows to evaluate (1 to 25)","items":{"$ref":"#/components/schemas/Item"},"maxItems":25,"minItems":1}},"required":["lat","lon","windows"]}}}}