{
  "openapi": "3.1.0",
  "info": {
    "title": "tiny weather",
    "version": "0.1.0",
    "summary": "Current conditions and a ten-day forecast for a point.",
    "description": "Forecast data from MET Norway under NLOD 2.0 and CC BY 4.0. Every response carries the attribution notice those licences require. Follows the tiny platform contract: Ed25519 bearer tokens from tiny.kjuulh.io, RFC 9457 problem responses, RateLimit headers.",
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://weather.tiny.kjuulh.io"
    }
  ],
  "security": [
    {
      "tinyToken": [
        "api:read"
      ]
    }
  ],
  "components": {
    "securitySchemes": {
      "tinyToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "An Ed25519 access token from POST https://tiny.kjuulh.io/v1/tokens, with audience https://weather.tiny.kjuulh.io, scope api:read, and 'weather' in its services claim."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests per minute for this account and service."
      },
      "RateLimitRemaining": {
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds until replenishment."
      },
      "XRequestId": {
        "schema": {
          "type": "string"
        },
        "description": "Echoed if the caller sent a sane one, otherwise a UUIDv7. Same ID as the usage event."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "instance",
          "code",
          "request_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string",
            "description": "For people. May change."
          },
          "instance": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable machine value.",
            "enum": [
              "invalid_request",
              "unauthenticated",
              "service_not_enabled",
              "insufficient_scope",
              "rate_limited",
              "spend_limit_reached",
              "not_found",
              "not_acceptable",
              "upstream_unavailable",
              "internal"
            ]
          },
          "request_id": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "reason"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "required",
                    "not_a_number",
                    "out_of_range",
                    "unknown_field",
                    "unknown_value",
                    "malformed",
                    "no_data"
                  ]
                }
              }
            }
          }
        }
      },
      "Condition": {
        "type": "object",
        "required": [
          "code",
          "showers",
          "thunder",
          "symbol"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "clear",
              "fair",
              "partly_cloudy",
              "cloudy",
              "fog",
              "rain",
              "sleet",
              "snow",
              "unknown"
            ]
          },
          "intensity": {
            "type": "string",
            "enum": [
              "light",
              "moderate",
              "heavy"
            ]
          },
          "showers": {
            "type": "boolean"
          },
          "thunder": {
            "type": "boolean"
          },
          "symbol": {
            "type": "string",
            "description": "MET Norway's symbol code, verbatim."
          },
          "daylight": {
            "type": "boolean",
            "description": "Absent when the symbol does not distinguish day from night."
          }
        }
      },
      "Forecast": {
        "type": "object",
        "required": [
          "location",
          "units",
          "utc_offset_minutes",
          "model",
          "cache",
          "attribution",
          "request_id"
        ],
        "properties": {
          "location": {
            "type": "object",
            "description": "Three points: what you asked for, the grid cell it was snapped to, and what the provider answered for.",
            "required": [
              "requested",
              "grid",
              "grid_resolution_deg",
              "resolved"
            ],
            "properties": {
              "requested": {
                "$ref": "#/components/schemas/Point"
              },
              "grid": {
                "$ref": "#/components/schemas/Point"
              },
              "grid_resolution_deg": {
                "type": "number"
              },
              "resolved": {
                "$ref": "#/components/schemas/Point"
              },
              "elevation_m": {
                "type": "number"
              }
            }
          },
          "units": {
            "type": "object"
          },
          "utc_offset_minutes": {
            "type": "integer"
          },
          "current": {
            "type": "object"
          },
          "hourly": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "daily": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "date",
                "steps",
                "complete"
              ],
              "properties": {
                "date": {
                  "type": "string"
                },
                "temperature_min": {
                  "type": "number"
                },
                "temperature_max": {
                  "type": "number"
                },
                "precipitation": {
                  "type": "number"
                },
                "precipitation_probability_max": {
                  "type": "number"
                },
                "wind_speed_max": {
                  "type": "number"
                },
                "wind_gust_max": {
                  "type": "number"
                },
                "condition": {
                  "$ref": "#/components/schemas/Condition"
                },
                "steps": {
                  "type": "integer"
                },
                "complete": {
                  "type": "boolean",
                  "description": "False when the day is only partly covered, which is normal for today and for the last day."
                }
              }
            }
          },
          "model": {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "cache": {
            "type": "object",
            "required": [
              "status",
              "age_seconds",
              "expires_at",
              "degraded"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "hit",
                  "miss",
                  "revalidated",
                  "stale",
                  "degraded"
                ]
              },
              "age_seconds": {
                "type": "integer"
              },
              "expires_at": {
                "type": "string"
              },
              "degraded": {
                "type": "boolean",
                "description": "True when the provider is failing and this is the last good answer we hold."
              }
            }
          },
          "attribution": {
            "type": "object",
            "description": "Required by the data licence. Reproduce the notice where you show the data."
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "Point": {
        "type": "object",
        "required": [
          "lat",
          "lon"
        ],
        "properties": {
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          }
        }
      }
    },
    "responses": {
      "Problem": {
        "description": "RFC 9457 problem document. The stable field is code.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/forecasts": {
      "get": {
        "operationId": "forecast.read",
        "summary": "The forecast for a point",
        "description": "Metered: one unit per 2xx response, whether or not it was served from cache. Not idempotency-keyed: GET has no side effects.",
        "security": [
          {
            "tinyToken": [
              "api:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "current,hourly,daily"
            }
          },
          {
            "name": "hours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 72,
              "default": 48
            }
          },
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10,
              "default": 7
            }
          },
          {
            "name": "utc_offset",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "Z"
            },
            "description": "Offset for daily buckets, e.g. +02:00."
          },
          {
            "name": "X-Request-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A forecast. May be served from cache, or, during a provider outage, from the last good copy; see cache.degraded.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Tiny-Cache": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "hit",
                    "miss",
                    "revalidated",
                    "stale",
                    "degraded"
                  ]
                }
              },
              "X-Tiny-Age": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds since this forecast was fetched from the provider."
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "max-age runs to the provider's own Expires, so a caller that honours it costs nothing and is billed nothing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Forecast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "402": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "406": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "503": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/health/live": {
      "get": {
        "summary": "Process can accept HTTP",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/health/ready": {
      "get": {
        "summary": "Dependencies needed for normal requests are usable",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "This document",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    }
  }
}