---
topic: API endpoints (ows-playlist, graphql-analytics, graphql-knowledge)
last_updated: 2026-03-16
---

# Playlist Pages API Endpoints

## ows-playlist (Python/Flask)

Repo: `/Users/cbeesley/code/ows-playlist`
Constants: `/Users/cbeesley/code/ows-playlist/playlist/queries/constants.py`
Handlers: `/Users/cbeesley/code/ows-playlist/playlist/handlers.py`

### Endpoints

| Method | Route | Handler | Notes |
|--------|-------|---------|-------|
| POST | `/playlist/analytics-bulk-timeseries` | `get_playlists_analytics_timeseries` | PoT chart streams+listeners timeseries |
| POST | `/playlist/analytics-bulk` | `get_playlists_analytics` | Bulk analytics aggregates |
| POST | `/playlist-metadata-bulk` | `get_playlist_metadata_bulk` | Bulk metadata lookup |
| GET | `/playlist-ids` | `get_playlist_ids` | Returns list of tracked playlist IDs |
| GET | `/playlist/<store_playlist_id>/dates/` | `get_playlist_available_dates` | Available date range for playlist |
| GET | `/playlist/<store_playlist_id>/placements` | `get_placements_by_store_playlist_id` | Placements on a given playlist |
| POST | `/playlist/<store_playlist_id>/placements` | `get_placements_by_store_playlist_id` | Same (POST variant) |
| GET | `/playlist/<store_playlist_id>/placements/on-date` | `get_placements_by_store_playlist_id_on_date` | Tracklist on specific date |
| GET | `/playlist/<store_playlist_id>/placement/<isrc>` | `get_placement` | Single placement detail |
| GET | `/playlist/<store_playlist_id>/placement/<isrc>/positions` | `get_placement_position_time_series` | Position timeseries |
| GET | `/playlist/<store_playlist_id>/placement/<isrc>/streams` | `get_placement_streams_time_series` | Streams timeseries for placement |
| GET | `/playlist/<store_playlist_id>/demographics/` | `get_playlist_demographics` | Demographics by country |
| GET | `/placements` | `get_placements` | Placements query (ISRC/product scoped) |
| GET | `/placements_count` | `get_placements_count` | Count of placements |
| GET | `/placements/recent` | `get_recent_placements` | Recent placements (track-playlist-distributor) |
| GET | `/placements/breakdown` | `get_placement_metrics_by_store_playlisttype` | Breakdown by store and playlist type |
| GET | `/placements/total_vs_playlist_streams_by_store` | `get_total_vs_playlist_streams_by_store` | Total vs playlist streams |
| GET | `/placements/country/<store_playlist_id>/<isrc>` | `get_placement_metrics_by_country` | Country-level placement metrics |
| GET | `/placements_by_isrc` | `get_placements_by_isrc` | Placements filtered by ISRC |
| GET | `/product/<product_id>/placements` | `get_placements_by_product` | Placements for product |
| GET | `/global-participant/<global_participant_id>/placements` | `get_placements_by_global_participant_id` | Placements for participant |
| POST | `/playlist/placement/company-brand` | `get_company_brand_for_playlist_placement` | Company brand for placement |

### Key Recent Changes (ows-playlist)

- `2c149864a` — **Migrate remaining table references to V_ views and prevent double-prefixing** — follow-up to `9af87fb71`, catches any remaining direct table refs and fixes a double-prefix bug introduced in that migration
- `9af87fb71` — **Migrate external table references to V_ views** — All Snowflake table references now use `V_` unioned views as stable external interface, per the dbt tier split. JinjaSQL templates also updated.
- `64a926637` — Remove unused DISTRIBUTOR filter from historical tracklist query
- `fe88dc8c8` — `IN-16847`: Add `first_added_on_date` field
- `4e2c01b9d` — `IN-16817`: Replace hardcoded FF value, use pre-agg table for placements
- `e460d1853` / `64cb16caf` — `IN-16834`: Fix non-deterministic position time series; fix streams missing data

