From aab48dabae624d3b26ba64671a5f0d1bc922fd8f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 16 Apr 2026 16:12:42 -0500 Subject: [PATCH] docs: align 7 research response schemas with actual API behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced while comparing the merged docs to a full re-test of the research endpoints on the recoupable/api#366 preview (see https://github.com/recoupable/api/pull/366#issuecomment-4263096425 through #issuecomment-4263097490). Each of the schemas below either advertised fields that never appear in responses or omitted fields that always do. All changes are spec-only — no api behavior change. /research/track response - Document `cm_statistics` (nested platform stats blob) - Document tempo / moods / activities (common nullable fields) /research/playlist response - Drop `curator_name` (never returned) → surface as `owner_name` - Drop `num_tracks` (never returned) → real field is `num_track` - Add playlist_id (platform-native), owner_id, user_id, image_url, editorial, personalized, catalog, code2, active_ratio, suspicion_score, fdiff_week/month, last_updated, sys_last_updated, genres/moods/activities + their smart-ordered counterparts, tags /research/charts response - `data` is an array, not an object (50 entries in practice) - Document `length` (total entry count) /research/profile response - Drop `hometown`, `sp_followers`, `sp_monthly_listeners`, `tags` — none are returned (longitudinal platform metrics live behind /research/metrics; use `hometown_city`/`current_city` for geography) - Add: band/band_members, gender fields, code2, isni, cover_url, hometown_city, current_city(_id), cm_artist_rank/cm_artist_score, cm_statistics, career_status, genreRank/subGenreRank1/2, genre_smart_ordered, moods, activities, booking_agent, press_contact, general_manager, topSongwriterCollaborators /research/metrics response - Replace documented `{data: [...]}` wrapper with flat per-platform time-series fields. For Spotify: `link`, `followers`, `listeners`, `popularity`, `followers_to_listeners_ratio` — each an array of `{timestp, value}` points. /research/audience response - Add the likers/engagement field family: audience_likers_genders (+per_age), audience_likers_ethnicities, audience_likers_interests, audience_likers_brand_affinities, likers_top_countries, likers_top_cities, notable_followers, followers, avg_likes_per_post, avg_commments_per_post (sic — upstream typo), engagement_rate, timestp - Add audience_ethnicities, audience_interests /research/curator response - Drop `followers` / `num_playlists` (neither returned) - Add per-platform follower fields: instagram_followers, facebook_followers/fans, twitter_followers/retweets, youtube_subscribers/views, soundcloud_followers, tiktok_followers/likes (all nullable) - Add user_id, submithub_id, last_updated, suspicion_score, tags, tag_ids, tag_names, spotifySocialUrls, cm_statistics /research/extract response - No change. Prior audit flagged errors as drift, but the field is already correctly optional (not in `required`) with a description noting conditional presence. Co-Authored-By: Claude Opus 4.7 (1M context) --- api-reference/openapi/research.json | 513 ++++++++++++++++++++++++---- 1 file changed, 442 insertions(+), 71 deletions(-) diff --git a/api-reference/openapi/research.json b/api-reference/openapi/research.json index 7b2b730..4ce11b7 100644 --- a/api-reference/openapi/research.json +++ b/api-reference/openapi/research.json @@ -3774,51 +3774,98 @@ }, "ResearchAudienceResponse": { "type": "object", - "description": "Audience demographics from Chartmetric — includes gender breakdown, age-by-gender splits, top countries and cities, and brand affinities. Over 20 fields may be returned; only the most common are listed here.", + "description": "Audience demographics and engagement metrics from Chartmetric. Contains two parallel families of fields: **followers** (`audience_*` and `top_*`) describe the artist's follower base, while **likers** (`audience_likers_*`, `likers_top_*`) describe the people who engage (like/comment) with the artist's posts. Engagement aggregates (`followers`, `avg_likes_per_post`, `engagement_rate`) and `notable_followers` round out the payload. All array shapes are upstream Chartmetric objects that pass through via `additionalProperties`.", "properties": { "status": { "type": "string", "example": "success" }, - "audience_genders": { - "type": "array", - "description": "Gender breakdown with percentages.", - "items": { - "type": "object", - "additionalProperties": true - } + "timestp": { + "type": "string", + "description": "ISO timestamp at which this audience snapshot was captured." }, - "audience_genders_per_age": { - "type": "array", - "description": "Gender split per age bracket.", - "items": { - "type": "object", - "additionalProperties": true - } + "followers": { + "type": "integer", + "description": "Follower count on the underlying platform at snapshot time." + }, + "avg_likes_per_post": { + "type": "integer" + }, + "avg_commments_per_post": { + "type": "integer", + "description": "Note: field name is misspelled upstream as `avg_commments_per_post`." + }, + "engagement_rate": { + "type": "number" }, "top_countries": { "type": "array", "description": "Top countries by audience share.", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } }, "top_cities": { "type": "array", "description": "Top cities by audience concentration.", - "items": { - "type": "object", - "additionalProperties": true - } + "items": { "type": "object", "additionalProperties": true } + }, + "likers_top_countries": { + "type": "array", + "description": "Top countries for the likers (engagement) cohort.", + "items": { "type": "object", "additionalProperties": true } + }, + "likers_top_cities": { + "type": "array", + "description": "Top cities for the likers cohort.", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_genders": { + "type": "array", + "description": "Gender breakdown of the follower base.", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_genders_per_age": { + "type": "array", + "description": "Follower gender split per age bracket.", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_ethnicities": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_interests": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } }, "audience_brand_affinities": { "type": "array", - "description": "Brand affinity scores for the audience.", - "items": { - "type": "object", - "additionalProperties": true - } + "description": "Brand affinity scores for the follower cohort.", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_likers_genders": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_likers_genders_per_age": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_likers_ethnicities": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_likers_interests": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "audience_likers_brand_affinities": { + "type": "array", + "description": "Brand affinity scores for the likers cohort.", + "items": { "type": "object", "additionalProperties": true } + }, + "notable_followers": { + "type": "array", + "description": "Notable public/influential accounts in the follower base.", + "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true @@ -3852,10 +3899,17 @@ ], "example": "success" }, + "length": { + "type": "integer", + "description": "Number of entries returned in `data` (typically 50)." + }, "data": { - "type": "object", - "description": "Chart data — structure varies by platform.", - "additionalProperties": true + "type": "array", + "description": "Chart entries for the requested platform/type/country/interval combination. Entry shape varies by platform; common fields include track/artist names, ranks, and platform-specific metrics.", + "items": { + "type": "object", + "additionalProperties": true + } } } }, @@ -3898,6 +3952,7 @@ }, "ResearchCuratorResponse": { "type": "object", + "description": "Curator profile. Follower counts are split by platform (`instagram_followers`, `facebook_followers`, `twitter_followers`, `youtube_subscribers`, `soundcloud_followers`, `tiktok_followers`), each nullable when unknown. Aggregated signals live under `cm_statistics`.", "properties": { "status": { "type": "string", @@ -3907,22 +3962,93 @@ ], "example": "success" }, + "id": { + "type": "integer", + "description": "Chartmetric curator ID." + }, + "user_id": { + "type": "string", + "description": "Platform-native curator/user ID (e.g. Spotify user ID)." + }, "name": { "type": "string" }, - "id": { - "type": "integer" - }, "image_url": { "type": "string", "format": "uri", "nullable": true }, - "num_playlists": { + "submithub_id": { + "type": "string", + "nullable": true + }, + "last_updated": { + "type": "string", + "description": "ISO timestamp of the last Chartmetric sync." + }, + "suspicion_score": { + "type": "integer", + "description": "Chartmetric's internal suspicion score (lower is better)." + }, + "tags": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "tag_ids": { + "type": "array", + "items": { "type": "integer" } + }, + "tag_names": { + "type": "array", + "items": { "type": "string" } + }, + "spotifySocialUrls": { + "type": "array", + "description": "Spotify-linked social URLs for the curator.", + "items": { "type": "object", "additionalProperties": true } + }, + "cm_statistics": { + "type": "object", + "description": "Aggregated Chartmetric statistics — playlist reach, editorial counts, velocity deltas, etc.", + "additionalProperties": true + }, + "instagram_followers": { "type": "integer", "nullable": true }, - "followers": { + "facebook_followers": { + "type": "integer", + "nullable": true + }, + "facebook_fans": { + "type": "integer", + "nullable": true + }, + "twitter_followers": { + "type": "integer", + "nullable": true + }, + "twitter_retweets": { + "type": "integer", + "nullable": true + }, + "youtube_subscribers": { + "type": "integer", + "nullable": true + }, + "youtube_views": { + "type": "integer", + "nullable": true + }, + "soundcloud_followers": { + "type": "integer", + "nullable": true + }, + "tiktok_followers": { + "type": "integer", + "nullable": true + }, + "tiktok_likes": { "type": "integer", "nullable": true } @@ -4370,16 +4496,60 @@ }, "ResearchMetricsResponse": { "type": "object", - "description": "Time-series metrics for a specific platform. Shape varies by source — typically an array of data points with timestamps and values (followers, listeners, views, etc.).", + "description": "Time-series metrics for the artist on the selected platform. Shape varies by platform — fields at the root (not wrapped in a `data` envelope) each hold their own time-series array of `{timestp, value}` points.\n\nFor Spotify, expect `followers`, `listeners`, `popularity`, and `followers_to_listeners_ratio`. Other platforms expose their own set (e.g. `subscribers` and `views` for `youtube_channel`).", "properties": { "status": { "type": "string" }, - "data": { + "link": { + "type": "string", + "description": "Canonical source URL on the upstream platform (e.g. the artist's Spotify URL)." + }, + "followers": { + "type": "array", + "description": "Time series of follower counts, when applicable to the source.", + "items": { + "type": "object", + "properties": { + "timestp": { "type": "string" }, + "value": { "type": "number" } + }, + "additionalProperties": true + } + }, + "listeners": { + "type": "array", + "description": "Time series of listener counts (e.g. Spotify monthly listeners), when applicable.", + "items": { + "type": "object", + "properties": { + "timestp": { "type": "string" }, + "value": { "type": "number" } + }, + "additionalProperties": true + } + }, + "popularity": { "type": "array", - "description": "Array of time-series data points. Fields vary by platform.", + "description": "Time series of the platform's popularity score, when applicable.", "items": { "type": "object", + "properties": { + "timestp": { "type": "string" }, + "value": { "type": "number" } + }, + "additionalProperties": true + } + }, + "followers_to_listeners_ratio": { + "type": "array", + "description": "Derived time series, when both followers and listeners are available (Spotify).", + "items": { + "type": "object", + "properties": { + "timestp": { "type": "string" }, + "value": { "type": "number" } + }, "additionalProperties": true } } @@ -4550,28 +4720,114 @@ }, "ResearchPlaylistResponse": { "type": "object", - "description": "Playlist metadata — name, description, follower count, track count, and curator info.", + "description": "Playlist metadata — name, description, follower count, track count, curator info, and Chartmetric-derived signals (editorial flag, suspicion score, active-ratio, week/month delta, mood/activity/genre tags).", "properties": { "status": { "type": "string" }, + "id": { + "type": "integer", + "description": "Chartmetric playlist ID (same as the `id` query param)." + }, "name": { "type": "string" }, - "id": { - "type": "integer" - }, "description": { - "type": "string" + "type": "string", + "nullable": true + }, + "image_url": { + "type": "string", + "nullable": true + }, + "playlist_id": { + "type": "string", + "description": "Platform-native playlist ID (e.g. Spotify base62)." + }, + "owner_name": { + "type": "string", + "description": "Curator display name (what used to be advertised as `curator_name`).", + "nullable": true + }, + "owner_id": { + "type": "integer", + "nullable": true + }, + "user_id": { + "type": "string", + "nullable": true }, "followers": { - "type": "integer" + "type": "integer", + "nullable": true }, - "num_tracks": { - "type": "integer" + "num_track": { + "type": "integer", + "description": "Track count on the playlist (note: field is `num_track`, not `num_tracks`)." }, - "curator_name": { - "type": "string" + "editorial": { + "type": "boolean", + "description": "True if this is a first-party editorial playlist (e.g. Spotify's RapCaviar)." + }, + "personalized": { + "type": "boolean" + }, + "code2": { + "type": "string", + "nullable": true, + "description": "ISO-3166 alpha-2 country code associated with the playlist, when known." + }, + "catalog": { + "type": "string", + "nullable": true + }, + "active_ratio": { + "type": "integer", + "description": "Fraction of days recently that the playlist was updated (Chartmetric-internal)." + }, + "suspicion_score": { + "type": "integer", + "description": "Chartmetric's internal playlist-suspicion score (lower is better)." + }, + "fdiff_week": { + "type": "integer", + "description": "Follower change over the last week." + }, + "fdiff_month": { + "type": "integer", + "description": "Follower change over the last month." + }, + "last_updated": { + "type": "string", + "description": "ISO timestamp the playlist was last modified upstream." + }, + "sys_last_updated": { + "type": "string", + "description": "ISO timestamp Chartmetric last synced the playlist." + }, + "genres": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "moods": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "activities": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "mood_smart_ordered": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "activity_smart_ordered": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "tags": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true @@ -4597,47 +4853,140 @@ }, "ResearchProfileResponse": { "type": "object", - "description": "Full artist profile — bio, genres, social links, label, images, and basic stats.", + "description": "Full artist profile — identity, biography, genres, geography, label/management, and Chartmetric-derived ranks/statistics. For longitudinal platform metrics (Spotify followers/listeners over time, social stats, etc.) call `GET /api/research/metrics` instead.", "properties": { "status": { "type": "string" }, + "id": { + "type": "integer", + "description": "Chartmetric artist ID." + }, "name": { "type": "string" }, - "id": { - "type": "integer", - "description": "Chartmetric artist ID" + "description": { + "type": "string", + "nullable": true }, "image_url": { - "type": "string" + "type": "string", + "nullable": true + }, + "cover_url": { + "type": "string", + "nullable": true + }, + "code2": { + "type": "string", + "nullable": true, + "description": "ISO country code associated with the artist." + }, + "isni": { + "type": "string", + "nullable": true + }, + "band": { + "type": "boolean", + "description": "True if the artist is a band/group rather than a solo act." + }, + "band_members": { + "nullable": true, + "description": "Band-member objects when known." + }, + "gender": { + "type": "string", + "nullable": true + }, + "gender_title": { + "type": "string", + "nullable": true + }, + "pronoun_title": { + "type": "string", + "nullable": true + }, + "hometown_city": { + "nullable": true, + "description": "Chartmetric hometown-city object (null when unknown)." + }, + "current_city": { + "type": "string", + "nullable": true + }, + "current_city_id": { + "type": "integer", + "nullable": true + }, + "cm_artist_rank": { + "type": "integer", + "description": "Overall Chartmetric artist rank (lower is better)." + }, + "cm_artist_score": { + "type": "number", + "description": "Chartmetric artist-score aggregate (higher is better)." + }, + "cm_statistics": { + "type": "object", + "additionalProperties": true, + "description": "Aggregated cross-platform snapshot — Spotify followers/monthly_listeners, Instagram/TikTok/YouTube counts, chart positions, etc. Fields may be null per-platform." + }, + "career_status": { + "type": "object", + "additionalProperties": true, + "description": "Current career stage + momentum (e.g. `superstar`/`mainstream`/`developing` + `growth`/`steady`/`decline`)." }, "genres": { - "type": "array", - "items": { - "type": "string" - } + "description": "Primary/secondary/sub-genre breakdown. Object with keyed subgroups, not a flat array.", + "type": "object", + "additionalProperties": true }, - "tags": { + "genre_smart_ordered": { "type": "array", - "items": { - "type": "string" - } + "items": { "type": "object", "additionalProperties": true } }, - "description": { - "type": "string" + "genreRank": { + "type": "object", + "additionalProperties": true }, - "hometown": { - "type": "string" + "subGenreRank1": { + "type": "object", + "additionalProperties": true + }, + "subGenreRank2": { + "type": "object", + "additionalProperties": true + }, + "moods": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "activities": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } }, "record_label": { - "type": "string" + "type": "string", + "nullable": true }, - "sp_followers": { - "type": "integer" + "booking_agent": { + "type": "string", + "nullable": true }, - "sp_monthly_listeners": { - "type": "integer" + "press_contact": { + "type": "string", + "nullable": true + }, + "general_manager": { + "type": "string", + "nullable": true + }, + "topSongwriterCollaborators": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "created_at": { + "type": "string" } }, "additionalProperties": true @@ -4940,6 +5289,28 @@ }, "additionalProperties": true } + }, + "cm_statistics": { + "type": "object", + "description": "Chartmetric's aggregated cross-platform statistics for the track — Spotify popularity/streams, TikTok video counts, YouTube views, Shazam counts, Apple/Deezer/Amazon playlist reach, weekly deltas, etc. Fields are often null for less-popular tracks. Opaque object; inspect an actual response for the full field set.", + "additionalProperties": true + }, + "tempo": { + "type": "number", + "nullable": true, + "description": "Track tempo in BPM, when available." + }, + "moods": { + "type": "array", + "nullable": true, + "description": "Chartmetric-inferred mood tags (e.g. `Happy`, `Melancholic`).", + "items": { "type": "object", "additionalProperties": true } + }, + "activities": { + "type": "array", + "nullable": true, + "description": "Chartmetric-inferred activity tags (e.g. `Workout`, `Studying`).", + "items": { "type": "object", "additionalProperties": true } } }, "additionalProperties": true