Analytics
Generate and download daily streaming analytics for Spotify, YouTube Content ID, YouTube Art Track, Apple Music, Amazon Music and TikTok.
Resource Overview
Analytics Reports allow you to export full daily streaming statistics for a specific DSP as a downloadable ZIP file containing JSON data. Reports are generated asynchronously — you request the report, then poll for its status until it completes, and finally download the file via a pre-signed URL.
/analytics-reports
POST, GET
ZIP (JSON inside)
Owner-restricted
How It Works
Unlike standard JSON:API resources that return data synchronously, Analytics Reports use an asynchronous workflow because the datasets can be very large (10,000+ records per day). The process has three steps:
Request a report
Send a POST with the DSP ID and date. You receive a report ID and status pending.
Poll for status
Send a GET with the report ID. The status progresses: pending → processing → completed (or failed).
Download the file
When completed, the response includes a download_url — a pre-signed URL valid for 12 hours. The file is a ZIP containing a single JSON file.
Supported DSPs
| DSP | dsp_id | Description |
|---|---|---|
| Spotify | 1 | Daily track stats including streams, skips, demographics, device/source breakdown, engagement metrics. |
| YouTube Content ID | 2 | Daily track stats including views, watch time, engagement, estimated revenue, and country, source, device and playback breakdowns. |
| YouTube Art Track | 3 | Same record shape as YouTube Content ID, for Art Track content. |
| Apple Music | 4 | Daily track stats including plays, listeners, and storefront, device, source, format, subscription, city and demographic breakdowns. |
| Amazon Music | 6 | Daily track stats including streams, offline plays, play duration, and country, device, source, quality, and subscription-plan breakdowns. |
| TikTok | 9 | Daily track stats including views, watch time, video creations, engagement (likes, comments, shares, favourites), and country breakdown. |
/dsps. Only the DSPs listed above are supported for analytics reports.
Step 1: Create a Report
/analytics-reports
POST /analytics-reports
Authorization: Bearer {token}
Content-Type: application/json
Accept: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| dsp_id | integer | Yes | The DSP to generate the report for. Must be 1 (Spotify), 2 (YouTube Content ID), 3 (YouTube Art Track), 4 (Apple Music), 6 (Amazon Music) or 9 (TikTok). |
| date | string | Yes | The date to export in YYYY-MM-DD format. Only a single day can be requested per report. |
Example Request
POST /analytics-reports
Authorization: Bearer {token}
Content-Type: application/json
{
"dsp_id": 1,
"date": "2026-03-28"
}
Example Response (201 Created)
{
"data": {
"id": 6,
"dsp_id": 1,
"dsp_name": "Spotify",
"date": "2026-03-28",
"status": "pending",
"total_records": null,
"file_size_bytes": null,
"error": null,
"download_url": null,
"started_at": null,
"completed_at": null,
"created_at": "2026-04-01T16:00:39+00:00"
}
}
Error: Unsupported DSP (422)
{
"error": "DSP not supported for analytics export",
"supported_dsp_ids": [1, 2, 3, 4, 6, 9]
}
Error: Too many active reports (409)
{
"error": "You have reached the maximum of 20 active reports",
"active_count": 20
}
Error: Report could not be created (502)
{
"error": "Report could not be created"
}
The report was not created. Retry later.
Step 2: Poll Report Status
/analytics-reports/{id}
GET /analytics-reports/{id}
Authorization: Bearer {token}
Accept: application/json
Status Values
| Status | Description | Action |
|---|---|---|
| pending | Report is queued and waiting to be processed. | Poll again in 1 minute. |
| processing | Report is currently being generated. | Poll again in 1 minute. |
| completed | Report is ready. download_url is available. |
Download the file from download_url. |
| failed | Report generation failed. The error field reads Report generation failed. |
Request the report again. |
Response Fields
Both POST (201) and GET (200) answer the same report object.
| Field | Type | Description |
|---|---|---|
| id | integer | Report ID |
| dsp_id | integer | DSP the report was requested for |
| dsp_name | string | DSP name |
| date | string | Requested date, YYYY-MM-DD |
| status | string | pending, processing, completed or failed |
| total_records | integer or null | Number of records in the file. A date with no data produces a completed report with total_records: 0 and an empty data array — it is not an error |
| file_size_bytes | integer or null | Size in bytes of the JSON file before compression (uncompressed) |
| error | string or null | Present only when status is failed, and always the text Report generation failed |
| download_url | string or null | Pre-signed URL valid for 12 hours, present only when status is completed. Each GET returns a fresh URL |
| started_at | string or null | When generation started |
| completed_at | string or null | When generation finished |
| created_at | string | When the report was requested |
Example Response (completed)
{
"data": {
"id": 6,
"dsp_id": 1,
"dsp_name": "Spotify",
"date": "2026-03-28",
"status": "completed",
"total_records": 9938,
"file_size_bytes": 3355443,
"error": null,
"download_url": "https://s3.amazonaws.com/...?X-Amz-Signature=...",
"started_at": "2026-04-01T16:00:41+00:00",
"completed_at": "2026-04-01T16:00:49+00:00",
"created_at": "2026-04-01T16:00:39+00:00"
}
}
Example Response (failed)
{
"data": {
"id": 7,
"dsp_id": 4,
"dsp_name": "Apple Music",
"date": "2026-03-28",
"status": "failed",
"total_records": null,
"file_size_bytes": null,
"error": "Report generation failed",
"download_url": null,
"started_at": "2026-04-01T16:00:42+00:00",
"completed_at": null,
"created_at": "2026-04-01T16:00:40+00:00"
}
}
Error: Report not found (404)
{
"error": "Report not found"
}
The report does not exist or is not yours.
Error: Report not available (502)
{
"error": "Report not available"
}
The report status could not be read right now. Retry later.
Polling Recommendation
We recommend polling every 1 minute.
If you use webhooks, the analytics_report.completed and analytics_report.failed events are sent when a report finishes.
Step 3: Download the File
The download_url is a pre-signed URL that does not require authentication. Simply perform a GET request to download the ZIP file.
# No authorization header needed — the URL is pre-signed
curl -o report.zip "https://s3.amazonaws.com/...?X-Amz-Signature=..."
Download URL Expiration
The download URL is valid for 12 hours. Each GET /analytics-reports/{id} returns a fresh URL, so after expiration simply poll the report again.
File Format
The ZIP file contains a single JSON file named analytics-report-{date}.json. The JSON structure is:
{
"meta": {
"dsp_id": 1,
"dsp_name": "Spotify",
"date": "2026-03-28",
"total_records": 1250,
"generated_at": "2026-03-29T10:15:30+00:00",
"generated_by": 42
},
"data": [
{ ... },
...
]
}
The data array contains all records for the requested date, with every nested object fully expanded. The record shape depends on the DSP and is described below.
- Records have no
idfield. A record is identified bytrack_id+data_date, pluscontent_typefor YouTube and TikTok. data_dateuses the formatYYYY-MM-DDT00:00:00.000000Z.created_atandupdated_atare the time the report was generated.- A date with no data produces an empty
dataarray.
Spotify Records (dsp_id 1)
{
"data_date": "2026-03-28T00:00:00.000000Z",
"dsp_name": "Spotify",
"track_id": 5012,
"track_name": "Summer Breeze",
"album_id": 1830,
"album_name": "Coastal Dreams",
"spotify_track_id": "4a7b9c2d1e0f3a5b8c6d7e9f",
"isrc": "USRC12345678",
"country_stats": {
"US": {
"streams_count": 142,
"completion_rate": 72.5,
"valid_streams_count": 142,
"track_length_seconds": 215,
"total_reproduction_time": 24.831,
"significant_streams_count": 103,
"avg_reproduction_percentage": 81.2
},
"GB": {
"streams_count": 38,
"completion_rate": 68.4,
"valid_streams_count": 38,
"track_length_seconds": 215,
"total_reproduction_time": 6.107,
"significant_streams_count": 26,
"avg_reproduction_percentage": 74.8
}
},
"demographics": {
"age_groups": {"18-22": 45, "23-27": 62, "28-34": 38, "35-44": 21, "45-59": 14},
"genders": {"female": 88, "male": 79}
},
"aggregated_data": {
"total_streams": 180,
"total_skips": 47,
"total_saves": 0,
"total_shares": 0
},
"content_type_stats": {
"audio_streams": 172,
"video_streams": 8,
"total_streams": 180,
"audio_percentage": 96,
"video_percentage": 4
},
"geographic_stats": {
"connection_countries": {"US": 142, "GB": 38},
"reporting_countries": {"US": 142, "GB": 38},
"total_geolocated_streams": 180
},
"device_platform_stats": {
"device_types": {"cell phone": 112, "computer": 48, "tablet": 20},
"operating_systems": {"iOS": 78, "Android": 54, "Windows": 32, "macOS": 16},
"shuffle_usage": {"enabled": 95, "disabled": 85},
"repeat_usage": {"enabled": 12, "disabled": 168}
},
"source_stats": {
"sources": {"others_playlist": 72, "collection": 45, "radio": 33, "search": 30},
"source_uris": {"spotify:playlist:37i9dQZF1DX...": 42}
},
"engagement_stats": {
"completion_rate": 73.89,
"lyrics_viewed": 8,
"canvas_viewed": 3,
"discovery_streams": 33,
"total_analyzed": 180
},
"timestamp_reliability": {
"online_streams": 165,
"offline_streams": 15,
"total_streams": 180,
"online_percentage": 92,
"offline_percentage": 8
},
"created_at": "2026-03-29T10:15:30.000000Z",
"updated_at": "2026-03-29T10:15:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
| data_date | string | Date of the statistics, as YYYY-MM-DDT00:00:00.000000Z |
| dsp_name | string | DSP name (Spotify) |
| track_id | integer | Limbo track ID |
| track_name | string | Track title |
| album_id | integer | Limbo album ID |
| album_name | string | Album title |
| spotify_track_id | string | Spotify track identifier |
| isrc | string | International Standard Recording Code |
| country_stats | object | Per-country breakdown keyed by country code: streams_count, completion_rate, valid_streams_count, track_length_seconds, total_reproduction_time, significant_streams_count, avg_reproduction_percentage |
| demographics | object | Listener demographics: age_groups and genders |
| aggregated_data | object | Totals: total_streams, total_skips, total_saves, total_shares |
| content_type_stats | object | Audio vs video stream breakdown: audio_streams, video_streams, total_streams, audio_percentage, video_percentage |
| geographic_stats | object | connection_countries, reporting_countries and total_geolocated_streams |
| device_platform_stats | object | device_types, operating_systems, shuffle_usage and repeat_usage (each with enabled / disabled) |
| source_stats | object | Playback sources and source_uris. Each one may be an empty array [] when there is nothing to report |
| engagement_stats | object | completion_rate, lyrics_viewed, canvas_viewed, discovery_streams, total_analyzed |
| timestamp_reliability | object | Online vs offline streams: online_streams, offline_streams, total_streams, online_percentage, offline_percentage |
| created_at | string | Time the report was generated |
| updated_at | string | Time the report was generated |
YouTube Content ID and YouTube Art Track Records (dsp_id 2 and 3)
Both DSPs share the same record shape. content_type is content_id for dsp_id 2 and art_track for dsp_id 3.
{
"data_date": "2026-03-28T00:00:00.000000Z",
"track_id": 5012,
"dsp_id": 3,
"album_id": 1830,
"genre_id": 12,
"content_type": "art_track",
"views": 80,
"watch_time_minutes": 120,
"likes": 6,
"dislikes": 0,
"comments": 1,
"shares": 1,
"subscribers_gained": 0,
"subscribers_lost": 0,
"estimated_revenue": "0.09",
"estimated_ad_revenue": "0.01",
"estimated_red_partner_revenue": "0.08",
"country_stats": {
"US": {"views": 50, "watch_time_minutes": 70},
"GB": {"views": 30, "watch_time_minutes": 50}
},
"source_stats": {
"SEARCH": {"views": 20, "watch_time_minutes": 30},
"ADVERTISING": {"views": 60, "watch_time_minutes": 90}
},
"demographics": {
"AGE_25_34_MALE": {"views": 2},
"AGE_25_34_FEMALE": {"views": 1}
},
"device_stats": {
"TABLET": {"views": 70, "watch_time_minutes": 110},
"COMPUTER": {"views": 10, "watch_time_minutes": 10}
},
"playback_stats": {
"0": {"views": 62, "watch_time_minutes": 121}
},
"created_at": "2026-03-29T10:15:30.000000Z",
"updated_at": "2026-03-29T10:15:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
| data_date | string | Date of the statistics, as YYYY-MM-DDT00:00:00.000000Z |
| track_id | integer | Limbo track ID |
| dsp_id | integer | 2 (YouTube Content ID) or 3 (YouTube Art Track) |
| album_id | integer | Limbo album ID |
| genre_id | integer | Genre ID |
| content_type | string | content_id for dsp_id 2, art_track for dsp_id 3 |
| views | integer | Views |
| watch_time_minutes | integer | Watch time in minutes |
| likes | integer | Likes |
| dislikes | integer | Dislikes |
| comments | integer | Comments |
| shares | integer | Shares |
| subscribers_gained | integer | Subscribers gained |
| subscribers_lost | integer | Subscribers lost |
| estimated_revenue | string | Estimated revenue, as a decimal string (e.g. "0.09") |
| estimated_ad_revenue | string | Estimated ad revenue, as a decimal string |
| estimated_red_partner_revenue | string | Estimated Red partner revenue, as a decimal string |
| country_stats | object | Keyed by country code: {"US": {"views": n, "watch_time_minutes": n}} |
| source_stats | object | Keyed by traffic source: {"SEARCH": {"views": n, "watch_time_minutes": n}} |
| demographics | object or null | Keyed by age group and gender: {"AGE_25_34_MALE": {"views": n}}. null when empty |
| device_stats | object | Keyed by device type: {"TABLET": {"views": n, "watch_time_minutes": n}} |
| playback_stats | object | Keyed by playback type, each with views and watch_time_minutes |
| created_at | string | Time the report was generated |
| updated_at | string | Time the report was generated |
Apple Music Records (dsp_id 4)
Every *_stats, city_stats and demographics entry has the shape {KEY: {"plays": n, "listeners": n}}.
{
"data_date": "2026-03-28T00:00:00.000000Z",
"track_id": 5012,
"apple_track_id": "1600000001",
"apple_album_id": null,
"apple_artist_id": null,
"listener_count": 3,
"play_count": 5,
"storefront_stats": {"US": {"plays": 5, "listeners": 3}},
"device_stats": {"MOBILE": {"plays": 5, "listeners": 3}},
"source_stats": {"DISCOVERY": {"plays": 5, "listeners": 3}},
"audio_format_stats": {"COMPRESSED_AUDIO": {"plays": 5, "listeners": 3}},
"end_reason_stats": {"SKIP": {"plays": 2, "listeners": 1}},
"container_stats": {"RADIO_STATION": {"plays": 5, "listeners": 3}},
"container_sub_stats": {"DISCOVERY_RADIO_STATION": {"plays": 5, "listeners": 3}},
"device_os_stats": {"IOS": {"plays": 5, "listeners": 3}},
"subscription_type_stats": {"INDIVIDUAL_MONTHLY": {"plays": 5, "listeners": 3}},
"subscription_mode_stats": {"PAID": {"plays": 5, "listeners": 3}},
"city_stats": {"1000001": {"plays": 2, "listeners": 1}},
"demographics": {"AGE_25_TO_34_FEMALE": {"plays": 2, "listeners": 1}},
"created_at": "2026-03-29T10:15:30.000000Z",
"updated_at": "2026-03-29T10:15:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
| data_date | string | Date of the statistics, as YYYY-MM-DDT00:00:00.000000Z |
| track_id | integer | Limbo track ID |
| apple_track_id | string | Apple Music track identifier |
| apple_album_id | string or null | Apple Music album identifier. May be null |
| apple_artist_id | string or null | Apple Music artist identifier. May be null |
| listener_count | integer | Listeners |
| play_count | integer | Plays |
| storefront_stats | object | Keyed by storefront (country code) |
| device_stats | object | Keyed by device type |
| source_stats | object | Keyed by playback source |
| audio_format_stats | object | Keyed by audio format |
| end_reason_stats | object | Keyed by the reason playback ended |
| container_stats | object | Keyed by playback container |
| container_sub_stats | object | Keyed by playback container subtype |
| device_os_stats | object | Keyed by device operating system |
| subscription_type_stats | object | Keyed by subscription type |
| subscription_mode_stats | object | Keyed by subscription mode |
| city_stats | object | Keyed by city ID |
| demographics | object | Keyed by age group and gender |
| created_at | string | Time the report was generated |
| updated_at | string | Time the report was generated |
Amazon Music Records (dsp_id 6)
Every breakdown has the shape {KEY: {"streams": n, "offline_plays": n, "play_duration_seconds": n}}.
{
"data_date": "2026-03-28T00:00:00.000000Z",
"track_id": 5012,
"album_id": 1830,
"genre_id": 12,
"streams": 4,
"offline_plays": 1,
"play_duration_seconds": 800,
"country_stats": {"US": {"streams": 4, "offline_plays": 1, "play_duration_seconds": 800}},
"device_stats": {"MOBILE": {"streams": 4, "offline_plays": 1, "play_duration_seconds": 800}},
"source_stats": {"SONGS": {"streams": 4, "offline_plays": 1, "play_duration_seconds": 800}},
"quality_stats": {"STANDARD": {"streams": 4, "offline_plays": 1, "play_duration_seconds": 800}},
"plan_stats": {"Individual Monthly": {"streams": 4, "offline_plays": 1, "play_duration_seconds": 800}},
"created_at": "2026-03-29T10:15:30.000000Z",
"updated_at": "2026-03-29T10:15:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
| data_date | string | Date of the statistics, as YYYY-MM-DDT00:00:00.000000Z |
| track_id | integer | Limbo track ID |
| album_id | integer | Limbo album ID |
| genre_id | integer | Genre ID |
| streams | integer | Streams |
| offline_plays | integer | Offline plays |
| play_duration_seconds | integer | Total play duration in seconds |
| country_stats | object | Keyed by country code |
| device_stats | object | Keyed by device type |
| source_stats | object | Keyed by playback source |
| quality_stats | object | Keyed by audio quality |
| plan_stats | object | Keyed by subscription plan |
| created_at | string | Time the report was generated |
| updated_at | string | Time the report was generated |
TikTok Records (dsp_id 9)
There is one record per track and content_type (UGC or PGC).
{
"data_date": "2026-03-28T00:00:00.000000Z",
"track_id": 5012,
"album_id": 1830,
"content_type": "UGC",
"genre_id": 12,
"views": 300,
"watch_time_seconds": 900,
"creations": 2,
"comments": 4,
"likes": 20,
"shares": 3,
"favourites": 5,
"country_stats": {
"US": {
"views": 300,
"watch_time_seconds": 900,
"creations": 2,
"comments": 4,
"likes": 20,
"shares": 3,
"favourites": 5
}
},
"created_at": "2026-03-29T10:15:30.000000Z",
"updated_at": "2026-03-29T10:15:30.000000Z"
}
| Field | Type | Description |
|---|---|---|
| data_date | string | Date of the statistics, as YYYY-MM-DDT00:00:00.000000Z |
| track_id | integer | Limbo track ID |
| album_id | integer | Limbo album ID |
| content_type | string | UGC or PGC |
| genre_id | integer | Genre ID |
| views | integer | Views |
| watch_time_seconds | integer | Watch time in seconds |
| creations | integer | Videos created with the track |
| comments | integer | Comments |
| likes | integer | Likes |
| shares | integer | Shares |
| favourites | integer | Favourites |
| country_stats | object | Keyed by country code: views, watch_time_seconds, creations, comments, likes, shares, favourites |
| created_at | string | Time the report was generated |
| updated_at | string | Time the report was generated |
Limits
- One day per report — each request generates a report for a single date.
- Up to 20 active reports per user — you can request reports for every supported DSP in parallel.
- Download URL valid for 12 hours — poll again to get a fresh URL if expired.
- Owner-restricted — you can only see your own reports and your own tracks' data.
Complete Example
Here's a complete workflow requesting every supported DSP for the same day:
# 1. Request one report per supported DSP
for dsp in 1 2 3 4 6 9; do
curl -X POST /analytics-reports \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d "{\"dsp_id\": $dsp, \"date\": \"2026-03-28\"}"
done
# → {"data": {"id": 6, "status": "pending", ...}} (one response per DSP: keep each id)
# 2. Poll each id from step 1 until it is completed (every 1 minute); report 6 shown
curl /analytics-reports/6 -H "Authorization: Bearer {token}"
# → {"data": {"status": "completed", "download_url": "https://...", ...}}
# 3. Download each completed report's ZIP from its own download_url (no auth needed)
curl -o report-6-2026-03-28.zip "{download_url}"