### V_ View References in Constants

Streams timeseries: `V_STREAMS_BY_PLAYLIST_COUNTRY_FEED_DISTRIBUTOR_DAILY`
Followers: `V_PLAYLISTS_FOLLOWERS_BY_PLAYLIST_DATE_SPOTIFY`
Demographics: `V_DEMOGRAPHICS_BY_PLAYLIST_COUNTRY`
Placement metadata: `v_playlists_by_playlist`
Private placements: `v_playlists_placements_by_isrc_playlist_country_private`
Placement events: `v_playlists_placement_events_by_isrc_playlist`

### Storefront Handling (ows-playlist)

- `STOREFRONT_ENABLED` flag (from `handler_constants`) routes queries to storefront-aware tables
- Apple Music playlists use `storefront` (e.g., "US", "GB") for regional variants
- `_map_country_to_storefront()` normalizes country params
- `_fetch_playlist_data_separated_by_storefront()` handles Apple Music multi-storefront responses

## graphql-analytics

Repo: `/Users/cbeesley/code/graphql-analytics`
OWS connector: `src/connectors/ows-playlist/`

### DataLoaders (all call ows-playlist)

- `playlistAnalyticsByDimensionDataLoader`
- `playlistAnalyticsDataLoader`
- `playlistAnalyticsTimeseriesByDimensionDataLoader`
- `playlistAnalyticsTimeseriesDataLoader`
- `playlistAnalyticsTotalStreamsAndListenersDataloader`
- `playlistDatesDataLoader`
- `playlistIdsDataLoader`
- `playlistMetadataDataLoader`
- `playlistPlacementAnalyticsDataLoader`
- `playlistPlacementCompanyBrandsDataLoader`
- `playlistPlacementPositionTimeSeriesDataLoader`
- `playlistPlacementsByDateDataLoader`
- `playlistPlacementsByPlaylistIdDataLoader`

### Playlist Placement API: `placements` vs `placementsV2`

The `playlistPlacements` field on `Playlist` returns `TopPlaylistsPlacements`, which has two tracklist fields:

| Field | Type | Status | Notes |
|-------|------|--------|-------|
| `placements` | `[PlaylistPlacement!]!` | **@deprecated** | Silently omits placeholder tracks |
| `placementsV2` | `[PlaylistPlacementResult!]!` | **Use this** | Includes both `PlaylistPlacement` and `PlaylistPlacementPlaceholder` rows |

`PlaylistPlacementResult` is a union type:
```graphql
union PlaylistPlacementResult = PlaylistPlacement | PlaylistPlacementPlaceholder
```

Always use `placementsV2` with `__typename` inline fragments:
```graphql
placementsV2 {
  ... on PlaylistPlacement {
    globalSoundRecording { isrc name }
    currentPosition
    playlistPlacementStreams { avgStreams7d growthPeriods { period value completionRate } }
  }
  ... on PlaylistPlacementPlaceholder {
    isrc
    trackName   # Chartmetric data; fallback "Unknown Track" set in graphql-analytics formatter
    artistName  # Chartmetric data; nullable
    imageUrl    # Chartmetric artwork URL; null → grey avatar in frontend
    currentPosition
    playlistPlacementStreams { avgStreams7d growthPeriods { period value completionRate } }
  }
}
```

**ows-playlist response shape** (validated by `topPlaylistPlacementsSchema` Zod schema):
```json
{
  "data": {
    "placements": [...],                   // tracks with Neo4j GSR match
    "placeholder_placements": [...],       // tracks without Neo4j GSR
    "total_count": 50
  }
}
```
Placeholder fields from ows-playlist: `track_name`, `artist_name`, `artwork_url` (all nullable). The formatter merges both arrays, sorts by `currentPosition`, and produces `placementsV2`.

**"Unknown Track"** is injected in the formatter at `topPlaylistPlacements.ts` line ~171: `trackName: placement.track_name ?? 'Unknown Track'` — NOT in the frontend.

