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 originally lived in the handwritten `shorebird_code_push_protocol` and `shorebird_code_push_client` packages; this spec is the OpenAPI representation of that surface area. 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 # ------------------------------------------------------------------- /users: post: tags: [users] summary: Create a new user. description: | Creates a new Shorebird user. Email is retrieved from the user's auth token; only the display name is provided in the body. operationId: createUser requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" responses: default: $ref: "#/components/responses/Error" "200": description: The newly-created user. content: application/json: schema: $ref: "#/components/schemas/PrivateUser" /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 # ------------------------------------------------------------------- /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 (the picked release's id), set by `AppMetadata.iconUrl`. Ignored by the server, which always serves the currently-picked icon; clients re-fetch when the URL changes. 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 release (or Android, when iOS has no analyzed release with an icon). The response is labeled `immutable` because the URL embedded in `AppMetadata.iconUrl` changes whenever the picked release changes. Returns 404 when no analyzed release has an icon. 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. # ------------------------------------------------------------------- # App collaborators # ------------------------------------------------------------------- /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. # ------------------------------------------------------------------- # Channels # ------------------------------------------------------------------- /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" # ------------------------------------------------------------------- # Releases # ------------------------------------------------------------------- /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 URL; clients then `POST` the artifact bytes to that URL. operationId: createReleaseArtifact requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateReleaseArtifactRequest" # The server also accepts multipart/form-data for compatibility # with existing CLI releases; newly-generated clients should # prefer application/json (listed first). 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. # ------------------------------------------------------------------- # Patches # ------------------------------------------------------------------- /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" # The server also accepts multipart/form-data for compatibility # with existing CLI releases; newly-generated clients should # prefer application/json (listed first). 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. # ------------------------------------------------------------------- # Metrics — MAU v2 customer-facing analytics # ------------------------------------------------------------------- /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 the app, grouped by `release_version`. "Currently active" is bounded by a server-side active-device window; rows with no reported `release_version` are returned with `releaseVersion: null` and represent devices on Flutter versions too old to emit the field. The result is sorted by `deviceCount` descending, then `releaseVersion` ascending with NULLs last. 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 falls within the last `window_days` days — a device-growth signal — together with the count for the equal-length window immediately preceding it, which powers a period-over-period delta. Counts are exact (non-HLL). `previous` is null when the preceding window would begin before the metrics data floor, where a comparison against partially-recorded history would show a misleading delta. 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: the distinct devices running patch `>= N` (an HLL count over `hourly_user_sketches`) over the patch's *target* — the distinct devices on the release whose platform the patch was built for. Adoption percentages are computed server-side and clamped to `[0.0, 1.0]`; clients should render them rather than recompute. Values are cumulative ("patch `>= N`"), so they are monotonic non-increasing in `patch_number`. The release is the `release_version` query parameter, or — when omitted — the app's latest release by creation date (`is_latest` flags that case). The window defaults to the last 28 days and is capped at 32 days (the effective window is echoed in `range`). With `granularity` omitted, each patch carries a single full-window value (`period: null`); set it for a curve at that resolution. 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 creation date. - name: start in: query required: false schema: { type: string, format: date-time } description: | Window start (UTC, inclusive). Defaults to `end` minus 28 days; clamped so the window never exceeds 32 days. - name: end in: query required: false schema: { type: string, format: date-time } description: | Window end (UTC, exclusive). Defaults to now. A window "ending now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution. Omitted ⇒ a single full-window value per patch (`period: null`); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`). 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 a server-side lookback window, plus the recommended lowest-activity consecutive window (the best time to push a patch with minimal user disruption) and the busiest hour for contrast. Recommendation fields are null when there is insufficient history (fewer than 7 days). 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 (UTC day-of-week, UTC hour-of-day) cell over a server-side lookback window — a 7×24 activity profile for an insights heatmap. Sourced from the same `hourly_user_sketches` data as the active-hours advisor, just sliced by weekday as well as hour. 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 a time window — an HLL-approximated count over `hourly_user_sketches` — as a current/previous envelope: `current` covers the effective window, `previous` the equal-length window immediately preceding it (always computed; powers period-over-period deltas and overlays). With `granularity` set, each window also carries a `time_series` at that resolution; with `group_by` set, `current` carries a `breakdown` per group (each with its own `time_series` when a `granularity` is also requested). `previous` never carries a breakdown. Counts are HLL approximations. The window defaults to the last 28 days and is capped at 450 days; every bound clamps and the effective range is echoed in each window's `range`, which clients must treat as authoritative. 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 now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution for the time series. Omitted ⇒ no `time_series` (one merged value per window); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`), which guarantees `current` and `previous` have equal bucket counts. - 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`. For `release_version`, devices whose client is too old to report a release version group under the empty-string `group_value`. 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 Redis time-series buckets — as a current/previous envelope: `current` covers the effective window, `previous` the equal-length window immediately preceding it (powers period-over-period deltas and overlays). With `granularity` set, each window also carries a `time_series` at that resolution; with `group_by=release` set, `current` carries a `breakdown` with one entry per release (each with its own `time_series` when a `granularity` is also requested). `previous` never carries a breakdown. The window defaults to the last 28 days and is capped at 450 days; every bound clamps and the effective range is echoed in each window's `range`, which clients must treat as authoritative. 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 now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution for the time series. Omitted ⇒ no `time_series` (one summed value per window); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`), which guarantees `current` and `previous` have equal bucket counts. - name: group_by in: query required: false schema: { type: string, enum: [release] } description: | Dimension to break the total down by. Omitted ⇒ no `breakdown`. App scope breaks down by `release`. 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 Redis time-series buckets — as a current/previous envelope, identical in shape to `patch-installs`: `current` covers the effective window, `previous` the equal-length window immediately preceding it. With `granularity` set, each window carries a `time_series`; with `group_by=release` set, `current` carries a per-release `breakdown`. `previous` never carries a breakdown. The window defaults to the last 28 days and is capped at 450 days; the effective range is echoed in each window's `range`. 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 now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution for the time series. Omitted ⇒ no `time_series` (one summed value per window); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`), which guarantees `current` and `previous` have equal bucket counts. - name: group_by in: query required: false schema: { type: string, enum: [release] } description: | Dimension to break the total down by. Omitted ⇒ no `breakdown`. App scope breaks down by `release`. 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 — summed over Redis time-series buckets — as a current/previous envelope, identical in shape to the app-scoped variant. With `granularity` set, each window carries a `time_series`; with `group_by=patch` set, `current` carries a `breakdown` with one entry per patch of the release (each with its own `time_series` when a `granularity` is also requested). `previous` never carries a breakdown. The window defaults to the last 28 days and is capped at 450 days; the effective range is echoed in each window's `range`. 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 now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution for the time series. Omitted ⇒ no `time_series` (one summed value per window); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`), which guarantees `current` and `previous` have equal bucket counts. - name: group_by in: query required: false schema: { type: string, enum: [patch] } description: | Dimension to break the total down by. Omitted ⇒ no `breakdown`. Release scope breaks down by `patch`. 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 — summed over Redis time-series buckets — as a current/previous envelope, identical in shape to the other patch-metric variants. With `granularity` set, each window carries a `time_series`; with `group_by=patch` set, `current` carries a per-patch `breakdown`. `previous` never carries a breakdown. The window defaults to the last 28 days and is capped at 450 days; the effective range is echoed in each window's `range`. 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 now" must omit this parameter rather than send a client-computed timestamp — clock skew would silently turn the request into a historical query. - name: granularity in: query required: false schema: { type: string, enum: [hour, day, week] } description: | Bucket resolution for the time series. Omitted ⇒ no `time_series` (one summed value per window); set ⇒ one point per bucket at this resolution. When set, the window length is rounded down to a whole multiple of the bucket size (echoed in `range`), which guarantees `current` and `previous` have equal bucket counts. - name: group_by in: query required: false schema: { type: string, enum: [patch] } description: | Dimension to break the total down by. Omitted ⇒ no `breakdown`. Release scope breaks down by `patch`. 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 # ------------------------------------------------------------------- /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 # ------------------------------------------------------------------- /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 console (Account > API Keys). Long-lived, no refresh needed. oauthJwt: type: http scheme: bearer bearerFormat: JWT description: | A JWT access token obtained via Shorebird's interactive OAuth login flow (`shorebird login`). Short-lived (15 min), auto-refreshed by the CLI using a stored refresh token. responses: Error: description: | Standard error response. Emitted for any non-2xx status where a body can be decoded into an [ErrorResponse]. 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: # ---------------- Core domain models ---------------- 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 release artifact for). Independent of `latest_releases`: an app can list a platform here even if no release on that platform has been analyzed yet. items: $ref: "#/components/schemas/ReleasePlatform" latest_releases: type: object description: | The latest analyzed release per platform. A platform is omitted when no release for that platform has been analyzed yet. When the latest release for a platform is not yet analyzed but a previous one is, the previous release is returned here and `pending_releases.{platform}` identifies the unanalyzed newer release. additionalProperties: $ref: "#/components/schemas/LatestRelease" propertyNames: $ref: "#/components/schemas/ReleasePlatform" pending_releases: type: object description: | The newest unanalyzed release per platform, whenever one exists. A platform is omitted when its most recent release has already been analyzed. Independent of `latest_releases`: both can be present (a newer release than the analyzed one is being processed) or only `pending_releases` can be present (no release on the platform has been analyzed yet). 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 the most recent analyzed iOS release (or Android, when iOS has no analyzed release with an icon). Requires the same auth as the rest of the apps API. The URL embeds the picked release's id as a `v` query parameter so it can be cached indefinitely; when a different release becomes the icon source the URL changes and clients re-fetch. Omitted when no analyzed release has an icon. Channel: type: object description: | A tag used to manage the subset of installs that receive a patch. By default a "stable" channel is created and used by devices to query for available patches. 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. A release can have zero or more patches applied to it. 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 because it was added later; older releases do not have it. 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 platform. 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 (e.g. AndroidManifest `application:label`). May differ from the user-curated `App.display_name`. 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 (Android API level for android, iOS deployment target for ios). target_sdk_version: type: string description: | The SDK level the artifact targets (Android targetSdk for android, iOS SDK for ios). architectures: type: array description: | CPU architectures present in the artifact (e.g. `["arm64-v8a", "armeabi-v7a"]`). items: type: string LatestRelease: type: object description: | Per-platform projection of an analyzed release as surfaced by `AppMetadata.latest_releases`. Each entry corresponds to the most recent analyzed release on the keying platform, so `analysis` is always populated and `status` is the single per-platform status (rather than the cross-platform `platform_statuses` map carried by `Release`). 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 because it was added later; older releases do not have it. 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 platform. Always present (entries without analysis are omitted from `latest_releases`). PendingRelease: type: object description: | A newer release that has been created but not yet analyzed. Surfaced alongside the most recent analyzed release so clients can show an "analyzing…" indicator without losing the stable icon and metadata. 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 release for a specific platform and architecture. 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. All patches have a patch number (auto-incrementing integer) and multiple patches can be published for a given release. 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 platform and architecture. 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 organizations (multi-user). 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 can be personal (single-user) or team (multi-user). 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. 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 information. Should only be used when querying the user's own information. 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 pay for as part of a pay-as-you-go plan. 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. # ---------------- Request / response wrappers ---------------- 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}. 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 update the release and the environment it was run in. 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." CreateUserRequest: type: object description: | The request body for POST /users. The user's email is taken from the auth token; only the display name is provided here. required: [name] properties: name: type: string description: The new user's display name. 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 create the patch and the environment it was run in. CreatePatchResponse: type: object description: | The response body for POST /apps/{appId}/patches. Deliberately narrower than [Patch]: a freshly-created patch has no `notes` yet (those are set via PATCH /.../{patchId}), so this endpoint exposes only the identifiers the client needs to upload artifacts. 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 /apps/{appId}/patches/{patchId}/artifacts and use the returned signed upload URL to upload the artifact bytes separately. 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 (legacy), or a PUT (chunked, resumable) to a resumable session URI. Absent on responses from older servers, which implied multipart. CreateReleaseArtifactRequest: type: object description: | Metadata for a new release artifact. POST to /apps/{appId}/releases/{releaseId}/artifacts and use the returned signed upload URL to upload the artifact bytes separately. 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 (legacy), or a PUT (chunked, resumable) to a resumable session URI. Absent on responses from older servers, which implied multipart. UpdatePatchRequest: type: object description: | The request body for PATCH /apps/{appId}/releases/{releaseId}/patches/{patchId}. properties: notes: type: string description: | Freeform notes about the patch. If null, notes are unchanged. 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. If provided, the server only returns patches with a higher number. If omitted, the server returns the latest available. 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 unique per app. Optional for backward compatibility. current_patch_number: type: integer format: int64 description: | The patch number currently running on the device, if any. Supersedes `patch_number` for newer clients; unlike `patch_number`, this does not affect the server's response. 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 specific platform and architecture. 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`." # ---------------- Metrics (MAU v2) ---------------- VersionDistributionEntry: type: object description: | One bucket in a version-distribution chart: the exact count of currently-active devices on a given release version. A null `release_version` represents devices whose client did not emit one (typically very old Flutter clients). 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 client did not emit one. device_count: type: integer format: int64 description: | Number of currently-active devices on this release version within the active-device window. percentage: type: number format: double description: | Fractional share of currently-active devices on this release version, in [0.0, 1.0]. Server is the source of truth; clients should render this value rather than recomputing it. GetVersionDistributionResponse: type: object description: | The response body for GET /apps/{appId}/metrics/version-distribution. 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` descending, then `release_version` ascending with NULLs last. total_devices: type: integer format: int64 description: | Sum of `device_count` across all entries. Convenience for clients; matches the server-side sum used to compute `percentage`. active_window_days: type: integer description: | The active-device window in days that bounds the query. Hardcoded server-side in v1; tier-gated per-caller windows land in a follow-up. as_of: type: string format: date-time description: | Server's UTC timestamp at the moment the response was constructed. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. GetNewDevicesResponse: type: object description: | The response body for GET /apps/{appId}/metrics/new-devices. 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)`. An exact (non-HLL) count. previous: type: integer format: int64 nullable: true description: | Devices first seen in the equal-length window immediately preceding the current one, or null when that window would begin before the metrics data floor (comparing against partially-recorded history would show a misleading delta). window_days: type: integer description: | The window length in days. Hardcoded server-side in v1; echoed so clients label the metric from the response rather than assuming a length. as_of: type: string format: date-time description: | Server's UTC timestamp at the moment the response was constructed. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. PatchAdoptionPoint: type: object description: | One point in a patch's adoption series: the cumulative distinct devices on `patch >= patch_number` (`devices`) over the patch's target (`target`), and their ratio (`adoption_pct`), for one bucket. 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 full-window value (no granularity requested). devices: type: integer format: int64 description: | Distinct devices running patch `>= patch_number` on the patch's target platform(s) within this bucket (an HLL count). target: type: integer format: int64 description: | Distinct devices on the release whose platform the patch targets — the patch's reachable denominator — within this bucket. adoption_pct: type: number format: double description: | `devices / target`, in [0.0, 1.0] (0 when `target` is 0). Server is the source of truth; clients should render this value rather than recomputing it. PatchAdoptionEntry: type: object description: | Cumulative adoption for one patch of the release. Values are cumulative — "patch `>= patch_number`" — so they are monotonic non-increasing in `patch_number`. 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. target_platforms: type: array items: $ref: "#/components/schemas/ReleasePlatform" description: | The platform(s) the patch was built for (from its artifacts). The denominator counts only devices on these platforms. 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 granularity was requested; otherwise one point per bucket, ordered by `period` ascending. MetricsRange: type: object description: | The effective (post-default, post-clamp) window a metrics response — or one window of a metrics envelope — covers. Always echoed by the server; clients must treat it as authoritative rather than reusing the requested range. 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 exactly one release. 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 creation date" default (no `release_version` was supplied). granularity: type: string nullable: true description: | The bucket resolution (`hour`, `day`, or `week`), or null when each patch carries a single full-window value. range: $ref: "#/components/schemas/MetricsRange" as_of: type: string format: date-time description: | Server's UTC timestamp at the moment the response was constructed. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. 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, averaged across all days in the lookback window (days with no activity in that hour count as zero). 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 over the lookback window with implicit zeros included. GetActiveHoursResponse: type: object description: | The response body for GET /apps/{appId}/metrics/active-hours. Powers the "best time to release" recommendation: the consecutive low-activity UTC window when the app's users are least active. 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, ordered by hour_utc ascending. recommended_window_start_utc: type: integer nullable: true description: | Start hour (UTC, 0–23) of the lowest-activity consecutive window — the recommended time to release. The window wraps past midnight. Null when there is insufficient data (fewer than 7 days). recommended_window_length_hours: type: integer description: | Length of the recommended window in hours. Fixed at 2 in v1. busiest_hour_utc: type: integer nullable: true description: | UTC hour (0–23) with the highest average active devices, for contrast in the recommendation copy. Null when insufficient data. lookback_days: type: integer description: | Number of days of history the profile is computed over. as_of: type: string format: date-time description: | Server's UTC timestamp at the moment the response was constructed. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. ActivityHeatmapCell: type: object description: | Average number of distinct active devices during one (UTC day-of-week, UTC hour-of-day) cell, averaged across every occurrence of that weekday in the lookback window (occurrences with no activity count as zero). 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. 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, averaged over the weekday's occurrences in the window with implicit zeros included. GetActivityHeatmapResponse: type: object description: | The response body for GET /apps/{appId}/metrics/activity-heatmap. A 7×24 grid of average active devices per UTC weekday-hour, powering the insights activity heatmap. 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 day_of_week_utc (1–7) then hour_utc (0–23) ascending. busiest_day_of_week_utc: type: integer nullable: true description: | UTC day-of-week (1–7) of the cell with the highest average active devices, for peak-relative coloring and labeling. Null when there is no data. busiest_hour_utc: type: integer nullable: true description: | UTC hour (0–23) of the busiest cell. Null when there is no data. lookback_days: type: integer description: | Number of days of history the heatmap is computed over. as_of: type: string format: date-time description: | Server's UTC timestamp at the moment the response was constructed. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. UniqueUsersTimeSeriesEntry: type: object description: | One bucket of a unique-users time series: the HLL count of distinct active devices in the bucket starting at `period`. 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 platform), optionally with its own time series. 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 `group_by=release_version`, the empty string is the unknown-version group — devices whose client is too old to report a release version. unique_users: type: integer format: int64 description: | Distinct active devices for this group over the window (an HLL count). time_series: type: array nullable: true items: $ref: "#/components/schemas/UniqueUsersTimeSeriesEntry" description: | Per-bucket series for this group, present only when a `granularity` was requested; otherwise null. UniqueUsersWindow: type: object description: | One window of the unique-users envelope: the HLL-merged total over the window's effective range, with a per-bucket series when a `granularity` was requested. This base atom is the full shape of `previous`; `current` extends it (see UniqueUsersCurrentWindow). required: [unique_users, range] properties: unique_users: type: integer format: int64 description: | Distinct active devices over this window (an HLL count). Note: per-bucket `time_series` values do not sum to this — an HLL merge over the window is not a sum of per-bucket merges. 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 `granularity` was requested; otherwise null. On `previous`, also null when the prior window reaches past the plan's metrics-history horizon (the total is still present — only the granular overlay is withheld). Empty buckets are omitted — gap-fill against this window's `range`. UniqueUsersCurrentWindow: description: | The `current` window of the unique-users envelope: the base window atom plus the optional `breakdown`. Only `current` carries a breakdown — no chart renders a previous-window breakdown, so the asymmetry is declared in the contract rather than left as an optional-but-never-populated field. 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 `group_by` was requested; otherwise null. GetUniqueUsersResponse: type: object description: | The response body for GET /apps/{appId}/metrics/unique-users: a current/previous envelope. `previous` covers the equal-length window immediately preceding `current`; period-over-period deltas are client display logic over the two totals. `previous` is always present but is null when the prior window predates the data floor (no comparison data exists). When it reaches past the plan's metrics-history horizon, `previous` is non-null with its total — the delta renders — but without a `time_series` (no prior-window overlay): granular history is the resolution the horizon gates, the scalar comparison is not. 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. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled query and may lag by up to ~1 hour. granularity: type: string nullable: true description: | The time-series bucket resolution (`hour`, `day`, or `week`), or null when no time series was requested. Applies to both windows. current: $ref: "#/components/schemas/UniqueUsersCurrentWindow" previous: anyOf: - $ref: "#/components/schemas/UniqueUsersWindow" - type: "null" description: | The equal-length window immediately preceding `current`. Always present, but null when that prior window predates the data floor — there is no comparison data, so reporting a fabricated undercount is worse than returning null. When the prior window instead reaches past the plan's metrics-history horizon, this is non-null with its `unique_users` total (the delta) but no `time_series` (the overlay is the granular resolution the horizon gates). PatchMetricTimeSeriesEntry: type: object description: | One bucket of a patch-metric time series: the summed count (installs or downloads, per the endpoint) in the bucket starting at `period`. 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 at app scope, or one patch at release scope), optionally with its own time series. 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 number as a string. 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` was requested; otherwise null. PatchMetricWindow: type: object description: | One window of the patch-metric envelope: the summed count over the window's effective range, with a per-bucket series when a `granularity` was requested. This base atom is the full shape of `previous`; `current` extends it (see PatchMetricCurrentWindow). required: [count, range] properties: count: type: integer format: int64 description: | The summed count (installs or downloads) over this window. Unlike the HLL-based metrics, per-bucket `time_series` values sum to this total. 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 `granularity` was requested; otherwise null. On `previous`, also null when the prior window reaches past the plan's metrics-history horizon (the total is still present — only the granular overlay is withheld). Empty buckets are omitted — gap-fill against this window's `range`. PatchMetricCurrentWindow: description: | The `current` window of the patch-metric envelope: the base window atom plus the optional `breakdown`. Only `current` carries a breakdown — no chart renders a previous-window breakdown, so the asymmetry is declared in the contract rather than left as an optional-but-never-populated field. 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 `group_by` was requested; otherwise null. GetPatchMetricResponse: type: object description: | The response body for the patch-installs and patch-downloads metric endpoints (app- and release-scoped): a current/previous envelope. `previous` covers the equal-length window immediately preceding `current`; period-over-period deltas are client display logic over the two totals. `previous` is omitted only when the prior window predates the data floor (no comparison data exists). When it reaches past the plan's metrics-history horizon, `previous` is still present with its total — the delta renders — but without a `time_series` (no prior-window overlay): granular history is the resolution the horizon gates, the scalar comparison is not. 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. Not a freshness indicator for the underlying data, which is refreshed by an hourly scheduled job and may lag by up to ~1 hour. granularity: type: string nullable: true description: | The time-series bucket resolution (`hour`, `day`, or `week`), or null when no time series was requested. Applies to both windows. current: $ref: "#/components/schemas/PatchMetricCurrentWindow" previous: $ref: "#/components/schemas/PatchMetricWindow" description: | The equal-length window immediately preceding `current`. Omitted only when that prior window predates the data floor — there is no comparison data, so reporting a fabricated count is worse than omitting it. When the prior window instead reaches past the plan's metrics-history horizon, this is present with its `count` total (the delta) but no `time_series` (the overlay is the granular resolution the horizon gates).