API Endpoints
All endpoints are relative to the base URL http://localhost:8000. Versioned endpoints require authentication via session cookie unless noted otherwise.
Global middleware: All requests pass through rate limiting (100 req/60s), request ID injection, 30s timeout (120s for slow endpoints), GZIP compression, CORS, and security headers.
Health
GET /api/health
No auth required. Returns basic service status.
Response:
{ "status": "healthy", "service": "echostats-api", "version": "0.1.0"}GET /api/health/ready
No auth required. Checks MongoDB and Redis connectivity.
Response (ready):
{ "status": "ready" }Response (not ready):
{ "status": "not_ready", "reason": "database not connected, cache unreachable"}GET /api/health/update
No auth required. Checks for new releases on GitHub (cached for 1 hour).
Response:
{ "current_version": "0.1.0", "latest_version": "0.2.0", "update_available": true, "release_url": "https://github.com/spotify-devs/echostats/releases/tag/v0.2.0", "release_notes": "Bug fixes and performance improvements...", "published_at": "2025-01-15T12:00:00Z"}Authentication
All auth endpoints are prefixed with /api/v1/auth.
GET /api/v1/auth/login
No auth required. Initiates Spotify OAuth flow.
- Browser (
Accept: text/html): Redirects (302) to Spotify authorization page. - API client: Returns the authorization URL as JSON.
Response (API client):
{ "url": "https://accounts.spotify.com/authorize?client_id=...", "state": "abc123"}GET /api/v1/auth/callback
No auth required. Handles the Spotify OAuth callback.
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | yes | Authorization code from Spotify |
state | string | yes | CSRF state token |
error | string | no | Error returned by Spotify |
Success: Redirects to {frontend_url}/auth/callback?status=success and sets an httpOnly session cookie. Creates or updates the user document and stores encrypted Spotify tokens. Triggers a background sync.
Error: Returns 400 (bad request) or 502 (Spotify API error).
GET /api/v1/auth/status
No auth required. Returns current authentication state.
Response (authenticated):
{ "authenticated": true, "user": { "id": "682a3b...", "spotify_id": "atul123", "display_name": "Atul", "image_url": "https://i.scdn.co/image/...", "product": "premium" }, "token_expires_at": "2025-02-01T12:00:00Z"}Response (not authenticated):
{ "authenticated": false }POST /api/v1/auth/refresh
Refreshes the Spotify access token using the stored refresh token.
Response:
{ "status": "refreshed" }Error: 401 if not authenticated or token refresh fails.
POST /api/v1/auth/logout
No auth required. Clears the session cookie.
Response:
{ "status": "logged_out" }GET /api/v1/auth/dev-login
Response:
{ "status": "authenticated", "user": { "id": "682a3b...", "spotify_id": "demo_user", "display_name": "Demo User" }, "token": "eyJhbGciOi..."}Analytics
All analytics endpoints are prefixed with /api/v1/analytics.
GET /api/v1/analytics/overview
Returns pre-computed analytics for the given period.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
Response:
{ "period": "week", "total_tracks_played": 319, "total_ms_played": 64140000, "total_hours": 17.8, "unique_tracks": 279, "unique_artists": 81, "unique_albums": 45, "unique_genres": 11, "listening_streak_days": 5, "top_artists": [ { "name": "Radiohead", "play_count": 15, "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb", "image_url": "https://...", "rank": 1 } ], "top_tracks": [ { "name": "Everything In Its Right Place", "play_count": 5, "spotify_id": "2kRFrWaLWifQkBFasAWgMo", "rank": 1 } ], "top_albums": [ { "name": "Kid A", "play_count": 12, "spotify_id": "6GjwtEZcfenmof6l18N7T7", "rank": 1 } ], "top_genres": [ { "name": "art rock", "play_count": 30, "rank": 1 } ], "hourly_distribution": [ { "hour": 0, "plays": 5 }, { "hour": 14, "plays": 42 } ], "daily_distribution": [ { "day": "Monday", "plays": 48 } ], "avg_audio_features": { "danceability": 0.52, "energy": 0.61, "valence": 0.38, "acousticness": 0.24, "instrumentalness": 0.15, "liveness": 0.12, "speechiness": 0.05, "tempo": 122.5 }, "computed_at": "2025-01-15T12:00:00Z"}POST /api/v1/analytics/refresh
Recomputes analytics for the given period. If period is all_time, all periods are refreshed.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
Response:
{ "status": "refreshed", "periods": ["week", "month", "quarter", "year", "all_time"], "computed_at": "2025-01-15T12:05:00Z"}GET /api/v1/analytics/trend
Returns listening trend data points over the given period.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
Granularity rules:
week/month→ daily points (2025-01-15)quarter→ weekly points (2025-W03)year/all_time→ monthly points (2025-01)
Response:
{ "period": "month", "granularity": "%Y-%m-%d", "points": [ { "label": "2025-01-01", "plays": 42, "ms": 8400000, "hours": 2.3 }, { "label": "2025-01-02", "plays": 38, "ms": 7600000, "hours": 2.1 } ]}GET /api/v1/analytics/rollup-status
Returns the status of daily rollup pre-aggregation.
Response:
{ "last_date": "2025-01-15", "total_days": 90, "complete": true, "coverage": 100.0}POST /api/v1/analytics/rollup-build
Triggers a background job to rebuild daily rollups.
Response:
{ "status": "started", "job_id": "682a3b..."}Returns "status": "already_running" if a rollup build is in progress.
History
All history endpoints are prefixed with /api/v1/history.
GET /api/v1/history
Returns paginated listening history with optional filters.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (min: 1) |
limit | int | 50 | Items per page (1–100) |
artist | string | — | Filter by artist name |
track | string | — | Filter by track name |
start_date | string | — | ISO 8601 start date |
end_date | string | — | ISO 8601 end date |
Response:
{ "items": [ { "id": "682a3b...", "user_id": "682a3a...", "track": { "spotify_id": "2kRFrWaLWifQkBFasAWgMo", "name": "Everything In Its Right Place", "artist_name": "Radiohead", "album_name": "Kid A", "album_image_url": "https://i.scdn.co/image/...", "duration_ms": 250000 }, "played_at": "2025-01-15T14:32:00Z", "ms_played": 245000, "source": "api", "context_type": "album", "context_uri": "spotify:album:6GjwtEZcfenmof6l18N7T7" } ], "total": 1520, "page": 1, "limit": 50, "pages": 31}POST /api/v1/history/import
Imports listening history from a Spotify privacy data export (JSON file).
Request: Multipart file upload.
| Field | Type | Description |
|---|---|---|
file | UploadFile | JSON file from Spotify extended streaming history export |
Response:
{ "job_id": "682a3b...", "status": "pending", "items_processed": 0, "items_total": 12450}The import runs as a background job. Use the Sync Jobs endpoints to track progress.
Tracks
All track endpoints are prefixed with /api/v1/tracks.
GET /api/v1/tracks/top
Returns the user’s top tracks by play count for the given period.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
limit | int | 50 | Number of tracks (1–100) |
Response:
{ "items": [ { "id": "682a3b...", "spotify_id": "2kRFrWaLWifQkBFasAWgMo", "name": "Everything In Its Right Place", "artists": [{ "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb", "name": "Radiohead" }], "album": { "spotify_id": "6GjwtEZcfenmof6l18N7T7", "name": "Kid A", "image_url": "https://i.scdn.co/image/..." }, "duration_ms": 250000, "popularity": 72, "external_url": "https://open.spotify.com/track/2kRFrWaLWifQkBFasAWgMo", "audio_features": { "danceability": 0.45, "energy": 0.63, "valence": 0.22, "tempo": 156.1 } } ], "period": "all_time", "total": 279}GET /api/v1/tracks/{track_id}
No auth required. Returns a single track by Spotify ID.
| Parameter | Type | Description |
|---|---|---|
track_id | string | Spotify track ID |
Response:
{ "id": "682a3b...", "spotify_id": "2kRFrWaLWifQkBFasAWgMo", "name": "Everything In Its Right Place", "artists": [{ "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb", "name": "Radiohead" }], "album": { "spotify_id": "6GjwtEZcfenmof6l18N7T7", "name": "Kid A", "image_url": "https://i.scdn.co/image/...", "release_date": "2000-10-02" }, "duration_ms": 250000, "popularity": 72, "explicit": false, "external_url": "https://open.spotify.com/track/2kRFrWaLWifQkBFasAWgMo", "audio_features": { "danceability": 0.45, "energy": 0.63, "valence": 0.22, "acousticness": 0.04, "instrumentalness": 0.73, "liveness": 0.11, "speechiness": 0.04, "tempo": 156.1, "key": 4, "mode": 1, "time_signature": 4 }}Error: 404 if the track is not found.
Artists
All artist endpoints are prefixed with /api/v1/artists.
GET /api/v1/artists/top
Returns the user’s top artists by play count for the given period.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
limit | int | 50 | Number of artists (1–100) |
Response:
{ "items": [ { "id": "682a3b...", "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb", "name": "Radiohead", "image_url": "https://i.scdn.co/image/...", "genres": ["art rock", "alternative rock", "experimental"], "popularity": 78, "followers": 8500000, "external_url": "https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb" } ], "period": "all_time", "total": 81}GET /api/v1/artists/{artist_id}
No auth required. Returns a single artist by Spotify ID.
| Parameter | Type | Description |
|---|---|---|
artist_id | string | Spotify artist ID |
Response:
{ "id": "682a3b...", "spotify_id": "4Z8W4fKeB5YxbusRsdQVPb", "name": "Radiohead", "image_url": "https://i.scdn.co/image/...", "genres": ["art rock", "alternative rock", "experimental"], "popularity": 78, "followers": 8500000, "external_url": "https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb"}Error: 404 if the artist is not found.
Albums
All album endpoints are prefixed with /api/v1/albums.
GET /api/v1/albums/top
Returns the user’s top albums by play count for the given period.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
limit | int | 50 | Number of albums (1–100) |
Response:
{ "items": [ { "id": "682a3b...", "spotify_id": "6GjwtEZcfenmof6l18N7T7", "name": "Kid A", "artists": ["Radiohead"], "image_url": "https://i.scdn.co/image/...", "release_date": "2000-10-02", "total_tracks": 10, "type": "album", "external_url": "https://open.spotify.com/album/6GjwtEZcfenmof6l18N7T7" } ], "period": "all_time", "total": 45}Genres
All genre endpoints are prefixed with /api/v1/genres.
GET /api/v1/genres/distribution
Returns genre distribution for the user’s listening history.
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | all_time | One of week, month, quarter, year, all_time |
Response:
{ "genres": [ { "name": "art rock", "play_count": 120, "percentage": 18.5 }, { "name": "alternative rock", "play_count": 95, "percentage": 14.6 }, { "name": "indie rock", "play_count": 72, "percentage": 11.1 } ], "period": "all_time", "total_genres": 11}Playlists
All playlist endpoints are prefixed with /api/v1/playlists.
GET /api/v1/playlists
Returns the user’s synced playlists with pagination.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (min: 1) |
limit | int | 50 | Items per page (1–100) |
Response:
{ "items": [ { "id": "682a3b...", "user_id": "682a3a...", "spotify_id": "37i9dQZF1DXcBWIGoYBM5M", "name": "Today's Top Hits", "description": "The hottest tracks right now.", "image_url": "https://i.scdn.co/image/...", "tracks_total": 50, "owner": "Spotify", "public": true, "collaborative": false, "external_url": "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M" } ], "total": 24, "page": 1, "limit": 50}Player
All player endpoints are prefixed with /api/v1/player. These endpoints proxy directly to the Spotify Web API and require a valid Spotify access token.
GET /api/v1/player/current
Returns the currently playing track.
Response (playing):
{ "is_playing": true, "track_name": "Everything In Its Right Place", "artist_name": "Radiohead", "album_name": "Kid A", "album_image": "https://i.scdn.co/image/...", "track_id": "2kRFrWaLWifQkBFasAWgMo", "duration_ms": 250000, "progress_ms": 120000, "device": "MacBook Pro", "shuffle": false, "repeat": "off"}Response (not playing):
{ "is_playing": false }GET /api/v1/player/devices
Returns available Spotify Connect devices.
Response:
{ "devices": [ { "id": "abc123", "name": "MacBook Pro", "type": "Computer", "is_active": true, "volume_percent": 65 } ]}GET /api/v1/player/queue
Returns the current playback queue (up to 20 items).
Response:
{ "currently_playing": { "id": "2kRFrWaLWifQkBFasAWgMo", "name": "Everything In Its Right Place", "artist": "Radiohead", "album": "Kid A", "image": "https://i.scdn.co/image/...", "duration_ms": 250000 }, "queue": [ { "id": "6LgJvl0Xdtc73RJ1mZoUnj", "name": "Kid A", "artist": "Radiohead", "album": "Kid A", "image": "https://i.scdn.co/image/...", "duration_ms": 280000 } ]}POST /api/v1/player/play
Start or resume playback.
Request body (all fields optional):
{ "context_uri": "spotify:album:6GjwtEZcfenmof6l18N7T7", "uris": ["spotify:track:2kRFrWaLWifQkBFasAWgMo"], "device_id": "abc123"}Response:
{ "status": "playing" }POST /api/v1/player/pause
Pause playback.
Response:
{ "status": "paused" }POST /api/v1/player/next
Skip to next track.
Response:
{ "status": "skipped" }POST /api/v1/player/previous
Skip to previous track.
Response:
{ "status": "skipped_back" }Browse
All browse endpoints are prefixed with /api/v1/browse. These proxy to the Spotify Web API.
GET /api/v1/browse/new-releases
Returns new album releases from Spotify.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of results |
Response:
{ "items": [ { "id": "6GjwtEZcfenmof6l18N7T7", "name": "Kid A Mnesia", "artists": "Radiohead", "image": "https://i.scdn.co/image/...", "release_date": "2021-11-05", "total_tracks": 34, "type": "compilation" } ]}GET /api/v1/browse/featured-playlists
Returns Spotify’s featured playlists.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of results |
Response:
{ "message": "Good afternoon", "items": [ { "id": "37i9dQZF1DXcBWIGoYBM5M", "name": "Today's Top Hits", "description": "The hottest tracks right now.", "image": "https://i.scdn.co/image/...", "tracks_total": 50, "owner": "Spotify" } ]}GET /api/v1/browse/categories
Returns Spotify browse categories.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Number of results |
Response:
{ "items": [ { "id": "toplists", "name": "Top Lists", "image": "https://t.scdn.co/media/..." } ]}Library
All library endpoints are prefixed with /api/v1/library. These proxy to the Spotify Web API.
GET /api/v1/library/followed-artists
Returns artists the user follows on Spotify.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Number of artists (1–50) |
Response:
{ "items": [ { "id": "4Z8W4fKeB5YxbusRsdQVPb", "name": "Radiohead", "image": "https://i.scdn.co/image/...", "followers": 8500000, "genres": ["art rock", "alternative rock"] } ], "total": 142}GET /api/v1/library/saved-albums
Returns albums saved to the user’s Spotify library.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Number of albums (1–50) |
offset | int | 0 | Pagination offset |
Response:
{ "items": [ { "id": "6GjwtEZcfenmof6l18N7T7", "name": "Kid A", "artists": "Radiohead", "image": "https://i.scdn.co/image/...", "release_date": "2000-10-02", "total_tracks": 10, "added_at": "2024-06-15T10:30:00Z" } ], "total": 85, "limit": 50, "offset": 0}Recommendations
GET /api/v1/recommendations
Returns track recommendations based on the user’s listening analytics.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of recommendations (1–50) |
seed_type | string | mixed | One of artists, tracks, genres, mixed |
Seeds are derived from the user’s analytics snapshot (top artists, tracks, and genres).
Response:
{ "items": [ { "id": "3EYHN9PZiFaz5tEspCPPDr", "name": "Paranoid Android", "artists": ["Radiohead"], "artist": "Radiohead", "album": "OK Computer", "album_image": "https://i.scdn.co/image/...", "duration_ms": 383000, "popularity": 75, "audio_features": { "danceability": 0.38, "energy": 0.72, "valence": 0.19 }, "external_url": "https://open.spotify.com/track/3EYHN9PZiFaz5tEspCPPDr" } ], "total": 20, "seed_info": { "type": "mixed", "top_artist": "Radiohead", "top_genre": "art rock" }}Returns {"items": [], "seed_info": {}} if no analytics snapshot is available.
Sync Jobs
All sync job endpoints are prefixed with /api/v1/sync-jobs.
GET /api/v1/sync-jobs
Returns paginated list of sync jobs.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (min: 1) |
limit | int | 20 | Items per page (1–100) |
status | string | — | Filter: pending, running, completed, failed |
job_type | string | — | Filter: periodic, rollup_build, import, initial |
Response:
{ "items": [ { "id": "682a3b...", "job_type": "periodic", "status": "completed", "items_processed": 50, "items_total": 50, "error_message": null, "started_at": "2025-01-15T14:00:00Z", "completed_at": "2025-01-15T14:00:12Z", "created_at": "2025-01-15T14:00:00Z", "steps": [ { "action": "recently_played", "status": "completed", "detail": "Fetched 50 recent tracks", "items": 50, "error": null }, { "action": "audio_features", "status": "completed", "detail": "Updated audio features for 12 tracks", "items": 12, "error": null } ] } ], "total": 145, "page": 1, "pages": 8}GET /api/v1/sync-jobs/stats
Returns aggregate sync job statistics.
Response:
{ "total_jobs": 145, "completed": 140, "failed": 3, "running": 1, "total_items_synced": 15200, "last_sync_at": "2025-01-15T14:00:12Z"}POST /api/v1/sync-jobs/trigger
Manually triggers a data sync. Runs: recently played → audio features → playlists → top items → analytics refresh.
Response:
{ "status": "started", "job_id": "682a3b...", "message": "Sync job started"}Returns "status": "already_running" if a sync is in progress.
API Logs
All API log endpoints are prefixed with /api/v1/api-logs. These track outbound calls to the Spotify API.
GET /api/v1/api-logs
Returns paginated API call logs.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (min: 1) |
limit | int | 50 | Items per page (1–200) |
service | string | — | Filter by service (e.g. spotify) |
status_min | int | — | Minimum HTTP status code |
status_max | int | — | Maximum HTTP status code |
method | string | — | HTTP method (GET, POST, etc.) |
endpoint_contains | string | — | Filter by endpoint substring |
Response:
{ "items": [ { "id": "682a3b...", "service": "spotify", "method": "GET", "endpoint": "/v1/me/player/recently-played", "status_code": 200, "latency_ms": 142.5, "error": null, "timestamp": "2025-01-15T14:00:01Z" } ], "total": 3200, "page": 1, "limit": 50, "pages": 64}GET /api/v1/api-logs/stats
Returns aggregate API call statistics.
Response:
{ "total_calls": 3200, "error_count": 15, "rate_limit_count": 2, "success_rate": 99.5, "avg_latency_ms": 185.3, "status_distribution": { "2xx": 3180, "4xx": 12, "5xx": 8 }, "top_endpoints": [ { "endpoint": "GET /v1/me/player/recently-played", "count": 1450 }, { "endpoint": "GET /v1/audio-features", "count": 820 } ]}Error Responses
All endpoints use consistent error shapes.
Validation Error (422)
{ "detail": [ { "loc": ["query", "limit"], "msg": "ensure this value is less than or equal to 100", "type": "value_error" } ]}Authentication Error (401)
{ "detail": "Not authenticated" }Not Found (404)
{ "detail": "Track not found" }Service Unavailable (503)
Returned when MongoDB or Redis is unreachable. Includes a Retry-After header.
{ "detail": "Service temporarily unavailable" }Internal Server Error (500)
{ "detail": "Internal server error" }