### Key Recent Changes (graphql-analytics)

- `fb45e2ec` — Expose `insights_playlist_page_hide_compilation_art` flag to enable `INFERRED_COMPILATION` flagging
- `cf88e652` — Add formatter for `inferredCompilation` on past tracklist
- `3a6005c1` — Integration tests for `inferredCompilation`
- `e74f3d36` — Pass compilations FF to ows-playlist
- `5d0221f9` — `IN-16790`: Expose `INFERRED_COMPILATION` flag in response for client
- `35a31a2d` — Remove unused deprecated playlist demographic queries
- `fea369a7` — `IN-16755`: Update playlist page schemas to support storefront
- `0ae3516d` — `IN-16414-2`: Add `playlistDemographicsV2` returning streams for each country
- `ceeabe8c` — `IN-16422`: Implement Followers backend PoT data

### Feature Flags Passed to ows-playlist

- `INSIGHTS_PLAYLIST_PAGE_HIDE_COMPILATION_ART_KEY = 'insights_playlist_page_hide_compilation_art'`
- `STOREFRONT_ENABLED_KEY = 'storefront_enabled'`
- Passed as query params when calling ows-playlist endpoints

## graphql-knowledge

Repo: `/Users/cbeesley/code/graphql-knowledge`
Snowflake connector: `src/connectors/snowflake/`

### Playlist Metadata Dataloader

File: `src/connectors/snowflake/dataloaders/playlistMetadataById.ts`

Queries: `V_PLAYLIST_METADATA` view via `INNER JOIN` with playlist filter tables.

Cache key format:

- Spotify/non-Apple: `Playlist:id:{storePlaylistId}:storeId:{storeId}`
- Apple Music with storefront: `Playlist:id:{storePlaylistId}:storeId:{storeId}:storefront:{storefront}`
- TTL: 1 hour (Redis)

`includeHourlyPlaylists` parameter controls whether `hourly_playlists` table is included in the filter subquery.

### Playlist Search — Direct ID Lookup Behaviour

`searchPlaylists()` in `src/connectors/snowflake/snowflake.ts` detects playlist IDs before falling through to Cortex search:

1. **Spotify URI/URL** (`spotify:playlist:...` or `open.spotify.com/playlist/...`) — extracts ID and calls `getPlaylistMetadataById` directly. No search performed.
2. **Bare Spotify ID** (exactly 22 alphanumeric chars) — optimistically tries direct lookup; falls back to Cortex search only if not found.
3. **Apple Music playlist ID** (detected via `extractAppleMusicPlaylistId`) — direct lookup. No storefront specified; dataloader prefers US variant, falls back to first available.
4. **Anything else** — full Cortex search via Snowflake REST API.

For Apple Music direct lookups, no storefront is specified — the dataloader will prefer the US storefront variant and fall back to the first available if US is not found.

### Cortex Search (Snowflake REST API)

graphql-knowledge calls the Snowflake Cortex Search REST API via `src/connectors/snowflake/snowflakeSearchApi.ts`.

- Auth: JWT key-pair (`SNOWFLAKE_ACCOUNT`, `SNOWFLAKE_USERNAME`, `SNOWFLAKE_KEY` or `SNOWFLAKE_PRIVATE_KEY_PATH`)
- Endpoint env var: `SNOWFLAKE_SEARCH_API_URL`
- Timeout: `SNOWFLAKE_SEARCH_API_TIMEOUT_MS` (default 2000ms)

**⚠️ Scoring profiles cannot be Terraformed and must be re-applied manually after every deploy of the Cortex Search service.** Recreating the service (via Liquibase or Terraform) destroys the `POPULARITY_BOOST` profile — it must be re-applied by hand in Snowflake console with `ACCOUNTADMIN`. No Terraform provider support as of 2026-03-16. Observable symptom: search silently falls back (code retries without profile on "Scoring profile not found" error).

