{
  "openapi": "3.1.0",
  "info": {
    "title": "Shorebird CodePush API",
    "version": "1.0.0",
    "description": "The Shorebird over-the-air code push API. Models and message DTOs\noriginally lived in the handwritten `shorebird_code_push_protocol`\nand `shorebird_code_push_client` packages; this spec is the\nOpenAPI representation of that surface area.\n\nThis surface is published so that it can be read and called directly,\nby people and by agents, rather than only through the Shorebird CLI.\nIt does not yet carry a compatibility guarantee: it is the API the\nCLI and console use, and it changes when they do. Build on it, and\nexpect to follow it.\n\nAuthenticate with an `sb_api_*` API key from the Shorebird console\n(Account > API Keys). The `x-version` header some clients send\nidentifies `shorebird_code_push_client` and is not part of this\ncontract; omit it.\n\nRequests are rate limited per caller (per API key, or per client IP\nwhen unauthenticated). Every response carries `RateLimit-Policy` and\n`RateLimit` (draft-ietf-httpapi-ratelimit-headers), so a client can\npace itself. A refused request is a `429` with `Retry-After` and the\nstandard error envelope (`ErrorResponse`, code `rate_limited`); it is\ncovered by each operation's `default` response rather than listed on\nevery one.\n",
    "contact": {
      "name": "Shorebird",
      "url": "https://docs.shorebird.dev/"
    }
  },
  "servers": [
    {
      "url": "https://api.shorebird.dev/api/v1"
    }
  ],
  "security": [
    {
      "apiKey": []
    },
    {
      "oauthJwt": []
    }
  ],
  "tags": [
    {
      "name": "apps",
      "description": "Applications and their metadata."
    },
    {
      "name": "releases",
      "description": "Release builds and their artifacts."
    },
    {
      "name": "patches",
      "description": "Over-the-air patches for releases."
    },
    {
      "name": "channels",
      "description": "Release channels."
    },
    {
      "name": "collaborators",
      "description": "App collaborators and their roles."
    },
    {
      "name": "organizations",
      "description": "Organizations and membership."
    },
    {
      "name": "users",
      "description": "Shorebird user accounts."
    },
    {
      "name": "diagnostics",
      "description": "Infrastructure diagnostics endpoints."
    },
    {
      "name": "metrics",
      "description": "Customer-facing analytics charts (MAU v2)."
    }
  ],
  "paths": {
    "/users/me": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Get the currently logged-in user.",
        "operationId": "getCurrentUser",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The currently logged-in user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrivateUser"
                }
              }
            }
          },
          "404": {
            "description": "No user is currently logged in."
          }
        }
      }
    },
    "/apps": {
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "List all apps for the current account.",
        "operationId": "getApps",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "A list of apps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetAppsResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "apps"
        ],
        "summary": "Create a new app.",
        "operationId": "createApp",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAppRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The newly-created app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/App"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "Get the specified app.",
        "operationId": "getApp",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The requested app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppMetadata"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "apps"
        ],
        "summary": "Delete the specified app.",
        "operationId": "deleteApp",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The app was deleted."
          }
        }
      }
    },
    "/apps/{appId}/icon": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "name": "v",
          "in": "query",
          "required": false,
          "description": "Cache-bust token (a digest of the icon), set by\n`AppMetadata.iconUrl`. Ignored by the server, which always\nserves the currently-picked icon; clients re-fetch when the\nURL changes.\n",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "Get the app's launcher icon as a PNG.",
        "description": "Returns the launcher icon for the most recent analyzed iOS\nrelease (or Android, when iOS has no analyzed release with an\nicon). The response is labeled `immutable` because the URL\nembedded in `AppMetadata.iconUrl` changes whenever the icon\ndoes. Returns 404 when no analyzed release has an icon.\n",
        "operationId": "getAppIcon",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The icon bytes.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "`private, max-age=31536000, immutable`."
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No analyzed release has an icon."
          }
        }
      }
    },
    "/apps/{appId}/collaborators": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "post": {
        "tags": [
          "collaborators"
        ],
        "summary": "Add a collaborator to the app by email.",
        "operationId": "createAppCollaborator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAppCollaboratorRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The collaborator was added."
          }
        }
      }
    },
    "/apps/{appId}/collaborators/{collaboratorId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "name": "collaboratorId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "patch": {
        "tags": [
          "collaborators"
        ],
        "summary": "Update a collaborator's role on an app.",
        "operationId": "updateAppCollaborator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAppCollaboratorRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The collaborator was updated."
          }
        }
      }
    },
    "/apps/{appId}/channels": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "channels"
        ],
        "summary": "List channels for the app.",
        "operationId": "getChannels",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The channels for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Channel"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "channels"
        ],
        "summary": "Create a new channel for the app.",
        "operationId": "createChannel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateChannelRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The newly-created channel.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Channel"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "releases"
        ],
        "summary": "List releases for the app.",
        "operationId": "getReleases",
        "parameters": [
          {
            "name": "sideloadable",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "If true, only return releases that can be sideloaded."
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The releases for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetReleasesResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "releases"
        ],
        "summary": "Create a new release for the app.",
        "operationId": "createRelease",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateReleaseRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The newly-created release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateReleaseResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        }
      ],
      "get": {
        "tags": [
          "releases"
        ],
        "summary": "Get a specific release.",
        "operationId": "getRelease",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The requested release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetReleaseResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "releases"
        ],
        "summary": "Update the release.",
        "operationId": "updateRelease",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateReleaseRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The release was updated."
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}/artifacts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        }
      ],
      "get": {
        "tags": [
          "releases"
        ],
        "summary": "Get release artifacts.",
        "operationId": "getReleaseArtifacts",
        "parameters": [
          {
            "name": "arch",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ReleasePlatform"
            }
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The artifacts for the release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetReleaseArtifactsResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "releases"
        ],
        "summary": "Register a new release artifact and obtain an upload URL.",
        "description": "The server returns a record for the artifact plus a signed upload\nURL; clients then `POST` the artifact bytes to that URL.\n",
        "operationId": "createReleaseArtifact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateReleaseArtifactRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreateReleaseArtifactRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The newly-registered release artifact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateReleaseArtifactResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}/patches": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        }
      ],
      "get": {
        "tags": [
          "patches"
        ],
        "summary": "List patches for a release.",
        "operationId": "getReleasePatches",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patches for the release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetReleasePatchesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}/patches/{patchId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        },
        {
          "$ref": "#/components/parameters/PatchId"
        }
      ],
      "patch": {
        "tags": [
          "patches"
        ],
        "summary": "Update a patch.",
        "operationId": "updatePatch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePatchRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The patch was updated."
          }
        }
      }
    },
    "/apps/{appId}/patches": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "post": {
        "tags": [
          "patches"
        ],
        "summary": "Create a new patch.",
        "operationId": "createPatch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePatchRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "201": {
            "description": "The newly-created patch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePatchResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/patches/{patchId}/artifacts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/PatchId"
        }
      ],
      "post": {
        "tags": [
          "patches"
        ],
        "summary": "Register a new patch artifact and obtain an upload URL.",
        "operationId": "createPatchArtifact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePatchArtifactRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreatePatchArtifactRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The newly-registered patch artifact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePatchArtifactResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/patches/promote": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "post": {
        "tags": [
          "patches"
        ],
        "summary": "Promote a patch to a channel.",
        "operationId": "promotePatch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PromotePatchRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "204": {
            "description": "The patch was promoted."
          }
        }
      }
    },
    "/apps/{appId}/metrics/version-distribution": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Distribution of currently-active devices by release version.",
        "description": "Returns the exact (non-HLL) count of currently-active devices for\nthe app, grouped by `release_version`. \"Currently active\" is\nbounded by a server-side active-device window; rows with no\nreported `release_version` are returned with `releaseVersion: null`\nand represent devices on Flutter versions too old to emit the\nfield. The result is sorted by `deviceCount` descending, then\n`releaseVersion` ascending with NULLs last.\n",
        "operationId": "getVersionDistribution",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The version distribution for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetVersionDistributionResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/new-devices": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Devices first seen within the active-device window.",
        "description": "Returns the count of the app's devices whose first recorded activity\nfalls within the last `window_days` days — a device-growth signal —\ntogether with the count\nfor the equal-length window immediately preceding it, which powers a\nperiod-over-period delta. Counts are exact (non-HLL). `previous` is\nnull when the preceding window would begin before the metrics data\nfloor, where a comparison against partially-recorded history would\nshow a misleading delta.\n",
        "operationId": "getNewDevices",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The new-device counts for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetNewDevicesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/patch-adoption": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Cumulative patch-adoption curves for a release.",
        "description": "Returns, for a single release, the cumulative adoption of each patch:\nthe distinct devices running patch `>= N` (an HLL count over\n`hourly_user_sketches`) over the patch's *target* — the distinct\ndevices on the release whose platform the patch was built for. Adoption\npercentages are computed server-side and clamped to `[0.0, 1.0]`;\nclients should render them rather than recompute.\n\nValues are cumulative (\"patch `>= N`\"), so they are monotonic\nnon-increasing in `patch_number`.\n\nThe release is the `release_version` query parameter, or — when\nomitted — the app's latest release by creation date (`is_latest` flags\nthat case). The window defaults to the last 28 days and is capped at 32\ndays (the effective window is echoed in `range`). With `granularity`\nomitted, each patch carries a single full-window value (`period: null`);\nset it for a curve at that resolution.\n",
        "operationId": "getPatchAdoption",
        "parameters": [
          {
            "name": "release_version",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The release to report on. Omitted ⇒ the app's latest release by\ncreation date.\n"
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days;\nclamped so the window never exceeds 32 days.\n"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution. Omitted ⇒ a single full-window value per patch\n(`period: null`); set ⇒ one point per bucket at this resolution.\nWhen set, the window length is rounded down to a whole multiple of\nthe bucket size (echoed in `range`).\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch-adoption data for one release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPatchAdoptionResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/active-hours": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Best time to release — lowest-activity UTC window.",
        "description": "Returns the app's average active-device count per UTC hour-of-day over\na server-side lookback window, plus the recommended lowest-activity\nconsecutive window (the best time to push a patch with minimal user\ndisruption) and the busiest hour for contrast. Recommendation fields\nare null when there is insufficient history (fewer than 7 days).\n",
        "operationId": "getActiveHours",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The active-hours profile and release-time recommendation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetActiveHoursResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/activity-heatmap": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Activity heatmap — average active devices per UTC weekday-hour.",
        "description": "Returns the app's average distinct active-device count for each\n(UTC day-of-week, UTC hour-of-day) cell over a server-side lookback\nwindow — a 7×24 activity profile for an insights heatmap. Sourced from\nthe same `hourly_user_sketches` data as the active-hours advisor, just\nsliced by weekday as well as hour.\n",
        "operationId": "getActivityHeatmap",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The 7×24 UTC activity heatmap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetActivityHeatmapResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/unique-users": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Unique active devices (MAU) for an app over a window.",
        "description": "Returns the distinct active devices (\"unique users\") for the app over\na time window — an HLL-approximated count over `hourly_user_sketches`\n— as a current/previous envelope: `current` covers the effective\nwindow, `previous` the equal-length window immediately preceding it\n(always computed; powers period-over-period deltas and overlays).\nWith `granularity` set, each window also carries a `time_series` at\nthat resolution; with `group_by` set, `current` carries a `breakdown`\nper group (each with its own `time_series` when a `granularity` is\nalso requested). `previous` never carries a breakdown. Counts are HLL\napproximations. The window defaults to the last 28 days and is capped\nat 450 days; every bound clamps and the effective range is echoed in\neach window's `range`, which clients must treat as authoritative.\n",
        "operationId": "getUniqueUsers",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution for the time series. Omitted ⇒ no `time_series`\n(one merged value per window); set ⇒ one point per bucket at this\nresolution. When set, the window length is rounded down to a whole\nmultiple of the bucket size (echoed in `range`), which guarantees\n`current` and `previous` have equal bucket counts.\n"
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "platform",
                "release_version"
              ]
            },
            "description": "Dimension to break the total down by. Omitted ⇒ no `breakdown`.\nFor `release_version`, devices whose client is too old to report\na release version group under the empty-string `group_value`.\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The unique-users metric for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetUniqueUsersResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/patch-installs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Patch installs for an app over a window.",
        "description": "Returns patch installs for the app over a time window — summed over\nRedis time-series buckets — as a current/previous envelope: `current`\ncovers the effective window, `previous` the equal-length window\nimmediately preceding it (powers period-over-period deltas and\noverlays). With `granularity` set, each window also carries a\n`time_series` at that resolution; with `group_by=release` set,\n`current` carries a `breakdown` with one entry per release (each with\nits own `time_series` when a `granularity` is also requested).\n`previous` never carries a breakdown. The window defaults to the last\n28 days and is capped at 450 days; every bound clamps and the\neffective range is echoed in each window's `range`, which clients must\ntreat as authoritative.\n",
        "operationId": "getAppPatchInstalls",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution for the time series. Omitted ⇒ no `time_series`\n(one summed value per window); set ⇒ one point per bucket at this\nresolution. When set, the window length is rounded down to a whole\nmultiple of the bucket size (echoed in `range`), which guarantees\n`current` and `previous` have equal bucket counts.\n"
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "release"
              ]
            },
            "description": "Dimension to break the total down by. Omitted ⇒ no `breakdown`.\nApp scope breaks down by `release`.\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch-installs metric for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPatchMetricResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/metrics/patch-downloads": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Patch downloads for an app over a window.",
        "description": "Returns patch downloads for the app over a time window — summed over\nRedis time-series buckets — as a current/previous envelope, identical\nin shape to `patch-installs`: `current` covers the effective window,\n`previous` the equal-length window immediately preceding it. With\n`granularity` set, each window carries a `time_series`; with\n`group_by=release` set, `current` carries a per-release `breakdown`.\n`previous` never carries a breakdown. The window defaults to the last\n28 days and is capped at 450 days; the effective range is echoed in\neach window's `range`.\n",
        "operationId": "getAppPatchDownloads",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution for the time series. Omitted ⇒ no `time_series`\n(one summed value per window); set ⇒ one point per bucket at this\nresolution. When set, the window length is rounded down to a whole\nmultiple of the bucket size (echoed in `range`), which guarantees\n`current` and `previous` have equal bucket counts.\n"
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "release"
              ]
            },
            "description": "Dimension to break the total down by. Omitted ⇒ no `breakdown`.\nApp scope breaks down by `release`.\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch-downloads metric for the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPatchMetricResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}/metrics/patch-installs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Patch installs for one release over a window.",
        "description": "Returns patch installs for a single release over a time window —\nsummed over Redis time-series buckets — as a current/previous\nenvelope, identical in shape to the app-scoped variant. With\n`granularity` set, each window carries a `time_series`; with\n`group_by=patch` set, `current` carries a `breakdown` with one entry\nper patch of the release (each with its own `time_series` when a\n`granularity` is also requested). `previous` never carries a\nbreakdown. The window defaults to the last 28 days and is capped at\n450 days; the effective range is echoed in each window's `range`.\n",
        "operationId": "getReleasePatchInstalls",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution for the time series. Omitted ⇒ no `time_series`\n(one summed value per window); set ⇒ one point per bucket at this\nresolution. When set, the window length is rounded down to a whole\nmultiple of the bucket size (echoed in `range`), which guarantees\n`current` and `previous` have equal bucket counts.\n"
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "patch"
              ]
            },
            "description": "Dimension to break the total down by. Omitted ⇒ no `breakdown`.\nRelease scope breaks down by `patch`.\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch-installs metric for the release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPatchMetricResponse"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{appId}/releases/{releaseId}/metrics/patch-downloads": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AppId"
        },
        {
          "$ref": "#/components/parameters/ReleaseId"
        }
      ],
      "get": {
        "tags": [
          "metrics"
        ],
        "summary": "Patch downloads for one release over a window.",
        "description": "Returns patch downloads for a single release over a time window —\nsummed over Redis time-series buckets — as a current/previous\nenvelope, identical in shape to the other patch-metric variants. With\n`granularity` set, each window carries a `time_series`; with\n`group_by=patch` set, `current` carries a per-patch `breakdown`.\n`previous` never carries a breakdown. The window defaults to the last\n28 days and is capped at 450 days; the effective range is echoed in\neach window's `range`.\n",
        "operationId": "getReleasePatchDownloads",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (UTC, inclusive). Defaults to `end` minus 28 days."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (UTC, exclusive). Defaults to now. A window \"ending\nnow\" must omit this parameter rather than send a client-computed\ntimestamp — clock skew would silently turn the request into a\nhistorical query.\n"
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ]
            },
            "description": "Bucket resolution for the time series. Omitted ⇒ no `time_series`\n(one summed value per window); set ⇒ one point per bucket at this\nresolution. When set, the window length is rounded down to a whole\nmultiple of the bucket size (echoed in `range`), which guarantees\n`current` and `previous` have equal bucket counts.\n"
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "patch"
              ]
            },
            "description": "Dimension to break the total down by. Omitted ⇒ no `breakdown`.\nRelease scope breaks down by `patch`.\n"
          }
        ],
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch-downloads metric for the release.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPatchMetricResponse"
                }
              }
            }
          }
        }
      }
    },
    "/patches/check": {
      "post": {
        "tags": [
          "patches"
        ],
        "summary": "Check for an available patch for an install.",
        "operationId": "patchCheck",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchCheckRequest"
              }
            }
          }
        },
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The patch check result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatchCheckResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations": {
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "List organizations the current user is a member of.",
        "operationId": "getOrganizationMemberships",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The organizations the user is a member of.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetOrganizationsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{organizationId}/apps": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationId"
        }
      ],
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "List apps in an organization.",
        "operationId": "getOrganizationApps",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The apps belonging to the organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetOrganizationAppsResponse"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{organizationId}/users": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationId"
        }
      ],
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "List users in an organization.",
        "operationId": "getOrganizationUsers",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The users belonging to the organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetOrganizationUsersResponse"
                }
              }
            }
          }
        }
      }
    },
    "/diagnostics/gcp_upload": {
      "get": {
        "tags": [
          "diagnostics"
        ],
        "summary": "Get a GCP upload link for measuring upload speed.",
        "operationId": "getGcpUploadSpeedTestUrl",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The upload URL to measure against.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The upload URL to measure against.",
                  "required": [
                    "upload_url"
                  ],
                  "properties": {
                    "upload_url": {
                      "type": "string",
                      "description": "The GCP-signed upload URL."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/diagnostics/gcp_download": {
      "get": {
        "tags": [
          "diagnostics"
        ],
        "summary": "Get a GCP download link for measuring download speed.",
        "operationId": "getGcpDownloadSpeedTestUrl",
        "responses": {
          "default": {
            "$ref": "#/components/responses/Error"
          },
          "200": {
            "description": "The download URL to measure against.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The download URL to measure against.",
                  "required": [
                    "download_url"
                  ],
                  "properties": {
                    "download_url": {
                      "type": "string",
                      "description": "The GCP-signed download URL."
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "**Recommended.** An `sb_api_*` API key minted in the Shorebird\nconsole (Account > API Keys). Long-lived, no refresh needed.\n"
      },
      "oauthJwt": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "A JWT access token obtained via Shorebird's interactive OAuth\nlogin flow (`shorebird login`). Short-lived (15 min),\nauto-refreshed by the CLI using a stored refresh token.\n"
      }
    },
    "responses": {
      "Error": {
        "description": "Standard error response. Emitted for any non-2xx status where a\nbody can be decoded into an [ErrorResponse].\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "parameters": {
      "AppId": {
        "name": "appId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "The ID of the app."
      },
      "ReleaseId": {
        "name": "releaseId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "description": "The ID of the release."
      },
      "PatchId": {
        "name": "patchId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "description": "The ID of the patch."
      },
      "OrganizationId": {
        "name": "organizationId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "description": "The ID of the organization."
      }
    },
    "schemas": {
      "App": {
        "type": "object",
        "description": "The application downloaded and run on various devices/platforms.",
        "required": [
          "id",
          "display_name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The ID of the app."
          },
          "display_name": {
            "type": "string",
            "description": "The display name of the app."
          }
        }
      },
      "AppMetadata": {
        "type": "object",
        "description": "A single app which contains zero or more releases.",
        "required": [
          "app_id",
          "display_name",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "app_id": {
            "type": "string",
            "description": "The ID of the app."
          },
          "display_name": {
            "type": "string",
            "description": "The display name of the app."
          },
          "latest_release_version": {
            "type": "string",
            "description": "The latest release version of the app."
          },
          "latest_patch_number": {
            "type": "integer",
            "format": "int64",
            "description": "The latest patch number of the app."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the app was created."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the app was last updated."
          },
          "platforms": {
            "type": "array",
            "description": "Every platform the app has shipped to (i.e. has at least one\nrelease artifact for). Independent of `latest_releases`:\nan app can list a platform here even if no release on that\nplatform has been analyzed yet.\n",
            "items": {
              "$ref": "#/components/schemas/ReleasePlatform"
            }
          },
          "latest_releases": {
            "type": "object",
            "description": "The latest analyzed release per platform. A platform is\nomitted when no release for that platform has been analyzed\nyet. When the latest release for a platform is not yet\nanalyzed but a previous one is, the previous release is\nreturned here and `pending_releases.{platform}` identifies\nthe unanalyzed newer release.\n",
            "additionalProperties": {
              "$ref": "#/components/schemas/LatestRelease"
            },
            "propertyNames": {
              "$ref": "#/components/schemas/ReleasePlatform"
            }
          },
          "pending_releases": {
            "type": "object",
            "description": "The newest unanalyzed release per platform, whenever one\nexists. A platform is omitted when its most recent release\nhas already been analyzed. Independent of `latest_releases`:\nboth can be present (a newer release than the analyzed one\nis being processed) or only `pending_releases` can be\npresent (no release on the platform has been analyzed yet).\n",
            "additionalProperties": {
              "$ref": "#/components/schemas/PendingRelease"
            },
            "propertyNames": {
              "$ref": "#/components/schemas/ReleasePlatform"
            }
          },
          "icon_url": {
            "type": "string",
            "description": "Server-emitted URL for the app's launcher icon, sourced from\nthe most recent analyzed iOS release (or Android, when iOS\nhas no analyzed release with an icon). Requires the same\nauth as the rest of the apps API. The URL embeds a digest\nof the icon as a `v` query parameter so it can be cached\nindefinitely; a release that leaves the icon unchanged\nleaves the URL unchanged too. Omitted when no analyzed\nrelease has an icon.\n"
          }
        }
      },
      "Channel": {
        "type": "object",
        "description": "A tag used to manage the subset of installs that receive a patch.\nBy default a \"stable\" channel is created and used by devices to\nquery for available patches.\n",
        "required": [
          "id",
          "app_id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the channel."
          },
          "app_id": {
            "type": "string",
            "description": "The ID of the app."
          },
          "name": {
            "type": "string",
            "description": "The channel name."
          }
        }
      },
      "Release": {
        "type": "object",
        "description": "A release build of an application that is distributed to devices.\nA release can have zero or more patches applied to it.\n",
        "required": [
          "id",
          "app_id",
          "version",
          "flutter_revision",
          "platform_statuses",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the release."
          },
          "app_id": {
            "type": "string",
            "description": "The ID of the app."
          },
          "version": {
            "type": "string",
            "description": "The version of the release."
          },
          "flutter_revision": {
            "type": "string",
            "description": "The Flutter revision used to create the release."
          },
          "flutter_version": {
            "type": "string",
            "description": "The Flutter version used to create the release. Optional\nbecause it was added later; older releases do not have it.\n"
          },
          "display_name": {
            "type": "string",
            "description": "The display name for the release."
          },
          "platform_statuses": {
            "type": "object",
            "description": "The status of the release for each platform.",
            "additionalProperties": {
              "$ref": "#/components/schemas/ReleaseStatus"
            },
            "propertyNames": {
              "$ref": "#/components/schemas/ReleasePlatform"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the release was created."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the release was last updated."
          },
          "notes": {
            "type": "string",
            "description": "Freeform notes associated with the release, if any."
          }
        }
      },
      "ReleaseAnalysis": {
        "type": "object",
        "description": "Analyzer-extracted metadata for a release artifact on a single\nplatform.\n",
        "required": [
          "display_name",
          "package_name",
          "min_sdk_version",
          "target_sdk_version",
          "architectures"
        ],
        "properties": {
          "display_name": {
            "type": "string",
            "description": "The user-visible application name extracted from the artifact\n(e.g. AndroidManifest `application:label`). May differ from\nthe user-curated `App.display_name`.\n"
          },
          "package_name": {
            "type": "string",
            "description": "The application package name (e.g. `com.example.app`)."
          },
          "min_sdk_version": {
            "type": "string",
            "description": "The minimum SDK level required to install the artifact\n(Android API level for android, iOS deployment target for ios).\n"
          },
          "target_sdk_version": {
            "type": "string",
            "description": "The SDK level the artifact targets (Android targetSdk for\nandroid, iOS SDK for ios).\n"
          },
          "architectures": {
            "type": "array",
            "description": "CPU architectures present in the artifact\n(e.g. `[\"arm64-v8a\", \"armeabi-v7a\"]`).\n",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "LatestRelease": {
        "type": "object",
        "description": "Per-platform projection of an analyzed release as surfaced by\n`AppMetadata.latest_releases`. Each entry corresponds to the\nmost recent analyzed release on the keying platform, so\n`analysis` is always populated and `status` is the single\nper-platform status (rather than the cross-platform\n`platform_statuses` map carried by `Release`).\n",
        "required": [
          "id",
          "version",
          "flutter_revision",
          "created_at",
          "updated_at",
          "status",
          "analysis"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the release."
          },
          "version": {
            "type": "string",
            "description": "The version of the release."
          },
          "flutter_revision": {
            "type": "string",
            "description": "The Flutter revision used to create the release."
          },
          "flutter_version": {
            "type": "string",
            "description": "The Flutter version used to create the release. Optional\nbecause it was added later; older releases do not have it.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the release was created."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the release was last updated."
          },
          "status": {
            "$ref": "#/components/schemas/ReleaseStatus",
            "description": "The release's status on the keying platform."
          },
          "notes": {
            "type": "string",
            "description": "Freeform notes associated with the release, if any."
          },
          "analysis": {
            "$ref": "#/components/schemas/ReleaseAnalysis",
            "description": "Analyzer-extracted metadata for this release on the keying\nplatform. Always present (entries without analysis are\nomitted from `latest_releases`).\n"
          }
        }
      },
      "PendingRelease": {
        "type": "object",
        "description": "A newer release that has been created but not yet analyzed.\nSurfaced alongside the most recent analyzed release so clients\ncan show an \"analyzing…\" indicator without losing the stable\nicon and metadata.\n",
        "required": [
          "id",
          "version",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the pending release."
          },
          "version": {
            "type": "string",
            "description": "The version of the pending release."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the pending release was created."
          }
        }
      },
      "ReleaseArtifact": {
        "type": "object",
        "description": "An artifact contains metadata about the contents of a specific\nrelease for a specific platform and architecture.\n",
        "required": [
          "id",
          "release_id",
          "arch",
          "platform",
          "hash",
          "size",
          "url",
          "can_sideload"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the artifact."
          },
          "release_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the release."
          },
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          },
          "url": {
            "type": "string",
            "description": "The url of the artifact."
          },
          "podfile_lock_hash": {
            "type": "string",
            "description": "sha256 of the Podfile.lock used to create the artifact (iOS only)."
          },
          "can_sideload": {
            "type": "boolean",
            "description": "Whether the artifact can be sideloaded onto a device."
          }
        }
      },
      "Patch": {
        "type": "object",
        "description": "An over-the-air update which is applied to a specific release.\nAll patches have a patch number (auto-incrementing integer) and\nmultiple patches can be published for a given release.\n",
        "required": [
          "id",
          "number"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The unique patch identifier."
          },
          "number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number. A larger number equates to a newer patch."
          },
          "notes": {
            "type": "string",
            "description": "Freeform notes associated with the patch, if any."
          }
        }
      },
      "PatchArtifact": {
        "type": "object",
        "description": "Metadata about the contents of a specific patch for a specific\nplatform and architecture.\n",
        "required": [
          "id",
          "patch_id",
          "arch",
          "platform",
          "hash",
          "size",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the artifact."
          },
          "patch_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the patch."
          },
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "The date and time the artifact was created."
          }
        }
      },
      "ReleasePatch": {
        "type": "object",
        "description": "A patch for a given release.",
        "required": [
          "id",
          "number",
          "artifacts",
          "is_rolled_back"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The patch id."
          },
          "number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number."
          },
          "channel": {
            "type": "string",
            "description": "The channel associated with the patch."
          },
          "artifacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PatchArtifact"
            },
            "description": "The associated patch artifacts."
          },
          "is_rolled_back": {
            "type": "boolean",
            "description": "Whether the patch has been rolled back."
          },
          "notes": {
            "type": "string",
            "description": "Freeform notes associated with the patch, if any."
          }
        }
      },
      "ReleasePlatform": {
        "type": "string",
        "description": "A platform to which a Shorebird release can be deployed.",
        "enum": [
          "android",
          "ios",
          "linux",
          "macos",
          "windows"
        ],
        "x-enum-descriptions": [
          "Android.",
          "iOS.",
          "Linux.",
          "macOS.",
          "Windows."
        ]
      },
      "ReleaseStatus": {
        "type": "string",
        "description": "The status of a release.",
        "enum": [
          "draft",
          "active"
        ],
        "x-enum-descriptions": [
          "The release has been created, but not all platform artifacts have been uploaded.",
          "All platform artifacts have been uploaded for this release."
        ]
      },
      "Role": {
        "type": "string",
        "description": "A role that a user can have relative to an Organization or App.",
        "enum": [
          "owner",
          "admin",
          "appManager",
          "developer",
          "viewer",
          "none"
        ],
        "x-enum-descriptions": [
          "User that created the organization.",
          "Users who have permissions to manage the organization.",
          "Users who have permissions to manage an app.",
          "Users who are part of the organization but have limited permissions.",
          "Users who have read-only access to the organization.",
          "Users who are not part of the organization but have visibility into it via app collaborator permissions."
        ]
      },
      "AppCollaboratorRole": {
        "type": "string",
        "description": "A role a user can have on a specific app.",
        "enum": [
          "admin",
          "developer"
        ],
        "x-enum-descriptions": [
          "A user with this role can perform all available actions on apps, releases, patches, channels, and collaborators.",
          "A user with this role can manage releases and patches, but cannot manage collaborators or the application itself."
        ]
      },
      "AuthProvider": {
        "type": "string",
        "description": "The authentication provider used to sign in the user.",
        "enum": [
          "google",
          "microsoft",
          "shorebird"
        ],
        "x-enum-descriptions": [
          "Signed in via Google.",
          "Signed in via Microsoft.",
          "Signed in via a Shorebird-managed account."
        ]
      },
      "OrganizationType": {
        "type": "string",
        "description": "Distinguishes personal organizations (single-user) from team\norganizations (multi-user).\n",
        "enum": [
          "personal",
          "team"
        ],
        "x-enum-descriptions": [
          "A single-user organization implicitly created with each user account.",
          "A multi-user organization with collaborators."
        ]
      },
      "Organization": {
        "type": "object",
        "description": "An organization groups users and apps together. Organizations\ncan be personal (single-user) or team (multi-user).\n",
        "required": [
          "id",
          "name",
          "organization_type",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The unique identifier for the organization."
          },
          "name": {
            "type": "string",
            "description": "The name of the organization."
          },
          "organization_type": {
            "$ref": "#/components/schemas/OrganizationType",
            "description": "The type of organization."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this organization was created."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this organization was last updated."
          }
        }
      },
      "OrganizationMembership": {
        "type": "object",
        "description": "An organization and the current user's role in it.",
        "required": [
          "organization",
          "role"
        ],
        "properties": {
          "organization": {
            "$ref": "#/components/schemas/Organization",
            "description": "The organization."
          },
          "role": {
            "$ref": "#/components/schemas/Role",
            "description": "The user's role in the organization."
          }
        }
      },
      "OrganizationUser": {
        "type": "object",
        "description": "A member of an organization and their role.",
        "required": [
          "user",
          "role"
        ],
        "properties": {
          "user": {
            "$ref": "#/components/schemas/PublicUser",
            "description": "The user that is a member of the organization."
          },
          "role": {
            "$ref": "#/components/schemas/Role",
            "description": "The role [user] has in the organization."
          }
        }
      },
      "PublicUser": {
        "type": "object",
        "description": "A Shorebird user with non-sensitive information only.\n",
        "required": [
          "id",
          "email"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The user's unique identifier."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The user's email address."
          },
          "display_name": {
            "type": "string",
            "description": "The user's name."
          }
        }
      },
      "PrivateUser": {
        "type": "object",
        "description": "A fully-detailed user object, possibly including sensitive\ninformation. Should only be used when querying the user's own\ninformation.\n",
        "required": [
          "id",
          "email",
          "jwt_issuer"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The unique user identifier."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The user's email address, as provided by the user during signup."
          },
          "display_name": {
            "type": "string",
            "description": "The user's name, as provided by the user during signup."
          },
          "stripe_customer_id": {
            "type": "string",
            "description": "The user's Stripe customer ID, if they have one."
          },
          "jwt_issuer": {
            "type": "string",
            "description": "The JWT issuer used to create the user."
          },
          "patch_overage_limit": {
            "type": "integer",
            "format": "int64",
            "description": "The maximum number of patch installs the user has agreed to\npay for as part of a pay-as-you-go plan.\n"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Standard error response body from the Shorebird CodePush API.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The unique error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "details": {
            "type": "string",
            "description": "Optional details associated with the error."
          }
        }
      },
      "CreateAppRequest": {
        "type": "object",
        "description": "The request body for POST /apps.",
        "required": [
          "display_name",
          "organization_id"
        ],
        "properties": {
          "display_name": {
            "type": "string",
            "description": "The display name of the app."
          },
          "organization_id": {
            "type": "integer",
            "format": "int64",
            "description": "The id of organization that this app will belong to."
          }
        }
      },
      "CreateAppCollaboratorRequest": {
        "type": "object",
        "description": "The request body for POST /apps/{appId}/collaborators.",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The email of the collaborator to add."
          }
        }
      },
      "UpdateAppCollaboratorRequest": {
        "type": "object",
        "description": "The request body for PATCH /apps/{appId}/collaborators/{collaboratorId}.\n",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "$ref": "#/components/schemas/AppCollaboratorRole",
            "description": "The new role for the collaborator."
          }
        }
      },
      "CreateChannelRequest": {
        "type": "object",
        "description": "The request body for POST /apps/{appId}/channels.",
        "required": [
          "channel"
        ],
        "properties": {
          "channel": {
            "type": "string",
            "description": "The channel name."
          }
        }
      },
      "CreateReleaseRequest": {
        "type": "object",
        "description": "The request body for POST /apps/{appId}/releases.",
        "required": [
          "version",
          "flutter_revision"
        ],
        "properties": {
          "version": {
            "type": "string",
            "description": "The release version."
          },
          "flutter_revision": {
            "type": "string",
            "description": "The Flutter revision used to create the release."
          },
          "flutter_version": {
            "type": "string",
            "description": "The Flutter version used to create the release.  This field is optional because it was newly added and older releases do not have this information."
          },
          "display_name": {
            "type": "string",
            "description": "The display name for the release."
          }
        }
      },
      "CreateReleaseResponse": {
        "type": "object",
        "description": "The response body for POST /apps/{appId}/releases.",
        "required": [
          "release"
        ],
        "properties": {
          "release": {
            "$ref": "#/components/schemas/Release",
            "description": "The newly-created release."
          }
        }
      },
      "UpdateReleaseRequest": {
        "type": "object",
        "description": "The request body for PATCH /apps/{appId}/releases/{releaseId}.",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/ReleaseStatus",
            "description": "The desired status of the release. If provided, [platform] must also be provided If null, the status will not be updated."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the release. If provided, [status] must also be provided."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Additional information about the command that was run to\nupdate the release and the environment it was run in.\n"
          },
          "notes": {
            "type": "string",
            "description": "Notes about the release. This is a free-form field that can be used to store additional information about the release. If null, the notes will not be updated."
          }
        }
      },
      "GetAppsResponse": {
        "type": "object",
        "description": "The response body for GET /apps.",
        "required": [
          "apps"
        ],
        "properties": {
          "apps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppMetadata"
            },
            "description": "The list of apps."
          }
        }
      },
      "GetReleasesResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/releases.",
        "required": [
          "releases"
        ],
        "properties": {
          "releases": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Release"
            },
            "description": "The list of releases for the app."
          }
        }
      },
      "GetReleaseResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/releases/{releaseId}.",
        "required": [
          "release"
        ],
        "properties": {
          "release": {
            "$ref": "#/components/schemas/Release",
            "description": "The requested release."
          }
        }
      },
      "GetReleaseArtifactsResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/releases/{releaseId}/artifacts.",
        "required": [
          "artifacts"
        ],
        "properties": {
          "artifacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReleaseArtifact"
            },
            "description": "The artifacts for the release."
          }
        }
      },
      "GetReleasePatchesResponse": {
        "type": "object",
        "description": "The response to GET /apps/{appId}/releases/{releaseId}/patches.",
        "required": [
          "patches"
        ],
        "properties": {
          "patches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReleasePatch"
            },
            "description": "List of patches."
          }
        }
      },
      "GetOrganizationsResponse": {
        "type": "object",
        "description": "The response body for GET /organizations.",
        "required": [
          "organizations"
        ],
        "properties": {
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationMembership"
            },
            "description": "Organizations that the user is a member of, as well as this user's role in each organization."
          }
        }
      },
      "GetOrganizationAppsResponse": {
        "type": "object",
        "description": "The response body for GET /organizations/{organizationId}/apps.",
        "required": [
          "apps"
        ],
        "properties": {
          "apps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppMetadata"
            },
            "description": "The apps that belong to the organization."
          }
        }
      },
      "GetOrganizationUsersResponse": {
        "type": "object",
        "description": "The response body for GET /organizations/{organizationId}/users.",
        "required": [
          "users"
        ],
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationUser"
            },
            "description": "The list of users that belong to the organization, as well as their roles in the organization."
          }
        }
      },
      "CreatePatchRequest": {
        "type": "object",
        "description": "The request body for POST /apps/{appId}/patches.",
        "required": [
          "release_id",
          "metadata"
        ],
        "properties": {
          "release_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the release."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Additional information about the command that was run to\ncreate the patch and the environment it was run in.\n"
          }
        }
      },
      "CreatePatchResponse": {
        "type": "object",
        "description": "The response body for POST /apps/{appId}/patches. Deliberately\nnarrower than [Patch]: a freshly-created patch has no `notes`\nyet (those are set via PATCH /.../{patchId}), so this endpoint\nexposes only the identifiers the client needs to upload\nartifacts.\n",
        "required": [
          "id",
          "number"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The unique patch identifier."
          },
          "number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number. A larger number equates to a newer patch."
          }
        }
      },
      "CreatePatchArtifactRequest": {
        "type": "object",
        "description": "Metadata for a new patch artifact. POST to\n/apps/{appId}/patches/{patchId}/artifacts and use the returned\nsigned upload URL to upload the artifact bytes separately.\n",
        "required": [
          "arch",
          "platform",
          "hash",
          "size"
        ],
        "properties": {
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "hash_signature": {
            "type": "string",
            "description": "The signature of the [hash].  Patch code signing is an opt in feature, introduced later in the life of the product, so when this field is null, the patch does not uses code signing."
          },
          "podfile_lock_hash": {
            "type": "string",
            "description": "The sha256 hash of the Podfile.lock file, if a Podfile.lock file was involved in the creation of the patch (iOS only)."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          }
        }
      },
      "CreatePatchArtifactResponse": {
        "type": "object",
        "description": "The response body for registering a patch artifact.",
        "required": [
          "id",
          "patch_id",
          "arch",
          "platform",
          "hash",
          "size",
          "url"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the artifact."
          },
          "patch_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the patch."
          },
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          },
          "url": {
            "type": "string",
            "description": "The upload URL for the artifact."
          },
          "upload_method": {
            "type": "string",
            "enum": [
              "multipart",
              "resumable"
            ],
            "description": "How to upload the artifact bytes to `url`: a single multipart POST\n(legacy), or a PUT (chunked, resumable) to a resumable session URI.\nAbsent on responses from older servers, which implied multipart.\n"
          }
        }
      },
      "CreateReleaseArtifactRequest": {
        "type": "object",
        "description": "Metadata for a new release artifact. POST to\n/apps/{appId}/releases/{releaseId}/artifacts and use the\nreturned signed upload URL to upload the artifact bytes\nseparately.\n",
        "required": [
          "arch",
          "platform",
          "hash",
          "size",
          "filename"
        ],
        "properties": {
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "filename": {
            "type": "string",
            "description": "The name of the file."
          },
          "can_sideload": {
            "type": "boolean",
            "description": "Whether the artifact can installed and run on a device/emulator as-is."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          },
          "podfile_lock_hash": {
            "type": "string",
            "description": "The hash of the Podfile.lock file used to create this artifact (iOS only)."
          }
        }
      },
      "CreateReleaseArtifactResponse": {
        "type": "object",
        "description": "The response body for registering a release artifact.",
        "required": [
          "id",
          "release_id",
          "arch",
          "platform",
          "hash",
          "size",
          "url"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the artifact."
          },
          "release_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the release."
          },
          "arch": {
            "type": "string",
            "description": "The arch of the artifact."
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "size": {
            "type": "integer",
            "format": "int64",
            "description": "The size of the artifact in bytes."
          },
          "url": {
            "type": "string",
            "description": "The upload URL for the artifact."
          },
          "upload_method": {
            "type": "string",
            "enum": [
              "multipart",
              "resumable"
            ],
            "description": "How to upload the artifact bytes to `url`: a single multipart POST\n(legacy), or a PUT (chunked, resumable) to a resumable session URI.\nAbsent on responses from older servers, which implied multipart.\n"
          }
        }
      },
      "UpdatePatchRequest": {
        "type": "object",
        "description": "The request body for PATCH\n/apps/{appId}/releases/{releaseId}/patches/{patchId}.\n",
        "properties": {
          "notes": {
            "type": "string",
            "description": "Freeform notes about the patch. If null, notes are unchanged.\n"
          }
        }
      },
      "PromotePatchRequest": {
        "type": "object",
        "description": "The request body for POST /apps/{appId}/patches/promote.",
        "required": [
          "patch_id",
          "channel_id"
        ],
        "properties": {
          "patch_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the patch."
          },
          "channel_id": {
            "type": "integer",
            "format": "int64",
            "description": "The ID of the channel."
          }
        }
      },
      "PatchCheckRequest": {
        "type": "object",
        "description": "The request body for POST /patches/check.",
        "required": [
          "release_version",
          "platform",
          "arch",
          "app_id",
          "channel"
        ],
        "properties": {
          "release_version": {
            "type": "string",
            "description": "The release version of the app."
          },
          "patch_number": {
            "type": "integer",
            "format": "int64",
            "description": "The highest patch number the client has already downloaded.\nIf provided, the server only returns patches with a higher\nnumber. If omitted, the server returns the latest available.\n"
          },
          "platform": {
            "$ref": "#/components/schemas/ReleasePlatform",
            "description": "The platform of the app."
          },
          "arch": {
            "type": "string",
            "description": "The architecture of the app."
          },
          "app_id": {
            "type": "string",
            "description": "The ID of the app."
          },
          "channel": {
            "type": "string",
            "description": "The channel of the app."
          },
          "client_id": {
            "type": "string",
            "description": "Unique device ID for the install, generated on device and\nunique per app. Optional for backward compatibility.\n"
          },
          "current_patch_number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number currently running on the device, if any.\nSupersedes `patch_number` for newer clients; unlike\n`patch_number`, this does not affect the server's response.\n"
          }
        }
      },
      "PatchCheckResponse": {
        "type": "object",
        "description": "The response body for POST /patches/check.",
        "required": [
          "patch_available"
        ],
        "properties": {
          "patch_available": {
            "type": "boolean",
            "description": "Whether a patch is available."
          },
          "patch": {
            "$ref": "#/components/schemas/PatchCheckMetadata",
            "description": "The patch metadata."
          },
          "rolled_back_patch_numbers": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "The numbers of all patches that have been rolled back for the current release."
          }
        }
      },
      "PatchCheckMetadata": {
        "type": "object",
        "description": "Patch metadata representing the contents of a patch for a\nspecific platform and architecture.\n",
        "required": [
          "number",
          "download_url",
          "hash"
        ],
        "properties": {
          "number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number associated with the artifact."
          },
          "download_url": {
            "type": "string",
            "description": "The URL of the artifact."
          },
          "hash": {
            "type": "string",
            "description": "The hash of the artifact."
          },
          "hash_signature": {
            "type": "string",
            "description": "The signature of the `hash`."
          }
        }
      },
      "VersionDistributionEntry": {
        "type": "object",
        "description": "One bucket in a version-distribution chart: the exact count of\ncurrently-active devices on a given release version. A null\n`release_version` represents devices whose client did not emit one\n(typically very old Flutter clients).\n",
        "required": [
          "release_version",
          "device_count",
          "percentage"
        ],
        "properties": {
          "release_version": {
            "type": "string",
            "nullable": true,
            "description": "The release version for this bucket, or null for devices whose\nclient did not emit one.\n"
          },
          "device_count": {
            "type": "integer",
            "format": "int64",
            "description": "Number of currently-active devices on this release version\nwithin the active-device window.\n"
          },
          "percentage": {
            "type": "number",
            "format": "double",
            "description": "Fractional share of currently-active devices on this release\nversion, in [0.0, 1.0]. Server is the source of truth; clients\nshould render this value rather than recomputing it.\n"
          }
        }
      },
      "GetVersionDistributionResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/version-distribution.\n",
        "required": [
          "entries",
          "total_devices",
          "active_window_days",
          "as_of"
        ],
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VersionDistributionEntry"
            },
            "description": "One entry per release version, sorted by `device_count`\ndescending, then `release_version` ascending with NULLs last.\n"
          },
          "total_devices": {
            "type": "integer",
            "format": "int64",
            "description": "Sum of `device_count` across all entries. Convenience for\nclients; matches the server-side sum used to compute\n`percentage`.\n"
          },
          "active_window_days": {
            "type": "integer",
            "description": "The active-device window in days that bounds the query.\nHardcoded server-side in v1; tier-gated per-caller windows\nland in a follow-up.\n"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was\nconstructed. Not a freshness indicator for the underlying\ndata, which is refreshed by an hourly scheduled query and\nmay lag by up to ~1 hour.\n"
          }
        }
      },
      "GetNewDevicesResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/new-devices.\n",
        "required": [
          "current",
          "previous",
          "window_days",
          "as_of"
        ],
        "properties": {
          "current": {
            "type": "integer",
            "format": "int64",
            "description": "Devices first seen in the window `[as_of − window_days, as_of)`.\nAn exact (non-HLL) count.\n"
          },
          "previous": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Devices first seen in the equal-length window immediately\npreceding the current one, or null when that window would begin\nbefore the metrics data floor (comparing against\npartially-recorded history would show a misleading delta).\n"
          },
          "window_days": {
            "type": "integer",
            "description": "The window length in days. Hardcoded server-side in v1; echoed\nso clients label the metric from the response rather than\nassuming a length.\n"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was\nconstructed. Not a freshness indicator for the underlying\ndata, which is refreshed by an hourly scheduled query and\nmay lag by up to ~1 hour.\n"
          }
        }
      },
      "PatchAdoptionPoint": {
        "type": "object",
        "description": "One point in a patch's adoption series: the cumulative distinct\ndevices on `patch >= patch_number` (`devices`) over the patch's target\n(`target`), and their ratio (`adoption_pct`), for one bucket.\n",
        "required": [
          "period",
          "devices",
          "target",
          "adoption_pct"
        ],
        "properties": {
          "period": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The bucket start (UTC), or null when the response is a single\nfull-window value (no granularity requested).\n"
          },
          "devices": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct devices running patch `>= patch_number` on the patch's\ntarget platform(s) within this bucket (an HLL count).\n"
          },
          "target": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct devices on the release whose platform the patch targets —\nthe patch's reachable denominator — within this bucket.\n"
          },
          "adoption_pct": {
            "type": "number",
            "format": "double",
            "description": "`devices / target`, in [0.0, 1.0] (0 when `target` is 0). Server\nis the source of truth; clients should render this value rather\nthan recomputing it.\n"
          }
        }
      },
      "PatchAdoptionEntry": {
        "type": "object",
        "description": "Cumulative adoption for one patch of the release. Values are\ncumulative — \"patch `>= patch_number`\" — so they are monotonic\nnon-increasing in `patch_number`.\n",
        "required": [
          "patch_number",
          "target_platforms",
          "is_rolled_back",
          "series"
        ],
        "properties": {
          "patch_number": {
            "type": "integer",
            "format": "int64",
            "description": "The patch number these cumulative values are anchored at.\n"
          },
          "target_platforms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReleasePlatform"
            },
            "description": "The platform(s) the patch was built for (from its artifacts). The\ndenominator counts only devices on these platforms.\n"
          },
          "is_rolled_back": {
            "type": "boolean",
            "description": "Whether the patch has been rolled back."
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PatchAdoptionPoint"
            },
            "description": "The adoption series. Exactly one point (`period: null`) when no\ngranularity was requested; otherwise one point per bucket, ordered\nby `period` ascending.\n"
          }
        }
      },
      "MetricsRange": {
        "type": "object",
        "description": "The effective (post-default, post-clamp) window a metrics response —\nor one window of a metrics envelope — covers. Always echoed by the\nserver; clients must treat it as authoritative rather than reusing\nthe requested range.\n",
        "required": [
          "start",
          "end"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Window start (UTC, inclusive)."
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "Window end (UTC, exclusive)."
          }
        }
      },
      "GetPatchAdoptionResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/patch-adoption. Covers\nexactly one release.\n",
        "required": [
          "release_version",
          "is_latest",
          "granularity",
          "range",
          "as_of",
          "patches"
        ],
        "properties": {
          "release_version": {
            "type": "string",
            "description": "The release version this response is for."
          },
          "is_latest": {
            "type": "boolean",
            "description": "True when the release was resolved via the \"latest release by\ncreation date\" default (no `release_version` was supplied).\n"
          },
          "granularity": {
            "type": "string",
            "nullable": true,
            "description": "The bucket resolution (`hour`, `day`, or `week`), or null\nwhen each patch carries a single full-window value.\n"
          },
          "range": {
            "$ref": "#/components/schemas/MetricsRange"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was\nconstructed. Not a freshness indicator for the underlying\ndata, which is refreshed by an hourly scheduled query and\nmay lag by up to ~1 hour.\n"
          },
          "patches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PatchAdoptionEntry"
            },
            "description": "One entry per patch of the release."
          }
        }
      },
      "ActiveHourEntry": {
        "type": "object",
        "description": "Average number of distinct active devices during one UTC hour-of-day,\naveraged across all days in the lookback window (days with no activity\nin that hour count as zero).\n",
        "required": [
          "hour_utc",
          "average_active_devices"
        ],
        "properties": {
          "hour_utc": {
            "type": "integer",
            "description": "Hour of day in UTC, 0–23."
          },
          "average_active_devices": {
            "type": "number",
            "format": "double",
            "description": "Mean distinct active devices seen during this UTC hour, averaged\nover the lookback window with implicit zeros included.\n"
          }
        }
      },
      "GetActiveHoursResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/active-hours. Powers the\n\"best time to release\" recommendation: the consecutive low-activity UTC\nwindow when the app's users are least active.\n",
        "required": [
          "hourly",
          "recommended_window_start_utc",
          "recommended_window_length_hours",
          "busiest_hour_utc",
          "lookback_days",
          "as_of"
        ],
        "properties": {
          "hourly": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActiveHourEntry"
            },
            "description": "One entry per UTC hour: 24 entries, hour_utc 0–23, zero-filled,\nordered by hour_utc ascending.\n"
          },
          "recommended_window_start_utc": {
            "type": "integer",
            "nullable": true,
            "description": "Start hour (UTC, 0–23) of the lowest-activity consecutive window —\nthe recommended time to release. The window wraps past midnight.\nNull when there is insufficient data (fewer than 7 days).\n"
          },
          "recommended_window_length_hours": {
            "type": "integer",
            "description": "Length of the recommended window in hours. Fixed at 2 in v1.\n"
          },
          "busiest_hour_utc": {
            "type": "integer",
            "nullable": true,
            "description": "UTC hour (0–23) with the highest average active devices, for\ncontrast in the recommendation copy. Null when insufficient data.\n"
          },
          "lookback_days": {
            "type": "integer",
            "description": "Number of days of history the profile is computed over.\n"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was constructed.\nNot a freshness indicator for the underlying data, which is\nrefreshed by an hourly scheduled query and may lag by up to ~1 hour.\n"
          }
        }
      },
      "ActivityHeatmapCell": {
        "type": "object",
        "description": "Average number of distinct active devices during one (UTC day-of-week,\nUTC hour-of-day) cell, averaged across every occurrence of that weekday\nin the lookback window (occurrences with no activity count as zero).\n",
        "required": [
          "day_of_week_utc",
          "hour_utc",
          "average_active_devices"
        ],
        "properties": {
          "day_of_week_utc": {
            "type": "integer",
            "description": "Day of week in UTC, 1–7 where 1 = Sunday and 7 = Saturday.\n"
          },
          "hour_utc": {
            "type": "integer",
            "description": "Hour of day in UTC, 0–23."
          },
          "average_active_devices": {
            "type": "number",
            "format": "double",
            "description": "Mean distinct active devices seen during this UTC weekday-hour,\naveraged over the weekday's occurrences in the window with implicit\nzeros included.\n"
          }
        }
      },
      "GetActivityHeatmapResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/activity-heatmap. A 7×24\ngrid of average active devices per UTC weekday-hour, powering the\ninsights activity heatmap.\n",
        "required": [
          "cells",
          "busiest_day_of_week_utc",
          "busiest_hour_utc",
          "lookback_days",
          "as_of"
        ],
        "properties": {
          "cells": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActivityHeatmapCell"
            },
            "description": "168 entries (7 weekdays × 24 hours), zero-filled, ordered by\nday_of_week_utc (1–7) then hour_utc (0–23) ascending.\n"
          },
          "busiest_day_of_week_utc": {
            "type": "integer",
            "nullable": true,
            "description": "UTC day-of-week (1–7) of the cell with the highest average active\ndevices, for peak-relative coloring and labeling. Null when there is\nno data.\n"
          },
          "busiest_hour_utc": {
            "type": "integer",
            "nullable": true,
            "description": "UTC hour (0–23) of the busiest cell. Null when there is no data.\n"
          },
          "lookback_days": {
            "type": "integer",
            "description": "Number of days of history the heatmap is computed over.\n"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was constructed.\nNot a freshness indicator for the underlying data, which is\nrefreshed by an hourly scheduled query and may lag by up to ~1 hour.\n"
          }
        }
      },
      "UniqueUsersTimeSeriesEntry": {
        "type": "object",
        "description": "One bucket of a unique-users time series: the HLL count of distinct\nactive devices in the bucket starting at `period`.\n",
        "required": [
          "period",
          "unique_users"
        ],
        "properties": {
          "period": {
            "type": "string",
            "format": "date-time",
            "description": "The bucket start (UTC)."
          },
          "unique_users": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct active devices in this bucket (an HLL count)."
          }
        }
      },
      "UniqueUsersBreakdownEntry": {
        "type": "object",
        "description": "Unique users for one value of the `group_by` dimension (e.g. one\nplatform), optionally with its own time series.\n",
        "required": [
          "group_by",
          "group_value",
          "unique_users"
        ],
        "properties": {
          "group_by": {
            "type": "string",
            "description": "The dimension this entry breaks down by (e.g. \"platform\")."
          },
          "group_value": {
            "type": "string",
            "description": "The value within `group_by` (e.g. \"android\"). For\n`group_by=release_version`, the empty string is the\nunknown-version group — devices whose client is too old to\nreport a release version.\n"
          },
          "unique_users": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct active devices for this group over the window (an HLL\ncount).\n"
          },
          "time_series": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/UniqueUsersTimeSeriesEntry"
            },
            "description": "Per-bucket series for this group, present only when a `granularity`\nwas requested; otherwise null.\n"
          }
        }
      },
      "UniqueUsersWindow": {
        "type": "object",
        "description": "One window of the unique-users envelope: the HLL-merged total over\nthe window's effective range, with a per-bucket series when a\n`granularity` was requested. This base atom is the full shape of\n`previous`; `current` extends it (see UniqueUsersCurrentWindow).\n",
        "required": [
          "unique_users",
          "range"
        ],
        "properties": {
          "unique_users": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct active devices over this window (an HLL count). Note:\nper-bucket `time_series` values do not sum to this — an HLL merge\nover the window is not a sum of per-bucket merges.\n"
          },
          "range": {
            "$ref": "#/components/schemas/MetricsRange"
          },
          "time_series": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/UniqueUsersTimeSeriesEntry"
            },
            "description": "Per-bucket series for this window, present only when a\n`granularity` was requested; otherwise null. On `previous`, also\nnull when the prior window reaches past the plan's metrics-history\nhorizon (the total is still present — only the granular overlay is\nwithheld). Empty buckets are omitted — gap-fill against this\nwindow's `range`.\n"
          }
        }
      },
      "UniqueUsersCurrentWindow": {
        "description": "The `current` window of the unique-users envelope: the base window\natom plus the optional `breakdown`. Only `current` carries a\nbreakdown — no chart renders a previous-window breakdown, so the\nasymmetry is declared in the contract rather than left as an\noptional-but-never-populated field.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/UniqueUsersWindow"
          },
          {
            "type": "object",
            "properties": {
              "breakdown": {
                "type": "array",
                "nullable": true,
                "items": {
                  "$ref": "#/components/schemas/UniqueUsersBreakdownEntry"
                },
                "description": "Per-group unique users for this window, present only when a\n`group_by` was requested; otherwise null.\n"
              }
            }
          }
        ]
      },
      "GetUniqueUsersResponse": {
        "type": "object",
        "description": "The response body for GET /apps/{appId}/metrics/unique-users: a\ncurrent/previous envelope. `previous` covers the equal-length window\nimmediately preceding `current`; period-over-period deltas are client\ndisplay logic over the two totals. `previous` is always present but is\nnull when the prior window predates the data floor (no comparison data\nexists). When it reaches past the plan's metrics-history horizon,\n`previous` is non-null with its total — the delta renders — but without\na `time_series` (no prior-window overlay): granular history is the\nresolution the horizon gates, the scalar comparison is not.\n",
        "required": [
          "as_of",
          "granularity",
          "current",
          "previous"
        ],
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was constructed.\nNot a freshness indicator for the underlying data, which is\nrefreshed by an hourly scheduled query and may lag by up to ~1 hour.\n"
          },
          "granularity": {
            "type": "string",
            "nullable": true,
            "description": "The time-series bucket resolution (`hour`, `day`, or `week`), or\nnull when no time series was requested. Applies to both windows.\n"
          },
          "current": {
            "$ref": "#/components/schemas/UniqueUsersCurrentWindow"
          },
          "previous": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UniqueUsersWindow"
              },
              {
                "type": "null"
              }
            ],
            "description": "The equal-length window immediately preceding `current`. Always\npresent, but null when that prior window predates the data floor —\nthere is no comparison data, so reporting a fabricated undercount is\nworse than returning null. When the prior window instead reaches\npast the plan's metrics-history horizon, this is non-null with its\n`unique_users` total (the delta) but no `time_series` (the overlay\nis the granular resolution the horizon gates).\n"
          }
        }
      },
      "PatchMetricTimeSeriesEntry": {
        "type": "object",
        "description": "One bucket of a patch-metric time series: the summed count (installs or\ndownloads, per the endpoint) in the bucket starting at `period`.\n",
        "required": [
          "period",
          "count"
        ],
        "properties": {
          "period": {
            "type": "string",
            "format": "date-time",
            "description": "The bucket start (UTC)."
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "The summed count in this bucket."
          }
        }
      },
      "PatchMetricBreakdownEntry": {
        "type": "object",
        "description": "A patch metric for one value of the `group_by` dimension (one release\nat app scope, or one patch at release scope), optionally with its own\ntime series.\n",
        "required": [
          "group_by",
          "group_value",
          "count"
        ],
        "properties": {
          "group_by": {
            "type": "string",
            "description": "The dimension this entry breaks down by (\"release\" or \"patch\")."
          },
          "group_value": {
            "type": "string",
            "description": "The value within `group_by`: the release version, or the patch\nnumber as a string.\n"
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "The summed count for this group over the window."
          },
          "time_series": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/PatchMetricTimeSeriesEntry"
            },
            "description": "Per-bucket series for this group, present only when a `granularity`\nwas requested; otherwise null.\n"
          }
        }
      },
      "PatchMetricWindow": {
        "type": "object",
        "description": "One window of the patch-metric envelope: the summed count over the\nwindow's effective range, with a per-bucket series when a `granularity`\nwas requested. This base atom is the full shape of `previous`;\n`current` extends it (see PatchMetricCurrentWindow).\n",
        "required": [
          "count",
          "range"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "The summed count (installs or downloads) over this window. Unlike\nthe HLL-based metrics, per-bucket `time_series` values sum to this\ntotal.\n"
          },
          "range": {
            "$ref": "#/components/schemas/MetricsRange"
          },
          "time_series": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/PatchMetricTimeSeriesEntry"
            },
            "description": "Per-bucket series for this window, present only when a\n`granularity` was requested; otherwise null. On `previous`, also\nnull when the prior window reaches past the plan's metrics-history\nhorizon (the total is still present — only the granular overlay is\nwithheld). Empty buckets are omitted — gap-fill against this\nwindow's `range`.\n"
          }
        }
      },
      "PatchMetricCurrentWindow": {
        "description": "The `current` window of the patch-metric envelope: the base window atom\nplus the optional `breakdown`. Only `current` carries a breakdown — no\nchart renders a previous-window breakdown, so the asymmetry is declared\nin the contract rather than left as an optional-but-never-populated\nfield.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/PatchMetricWindow"
          },
          {
            "type": "object",
            "properties": {
              "breakdown": {
                "type": "array",
                "nullable": true,
                "items": {
                  "$ref": "#/components/schemas/PatchMetricBreakdownEntry"
                },
                "description": "Per-group counts for this window, present only when a\n`group_by` was requested; otherwise null.\n"
              }
            }
          }
        ]
      },
      "GetPatchMetricResponse": {
        "type": "object",
        "description": "The response body for the patch-installs and patch-downloads metric\nendpoints (app- and release-scoped): a current/previous envelope.\n`previous` covers the equal-length window immediately preceding\n`current`; period-over-period deltas are client display logic over the\ntwo totals. `previous` is omitted only when the prior window predates\nthe data floor (no comparison data exists). When it reaches past the\nplan's metrics-history horizon, `previous` is still present with its\ntotal — the delta renders — but without a `time_series` (no\nprior-window overlay): granular history is the resolution the horizon\ngates, the scalar comparison is not.\n",
        "required": [
          "as_of",
          "granularity",
          "current"
        ],
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server's UTC timestamp at the moment the response was constructed.\nNot a freshness indicator for the underlying data, which is\nrefreshed by an hourly scheduled job and may lag by up to ~1 hour.\n"
          },
          "granularity": {
            "type": "string",
            "nullable": true,
            "description": "The time-series bucket resolution (`hour`, `day`, or `week`), or\nnull when no time series was requested. Applies to both windows.\n"
          },
          "current": {
            "$ref": "#/components/schemas/PatchMetricCurrentWindow"
          },
          "previous": {
            "$ref": "#/components/schemas/PatchMetricWindow",
            "description": "The equal-length window immediately preceding `current`. Omitted\nonly when that prior window predates the data floor — there is no\ncomparison data, so reporting a fabricated count is worse than\nomitting it. When the prior window instead reaches past the plan's\nmetrics-history horizon, this is present with its `count` total\n(the delta) but no `time_series` (the overlay is the granular\nresolution the horizon gates).\n"
          }
        }
      }
    }
  }
}