From c3392f0fc1a48a2c214248fb1d6cd007cb176b24 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Jul 2026 18:55:07 -0500 Subject: [PATCH 1/4] =?UTF-8?q?docs(catalogs):=20v2=20measurements=20contr?= =?UTF-8?q?act=20=E2=80=94=20optional=20artist=5Faccount=5Fid=20filter,=20?= =?UTF-8?q?echo=20field,=20explicit=20no-cap=20semantics=20(chat#1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amends the GET /api/catalogs/measurements contract from docs#265: - optional query param artist_account_id (uuid): scope measurements + valuation to the catalog's songs linked to that artist via song_artists; absent = whole catalog. Malformed values are a 400. - response field artist_account_id (uuid, nullable): echoes the applied filter so clients can verify the response scope before trusting it. - measurements/total_streams explicitly cover ALL matching songs — no row cap (the current impl silently truncates at 1,000; fixed api-side). Co-Authored-By: Claude Fable 5 --- api-reference/openapi/releases.json | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index 46439b3..86d3523 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1482,7 +1482,7 @@ }, "/api/catalogs/measurements": { "get": { - "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song.", + "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song. Optionally scope the read to one artist with artist_account_id: measurements and the valuation band then cover only the catalog's songs linked to that artist account. The read is always exhaustive - every matching measured song is returned and counted, with no row cap, regardless of catalog size.", "security": [ { "apiKeyAuth": [] @@ -1501,6 +1501,16 @@ "type": "string", "format": "uuid" } + }, + { + "name": "artist_account_id", + "in": "query", + "description": "Optional artist account to scope the read to. When present, measurements and the valuation band cover only the catalog's songs linked to this artist account (via song_artists); when absent, the whole catalog. A song linked to multiple artists counts for each of them. An artist with no linked measured songs in the catalog yields an empty measurements array. Malformed (non-uuid) values are rejected with 400.", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -1515,7 +1525,7 @@ } }, "400": { - "description": "Bad request - missing or invalid catalogId", + "description": "Bad request - missing or invalid catalogId, or malformed artist_account_id", "content": { "application/json": { "schema": { @@ -2456,14 +2466,20 @@ "items": { "$ref": "#/components/schemas/CatalogTrackMeasurement" }, - "description": "One entry per catalog song that has at least one measurement: the newest capture per ISRC. Songs never measured are omitted." + "description": "One entry per catalog song that has at least one measurement: the newest capture per ISRC. Songs never measured are omitted. Covers ALL matching songs with no row cap - a catalog (or artist scope) with 5,000 measured songs returns 5,000 entries." }, "valuation": { "$ref": "#/components/schemas/ValuationBand" }, "total_streams": { "type": "integer", - "description": "Sum of the latest play counts across all measured songs" + "description": "Sum of the latest play counts across all measured songs in scope (no row cap)" + }, + "artist_account_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Echoes the applied artist_account_id filter: the uuid when the response was scoped to that artist, null when the response covers the whole catalog. Clients that request an artist scope should verify this echo before treating the numbers as artist-scoped." }, "catalog_age_years": { "type": "integer", From cfad8fb48029b37cad4e38f39208e0046b11dc5c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 7 Jul 2026 07:22:57 -0500 Subject: [PATCH 2/4] =?UTF-8?q?docs(catalogs):=20respec=20measurements=20v?= =?UTF-8?q?2=20=E2=80=94=20whole-scope=20SQL=20aggregates=20+=20caller-pag?= =?UTF-8?q?inated=20rows=20(chat#1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amendment per the 2026-07-07 decision on chat#1850: server-side loop-to-exhaustion is rejected. The contract becomes: - aggregates (measured_song_count, total_streams, valuation) computed in a single SQL aggregate over the latest-per-ISRC subquery — whole scope, no row cap, regardless of pagination - the measurements array is caller-paginated: page/limit params (default limit 50, max 100), same pagination envelope as GET /api/catalogs/songs, rows sorted by play count descending - artist_account_id filter + response echo unchanged; 400 also covers invalid page/limit Co-Authored-By: Claude Fable 5 --- api-reference/openapi/releases.json | 62 ++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index 86d3523..dc9b237 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1482,7 +1482,7 @@ }, "/api/catalogs/measurements": { "get": { - "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song. Optionally scope the read to one artist with artist_account_id: measurements and the valuation band then cover only the catalog's songs linked to that artist account. The read is always exhaustive - every matching measured song is returned and counted, with no row cap, regardless of catalog size.", + "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song. Optionally scope the read to one artist with artist_account_id: measurements and the valuation band then cover only the catalog's songs linked to that artist account. The aggregates (measured_song_count, total_streams, valuation) are computed in a single SQL aggregate over the ENTIRE scope - every matching measured song, no row cap, regardless of catalog size. The measurements array itself is paginated with page/limit and returns one page of the latest-per-ISRC rows sorted by play count descending.", "security": [ { "apiKeyAuth": [] @@ -1505,12 +1505,35 @@ { "name": "artist_account_id", "in": "query", - "description": "Optional artist account to scope the read to. When present, measurements and the valuation band cover only the catalog's songs linked to this artist account (via song_artists); when absent, the whole catalog. A song linked to multiple artists counts for each of them. An artist with no linked measured songs in the catalog yields an empty measurements array. Malformed (non-uuid) values are rejected with 400.", + "description": "Optional artist account to scope the read to. When present, measurements and the valuation band cover only the catalog's songs linked to this artist account (via song_artists); when absent, the whole catalog. A song linked to multiple artists counts for each of them. An artist with no linked measured songs in the catalog yields measured_song_count 0 and an empty measurements array. Malformed (non-uuid) values are rejected with 400.", "required": false, "schema": { "type": "string", "format": "uuid" } + }, + { + "name": "page", + "in": "query", + "description": "Page number of the measurements array (default 1). Pagination only windows the measurements rows - the aggregates always cover the entire scope. Invalid values are rejected with 400.", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of measurement rows per page (default 50, max 100). Invalid values are rejected with 400.", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } } ], "responses": { @@ -1525,7 +1548,7 @@ } }, "400": { - "description": "Bad request - missing or invalid catalogId, or malformed artist_account_id", + "description": "Bad request - missing or invalid catalogId, malformed artist_account_id, or invalid page/limit", "content": { "application/json": { "schema": { @@ -2466,14 +2489,21 @@ "items": { "$ref": "#/components/schemas/CatalogTrackMeasurement" }, - "description": "One entry per catalog song that has at least one measurement: the newest capture per ISRC. Songs never measured are omitted. Covers ALL matching songs with no row cap - a catalog (or artist scope) with 5,000 measured songs returns 5,000 entries." + "description": "One page of latest-per-ISRC measurements (the requested page/limit window), sorted by play count descending. One entry per measured song: the newest capture per ISRC; songs never measured are omitted. Use measured_song_count / pagination for the size of the full set - the aggregates are NOT limited to this page." + }, + "pagination": { + "$ref": "#/components/schemas/CatalogMeasurementsPagination" + }, + "measured_song_count": { + "type": "integer", + "description": "Total number of measured songs in scope (the whole catalog, or the artist subset when filtered), computed over the entire scope regardless of pagination - no row cap" }, "valuation": { "$ref": "#/components/schemas/ValuationBand" }, "total_streams": { "type": "integer", - "description": "Sum of the latest play counts across all measured songs in scope (no row cap)" + "description": "Sum of the latest play counts across ALL measured songs in scope, computed in a single SQL aggregate regardless of pagination - no row cap" }, "artist_account_id": { "type": "string", @@ -2491,6 +2521,28 @@ } } }, + "CatalogMeasurementsPagination": { + "type": "object", + "description": "Pagination metadata for the measurements array - the same envelope as catalog songs", + "properties": { + "total_count": { + "type": "integer", + "description": "Total number of measured songs in scope (equals measured_song_count)" + }, + "page": { + "type": "integer", + "description": "Current page number" + }, + "limit": { + "type": "integer", + "description": "Number of measurement rows per page" + }, + "total_pages": { + "type": "integer", + "description": "Total number of pages available" + } + } + }, "CatalogTrackMeasurement": { "type": "object", "description": "The most recent Spotify play-count measurement for one song (ISRC) in the catalog", From 29231ea0cd0273f827077e4506a9ab780215a0e6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 7 Jul 2026 07:38:31 -0500 Subject: [PATCH 3/4] =?UTF-8?q?docs(catalogs):=20KISS=20the=20measurements?= =?UTF-8?q?=20endpoint=20description=20=E2=80=94=20param=20details=20live?= =?UTF-8?q?=20on=20the=20params/schemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback on docs#267: restore the short docs#265 endpoint description; the artist filter, pagination, echo and no-cap semantics are documented on the query params and response schema. Co-Authored-By: Claude Fable 5 --- api-reference/openapi/releases.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index dc9b237..57b363c 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1482,7 +1482,7 @@ }, "/api/catalogs/measurements": { "get": { - "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song. Optionally scope the read to one artist with artist_account_id: measurements and the valuation band then cover only the catalog's songs linked to that artist account. The aggregates (measured_song_count, total_streams, valuation) are computed in a single SQL aggregate over the ENTIRE scope - every matching measured song, no row cap, regardless of catalog size. The measurements array itself is paginated with page/limit and returns one page of the latest-per-ISRC rows sorted by play count descending.", + "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song.", "security": [ { "apiKeyAuth": [] From acb90680f45b0217cb2342f8ac694ba86f83fa95 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 7 Jul 2026 07:49:55 -0500 Subject: [PATCH 4/4] =?UTF-8?q?docs(catalogs):=20catalogId=20moves=20to=20?= =?UTF-8?q?the=20path=20=E2=80=94=20GET=20/api/catalogs/{catalogId}/measur?= =?UTF-8?q?ements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REST amendment on chat#1850: path for identity, query for modifiers — mirrors the research measurements family (/api/research/albums/{id}/measurements). catalogId is a required path param; the query carries only the optional snake_case modifiers (artist_account_id, page, limit). A missing id is unroutable (404); a malformed uuid in the path is a 400. Co-Authored-By: Claude Fable 5 --- api-reference/openapi/releases.json | 8 ++++---- api-reference/songs/catalog-measurements.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index 57b363c..65114e9 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1480,7 +1480,7 @@ } } }, - "/api/catalogs/measurements": { + "/api/catalogs/{catalogId}/measurements": { "get": { "description": "Get the latest play counts and a derived valuation band for a catalog. Measurements are captured by [Create measurement job](/api-reference/research/measurement-jobs) runs; the band is computed at read time from the latest capture per song.", "security": [ @@ -1494,8 +1494,8 @@ "parameters": [ { "name": "catalogId", - "in": "query", - "description": "The unique identifier of the catalog to read measurements for. The catalog must belong to the authenticated account.", + "in": "path", + "description": "The unique identifier of the catalog to read measurements for. The catalog must belong to the authenticated account. Malformed (non-uuid) values are rejected with 400.", "required": true, "schema": { "type": "string", @@ -1548,7 +1548,7 @@ } }, "400": { - "description": "Bad request - missing or invalid catalogId, malformed artist_account_id, or invalid page/limit", + "description": "Bad request - malformed catalogId or artist_account_id, or invalid page/limit", "content": { "application/json": { "schema": { diff --git a/api-reference/songs/catalog-measurements.mdx b/api-reference/songs/catalog-measurements.mdx index 6860842..38b33b2 100644 --- a/api-reference/songs/catalog-measurements.mdx +++ b/api-reference/songs/catalog-measurements.mdx @@ -1,4 +1,4 @@ --- title: Get Catalog Measurements -openapi: get /api/catalogs/measurements +openapi: get /api/catalogs/{catalogId}/measurements ---