### Redis Cache Key Patterns (graphql-knowledge)

All playlist metadata caching uses `@theorchard/dataloader-redis-zod` (`RedisZodDataLoader`).

**playlistMetadataById** — TTL: 1 hour (3600s)

| Condition | Cache key |
|-----------|-----------|
| Spotify / non-Apple (no storefront) | `Playlist:id:{storePlaylistId}:storeId:{storeId}` |
| Apple Music (storeId=`'1'`) with storefront | `Playlist:id:{storePlaylistId}:storeId:{storeId}:storefront:{storefront}` |
| No storeId provided | `Playlist:id:{storePlaylistId}` |

Source: `playlistMetadataById.ts` → `cacheKeyFn`

### Redis Cache Key Patterns (graphql-analytics)

All loaders use `@theorchard/dataloader-redis-zod` unless noted. Storefront is resolved via `resolveStorefrontForCacheKey()` — for Apple Music with `streamCountries`, storefront is derived from countries; otherwise uses provided storefront or `'null'`.

| Loader | Redis? | TTL | Cache key pattern |
|--------|--------|-----|-------------------|
| `playlistMetadataDataLoader` | Yes | 24h | `PlaylistMetadata:{playlistId}-{storeId}-{storefront\|null}` |
| `playlistPlacementAnalyticsDataLoader` | Yes | 1h | `GSRPlaylistPlacementAnalytics:{isrc}-{playlistId}-{storeId}-{storefront\|null}-{args}` |
| `playlistPlacementsByPlaylistIdDataLoader` | Yes | 1h | `PlaylistPlacements:{playlistId}-{storeId}-{storefront\|null}-{args}` |
| `playlistPlacementsByDateDataLoader` | Yes | 1 week | (historical data immutable) |
| `playlistIdsDataLoader` | No (ows-playlist caches) | 1 week at ows-playlist | no GraphQL-layer cache key |
| `playlistAnalyticsByDimensionDataLoader` | No (ZodDataLoader, in-memory) | request-scoped | `PlaylistAnalyticsByDimension:{playlistId}-{storeId}-{storefront}-{dimension}-{orderBy\|null}-{orderDir\|null}` |
| `playlistAnalyticsTimeseriesDataLoader` | No (ZodDataLoader) | request-scoped | `PlaylistAnalyticsTimeseries:{playlistId}-{storeId}-{storefront}-{streamCountries}-{startDate}-...` |
| `playlistAnalyticsTimeseriesByDimensionDataLoader` | No (ZodDataLoader) | request-scoped | `PlaylistAnalyticsTimeseriesByDimension:{playlistId}-{storefront}-...` |
| `playlistPlacementPositionTimeSeriesDataLoader` | No (ZodDataLoader) | request-scoped | `PlacementPositionTS:{isrc}-{playlistId}-{storeId}-{storefront\|null}-{startDate}-{endDate}` |
| `playlistPlacementCompanyBrandsDataLoader` | No (ZodDataLoader) | request-scoped | `PlaylistPlacementCompanyBrands:{playlistId}-{storeId}-{isrc}-{storefront}` |

**Storefront normalisation in cache keys**: Apple Music requests always include storefront in the key. Spotify and others use `'null'` as the storefront segment when not provided, to avoid key collisions.

### Key Recent Changes (graphql-knowledge)

- `56d880b3e` — **Consolidate playlist metadata tables into V_PLAYLIST_METADATA view** (IN-1523, after revert+retry)
- `9641149d2` — `IN-16903`: Add curator market filter to metadata endpoint
- `3f5a49ef0` — `IN-16903`: Add pagination to playlistsMetadataList endpoint
- `8e04c153c` — `IN-16933`: Fix GLOBAL curator market filter
- `0afaa46b9` — `IN-16762`: Update playlist search to support playlist ID search for AM playlists
- `5e2732135` — `IN-16757`: Add `playlistStorefronts` endpoint
