{
  "openapi": "3.1.0",
  "info": {
    "title": "Keely API",
    "version": "1.0",
    "summary": "A person’s Keely numbers, for the apps they connect.",
    "description": "Each morning’s Recovery with what moved it, sleep, Load, stress and workouts, as the Keely app backs them up: summaries only, never raw heart data. Every answer carries `as_of` (the band’s last sync) and `methodology`. Missing data is `null` with a reason; what the app isn’t allowed to read is an error, never an empty answer. The guide is at https://meetkeely.com/developers. Every operation carries `x-keely-scope`: the scope it needs, `nights|days` for either, `any` for at least one, and an empty string for none.",
    "termsOfService": "https://meetkeely.com/terms",
    "contact": { "name": "Keely support", "email": "support@metrica.fit", "url": "https://meetkeely.com/support" }
  },
  "externalDocs": { "description": "Keely for developers", "url": "https://meetkeely.com/developers" },
  "servers": [{ "url": "https://meetkeely.com/api" }],
  "security": [{ "oauth2": [] }],
  "tags": [
    { "name": "About", "description": "The API and the connection." },
    { "name": "Days", "description": "Mornings and days: Recovery, the night, sleep, Load, stress, naps and workouts." },
    { "name": "Series", "description": "One metric over a range of dates." },
    { "name": "Workouts" },
    { "name": "Normals" },
    { "name": "Journal", "description": "The person’s journal, only while they share it in the Keely app." },
    { "name": "Changes", "description": "What changed since a time, for keeping a copy current." },
    { "name": "Events", "description": "What changed, typed, over the last 30 days: the events the webhooks deliver." },
    { "name": "Explain", "description": "How every number is computed." }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "index",
        "tags": ["About"],
        "summary": "The API’s name, version and endpoints",
        "x-keely-scope": "",
        "responses": {
          "200": { "description": "The index.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Index" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "me",
        "tags": ["About"],
        "summary": "The person’s id, the app’s scopes, and their profile and band",
        "description": "No scope for `person_id`, `scopes`, `app` and `journal_shared`. `profile` and `band` need the `profile` scope; without it they are left out and named in `not_allowed`.",
        "x-keely-scope": "",
        "responses": {
          "200": { "description": "The connection.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/days/latest": {
      "get": {
        "operationId": "latestDay",
        "tags": ["Days"],
        "summary": "The latest morning and day",
        "x-keely-scope": "nights|days",
        "responses": {
          "200": { "description": "One day.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Day" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/days/{date}": {
      "get": {
        "operationId": "day",
        "tags": ["Days"],
        "summary": "One morning and day",
        "description": "`recovery`, `night` and `sleep` need `nights`; `naps`, `load`, `steps` and `stress` need `days`; `workouts` needs `workouts` and `days`. 404 when the date has neither a night nor a day.",
        "x-keely-scope": "nights|days",
        "parameters": [{ "$ref": "#/components/parameters/date" }],
        "responses": {
          "200": { "description": "One day.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Day" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/days": {
      "get": {
        "operationId": "days",
        "tags": ["Days"],
        "summary": "Up to 92 days, one per date",
        "description": "`to` defaults to the latest date, `from` to 29 days before it. One object per date, in order, with `null` parts and a `missing` list for dates without data.",
        "x-keely-scope": "nights|days",
        "parameters": [{ "$ref": "#/components/parameters/from" }, { "$ref": "#/components/parameters/to" }],
        "responses": {
          "200": { "description": "The days.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Days" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/series/{metric}": {
      "get": {
        "operationId": "series",
        "tags": ["Series"],
        "summary": "One metric over a range, one point per date",
        "description": "Up to 366 days; defaults to the last 30. Night metrics (recovery, hrv, resting_hr, breathing_rate, skin_temp_deviation, asleep, sleep_need, sleep_performance, consistency, deep_sleep, rem_sleep) need `nights`; day metrics (load, balance, steps, stress_day, stress_night) need `days`.",
        "x-keely-scope": "nights|days",
        "parameters": [
          {
            "name": "metric", "in": "path", "required": true,
            "schema": { "type": "string", "enum": ["recovery", "hrv", "resting_hr", "breathing_rate", "skin_temp_deviation", "asleep", "sleep_need", "sleep_performance", "consistency", "deep_sleep", "rem_sleep", "load", "balance", "steps", "stress_day", "stress_night"] }
          },
          { "$ref": "#/components/parameters/from" },
          { "$ref": "#/components/parameters/to" }
        ],
        "responses": {
          "200": { "description": "The series.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Series" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/workouts": {
      "get": {
        "operationId": "workouts",
        "tags": ["Workouts"],
        "summary": "Up to 92 days of workouts",
        "description": "Defaults to the last 14 days. With `days` too, each workout carries the local `date` of the day that lists it; without, workouts are listed by their start between `from` 00:00Z and `to` 24:00Z, `date` is null and `note` says so.",
        "x-keely-scope": "workouts",
        "parameters": [{ "$ref": "#/components/parameters/from" }, { "$ref": "#/components/parameters/to" }],
        "responses": {
          "200": { "description": "The workouts.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Workouts" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/workouts/{id}": {
      "get": {
        "operationId": "workout",
        "tags": ["Workouts"],
        "summary": "One workout",
        "x-keely-scope": "workouts",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The workout’s id." }],
        "responses": {
          "200": { "description": "The workout.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorkoutAnswer" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/normals": {
      "get": {
        "operationId": "normals",
        "tags": ["Normals"],
        "summary": "The person’s current 30-night normals",
        "description": "`sleep_goal_minutes` needs `profile`; without it, it is left out and named in `not_allowed`.",
        "x-keely-scope": "nights",
        "responses": {
          "200": { "description": "The normals.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Normals" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/journal/tags": {
      "get": {
        "operationId": "journalTags",
        "tags": ["Journal"],
        "summary": "The journal’s tags",
        "x-keely-scope": "journal",
        "responses": {
          "200": { "description": "The tags.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JournalTags" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/JournalOff" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "post": {
        "operationId": "createJournalTag",
        "tags": ["Journal"],
        "summary": "Make a tag of the person’s own",
        "description": "Only when the person asks. When a tag already says the same thing, Keely returns it with `existing: true` rather than making another. At most 50 of the person’s own.",
        "x-keely-scope": "journal:write",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewJournalTag" } } } },
        "responses": {
          "201": { "description": "The tag, made or found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JournalTagAnswer" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/JournalOff" },
          "422": { "$ref": "#/components/responses/JournalRefused" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/journal/entries": {
      "get": {
        "operationId": "journalEntries",
        "tags": ["Journal"],
        "summary": "Up to 92 days of journal entries",
        "description": "Defaults to the last 14 days.",
        "x-keely-scope": "journal",
        "parameters": [{ "$ref": "#/components/parameters/from" }, { "$ref": "#/components/parameters/to" }],
        "responses": {
          "200": { "description": "The entries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JournalEntries" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/JournalOff" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      },
      "post": {
        "operationId": "logJournalEntries",
        "tags": ["Journal"],
        "summary": "Add entries, when the person asks",
        "description": "One to five tag keys, and either `when` or `date`. The database’s rules apply: only tags an app may log, times in the past 90 days, at most 50 entries a day from apps and assistants together.",
        "x-keely-scope": "journal:write",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewJournalEntries" } } } },
        "responses": {
          "201": { "description": "What was added.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoggedEntries" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/JournalOff" },
          "422": { "$ref": "#/components/responses/JournalRefused" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/journal/entries/{id}": {
      "delete": {
        "operationId": "deleteJournalEntry",
        "tags": ["Journal"],
        "summary": "Delete an entry an app added",
        "description": "What the person added in the app, or Keely noticed, is theirs to change in the app: 422 with `reason: not_added_by_an_app`.",
        "x-keely-scope": "journal:write",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The entry’s id." }],
        "responses": {
          "200": { "description": "Deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Deleted" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/JournalOff" },
          "422": { "$ref": "#/components/responses/JournalRefused" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "operationId": "changes",
        "tags": ["Changes"],
        "summary": "What changed since a time",
        "description": "Nights, days, workouts and journal rows changed since `since` (their `updated_at`), in the app’s scopes only, ordered by `updated_at` then `key`, at most 500 at a time. Needs at least one scope. `since` can be at most 90 days back. A workout the person removed comes with `removed: true`; a night or day that no longer exists simply stops being listed. Poll, overlap by 5 minutes, dedupe by kind and key.",
        "x-keely-scope": "any",
        "parameters": [
          { "name": "since", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "An ISO 8601 time, UTC. At most 90 days back." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 500 } }
        ],
        "responses": {
          "200": { "description": "The changes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Changes" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "events",
        "tags": ["Events"],
        "summary": "The events of the last 30 days",
        "description": "The events Keely wrote for this person in the last 30 days, the same ones the webhooks deliver (https://meetkeely.com/developers/webhooks): what changed, typed, with links to fetch it, and never a number from a summary. Only the events in the app’s scopes, plus the `connection.*` and `test.ping` addressed to it. In order of id, at most 500 at a time; when `has_more` is true, call again with `since=next`. Needs any scope.",
        "x-keely-scope": "any",
        "parameters": [
          { "name": "since", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "The number of the last event seen (123 for evt_123). 0 for everything kept." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 500 } }
        ],
        "responses": {
          "200": { "description": "The events.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Events" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/explain": {
      "get": {
        "operationId": "explainTopics",
        "tags": ["Explain"],
        "summary": "The topics Keely explains",
        "x-keely-scope": "",
        "responses": {
          "200": { "description": "The topics.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExplainTopics" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/explain/{topic}": {
      "get": {
        "operationId": "explain",
        "tags": ["Explain"],
        "summary": "How a number is computed",
        "x-keely-scope": "",
        "parameters": [{ "name": "topic", "in": "path", "required": true, "schema": { "type": "string", "enum": ["recovery", "hrv", "resting_hr", "baselines", "sleep", "load", "zones", "balance", "stress", "workouts"] } }],
        "responses": {
          "200": { "description": "The explanation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Explanation" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1, authorization code with PKCE (S256), on Supabase Auth. Send `scope=email` to the authorization endpoint: Keely’s permissions are not OAuth scopes but choices the person makes on the consent page, from the scopes the app registered at https://meetkeely.com/developers/apps. Read what was granted from `GET /v1/me`.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zndhrjuvjoxbcdmwzeim.supabase.co/auth/v1/oauth/authorize",
            "tokenUrl": "https://zndhrjuvjoxbcdmwzeim.supabase.co/auth/v1/oauth/token",
            "refreshUrl": "https://zndhrjuvjoxbcdmwzeim.supabase.co/auth/v1/oauth/token",
            "scopes": {
              "profile": "Who you are: your birth year, sex, height, weight, sleep goal and max heart rate, and your band",
              "nights": "Mornings: each morning’s Recovery and what moved it, HRV, resting heart rate, breathing rate, skin temperature, sleep and your normals",
              "days": "Days: each day’s Load and its target, time in heart-rate zones, steps, stress and naps",
              "workouts": "Workouts and activities: the sport, times, Load, heart rate and zones",
              "journal": "Read your journal and its tags, only while you share your journal in the Keely app",
              "journal:write": "Add entries and tags when you ask it to, and delete what it added"
            }
          }
        }
      }
    },
    "parameters": {
      "date": { "name": "date", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/LocalDate" }, "description": "A local date, the morning the night ended on." },
      "from": { "name": "from", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/LocalDate" }, "description": "The first local date." },
      "to": { "name": "to", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/LocalDate" }, "description": "The last local date. Defaults to the latest day." }
    },
    "responses": {
      "InvalidRequest": { "description": "A bad date, range, metric or body (`invalid_request`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": {
        "description": "No token (`unauthorized`) or a token that expired, was revoked or isn’t a client’s (`invalid_token`).",
        "headers": { "WWW-Authenticate": { "schema": { "type": "string" }, "description": "`Bearer resource_metadata=\"https://meetkeely.com/.well-known/oauth-protected-resource/api/v1\"`, with `error=\"invalid_token\"` for a bad token." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "`app_not_registered`, `app_not_approved`, `app_suspended`, or `insufficient_scope` (with `scope`, the one needed, and `scopes`, the ones granted).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": { "description": "An unknown path, or a day, workout or entry that doesn’t exist (`not_found`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "JournalOff": { "description": "The person hasn’t turned on journal sharing in the Keely app (`journal_off`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "JournalRefused": { "description": "The journal’s rules refused the write (`journal_refused`, with `reason`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": {
        "description": "About 60 requests a minute per app and person (`rate_limited`).",
        "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unavailable": {
        "description": "Keely’s backend didn’t answer (`unavailable`).",
        "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "5." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "LocalDate": { "type": "string", "format": "date", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "A local date, YYYY-MM-DD, as the Keely app stores it." },
      "Instant": { "type": ["string", "null"], "format": "date-time", "description": "UTC, ISO 8601 with a Z." },
      "Envelope": {
        "type": "object",
        "description": "What every answer carries.",
        "properties": {
          "as_of": { "$ref": "#/components/schemas/Instant", "description": "The band’s last sync to the person’s iPhone; null before the first." },
          "methodology": { "type": "string", "format": "uri", "const": "https://meetkeely.com/methodology" },
          "sample_data": { "type": "boolean", "description": "Present and true on Keely’s review account: every number is sample data." },
          "sample_note": { "type": "string" },
          "not_allowed": { "type": "array", "items": { "$ref": "#/components/schemas/Scope" }, "description": "Parts of the answer left out because the app lacks their scope." }
        },
        "required": ["as_of", "methodology"]
      },
      "Scope": { "type": "string", "enum": ["profile", "nights", "days", "workouts", "journal", "journal:write"] },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "enum": ["invalid_request", "unauthorized", "invalid_token", "app_not_registered", "app_not_approved", "app_suspended", "insufficient_scope", "not_found", "method_not_allowed", "journal_off", "journal_refused", "rate_limited", "unavailable"] },
          "message": { "type": "string", "description": "Plain words. Act on `error`; `message` may change." },
          "scope": { "$ref": "#/components/schemas/Scope", "description": "With `insufficient_scope`: the scope needed." },
          "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/Scope" }, "description": "With `insufficient_scope`: the scopes granted." },
          "reason": { "type": "string", "enum": ["unknown_tag", "bad_tags", "future", "too_old", "bad_window", "daily_limit", "bad_ids", "bad_label", "bad_category", "tag_cap", "not_found", "not_added_by_an_app"], "description": "With `journal_refused`." }
        },
        "required": ["error", "message"]
      },
      "Index": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "const": "Keely API" },
          "version": { "type": "string", "const": "1.0" },
          "documentation": { "type": "string", "format": "uri" },
          "openapi": { "type": "string", "format": "uri" },
          "methodology": { "type": "string", "format": "uri" },
          "endpoints": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["name", "version", "documentation", "openapi", "methodology", "endpoints"]
      },
      "Me": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "person_id": { "type": "string", "format": "uuid", "description": "The account’s id, stable for its life. The API never gives the person’s email." },
              "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/Scope" }, "description": "What the person allowed this app. Read it after connecting and whenever a call answers 403." },
              "app": {
                "type": ["object", "null"],
                "properties": { "name": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "approved", "suspended"] } },
                "required": ["name", "status"]
              },
              "journal_shared": { "type": "boolean", "description": "Whether the person shares their journal in the Keely app." },
              "profile": { "oneOf": [{ "$ref": "#/components/schemas/Profile" }, { "type": "null" }], "description": "Needs `profile`. Null when nothing is recorded." },
              "band": { "oneOf": [{ "$ref": "#/components/schemas/Band" }, { "type": "null" }], "description": "Needs `profile`. Null before a band is paired." }
            },
            "required": ["person_id", "scopes", "app", "journal_shared"]
          }
        ]
      },
      "Profile": {
        "type": "object",
        "properties": {
          "birth_year": { "type": ["integer", "null"] },
          "sex": { "type": ["string", "null"] },
          "height_cm": { "type": ["number", "null"] },
          "weight_kg": { "type": ["number", "null"] },
          "sleep_goal_minutes": { "type": ["integer", "null"] },
          "resting_hr_bpm": { "type": ["number", "null"], "description": "The person’s own setting, if they entered one; measured values are in each night." },
          "max_hr_bpm": { "type": ["number", "null"], "description": "The person’s own setting, if they entered one." }
        }
      },
      "Band": {
        "type": "object",
        "properties": {
          "name": { "type": ["string", "null"] },
          "model": { "type": ["string", "null"] },
          "firmware": { "type": ["string", "null"] },
          "paired_at": { "$ref": "#/components/schemas/Instant" },
          "last_sync_at": { "$ref": "#/components/schemas/Instant" }
        }
      },
      "DayBody": {
        "type": "object",
        "description": "One morning and the day it began. Parts the app lacks the scope for are left out and named in `not_allowed`.",
        "properties": {
          "date": { "$ref": "#/components/schemas/LocalDate" },
          "recovery": { "oneOf": [{ "$ref": "#/components/schemas/Recovery" }, { "type": "null" }], "description": "Needs `nights`. Null when no night ended on this date." },
          "night": { "oneOf": [{ "$ref": "#/components/schemas/Night" }, { "type": "null" }], "description": "Needs `nights`." },
          "sleep": { "oneOf": [{ "$ref": "#/components/schemas/Sleep" }, { "type": "null" }], "description": "Needs `nights`." },
          "naps": { "type": "array", "items": { "$ref": "#/components/schemas/Nap" }, "description": "Needs `days`. The day’s other sleeps." },
          "asleep_with_naps_minutes": { "type": ["integer", "null"] },
          "load": { "oneOf": [{ "$ref": "#/components/schemas/Load" }, { "type": "null" }], "description": "Needs `days`. Null when there is no day summary." },
          "steps": { "type": ["integer", "null"], "description": "Needs `days`." },
          "stress": { "oneOf": [{ "$ref": "#/components/schemas/Stress" }, { "type": "null" }], "description": "Needs `days`." },
          "workouts": { "type": "array", "items": { "$ref": "#/components/schemas/Workout" }, "description": "Needs `workouts` and `days`." },
          "missing": { "type": "array", "items": { "type": "string" }, "description": "What the date has no record of, in words." }
        },
        "required": ["date"]
      },
      "Day": { "allOf": [{ "$ref": "#/components/schemas/Envelope" }, { "$ref": "#/components/schemas/DayBody" }] },
      "Days": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "from": { "$ref": "#/components/schemas/LocalDate" },
              "to": { "$ref": "#/components/schemas/LocalDate" },
              "days": { "type": "array", "items": { "$ref": "#/components/schemas/DayBody" }, "description": "One per date, in order; a date without data has null parts and a `missing` list." }
            },
            "required": ["from", "to", "days"]
          }
        ]
      },
      "Recovery": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["scored", "calibrating", "unavailable"] },
          "score_pct": { "type": ["integer", "null"], "minimum": 0, "maximum": 100 },
          "state": { "type": ["string", "null"], "enum": ["Primed", "Balanced", "Restore", null], "description": "Primed 67–100, Balanced 34–66, Restore 0–33." },
          "focus": { "type": ["string", "null"], "description": "The app’s own line for the day." },
          "likely_range": { "type": "array", "items": { "type": "integer" }, "minItems": 2, "maxItems": 2, "description": "Where the score likely sits, from its inputs’ standard errors." },
          "hrv_read_from": { "type": "string", "description": "Present when HRV was read from last night alone." },
          "contributors": { "type": "array", "items": { "$ref": "#/components/schemas/Contributor" } },
          "nights_recorded": { "type": ["integer", "null"], "description": "While calibrating." },
          "nights_needed": { "type": ["integer", "null"], "description": "While calibrating." },
          "note": { "type": "string", "description": "Why there is no score." }
        },
        "required": ["status", "score_pct"]
      },
      "Contributor": {
        "type": "object",
        "description": "One input against the person’s own normal.",
        "properties": {
          "input": { "type": "string", "enum": ["HRV", "Resting heart rate", "Sleep", "Breathing rate", "Skin temperature"] },
          "value": { "type": ["number", "null"] },
          "unit": { "type": "string" },
          "normal": { "type": ["number", "null"] },
          "normal_range": { "type": ["array", "null"], "items": { "type": ["number", "null"] }, "minItems": 2, "maxItems": 2 },
          "z": { "type": ["number", "null"] },
          "weight_pct": { "type": ["integer", "null"] },
          "impact": { "type": ["string", "null"], "enum": ["helping", "normal", "holding back", null] },
          "measured_by": { "type": ["string", "null"], "description": "Keely, or the band." }
        },
        "required": ["input", "value", "unit", "normal", "z", "weight_pct", "impact"]
      },
      "Night": {
        "type": "object",
        "description": "Last night’s vitals.",
        "properties": {
          "hrv_ms": { "type": ["number", "null"], "description": "RMSSD." },
          "hrv_measured_over": { "type": ["string", "null"], "description": "early night, deep sleep, whole night or band summary." },
          "resting_hr_bpm": { "type": ["number", "null"] },
          "lowest_hr_bpm": { "type": ["number", "null"] },
          "early_night_hr_bpm": { "type": ["number", "null"] },
          "average_hr_bpm": { "type": ["number", "null"] },
          "breathing_rate_per_min": { "type": ["number", "null"] },
          "skin_temp_deviation_c": { "type": ["number", "null"], "description": "From the person’s normal." },
          "average_stress_asleep": { "type": ["number", "null"] }
        }
      },
      "Sleep": {
        "type": "object",
        "properties": {
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant" },
          "whose": { "type": "string", "enum": ["the band", "Keely", "yours"], "description": "Whose times these are." },
          "found": { "type": "array", "items": { "type": "object", "properties": { "start": { "$ref": "#/components/schemas/Instant" }, "end": { "$ref": "#/components/schemas/Instant" }, "whose": { "type": "string" } } }, "description": "On a night the person set: what Keely or the band had found." },
          "start_rule": { "type": ["object", "null"], "description": "What read awake at the night’s start.", "properties": {
            "learned": { "type": "boolean" }, "taught_by_nights": { "type": "integer" }, "corrected_nights": { "type": "integer" },
            "awake_at_stress": { "type": ["number", "null"] }, "heart_over_median_bpm": { "type": ["number", "null"] }, "wrist_cooler_c": { "type": ["number", "null"] }, "tells_evenings": { "type": ["boolean", "null"] }
          } },
          "in_bed_minutes": { "type": ["integer", "null"] },
          "asleep_minutes": { "type": ["integer", "null"] },
          "awake_minutes": { "type": ["integer", "null"] },
          "light_minutes": { "type": ["integer", "null"] },
          "deep_minutes": { "type": ["integer", "null"] },
          "rem_minutes": { "type": ["integer", "null"] },
          "wake_ups": { "type": ["integer", "null"] },
          "need_minutes": { "type": ["integer", "null"] },
          "need_parts_minutes": { "type": "object", "properties": { "base": { "type": ["integer", "null"] }, "recent_debt": { "type": ["integer", "null"] }, "hard_day": { "type": ["integer", "null"] } } },
          "performance_pct": { "type": ["integer", "null"], "description": "Time asleep ÷ need." },
          "consistency": { "type": ["number", "null"], "description": "The 14-day Sleep Regularity Index; 100 = asleep and awake at the same times every day." }
        }
      },
      "Nap": {
        "type": "object",
        "properties": {
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant" },
          "asleep_minutes": { "type": ["integer", "null"] },
          "whose": { "type": "string" }
        }
      },
      "Load": {
        "type": "object",
        "properties": {
          "value": { "type": ["number", "null"], "description": "Minutes weighted by heart-rate zone, no ceiling." },
          "target": { "type": ["object", "null"], "properties": { "low": { "type": "integer" }, "high": { "type": "integer" }, "provisional": { "type": "boolean" } } },
          "status": { "type": ["string", "null"], "description": "How far from the target range, in words." },
          "balance": { "type": ["number", "null"], "description": "7-day ÷ 28-day Load." },
          "balance_label": { "type": ["string", "null"], "enum": ["Well below usual", "Below usual", "Steady", "Above usual", "Well above usual", null] },
          "zone_minutes": { "oneOf": [{ "$ref": "#/components/schemas/ZoneMinutes" }, { "type": "null" }] }
        }
      },
      "ZoneMinutes": { "type": "object", "properties": { "z1": { "type": "integer" }, "z2": { "type": "integer" }, "z3": { "type": "integer" }, "z4": { "type": "integer" }, "z5": { "type": "integer" } } },
      "LevelMinutes": { "type": "object", "properties": { "calm": { "type": "integer" }, "mild": { "type": "integer" }, "elevated": { "type": "integer" }, "high": { "type": "integer" } } },
      "Stress": {
        "type": "object",
        "properties": {
          "day_average": { "type": ["number", "null"] },
          "day_level": { "type": ["string", "null"], "enum": ["Calm", "Mild", "Elevated", "High", null] },
          "night_average": { "type": ["number", "null"] },
          "still_average": { "type": ["number", "null"], "description": "The average over still, awake windows." },
          "minutes_by_level": { "oneOf": [{ "$ref": "#/components/schemas/LevelMinutes" }, { "type": "null" }] },
          "minutes_by_level_while_training": { "oneOf": [{ "$ref": "#/components/schemas/LevelMinutes" }, { "type": "null" }] },
          "moments": { "type": "array", "items": { "$ref": "#/components/schemas/StressMoment" }, "description": "At least 10 minutes above 50 by day, or above 30 at night." }
        }
      },
      "StressMoment": {
        "type": "object",
        "properties": {
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant" },
          "peak": { "type": ["number", "null"] },
          "average": { "type": ["number", "null"] },
          "asleep": { "type": "boolean" }
        }
      },
      "Workout": {
        "type": "object",
        "properties": {
          "date": { "oneOf": [{ "$ref": "#/components/schemas/LocalDate" }, { "type": "null" }], "description": "The local day that lists it; null without `days`." },
          "id": { "type": "string" },
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant" },
          "duration_minutes": { "type": ["integer", "null"] },
          "activity": { "type": "string", "description": "The sport’s name." },
          "activity_id": { "type": "string" },
          "kind": { "type": "string", "enum": ["training", "recovery"] },
          "added_by_person": { "type": "boolean" },
          "measured": { "type": "boolean", "description": "False for an activity the person added for a time the band has no heart rate for." },
          "measured_from": { "type": "string", "description": "The band, or a recording another app saved to Apple Health." },
          "load": { "type": ["number", "null"] },
          "average_hr_bpm": { "type": ["number", "null"] },
          "max_hr_bpm": { "type": ["number", "null"] },
          "zone_minutes": { "oneOf": [{ "$ref": "#/components/schemas/ZoneMinutes" }, { "type": "null" }] },
          "class": { "type": ["string", "null"], "enum": ["VO₂ max", "Threshold", "Tempo", "Recovery", "Endurance", "Easy", "Mixed", null], "description": "Training workouts only." },
          "recovery_hours": { "type": ["number", "null"] },
          "hr_recovery_1min_bpm": { "type": ["number", "null"] },
          "legs": { "type": "array", "items": { "$ref": "#/components/schemas/WorkoutLeg" }, "description": "On an event of several sports." },
          "unnamed": { "type": "boolean" },
          "suggested_activity": { "type": "string" },
          "note": { "type": "string" }
        },
        "required": ["id", "start", "end", "activity", "activity_id", "kind", "added_by_person", "measured"]
      },
      "WorkoutLeg": {
        "type": "object",
        "properties": {
          "kind": { "type": "string", "enum": ["swim", "bike", "run", "walk", "other", "transition"] },
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant" },
          "duration_minutes": { "type": ["integer", "null"] },
          "activity": { "type": ["string", "null"] },
          "load": { "type": ["number", "null"], "description": "Null on a transition." },
          "average_hr_bpm": { "type": ["number", "null"] }
        }
      },
      "Workouts": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "from": { "$ref": "#/components/schemas/LocalDate" },
              "to": { "$ref": "#/components/schemas/LocalDate" },
              "count": { "type": "integer" },
              "workouts": { "type": "array", "items": { "$ref": "#/components/schemas/Workout" } },
              "note": { "type": "string", "description": "Without `days`: that workouts are listed by their start, in UTC." }
            },
            "required": ["from", "to", "count", "workouts"]
          }
        ]
      },
      "WorkoutAnswer": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "workout": { "$ref": "#/components/schemas/Workout" } }, "required": ["workout"] }
        ]
      },
      "Series": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "metric": { "type": "string" },
              "unit": { "type": "string" },
              "from": { "$ref": "#/components/schemas/LocalDate" },
              "to": { "$ref": "#/components/schemas/LocalDate" },
              "points": { "type": "array", "items": { "type": "object", "properties": { "date": { "$ref": "#/components/schemas/LocalDate" }, "value": { "type": ["number", "null"] } }, "required": ["date", "value"] } },
              "stats": { "type": "object", "properties": { "days_with_data": { "type": "integer" }, "mean": { "type": "number" }, "min": { "type": "number" }, "max": { "type": "number" } }, "required": ["days_with_data"] },
              "note": { "type": "string", "description": "When the account has no data yet." }
            },
            "required": ["metric", "unit", "points"]
          }
        ]
      },
      "Normal": {
        "type": ["object", "null"],
        "properties": {
          "normal": { "type": ["number", "null"], "description": "The 30-night weighted average." },
          "sd": { "type": ["number", "null"] },
          "range": { "type": ["array", "null"], "items": { "type": "number" }, "minItems": 2, "maxItems": 2, "description": "± one standard deviation." },
          "nights": { "type": ["integer", "null"] }
        }
      },
      "Normals": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "from_night": { "oneOf": [{ "$ref": "#/components/schemas/LocalDate" }, { "type": "null" }], "description": "The latest scored night the normals come from." },
              "hrv_ms": { "$ref": "#/components/schemas/Normal" },
              "resting_hr_bpm": { "$ref": "#/components/schemas/Normal" },
              "breathing_rate_per_min": { "$ref": "#/components/schemas/Normal" },
              "skin_temp_deviation_c": { "$ref": "#/components/schemas/Normal" },
              "sleep_goal_minutes": { "type": ["integer", "null"], "description": "Needs `profile`." },
              "note": { "type": "string" }
            }
          }
        ]
      },
      "JournalTag": {
        "type": "object",
        "properties": {
          "key": { "type": "string", "pattern": "^[a-z0-9_]{1,64}$" },
          "label": { "type": "string" },
          "category": { "type": "string", "enum": ["foodDrink", "sleepSchedule", "training", "recovery", "unwell", "environment", "life", "other"] },
          "category_label": { "type": "string" },
          "summary": { "type": ["string", "null"] },
          "built_in": { "type": "boolean" },
          "can_log": { "type": "boolean", "description": "Whether an app may log it; some tags are the person’s or Keely’s own to log." },
          "status": { "type": "string", "enum": ["active", "hidden"] }
        },
        "required": ["key", "label", "category", "category_label", "built_in", "can_log", "status"]
      },
      "JournalTags": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "tags": { "type": "array", "items": { "$ref": "#/components/schemas/JournalTag" } } }, "required": ["tags"] }
        ]
      },
      "NewJournalTag": {
        "type": "object",
        "properties": {
          "label": { "type": "string", "minLength": 1, "maxLength": 40, "description": "Its name, in the person’s words." },
          "category": { "type": "string", "enum": ["foodDrink", "sleepSchedule", "training", "recovery", "unwell", "environment", "life", "other"] },
          "summary": { "type": "string", "maxLength": 200, "description": "What it means, in one line. Defaults to the name." }
        },
        "required": ["label", "category"]
      },
      "JournalTagAnswer": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "tag": { "$ref": "#/components/schemas/JournalTag" }, "existing": { "type": "boolean", "description": "True when a tag already said the same thing and was returned instead." } }, "required": ["tag", "existing"] }
        ]
      },
      "JournalEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "tag": { "type": "string" },
          "label": { "type": "string" },
          "category": { "type": "string" },
          "start": { "$ref": "#/components/schemas/Instant" },
          "end": { "$ref": "#/components/schemas/Instant", "description": "Null while ongoing." },
          "day": { "oneOf": [{ "$ref": "#/components/schemas/LocalDate" }, { "type": "null" }] },
          "pattern": { "type": "string", "enum": ["once", "occasional", "mostDays", "daily", "count"] },
          "pattern_label": { "type": "string" },
          "count": { "type": ["integer", "null"] },
          "source": { "type": "string", "enum": ["tap", "text", "inferred", "assistant", "app"], "description": "The person in the app, the person in their own words, Keely noticed it, an assistant, or an app." },
          "basis": { "type": ["string", "null"], "enum": ["workout", "activity", "day", "timeZone", null], "description": "What Keely noticed it from." },
          "can_delete": { "type": "boolean", "description": "True only for what an app added." },
          "changed_at": { "$ref": "#/components/schemas/Instant" }
        },
        "required": ["id", "tag", "label", "category", "start", "end", "pattern", "source", "can_delete"]
      },
      "JournalEntries": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "from": { "$ref": "#/components/schemas/LocalDate" },
              "to": { "$ref": "#/components/schemas/LocalDate" },
              "count": { "type": "integer" },
              "entries": { "type": "array", "items": { "$ref": "#/components/schemas/JournalEntry" } }
            },
            "required": ["from", "to", "count", "entries"]
          }
        ]
      },
      "NewJournalEntries": {
        "type": "object",
        "description": "`tags`, and either `when` or `date`.",
        "properties": {
          "tags": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]{1,64}$" }, "minItems": 1, "maxItems": 5, "uniqueItems": true },
          "when": { "type": "string", "enum": ["last_night", "today", "yesterday", "past_few_days", "past_week", "past_month", "ongoing", "tomorrow"], "description": "In the person’s time zone. `tomorrow` is for a planned race only (`tags: [\"race\"]`)." },
          "date": { "$ref": "#/components/schemas/LocalDate", "description": "Instead of `when`: one local date within the last 90 days, for that whole day." }
        },
        "required": ["tags"]
      },
      "LoggedEntries": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "entries": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" }, "tag": { "type": "string" },
                    "start": { "$ref": "#/components/schemas/Instant" }, "end": { "$ref": "#/components/schemas/Instant" },
                    "pattern": { "type": "string" }, "source": { "type": "string", "const": "app" }
                  },
                  "required": ["id", "tag", "start", "end", "pattern", "source"]
                }
              }
            },
            "required": ["entries"]
          }
        ]
      },
      "Deleted": {
        "type": "object",
        "properties": { "id": { "type": "string" }, "deleted": { "type": "boolean", "const": true } },
        "required": ["id", "deleted"]
      },
      "Change": {
        "type": "object",
        "properties": {
          "kind": { "type": "string", "enum": ["night", "day", "workout", "journal_tag", "journal_entry"] },
          "key": { "type": "string", "description": "The date, the workout’s id, the tag’s key or the entry’s id." },
          "date": { "oneOf": [{ "$ref": "#/components/schemas/LocalDate" }, { "type": "null" }] },
          "updated_at": { "$ref": "#/components/schemas/Instant" },
          "removed": { "type": "boolean", "description": "True for a workout the person removed." }
        },
        "required": ["kind", "key", "updated_at"]
      },
      "Changes": {
        "type": "object",
        "properties": {
          "since": { "type": "string", "format": "date-time" },
          "next": { "type": "string", "format": "date-time", "description": "The newest `updated_at` seen, or `since` when nothing changed. Poll next from 5 minutes before it." },
          "has_more": { "type": "boolean" },
          "changes": { "type": "array", "items": { "$ref": "#/components/schemas/Change" }, "maxItems": 500 }
        },
        "required": ["since", "next", "has_more", "changes"]
      },
      "Event": {
        "type": "object",
        "description": "What changed and for whom, never a number from a summary: fetch it with the person’s token.",
        "properties": {
          "id": { "type": "string", "description": "The event’s id, `evt_` and a number; stable across retries.", "examples": ["evt_123"] },
          "type": {
            "type": "string",
            "enum": ["night.created", "night.updated", "night.deleted", "day.created", "day.updated", "day.deleted", "workout.created", "workout.updated", "workout.removed", "journal.entry.created", "journal.entry.updated", "journal.entry.deleted", "journal.sharing_started", "journal.sharing_ended", "connection.granted", "connection.changed", "connection.revoked", "person.history_changed", "test.ping"]
          },
          "person_id": { "type": "string", "format": "uuid" },
          "occurred_at": { "type": "string", "format": "date-time", "description": "When it happened, UTC; for a folded correction, the latest." },
          "data": { "type": "object", "description": "The key and what changed, by type: a night’s or day’s `date`, a workout’s `id`, `activity` and `started_at`, an entry’s `id`, `tag`, `source` and `starts_at`, a connection’s `scopes` (and `before`), a revocation’s `reason`, a digest’s `since` and `events`.", "additionalProperties": true },
          "links": { "type": "object", "description": "What to fetch: `day`, `workout`, `journal` or `changes`; empty for `connection.*` and `test.ping`.", "additionalProperties": { "type": "string", "format": "uri" } }
        },
        "required": ["id", "type", "person_id", "occurred_at", "data", "links"]
      },
      "Events": {
        "type": "object",
        "properties": {
          "since": { "type": "integer" },
          "next": { "type": "integer", "description": "The number of the newest event listed, or `since` when there were none; pass it as `since` next time." },
          "has_more": { "type": "boolean" },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/Event" }, "maxItems": 500 }
        },
        "required": ["since", "next", "has_more", "events"]
      },
      "ExplainTopics": {
        "type": "object",
        "properties": {
          "topics": { "type": "array", "items": { "type": "string" } },
          "methodology": { "type": "string", "format": "uri" }
        },
        "required": ["topics"]
      },
      "Explanation": {
        "type": "object",
        "properties": {
          "topic": { "type": "string" },
          "title": { "type": "string" },
          "summary": { "type": "string", "description": "How the number is computed, in words." },
          "formula": { "type": "string" },
          "page": { "type": "string", "format": "uri", "description": "The page on meetkeely.com that explains it." },
          "methodology": { "type": "string", "format": "uri" }
        },
        "required": ["topic", "title", "summary", "page", "methodology"]
      }
    }
  }
}
