Say the track you promoted did 6,400 Spotify streams in August, and two older releases by the same artist jumped in the same month. The distributor statement shows both. It does not say which of those streams came from listeners who connected through the campaign.
Labels call that second movement catalog lift or catalog spillover. Spotify for Artists calls it “other releases” on Marquee and Showcase: listeners who saw the campaign, then streamed the rest of the catalog, counted for 14 days after the impression. That figure is a campaign report. It is not a royalty line. The streams still show up on the statement as unlabeled Spotify streams.
This guide is the statement version for a Soundlink track campaign. Split catalog engagement into the promoted recording and the other releases, then join both by ISRC and month.
TL;DR
On a track campaign, engagement_context = 'catalog' holds plays from listeners who connected through that campaign: the ISRC you paid to promote, and other tracks whose first listed artist matches that track. Split them with the campaign’s Spotify track id first, then ISRC. Price both at your statement’s revenue per stream. Use Soundlink for the campaign window, the statement for before and after.
This is part 3. Part 1 syncs the warehouse. Part 2 is the playlist version of this join. Position does not apply here.
The promoted track is not the whole campaign
A track campaign has one target: one Spotify track, one ISRC when it resolves. Listeners who connect and then play the artist’s other recordings still show up, on different ISRCs, in the same engagement export. They do not have to have streamed the promoted track first. Artist radio, autoplay, the artist page, and an album session after the ad all land in catalog if the first listed artist matches.
The statement counts every Spotify stream of each ISRC, from every source, including editorial, Release Radar, and playlist adds that had nothing to do with the ad. The answer sits in the split, and the split is easy to get wrong if you treat every catalog row as an other release. The promoted recording is in catalog too.

