From 8414699fecfe13fc6f72bfd51c707e521db29cbf Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 08:07:23 -0500 Subject: [PATCH 1/4] docs: add POST /api/catalogs create-catalog contract Documents the new endpoint that creates an account-owned catalog and optionally materializes it from a completed valuation snapshot (from.snapshot_id). Account is derived from credentials, never the body; re-claiming the same snapshot is idempotent. Part of recoupable/chat#1801 (Phase 2 contract; merges before the api PR). Co-Authored-By: Claude Opus 4.8 (1M context) --- api-reference/openapi/releases.json | 124 ++++++++++++++++++++++++ api-reference/songs/catalogs-create.mdx | 8 ++ docs.json | 1 + 3 files changed, 133 insertions(+) create mode 100644 api-reference/songs/catalogs-create.mdx diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index de1c2578..8bb4f575 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1200,6 +1200,78 @@ } } }, + "/api/catalogs": { + "post": { + "description": "Create a catalog owned by the authenticated account. The owning account is always derived from the request credentials (Privy bearer JWT or x-api-key) and never from the request body. Optionally materialize the catalog from a completed valuation run by passing `from.snapshot_id`: the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", + "security": [ + { "apiKeyAuth": [] }, + { "bearerAuth": [] } + ], + "requestBody": { + "description": "Catalog to create. Provide name, from, or both - at least one is required.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCatalogRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Catalog created, or the existing catalog returned when re-materializing the same snapshot (idempotent)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCatalogResponse" + } + } + } + }, + "400": { + "description": "Bad request - neither name nor from provided, or invalid snapshot_id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogSongsErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized - missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogSongsErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden - the snapshot belongs to a different account", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogSongsErrorResponse" + } + } + } + }, + "404": { + "description": "Not found - no snapshot exists for the supplied snapshot_id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogSongsErrorResponse" + } + } + } + } + } + } + }, "/api/catalogs/songs": { "get": { "description": "Retrieve songs within a specific catalog with pagination support. This endpoint joins catalog_songs with songs, song_artists, and accounts to provide comprehensive song information for a given catalog.", @@ -2003,6 +2075,58 @@ } } }, + "CatalogMaterializationSource": { + "type": "object", + "description": "Source to materialize catalog songs from. Currently a completed valuation run (playcount snapshot). The snapshot must be owned by the authenticated account.", + "required": [ + "snapshot_id" + ], + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid", + "description": "ID of a completed playcount snapshot. Its measured ISRCs are added to the new catalog as catalog songs." + } + } + }, + "CreateCatalogRequest": { + "type": "object", + "description": "Request body for creating a catalog. At least one of name or from must be supplied. The owning account is taken from the request credentials, never from this body.", + "properties": { + "name": { + "type": "string", + "description": "Optional. Display name for the catalog. If omitted when from is supplied, a name is derived from the source run." + }, + "from": { + "$ref": "#/components/schemas/CatalogMaterializationSource" + } + } + }, + "CreateCatalogResponse": { + "type": "object", + "description": "Response returned after creating, or idempotently re-fetching, a catalog", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "description": "Status of the request" + }, + "catalog": { + "$ref": "#/components/schemas/Catalog" + }, + "songs_added": { + "type": "integer", + "description": "Number of catalog songs materialized from the source. 0 when no from was supplied, or when the run was already materialized (idempotent re-claim)." + }, + "error": { + "type": "string", + "description": "Error message (only present if status is 'error')" + } + } + }, "Catalog": { "type": "object", "description": "A catalog with its metadata", diff --git a/api-reference/songs/catalogs-create.mdx b/api-reference/songs/catalogs-create.mdx new file mode 100644 index 00000000..7284d185 --- /dev/null +++ b/api-reference/songs/catalogs-create.mdx @@ -0,0 +1,8 @@ +--- +title: Create Catalog +openapi: post /api/catalogs +--- + + + The owning account is derived from your credentials (Privy bearer JWT or `x-api-key`), never from the request body. Pass `from.snapshot_id` to materialize the catalog from a completed valuation run - the snapshot's measured ISRCs are added as catalog songs. Re-submitting the same snapshot is idempotent and returns the catalog already created for that run. + diff --git a/docs.json b/docs.json index eb862140..550169de 100644 --- a/docs.json +++ b/docs.json @@ -156,6 +156,7 @@ "api-reference/songs/analyze", "api-reference/songs/analyze-presets", "api-reference/songs/catalogs", + "api-reference/songs/catalogs-create", "api-reference/songs/catalog-songs", "api-reference/songs/catalog-songs-add", "api-reference/songs/catalog-songs-delete" From 70f75d165de265fe7a9d763feb02f90a632dc3e1 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 12:44:20 -0500 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20trim?= =?UTF-8?q?=20description,=20frontmatter-only=20mdx,=20split=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KISS the POST /api/catalogs description (drop the redundant account-from- credentials sentence; the security scheme already conveys it). - catalogs-create.mdx → frontmatter-only, matching the sibling reference pages (no custom Note callout). - Rename the 'Releases' nav tab to 'Catalog' and split the monolithic 'Songs & Catalogs' group into 'Songs' (first, so Get Songs stays the first endpoint) and 'Catalogs'. Addresses review on PR #243. --- api-reference/openapi/releases.json | 2 +- api-reference/songs/catalogs-create.mdx | 4 ---- docs.json | 11 ++++++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index 8bb4f575..a40d8b40 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1202,7 +1202,7 @@ }, "/api/catalogs": { "post": { - "description": "Create a catalog owned by the authenticated account. The owning account is always derived from the request credentials (Privy bearer JWT or x-api-key) and never from the request body. Optionally materialize the catalog from a completed valuation run by passing `from.snapshot_id`: the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", + "description": "Create a catalog owned by the authenticated account. Optionally materialize the catalog from a completed valuation run by passing `from.snapshot_id`: the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } diff --git a/api-reference/songs/catalogs-create.mdx b/api-reference/songs/catalogs-create.mdx index 7284d185..037bb7ff 100644 --- a/api-reference/songs/catalogs-create.mdx +++ b/api-reference/songs/catalogs-create.mdx @@ -2,7 +2,3 @@ title: Create Catalog openapi: post /api/catalogs --- - - - The owning account is derived from your credentials (Privy bearer JWT or `x-api-key`), never from the request body. Pass `from.snapshot_id` to materialize the catalog from a completed valuation run - the snapshot's measured ISRCs are added as catalog songs. Re-submitting the same snapshot is idempotent and returns the catalog already created for that run. - diff --git a/docs.json b/docs.json index 550169de..26584304 100644 --- a/docs.json +++ b/docs.json @@ -146,15 +146,20 @@ ] }, { - "tab": "Releases", + "tab": "Catalog", "groups": [ { - "group": "Songs & Catalogs", + "group": "Songs", "pages": [ "api-reference/songs/songs", "api-reference/songs/create", "api-reference/songs/analyze", - "api-reference/songs/analyze-presets", + "api-reference/songs/analyze-presets" + ] + }, + { + "group": "Catalogs", + "pages": [ "api-reference/songs/catalogs", "api-reference/songs/catalogs-create", "api-reference/songs/catalog-songs", From c749f70b4f4f95ea4416dbdb7f2fc668bf17793a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 12:51:25 -0500 Subject: [PATCH 3/4] docs: flatten from.snapshot_id -> root snapshot + 2-way snapshot/catalog links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flatten the create-catalog request: drop the CatalogMaterializationSource wrapper; snapshot is now a root field (YAGNI — one source today, matches the flat field style of the catalog-songs endpoint). - DRY cross-reference: the snapshot field links to Create measurement job, and that endpoint's description links back to Create catalog. Addresses review on PR #243. --- api-reference/openapi/releases.json | 34 ++++++++++------------------- api-reference/openapi/research.json | 2 +- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/api-reference/openapi/releases.json b/api-reference/openapi/releases.json index a40d8b40..bde961b9 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1202,13 +1202,13 @@ }, "/api/catalogs": { "post": { - "description": "Create a catalog owned by the authenticated account. Optionally materialize the catalog from a completed valuation run by passing `from.snapshot_id`: the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", + "description": "Create a catalog owned by the authenticated account. Optionally materialize the catalog from a completed valuation run by passing `snapshot` (the run's snapshot id): the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] } ], "requestBody": { - "description": "Catalog to create. Provide name, from, or both - at least one is required.", + "description": "Catalog to create. Provide name, snapshot, or both - at least one is required.", "required": true, "content": { "application/json": { @@ -1230,7 +1230,7 @@ } }, "400": { - "description": "Bad request - neither name nor from provided, or invalid snapshot_id", + "description": "Bad request - neither name nor snapshot provided, or invalid snapshot", "content": { "application/json": { "schema": { @@ -1260,7 +1260,7 @@ } }, "404": { - "description": "Not found - no snapshot exists for the supplied snapshot_id", + "description": "Not found - no snapshot exists for the supplied snapshot", "content": { "application/json": { "schema": { @@ -2075,30 +2075,18 @@ } } }, - "CatalogMaterializationSource": { - "type": "object", - "description": "Source to materialize catalog songs from. Currently a completed valuation run (playcount snapshot). The snapshot must be owned by the authenticated account.", - "required": [ - "snapshot_id" - ], - "properties": { - "snapshot_id": { - "type": "string", - "format": "uuid", - "description": "ID of a completed playcount snapshot. Its measured ISRCs are added to the new catalog as catalog songs." - } - } - }, "CreateCatalogRequest": { "type": "object", - "description": "Request body for creating a catalog. At least one of name or from must be supplied. The owning account is taken from the request credentials, never from this body.", + "description": "Request body for creating a catalog. At least one of name or snapshot must be supplied. The owning account is taken from the request credentials, never from this body.", "properties": { "name": { "type": "string", - "description": "Optional. Display name for the catalog. If omitted when from is supplied, a name is derived from the source run." + "description": "Optional. Display name for the catalog. If omitted when snapshot is supplied, a name is derived from the source run." }, - "from": { - "$ref": "#/components/schemas/CatalogMaterializationSource" + "snapshot": { + "type": "string", + "format": "uuid", + "description": "Optional. ID of a completed playcount snapshot (valuation run) owned by the authenticated account. Its measured ISRCs are added to the new catalog as catalog songs. Create one with [Create measurement job](/api-reference/research/measurement-jobs)." } } }, @@ -2119,7 +2107,7 @@ }, "songs_added": { "type": "integer", - "description": "Number of catalog songs materialized from the source. 0 when no from was supplied, or when the run was already materialized (idempotent re-claim)." + "description": "Number of catalog songs materialized from the source. 0 when no snapshot was supplied, or when the run was already materialized (idempotent re-claim)." }, "error": { "type": "string", diff --git a/api-reference/openapi/research.json b/api-reference/openapi/research.json index 807f09b8..f149a2d0 100644 --- a/api-reference/openapi/research.json +++ b/api-reference/openapi/research.json @@ -2915,7 +2915,7 @@ "/api/research/measurement-jobs": { "post": { "summary": "Create a measurement job", - "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline. `source:\"historical\"` enqueues each resolved recording for Songstats deep backfill ranked by all-time streams (idempotent — songs already carrying `songstats` history are skipped; no track is fetched twice). Provide exactly one of `catalog_id` / `album_ids` / `isrcs` in `scope`.", + "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline. `source:\"historical\"` enqueues each resolved recording for Songstats deep backfill ranked by all-time streams (idempotent — songs already carrying `songstats` history are skipped; no track is fetched twice). Provide exactly one of `catalog_id` / `album_ids` / `isrcs` in `scope`. The returned `id` is a snapshot id you can pass to [Create catalog](/api-reference/songs/catalogs-create) to materialize the measured tracks into an account-owned catalog.", "requestBody": { "required": true, "content": { From 9f211e2f27c1be7def684c52b034b0f38a0ea3fc Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 18 Jun 2026 13:01:12 -0500 Subject: [PATCH 4/4] docs: KISS the POST /api/catalogs description to 'Create a catalog.' Materialization/idempotency detail lives in the snapshot field + 200 response descriptions, so the operation summary stays minimal. Addresses review on PR #243. --- 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 bde961b9..d6b01c37 100644 --- a/api-reference/openapi/releases.json +++ b/api-reference/openapi/releases.json @@ -1202,7 +1202,7 @@ }, "/api/catalogs": { "post": { - "description": "Create a catalog owned by the authenticated account. Optionally materialize the catalog from a completed valuation run by passing `snapshot` (the run's snapshot id): the snapshot's measured ISRCs are added as catalog songs and the catalog is linked to the account. Materialization is idempotent - re-submitting the same snapshot returns the catalog already created for that run instead of creating a duplicate.", + "description": "Create a catalog.", "security": [ { "apiKeyAuth": [] }, { "bearerAuth": [] }