From ac48fe329a4588493d2288379be3adb944e0a79e Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 16 Jun 2026 07:08:19 -0500 Subject: [PATCH 1/2] docs: add measurements + measurement-jobs resources; deprecate RPC endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract for chat#1796 — consolidate the /research play-count surface into two REST resources. - GET /research/tracks/{id}/measurements + /albums/{id}/measurements (reads; granularity/aggregate=run_rate/latest projections) - POST /research/measurement-jobs (+ GET /{id}) — ingest, source=current|historical - mark track/historic-stats, track/playcount-deltas, playcounts, snapshots deprecated with migration notes - 4 frontmatter-only MDX pages + Catalog nav entries mintlify openapi-check: valid. All $refs resolve. --- api-reference/openapi/research.json | 618 +++++++++++++++++- api-reference/research/album-measurements.mdx | 4 + api-reference/research/measurement-job.mdx | 4 + api-reference/research/measurement-jobs.mdx | 4 + api-reference/research/measurements.mdx | 4 + docs.json | 6 +- 6 files changed, 632 insertions(+), 8 deletions(-) create mode 100644 api-reference/research/album-measurements.mdx create mode 100644 api-reference/research/measurement-job.mdx create mode 100644 api-reference/research/measurement-jobs.mdx create mode 100644 api-reference/research/measurements.mdx diff --git a/api-reference/openapi/research.json b/api-reference/openapi/research.json index e321c17f..bf36137b 100644 --- a/api-reference/openapi/research.json +++ b/api-reference/openapi/research.json @@ -2691,7 +2691,7 @@ }, "/api/research/track/historic-stats": { "get": { - "description": "Get historic per-track, per-source stats by ISRC or track id over a date range — a daily time-series of cumulative counters (e.g. `streams_total`). Diff the window endpoints for streams gained over a period.", + "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?granularity=daily` instead.\n\nGet historic per-track, per-source stats by ISRC or track id over a date range — a daily time-series of cumulative counters (e.g. `streams_total`). Diff the window endpoints for streams gained over a period.", "parameters": [ { "name": "isrc", @@ -2833,12 +2833,13 @@ } } } - } + }, + "deprecated": true } }, "/api/research/playcounts": { "get": { - "description": "Latest platform-displayed play counts for all tracks on an album, served from the measurement store (populated by snapshots). Counts are scraped from public pages — not royalty-bearing stream counts.", + "description": "**Deprecated (2026-06).** Use `GET /api/research/albums/{id}/measurements?latest=true` instead.\n\nLatest platform-displayed play counts for all tracks on an album, served from the measurement store (populated by snapshots). Counts are scraped from public pages — not royalty-bearing stream counts.", "parameters": [ { "name": "spotify_album_id", @@ -2902,12 +2903,13 @@ } } } - } + }, + "deprecated": true } }, "/api/research/snapshots": { "post": { - "description": "Capture platform-displayed play counts for a whole catalog, album list, or ISRC list in one server-side snapshot. Executes asynchronously; one album call captures all of its tracks. The cost estimate is returned before any scraper spend. Two snapshots over time yield per-track run-rates via [GET /api/research/track/playcount-deltas](/api-reference/research/track-playcount-deltas).", + "description": "**Deprecated (2026-06).** Use `POST /api/research/measurement-jobs` with `source:\"current\"` instead.\n\nCapture platform-displayed play counts for a whole catalog, album list, or ISRC list in one server-side snapshot. Executes asynchronously; one album call captures all of its tracks. The cost estimate is returned before any scraper spend. Two snapshots over time yield per-track run-rates via [GET /api/research/track/playcount-deltas](/api-reference/research/track-playcount-deltas).", "requestBody": { "required": true, "content": { @@ -2959,12 +2961,13 @@ } } } - } + }, + "deprecated": true } }, "/api/research/track/playcount-deltas": { "get": { - "description": "Play-count change between [snapshots](/api-reference/research/snapshots) for one recording, with an annualized run-rate. Requires at least two captures in the window; returns an empty `deltas` array (not an error) when history is insufficient — create captures with [POST /api/research/snapshots](/api-reference/research/snapshots).", + "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?aggregate=run_rate` instead.\n\nPlay-count change between [snapshots](/api-reference/research/snapshots) for one recording, with an annualized run-rate. Requires at least two captures in the window; returns an empty `deltas` array (not an error) when history is insufficient — create captures with [POST /api/research/snapshots](/api-reference/research/snapshots).", "parameters": [ { "name": "isrc", @@ -3049,6 +3052,338 @@ } } } + }, + "deprecated": true + } + }, + "/api/research/tracks/{id}/measurements": { + "get": { + "summary": "Track measurements", + "description": "Time-series of a track's measured counts from the measurement store, projected by query params. Replaces `track/historic-stats` (use `granularity=daily`) and `track/playcount-deltas` (use `aggregate=run_rate`). `{id}` is a provider-neutral identifier (ISRC or Spotify track id). Spotify history is served from the store (snapshot captures + Songstats backfill); no vendor quota is spent in the request path.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Provider-neutral track id — ISRC or Spotify track id." + }, + { + "name": "platform", + "in": "query", + "schema": { + "type": "string", + "default": "spotify" + }, + "description": "Platform to read. Currently `spotify`." + }, + { + "name": "metric", + "in": "query", + "schema": { + "type": "string", + "default": "platform_displayed_play_count" + }, + "description": "Metric to read." + }, + { + "name": "from", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Inclusive start date (ISO) for the series." + }, + { + "name": "to", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Inclusive end date (ISO) for the series." + }, + { + "name": "granularity", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "daily" + ], + "default": "daily" + }, + "description": "Series granularity. `daily` returns the per-day cumulative series." + }, + { + "name": "aggregate", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "run_rate" + ] + }, + "description": "Return a derived aggregate instead of the raw series. `run_rate` returns the trailing-window annualized run-rate (a projection of the series)." + }, + { + "name": "window", + "in": "query", + "schema": { + "type": "string", + "default": "365d" + }, + "description": "Trailing window for `aggregate=run_rate` (e.g. `365d`)." + } + ], + "responses": { + "200": { + "description": "The measurement series (or the requested aggregate).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchMeasurementsResponse" + } + } + } + }, + "400": { + "description": "Validation error — bad identifier or query param.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "401": { + "description": "Authentication failed — invalid or missing API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "402": { + "description": "Insufficient credits.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" + } + } + } + }, + "404": { + "description": "No measurements for this track yet — create a `historical` measurement-job to backfill it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + } + } + } + }, + "/api/research/albums/{id}/measurements": { + "get": { + "summary": "Album measurements", + "description": "Latest measured count per track on an album, from the measurement store. Replaces `GET /api/research/playcounts` (`?latest=true`). `{id}` is a Spotify album id.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Spotify album id." + }, + { + "name": "platform", + "in": "query", + "schema": { + "type": "string", + "default": "spotify" + }, + "description": "Platform to read. Currently `spotify`." + }, + { + "name": "metric", + "in": "query", + "schema": { + "type": "string", + "default": "platform_displayed_play_count" + }, + "description": "Metric to read." + }, + { + "name": "latest", + "in": "query", + "schema": { + "type": "boolean", + "default": true + }, + "description": "Return the latest capture per track (the only supported mode today)." + } + ], + "responses": { + "200": { + "description": "Latest per-track measured counts for the album.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchAlbumMeasurementsResponse" + } + } + } + }, + "401": { + "description": "Authentication failed — invalid or missing API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "402": { + "description": "Insufficient credits.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" + } + } + } + }, + "404": { + "description": "No capture exists for this album yet — create a `current` measurement-job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + } + } + } + }, + "/api/research/measurement-jobs": { + "post": { + "summary": "Create a measurement job", + "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline (replaces `POST /api/research/snapshots`). `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`.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchMeasurementJobRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Job accepted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchMeasurementJobResponse" + } + } + } + }, + "400": { + "description": "Validation error — provide a `source` and exactly one of `scope.catalog_id`, `scope.album_ids`, `scope.isrcs`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "401": { + "description": "Authentication failed — invalid or missing API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "429": { + "description": "Per-organization monthly cap reached (current jobs).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + } + } + } + }, + "/api/research/measurement-jobs/{id}": { + "get": { + "summary": "Measurement job status", + "description": "Poll a `current` measurement job's status. `historical` jobs report their enqueue summary in the create response and drain via the daily worker.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Measurement-job id from the create response." + } + ], + "responses": { + "200": { + "description": "Job status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchMeasurementJobStatusResponse" + } + } + } + }, + "401": { + "description": "Authentication failed — invalid or missing API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + }, + "404": { + "description": "Unknown job id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchErrorResponse" + } + } + } + } } } } @@ -5459,6 +5794,275 @@ "required_credits", "checkoutUrl" ] + }, + "ResearchMeasurementPoint": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date", + "example": "2026-06-12" + }, + "value": { + "type": "number", + "description": "Cumulative metric value as of `date`.", + "example": 297289495 + }, + "data_source": { + "type": "string", + "example": "songstats" + } + } + }, + "ResearchMeasurementsResponse": { + "type": "object", + "description": "A track's measured series, or — when `aggregate` is set — the derived projection.", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "id": { + "type": "string", + "description": "The provider-neutral track id queried.", + "example": "USQY51771120" + }, + "platform": { + "type": "string", + "example": "spotify" + }, + "metric": { + "type": "string", + "example": "platform_displayed_play_count" + }, + "series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResearchMeasurementPoint" + }, + "description": "Present unless `aggregate` is set." + }, + "aggregate": { + "type": "object", + "nullable": true, + "description": "Present when `aggregate=run_rate`.", + "properties": { + "kind": { + "type": "string", + "example": "run_rate" + }, + "window_days": { + "type": "integer", + "example": 365 + }, + "delta": { + "type": "number", + "example": 42000000 + }, + "run_rate_annualized": { + "type": "number", + "example": 42000000 + } + } + } + } + }, + "ResearchAlbumMeasurementItem": { + "type": "object", + "properties": { + "isrc": { + "type": "string", + "example": "USQY51771120" + }, + "spotify_track_id": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "value": { + "type": "number", + "description": "Latest measured count.", + "example": 297289495 + }, + "captured_at": { + "type": "string", + "format": "date-time" + }, + "data_source": { + "type": "string", + "example": "apify_spotify_playcount" + } + } + }, + "ResearchAlbumMeasurementsResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "id": { + "type": "string", + "description": "Spotify album id.", + "example": "70Zkfb99ladZ3q0JVg97co" + }, + "platform": { + "type": "string", + "example": "spotify" + }, + "metric": { + "type": "string", + "example": "platform_displayed_play_count" + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResearchAlbumMeasurementItem" + } + } + } + }, + "ResearchMeasurementJobScope": { + "type": "object", + "description": "Provide exactly one of `catalog_id`, `album_ids`, or `isrcs`.", + "properties": { + "catalog_id": { + "type": "string", + "format": "uuid" + }, + "album_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "isrcs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ResearchMeasurementJobRequest": { + "type": "object", + "required": [ + "scope", + "source" + ], + "properties": { + "scope": { + "$ref": "#/components/schemas/ResearchMeasurementJobScope" + }, + "source": { + "type": "string", + "enum": [ + "current", + "historical" + ], + "description": "`current` = capture present counts (Apify snapshot). `historical` = enqueue Songstats deep backfill." + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "spotify" + ], + "example": [ + "spotify" + ] + } + } + }, + "ResearchMeasurementJobResponse": { + "type": "object", + "description": "For `current` jobs, `id` is pollable. For `historical` jobs, `enqueued`/`skipped` summarize the backfill enqueue (the queue drains via the daily worker).", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "source": { + "type": "string", + "example": "historical" + }, + "id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Job id for `current` jobs; null for `historical`." + }, + "state": { + "type": "string", + "enum": [ + "queued" + ], + "nullable": true, + "example": "queued" + }, + "enqueued": { + "type": "integer", + "nullable": true, + "description": "`historical`: recordings enqueued for backfill.", + "example": 842 + }, + "skipped": { + "type": "integer", + "nullable": true, + "description": "`historical`: recordings already backfilled (skipped).", + "example": 3 + }, + "album_count": { + "type": "integer", + "nullable": true, + "description": "`current`: albums the job will capture." + }, + "estimated_cost_usd": { + "type": "number", + "nullable": true, + "description": "`current`: scraper cost estimate before spend." + } + } + }, + "ResearchMeasurementJobStatusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "source": { + "type": "string", + "example": "current" + }, + "state": { + "type": "string", + "enum": [ + "queued", + "running", + "done", + "failed" + ], + "example": "running" + }, + "album_count": { + "type": "integer", + "nullable": true + }, + "estimated_cost_usd": { + "type": "number", + "nullable": true + } + } } }, "responses": { diff --git a/api-reference/research/album-measurements.mdx b/api-reference/research/album-measurements.mdx new file mode 100644 index 00000000..6fd8e6f7 --- /dev/null +++ b/api-reference/research/album-measurements.mdx @@ -0,0 +1,4 @@ +--- +title: 'Album measurements' +openapi: "/api-reference/openapi/research.json GET /api/research/albums/{id}/measurements" +--- diff --git a/api-reference/research/measurement-job.mdx b/api-reference/research/measurement-job.mdx new file mode 100644 index 00000000..cad33b03 --- /dev/null +++ b/api-reference/research/measurement-job.mdx @@ -0,0 +1,4 @@ +--- +title: 'Measurement job status' +openapi: "/api-reference/openapi/research.json GET /api/research/measurement-jobs/{id}" +--- diff --git a/api-reference/research/measurement-jobs.mdx b/api-reference/research/measurement-jobs.mdx new file mode 100644 index 00000000..22fcb509 --- /dev/null +++ b/api-reference/research/measurement-jobs.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create measurement job' +openapi: "/api-reference/openapi/research.json POST /api/research/measurement-jobs" +--- diff --git a/api-reference/research/measurements.mdx b/api-reference/research/measurements.mdx new file mode 100644 index 00000000..4500e049 --- /dev/null +++ b/api-reference/research/measurements.mdx @@ -0,0 +1,4 @@ +--- +title: 'Track measurements' +openapi: "/api-reference/openapi/research.json GET /api/research/tracks/{id}/measurements" +--- diff --git a/docs.json b/docs.json index 0576d06e..14d85b23 100644 --- a/docs.json +++ b/docs.json @@ -124,7 +124,11 @@ "api-reference/research/track-historic-stats", "api-reference/research/track-playcount-deltas", "api-reference/research/playcounts", - "api-reference/research/snapshots" + "api-reference/research/snapshots", + "api-reference/research/measurements", + "api-reference/research/album-measurements", + "api-reference/research/measurement-jobs", + "api-reference/research/measurement-job" ] }, { From d2efd844ce12b3ddd067355f19ac0073c17d01d0 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 16 Jun 2026 08:54:58 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20dele?= =?UTF-8?q?te=20deprecated=20endpoints,=20KISS=20descriptions,=20drop=20be?= =?UTF-8?q?spoke=20job-status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YAGNI: delete the 4 legacy endpoints outright (historic-stats, playcount-deltas, playcounts, snapshots) instead of deprecating them — plus their MDX pages, nav entries, and 8 now-orphaned schemas. - DRY: remove GET /research/measurement-jobs/{id}; run status is covered by the generic GET /api/tasks/runs, and the old snapshot flow never had a status endpoint. - KISS: shorten the measurements / album-measurements / measurement-jobs descriptions per the suggested edits. mintlify openapi-check: valid; all $refs resolve. --- api-reference/openapi/research.json | 806 +----------------- api-reference/research/measurement-job.mdx | 4 - api-reference/research/playcounts.mdx | 4 - api-reference/research/snapshots.mdx | 4 - .../research/track-historic-stats.mdx | 4 - .../research/track-playcount-deltas.mdx | 4 - docs.json | 7 +- 7 files changed, 18 insertions(+), 815 deletions(-) delete mode 100644 api-reference/research/measurement-job.mdx delete mode 100644 api-reference/research/playcounts.mdx delete mode 100644 api-reference/research/snapshots.mdx delete mode 100644 api-reference/research/track-historic-stats.mdx delete mode 100644 api-reference/research/track-playcount-deltas.mdx diff --git a/api-reference/openapi/research.json b/api-reference/openapi/research.json index bf36137b..807f09b8 100644 --- a/api-reference/openapi/research.json +++ b/api-reference/openapi/research.json @@ -2689,377 +2689,10 @@ } } }, - "/api/research/track/historic-stats": { - "get": { - "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?granularity=daily` instead.\n\nGet historic per-track, per-source stats by ISRC or track id over a date range — a daily time-series of cumulative counters (e.g. `streams_total`). Diff the window endpoints for streams gained over a period.", - "parameters": [ - { - "name": "isrc", - "in": "query", - "required": false, - "description": "Track ISRC. Provide exactly one track identifier.", - "schema": { - "type": "string", - "example": "USQY51771120" - } - }, - { - "name": "songstats_track_id", - "in": "query", - "required": false, - "description": "Songstats track id (from `GET /api/research/tracks`).", - "schema": { - "type": "string" - } - }, - { - "name": "spotify_track_id", - "in": "query", - "required": false, - "description": "Spotify track id.", - "schema": { - "type": "string" - } - }, - { - "name": "apple_music_track_id", - "in": "query", - "required": false, - "description": "Apple Music track id.", - "schema": { - "type": "string" - } - }, - { - "name": "source", - "in": "query", - "required": true, - "description": "Comma-separated list of sources, or `all`. e.g. `spotify`, `apple_music`, `deezer`.", - "schema": { - "type": "string", - "example": "spotify" - } - }, - { - "name": "start_date", - "in": "query", - "required": false, - "description": "Start of the historic window (ISO date).", - "schema": { - "type": "string", - "format": "date", - "example": "2024-06-09" - } - }, - { - "name": "end_date", - "in": "query", - "required": false, - "description": "End of the historic window (ISO date).", - "schema": { - "type": "string", - "format": "date", - "example": "2025-06-09" - } - }, - { - "name": "with_aggregates", - "in": "query", - "required": false, - "description": "Include Songstats-aggregated points (`playlists_current`, `charts_current`, …).", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "Historic per-track stats, one time-series per requested source.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchTrackHistoricStatsResponse" - } - } - } - }, - "400": { - "description": "Validation error — missing identifier or `source`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "401": { - "description": "Authentication failed — invalid or missing API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "404": { - "description": "No track matched the supplied identifier.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "402": { - "description": "Insufficient research credits — the body includes a `checkoutUrl` to top up.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" - } - } - } - }, - "429": { - "description": "Upstream provider error passthrough — e.g. 429 when the Songstats quota is exhausted.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - } - }, - "deprecated": true - } - }, - "/api/research/playcounts": { - "get": { - "description": "**Deprecated (2026-06).** Use `GET /api/research/albums/{id}/measurements?latest=true` instead.\n\nLatest platform-displayed play counts for all tracks on an album, served from the measurement store (populated by snapshots). Counts are scraped from public pages — not royalty-bearing stream counts.", - "parameters": [ - { - "name": "spotify_album_id", - "in": "query", - "required": true, - "description": "Spotify album id.", - "schema": { - "type": "string", - "example": "5SKnXCvB4fcGSZu32o3LRY" - } - } - ], - "responses": { - "200": { - "description": "Latest snapshot counts for every track on the album.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchPlaycountsResponse" - } - } - } - }, - "400": { - "description": "Validation error — missing `spotify_album_id`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "401": { - "description": "Authentication failed — invalid or missing API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "404": { - "description": "No snapshot exists for this album yet — create one with `POST /api/research/snapshots`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "402": { - "description": "Insufficient research credits — the body includes a `checkoutUrl` to top up.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" - } - } - } - } - }, - "deprecated": true - } - }, - "/api/research/snapshots": { - "post": { - "description": "**Deprecated (2026-06).** Use `POST /api/research/measurement-jobs` with `source:\"current\"` instead.\n\nCapture platform-displayed play counts for a whole catalog, album list, or ISRC list in one server-side snapshot. Executes asynchronously; one album call captures all of its tracks. The cost estimate is returned before any scraper spend. Two snapshots over time yield per-track run-rates via [GET /api/research/track/playcount-deltas](/api-reference/research/track-playcount-deltas).", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchSnapshotRequest" - } - } - } - }, - "responses": { - "202": { - "description": "Snapshot accepted and queued.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchSnapshotResponse" - } - } - } - }, - "400": { - "description": "Validation error — provide exactly one of `catalog_id`, `album_ids`, `isrcs`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "401": { - "description": "Authentication failed — invalid or missing API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "429": { - "description": "Per-organization monthly snapshot cap reached.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - } - }, - "deprecated": true - } - }, - "/api/research/track/playcount-deltas": { - "get": { - "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?aggregate=run_rate` instead.\n\nPlay-count change between [snapshots](/api-reference/research/snapshots) for one recording, with an annualized run-rate. Requires at least two captures in the window; returns an empty `deltas` array (not an error) when history is insufficient — create captures with [POST /api/research/snapshots](/api-reference/research/snapshots).", - "parameters": [ - { - "name": "isrc", - "in": "query", - "required": true, - "description": "Track ISRC.", - "schema": { - "type": "string", - "example": "USUYG1069896" - } - }, - { - "name": "since", - "in": "query", - "required": true, - "description": "Window start (inclusive).", - "schema": { - "type": "string", - "format": "date", - "example": "2026-06-09" - } - }, - { - "name": "until", - "in": "query", - "required": false, - "description": "Window end (inclusive). Defaults to the latest capture.", - "schema": { - "type": "string", - "format": "date" - } - } - ], - "responses": { - "200": { - "description": "Per-platform deltas between the nearest captures.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchPlaycountDeltasResponse" - } - } - } - }, - "400": { - "description": "Validation error — missing `isrc` or `since`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "401": { - "description": "Authentication failed — invalid or missing API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "404": { - "description": "Unknown ISRC.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "402": { - "description": "Insufficient research credits — the body includes a `checkoutUrl` to top up.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchInsufficientCreditsResponse" - } - } - } - } - }, - "deprecated": true - } - }, "/api/research/tracks/{id}/measurements": { "get": { "summary": "Track measurements", - "description": "Time-series of a track's measured counts from the measurement store, projected by query params. Replaces `track/historic-stats` (use `granularity=daily`) and `track/playcount-deltas` (use `aggregate=run_rate`). `{id}` is a provider-neutral identifier (ISRC or Spotify track id). Spotify history is served from the store (snapshot captures + Songstats backfill); no vendor quota is spent in the request path.", + "description": "Time-series of a track's measured counts.", "parameters": [ { "name": "id", @@ -3196,7 +2829,7 @@ "/api/research/albums/{id}/measurements": { "get": { "summary": "Album measurements", - "description": "Latest measured count per track on an album, from the measurement store. Replaces `GET /api/research/playcounts` (`?latest=true`). `{id}` is a Spotify album id.", + "description": "Latest measured count per track on an album, from the measurement store. `{id}` is a Spotify album id.", "parameters": [ { "name": "id", @@ -3282,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 (replaces `POST /api/research/snapshots`). `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`.", "requestBody": { "required": true, "content": { @@ -3295,71 +2928,21 @@ }, "responses": { "202": { - "description": "Job accepted.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchMeasurementJobResponse" - } - } - } - }, - "400": { - "description": "Validation error — provide a `source` and exactly one of `scope.catalog_id`, `scope.album_ids`, `scope.isrcs`.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "401": { - "description": "Authentication failed — invalid or missing API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - }, - "429": { - "description": "Per-organization monthly cap reached (current jobs).", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResearchErrorResponse" - } - } - } - } - } - } - }, - "/api/research/measurement-jobs/{id}": { - "get": { - "summary": "Measurement job status", - "description": "Poll a `current` measurement job's status. `historical` jobs report their enqueue summary in the create response and drain via the daily worker.", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Measurement-job id from the create response." - } - ], - "responses": { - "200": { - "description": "Job status.", + "description": "Job accepted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResearchMeasurementJobResponse" + } + } + } + }, + "400": { + "description": "Validation error — provide a `source` and exactly one of `scope.catalog_id`, `scope.album_ids`, `scope.isrcs`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResearchMeasurementJobStatusResponse" + "$ref": "#/components/schemas/ResearchErrorResponse" } } } @@ -3374,8 +2957,8 @@ } } }, - "404": { - "description": "Unknown job id.", + "429": { + "description": "Per-organization monthly cap reached (current jobs).", "content": { "application/json": { "schema": { @@ -5441,326 +5024,6 @@ }, "additionalProperties": true }, - "ResearchTrackHistoricStatsResponse": { - "type": "object", - "description": "Historic per-track statistics for the requested `source`(s). Same envelope as `ResearchTrackStatsResponse`, but each source's `data` holds a `history` time-series. When only `spotify` is requested, the response is the minimal envelope `{ result, stats }` served entirely from the measurement store.", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "result": { - "type": "string", - "example": "success" - }, - "message": { - "type": "string", - "example": "Data Retrieved." - }, - "stats": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResearchTrackHistoricStat" - } - }, - "track_info": { - "$ref": "#/components/schemas/ResearchTrackInfo" - }, - "source_ids": { - "type": "array", - "description": "All source identifiers Songstats tracks for this recording.", - "items": { - "type": "string" - } - } - } - }, - "ResearchTrackHistoricStat": { - "type": "object", - "description": "Per-source historic stats for a single track.", - "properties": { - "source": { - "type": "string", - "description": "Platform the stats belong to, e.g. `spotify`, `apple_music`, `deezer`." - }, - "data": { - "type": "object", - "properties": { - "history": { - "type": "array", - "description": "Per-date snapshots, oldest→newest. `streams_total` is the cumulative play count *as of* that date — diff two dates for streams gained between them. Keys vary by source; extra aggregate keys appear when `with_aggregates=true`.", - "items": { - "type": "object", - "additionalProperties": true, - "properties": { - "date": { - "type": "string", - "format": "date" - }, - "streams_total": { - "type": "integer", - "example": 574845367 - }, - "popularity_current": { - "type": "integer" - }, - "playlists_current": { - "type": "integer" - }, - "playlist_reach_current": { - "type": "integer" - }, - "data_source": { - "type": "string", - "description": "Per-point provenance. Provenance of the numbers: `apify_spotify_playcount` (platform-displayed count from the measurement store), `songstats` (Songstats backfill), or `granted_analytics` (customer-granted data).", - "example": "songstats" - } - } - } - } - } - } - }, - "additionalProperties": true - }, - "ResearchPlaycount": { - "type": "object", - "description": "Latest platform-displayed play count for one track, from the most recent album snapshot.", - "properties": { - "isrc": { - "type": "string", - "description": "ISRC of the recording. Tracks are mapped automatically on capture (ISRCs resolved via the Spotify API), so this is always present.", - "example": "USUYG1069896" - }, - "spotify_track_id": { - "type": [ - "string", - "null" - ], - "description": "Spotify track id; `null` when the recording has an album mapping but no track-id mapping yet." - }, - "name": { - "type": "string", - "description": "Track title as displayed on the platform." - }, - "platform_displayed_play_count": { - "type": "integer", - "format": "int64", - "description": "Play count as displayed on the public platform page. Scraped, not a royalty-bearing stream count — calibrate against customer-granted analytics for transaction-grade numbers.", - "example": 1331384578 - }, - "captured_at": { - "type": "string", - "format": "date-time", - "description": "When the snapshot captured this count." - }, - "data_source": { - "type": "string", - "description": "Provenance of the numbers: `apify_spotify_playcount` (platform-displayed count from the measurement store), `songstats` (Songstats backfill), or `granted_analytics` (customer-granted data).", - "example": "apify_spotify_playcount" - } - } - }, - "ResearchPlaycountsResponse": { - "type": "object", - "description": "Latest snapshot play counts for every track on an album.", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "album": { - "type": "object", - "properties": { - "spotify_album_id": { - "type": "string", - "example": "5SKnXCvB4fcGSZu32o3LRY" - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "Album name from the song record." - }, - "label": { - "type": [ - "string", - "null" - ], - "description": "Currently always `null` — populated once snapshots persist album metadata." - }, - "copyright": { - "type": [ - "string", - "null" - ], - "description": "℗-line for ownership re-verification. Currently always `null` — populated once snapshots persist album metadata." - } - } - }, - "playcounts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResearchPlaycount" - } - } - } - }, - "ResearchSnapshotRequest": { - "type": "object", - "description": "Provide exactly one of `catalog_id`, `album_ids`, or `isrcs`.", - "required": [ - "platforms" - ], - "properties": { - "catalog_id": { - "type": "string", - "format": "uuid", - "description": "Catalog to snapshot; resolves to its tracks server-side." - }, - "album_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Spotify album ids to snapshot (one album call captures all of its tracks)." - }, - "isrcs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "ISRCs to snapshot; resolved to albums server-side where a mapping exists." - }, - "platforms": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Platforms to capture. Currently `spotify`.", - "example": [ - "spotify" - ] - }, - "schedule": { - "type": "string", - "enum": [ - "once", - "monthly" - ], - "default": "once", - "description": "`once` runs a single snapshot; `monthly` registers a recurring snapshot." - } - } - }, - "ResearchSnapshotResponse": { - "type": "object", - "description": "Snapshot accepted. Execution is asynchronous; the cost estimate is returned before any scraper spend.", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "snapshot_id": { - "type": "string", - "format": "uuid" - }, - "state": { - "type": "string", - "enum": [ - "queued" - ], - "example": "queued" - }, - "album_count": { - "type": "integer", - "description": "Albums the snapshot will capture.", - "example": 570 - }, - "estimated_cost_usd": { - "type": "number", - "description": "Estimated scraper cost for the full snapshot, returned before execution.", - "example": 1.71 - } - } - }, - "ResearchPlaycountDelta": { - "type": "object", - "description": "Change in a metric between the two snapshots nearest the requested window bounds.", - "properties": { - "platform": { - "type": "string", - "example": "spotify" - }, - "metric": { - "type": "string", - "example": "platform_displayed_play_count" - }, - "since": { - "type": "object", - "description": "Nearest capture at/after the `since` date.", - "properties": { - "captured_at": { - "type": "string", - "format": "date-time" - }, - "value": { - "type": "integer", - "format": "int64" - } - } - }, - "until": { - "type": "object", - "description": "Nearest capture at/before the `until` date (latest capture when `until` is omitted).", - "properties": { - "captured_at": { - "type": "string", - "format": "date-time" - }, - "value": { - "type": "integer", - "format": "int64" - } - } - }, - "delta": { - "type": "integer", - "format": "int64", - "description": "`until.value - since.value`." - }, - "days": { - "type": "integer", - "description": "Days between the two captures." - }, - "run_rate_annualized": { - "type": "integer", - "format": "int64", - "description": "`delta` scaled to 365 days — a TTM proxy once captures span a meaningful window." - } - } - }, - "ResearchPlaycountDeltasResponse": { - "type": "object", - "description": "Per-platform deltas for one recording. `deltas` is empty when fewer than two captures exist in the window — the request never blocks on missing history.", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "isrc": { - "type": "string", - "example": "USUYG1069896" - }, - "deltas": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResearchPlaycountDelta" - } - } - } - }, "ResearchInsufficientCreditsResponse": { "type": "object", "description": "Returned (402) when the account lacks research credits and auto-recharge did not cover the call.", @@ -6028,41 +5291,6 @@ "description": "`current`: scraper cost estimate before spend." } } - }, - "ResearchMeasurementJobStatusResponse": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "source": { - "type": "string", - "example": "current" - }, - "state": { - "type": "string", - "enum": [ - "queued", - "running", - "done", - "failed" - ], - "example": "running" - }, - "album_count": { - "type": "integer", - "nullable": true - }, - "estimated_cost_usd": { - "type": "number", - "nullable": true - } - } } }, "responses": { diff --git a/api-reference/research/measurement-job.mdx b/api-reference/research/measurement-job.mdx deleted file mode 100644 index cad33b03..00000000 --- a/api-reference/research/measurement-job.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Measurement job status' -openapi: "/api-reference/openapi/research.json GET /api/research/measurement-jobs/{id}" ---- diff --git a/api-reference/research/playcounts.mdx b/api-reference/research/playcounts.mdx deleted file mode 100644 index 0c0311dc..00000000 --- a/api-reference/research/playcounts.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Play Counts' -openapi: "/api-reference/openapi/research.json GET /api/research/playcounts" ---- diff --git a/api-reference/research/snapshots.mdx b/api-reference/research/snapshots.mdx deleted file mode 100644 index 6f4438f3..00000000 --- a/api-reference/research/snapshots.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Snapshots' -openapi: "/api-reference/openapi/research.json POST /api/research/snapshots" ---- diff --git a/api-reference/research/track-historic-stats.mdx b/api-reference/research/track-historic-stats.mdx deleted file mode 100644 index 18913ca7..00000000 --- a/api-reference/research/track-historic-stats.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Track Historic Stats' -openapi: "/api-reference/openapi/research.json GET /api/research/track/historic-stats" ---- diff --git a/api-reference/research/track-playcount-deltas.mdx b/api-reference/research/track-playcount-deltas.mdx deleted file mode 100644 index 6c1123f1..00000000 --- a/api-reference/research/track-playcount-deltas.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Track Play Count Deltas' -openapi: "/api-reference/openapi/research.json GET /api/research/track/playcount-deltas" ---- diff --git a/docs.json b/docs.json index 14d85b23..eb862140 100644 --- a/docs.json +++ b/docs.json @@ -121,14 +121,9 @@ "api-reference/research/tracks", "api-reference/research/track", "api-reference/research/track-stats", - "api-reference/research/track-historic-stats", - "api-reference/research/track-playcount-deltas", - "api-reference/research/playcounts", - "api-reference/research/snapshots", "api-reference/research/measurements", "api-reference/research/album-measurements", - "api-reference/research/measurement-jobs", - "api-reference/research/measurement-job" + "api-reference/research/measurement-jobs" ] }, {