Soundlink only sees listeners who connected Spotify through the campaign soundlink. That is a different count from Spotify for Artists (impression-based, 14-day window), and a different count from the streams your distributor was actually paid for.
Everything below reduces to three buckets.
Three buckets
| Bucket | Where it lives | How you recognize it |
|---|---|---|
| Promoted track | Soundlink, engagement_context = 'catalog' |
engaged_spotify_track_id = campaign_target_spotify_track_id, or the ISRCs match |
| Other releases | Same rows | Spotify id and ISRC both differ from the target |
| Everything else on the statement | Distributor, Spotify rows | ISRCs with no Soundlink catalog row in the campaign window |
“Promoted” and “other releases” here are both a floor. They count plays from listeners who connected Spotify through the campaign soundlink, from the moment they connected, while the campaign is active. A save that turns into album plays months later, from someone who never connected, is not in either bucket. It can still show up on the statement.
These rows stay on the same primary artist. That means the first listed artist on the played track matches the first listed artist on the campaign track. A feature, remix, or compilation where your artist is not listed first will not appear. Plays of a different artist are not in this export.
Same recording, same ISRC, on the single and the album: that is the promoted bucket, not spillover. Clean vs explicit, radio edit, live, acoustic, or a new master are different ISRCs. If connected listeners play those, they belong in other releases.
Rules before you split
| Rule | Why |
|---|---|
| Track campaigns only | campaign_target_type = 'track'. Playlist campaigns use engagement_context = 'playlist' and are covered in part 2. |
| Split on Spotify id, then ISRC | The campaign target is a Spotify track. ISRCs can be null or disagree between the catalog and the play event. If the promoted title shows up under other releases, the ISRCs did not match. |
| Join the statement on ISRC | Royalties are paid on ISRC, not on title. Two versions of a song can share a name and not an ISRC. |
| Drop unresolved ISRCs | engaged_track_isrc is null when it could not be resolved. Those rows are not an other release and not the promoted track. Leave them out of both buckets. |
Sum streams, never listeners, across days |
The same listener can appear on more than one report_date. Summing listeners double-counts people. Summing streams counts plays. |
| Campaign window = days with spend | After the campaign ends, engagement sync for those listeners stops, so a drop in Soundlink streams is not proof they left. See Campaign metrics. |
| Statement for before and after | Soundlink does not have a complete “after.” The statement does, once the distributor has reported that month. Those reports often land one to three months after the streams, and they count what Spotify paid, not every play in Spotify for Artists. |
| Spotify rows only, sale month | Soundlink only sees Spotify. Filter the statement the same way, and use the sale month if the file also has a statement month. |
Leave the last 7 days of Soundlink data out of any conclusion. Those rows can still change. See data mutability.
The workflow
You need the same DuckDB file as parts 1 and 2: campaign_engagement_daily, campaign_country_daily, and distributor_monthly loaded from your statement (ISRC, Spotify-only, sale month, streams, revenue, currency). If the statement table is not there yet, part 2 has the load query.
1. Promoted track vs other releases
WITH campaign_days AS (
SELECT MIN(report_date) AS first_day, MAX(report_date) AS last_day
FROM campaign_country_daily
WHERE campaign_id = 'YOUR_CAMPAIGN_ID'
AND spend_total > 0
)
SELECT
CASE
WHEN e.engaged_spotify_track_id = e.campaign_target_spotify_track_id THEN 'promoted'
WHEN e.engaged_track_isrc = e.campaign_target_isrc THEN 'promoted'
ELSE 'other_releases'
END AS bucket,
e.engaged_track_isrc AS isrc,
ANY_VALUE(e.engaged_track_name) AS track_name,
SUM(e.streams) AS soundlink_streams
FROM campaign_engagement_daily e
CROSS JOIN campaign_days w
WHERE e.campaign_id = 'YOUR_CAMPAIGN_ID'
AND e.campaign_target_type = 'track'
AND e.engagement_context = 'catalog'
AND e.engaged_track_isrc IS NOT NULL
AND e.campaign_target_spotify_track_id IS NOT NULL
AND e.report_date BETWEEN w.first_day AND w.last_day
GROUP BY ALL
ORDER BY bucket, soundlink_streams DESC;If other releases are a few percent of the promoted track, stop. The rest of this post will not change whether the campaign paid for itself. If two or three other ISRCs carry a real share of soundlink_streams, keep going. That is the part finance cannot see on the promoted ISRC alone.
playlist_position is null on these rows. Do not reuse the position query from part 2.
These queries need campaign_target_spotify_track_id. On a track campaign that value comes from the promoted track. If it is null, do not guess a bucket. A null engaged_track_isrc is already excluded. That row cannot join to the statement.
2. What share of each ISRC the campaign listeners account for
WITH campaign_days AS (
SELECT MIN(report_date) AS first_day, MAX(report_date) AS last_day
FROM campaign_country_daily
WHERE campaign_id = 'YOUR_CAMPAIGN_ID'
AND spend_total > 0
),
soundlink AS (
SELECT
CASE
WHEN e.engaged_spotify_track_id = e.campaign_target_spotify_track_id THEN 'promoted'
WHEN e.engaged_track_isrc = e.campaign_target_isrc THEN 'promoted'
ELSE 'other_releases'
END AS bucket,
e.engaged_track_isrc AS isrc,
CAST(date_trunc('month', e.report_date) AS DATE) AS month,
SUM(e.streams) AS soundlink_streams
FROM campaign_engagement_daily e
CROSS JOIN campaign_days w
WHERE e.campaign_id = 'YOUR_CAMPAIGN_ID'
AND e.campaign_target_type = 'track'
AND e.engagement_context = 'catalog'
AND e.engaged_track_isrc IS NOT NULL
AND e.campaign_target_spotify_track_id IS NOT NULL
AND e.report_date BETWEEN w.first_day AND w.last_day
GROUP BY ALL
),
distributor AS (
SELECT isrc, month, SUM(streams) AS distributor_streams
FROM distributor_monthly
GROUP BY ALL
)
SELECT
b.bucket,
d.isrc,
d.month,
COALESCE(s.soundlink_streams, 0) AS soundlink_streams,
d.distributor_streams,
ROUND(COALESCE(s.soundlink_streams, 0) / NULLIF(d.distributor_streams, 0), 3) AS attributed_share
FROM distributor d
JOIN (SELECT DISTINCT bucket, isrc FROM soundlink) b USING (isrc)
LEFT JOIN soundlink s USING (bucket, isrc, month)
ORDER BY b.bucket, d.isrc, d.month;Months before and after the campaign show as zeros instead of missing rows.
Read the share as a signal, the same way as in the playlist post. Spotify for Artists, Soundlink, and the distributor statement do not count the same plays. People who have lined the three up usually find the statement later, and a bit lower, because unpaid and filtered streams never become royalties. 20% of an older release’s Spotify streams sitting in other_releases, in the campaign month only, is a result. 20.1% versus 19.8% is noise.
An other-release ISRC with Soundlink streams and no statement row is usually a report that has not closed yet, a store filter you missed, or a track below Spotify’s royalty threshold. Do not invent revenue for it.
3. Before, during, after, on the other releases only
This query ignores the promoted recording on purpose. You already know that one was advertised. The open question is whether the other ISRCs moved on the statement in the same months.
Do not dump every catalog ISRC that got a handful of plays into this. The query keeps other-release ISRCs that did at least 5% of the promoted track’s Soundlink streams in the spend window. Raise that if you only want the two or three finance would put next to spend. An older hit that picked up six campaign streams will otherwise drag the whole statement.
WITH campaign_days AS (
SELECT
MIN(report_date) AS first_day,
MAX(report_date) AS last_day,
date_trunc('month', MIN(report_date)) AS first_month,
date_trunc('month', MAX(report_date)) AS last_month
FROM campaign_country_daily
WHERE campaign_id = 'YOUR_CAMPAIGN_ID'
AND spend_total > 0
),
promoted AS (
SELECT SUM(e.streams) AS promoted_streams
FROM campaign_engagement_daily e
CROSS JOIN campaign_days w
WHERE e.campaign_id = 'YOUR_CAMPAIGN_ID'
AND e.campaign_target_type = 'track'
AND e.engagement_context = 'catalog'
AND e.engaged_track_isrc IS NOT NULL
AND e.campaign_target_spotify_track_id IS NOT NULL
AND (
e.engaged_spotify_track_id = e.campaign_target_spotify_track_id
OR e.engaged_track_isrc = e.campaign_target_isrc
)
AND e.report_date BETWEEN w.first_day AND w.last_day
),
other_release_tracks AS (
SELECT e.engaged_track_isrc AS isrc
FROM campaign_engagement_daily e
CROSS JOIN campaign_days w
CROSS JOIN promoted p
WHERE e.campaign_id = 'YOUR_CAMPAIGN_ID'
AND e.campaign_target_type = 'track'
AND e.engagement_context = 'catalog'
AND e.engaged_track_isrc IS NOT NULL
AND e.campaign_target_spotify_track_id IS NOT NULL
AND e.engaged_spotify_track_id IS DISTINCT FROM e.campaign_target_spotify_track_id
AND e.engaged_track_isrc IS DISTINCT FROM e.campaign_target_isrc
AND e.report_date BETWEEN w.first_day AND w.last_day
GROUP BY e.engaged_track_isrc, p.promoted_streams
HAVING SUM(e.streams) >= 0.05 * p.promoted_streams
)
SELECT
CASE
WHEN d.month < w.first_month THEN '1_before'
WHEN d.month <= w.last_month THEN '2_during'
ELSE '3_after'
END AS period,
COUNT(DISTINCT d.month) AS months,
SUM(d.streams) AS distributor_streams,
ROUND(SUM(d.streams) / COUNT(DISTINCT d.month)) AS streams_per_month
FROM distributor_monthly d
JOIN other_release_tracks USING (isrc)
CROSS JOIN campaign_days w
GROUP BY ALL
ORDER BY period;Compare streams_per_month. The periods are different lengths. A campaign that starts mid-month makes the first “during” month partly “before,” which only matters when the gap between periods is small.
A rise in the statement with an empty other_releases bucket in query 1 is not the campaign. It is the rest of Spotify: editorial, Release Radar, Discover Weekly, a playlist add, a sync. Query 3 without query 1 will show you that rise and call it the ad.

4. Did the ad spend pay for itself, including other releases
Part 2 priced playlist streams at the statement’s revenue per stream. Same estimate here, on both buckets. Spend stays spend_total from campaign_country_daily: what was billed, fees included.
This recoup_ratio is ad spend against estimated recording revenue on the statement. It is not a label-deal recoup. It does not apply an artist royalty rate, and it does not pay down an advance.
WITH campaign_days AS (
SELECT MIN(report_date) AS first_day, MAX(report_date) AS last_day
FROM campaign_country_daily
WHERE campaign_id = 'YOUR_CAMPAIGN_ID'
AND spend_total > 0
),
spend AS (
SELECT SUM(spend_total) AS spend_usd
FROM campaign_country_daily
WHERE campaign_id = 'YOUR_CAMPAIGN_ID'
),
soundlink AS (
SELECT
CASE
WHEN e.engaged_spotify_track_id = e.campaign_target_spotify_track_id THEN 'promoted'
WHEN e.engaged_track_isrc = e.campaign_target_isrc THEN 'promoted'
ELSE 'other_releases'
END AS bucket,
e.engaged_track_isrc AS isrc,
CAST(date_trunc('month', e.report_date) AS DATE) AS month,
SUM(e.streams) AS soundlink_streams
FROM campaign_engagement_daily e
CROSS JOIN campaign_days w
WHERE e.campaign_id = 'YOUR_CAMPAIGN_ID'
AND e.campaign_target_type = 'track'
AND e.engagement_context = 'catalog'
AND e.engaged_track_isrc IS NOT NULL
AND e.campaign_target_spotify_track_id IS NOT NULL
AND e.report_date BETWEEN w.first_day AND w.last_day
GROUP BY ALL
),
rate AS (
SELECT isrc, month, SUM(revenue) / NULLIF(SUM(streams), 0) AS revenue_per_stream
FROM distributor_monthly
WHERE currency = 'USD'
GROUP BY ALL
)
SELECT
s.bucket,
ROUND(SUM(s.soundlink_streams * r.revenue_per_stream), 2) AS attributed_revenue_estimate,
ANY_VALUE(spend.spend_usd) AS spend_usd,
ROUND(
SUM(s.soundlink_streams * r.revenue_per_stream) / ANY_VALUE(spend.spend_usd),
3
) AS recoup_ratio
FROM soundlink s
JOIN rate r USING (isrc, month)
CROSS JOIN spend
GROUP BY s.bucket
ORDER BY s.bucket;
Read the two rows before you add them. A ratio that only clears 1 because of other releases is a different decision from one that clears on the promoted track. spend_usd repeats on both rows; do not add it. The 5% cut from query 3 does not apply here: recoup prices the Soundlink streams, so a handful of plays on a catalog hit is cents, not the whole statement.
It is an estimate, not an audit. It leaves out listeners who never connected, plays after the spend window, and statement rows that are not USD. Tracks below Spotify’s 1,000-stream royalty eligibility threshold can show streams and zero revenue. spend_total is what Soundlink billed, fees included. Statement revenue may already be net of the distributor’s commission, and for a label it is before the artist’s share.
Mistakes that make the numbers lie
Calling every catalog row an other release. The promoted ISRC is in engagement_context = 'catalog' too. Skip the CASE and you will report the campaign track as spillover.
Treating a statement lift as attribution. Query 3 can rise because the artist had a release, an editorial add, or algorithm traffic, not because campaign listeners played the old songs. If query 1’s other_releases bucket is thin, do not quote query 3.
Counting the album as spillover when it is the same ISRC. If the single and the album use the same recording, those streams are the promoted bucket. Clean, explicit, live, and acoustic versions are the ones that belong in other releases.
Summing listeners to get “how many people spilled over.” Across days that sum is not a reach number. If you need people, stay inside one report_date, or use the streams.
Reading the post-campaign cliff in Soundlink. Engagement after spend stops is partial. The “after” column has to come from the statement.
Checklist
- [ ] Campaign is
campaign_target_type = 'track', not a playlist - [ ] Warehouse sync from part 1; statement table from part 2; Spotify rows, sale month
- [ ] Catalog rows split on Spotify track id, then ISRC; null ISRCs excluded; campaign window limited to spend days
- [ ] Attributed share and before/during/after run on other-release ISRCs at or above 5% of the promoted track’s Soundlink streams, not only the promoted track
- [ ] Payback shown per bucket. Add the two
recoup_ratiovalues for the total. Do not addspend_usd. Labeled as an estimate, not an advance recoup
Frequently Asked Questions
Does this include plays of other artists?
No. catalog is the promoted track plus other tracks whose first listed artist matches that track’s first listed artist. Another artist’s streams are not in these rows. A feature or remix where your artist is not listed first is not in them either.
Can I use this on a playlist campaign?
Use part 2. Playlist rows are engagement_context = 'playlist', and the split that matters there is position, not promoted-versus-catalog.
What if the target ISRC is null?
Split on engaged_spotify_track_id vs campaign_target_spotify_track_id. Do not fall back to track names. Rows with a null engaged_track_isrc still cannot join to the statement. If both Spotify ids are missing, do not guess a bucket.
Does the album version of the single count as other releases?
Only if it is a different ISRC. Same recording on the single and the album, same ISRC: promoted. A new mix, a live cut, or a different master: other releases, if connected listeners played it.
If query 1 shows other releases you would actually put next to the ad spend, the next argument with finance is no longer “did this one campaign work.” It is how that spend rolls up across the roster.
Next in the series: campaign spend by artist, so that payback conversation happens per artist instead of per campaign.



