Retention curve and pitch
Where viewers leave the video, how many reach the pitch, and where they click and buy
Three questions shape a sales video: where do people stop watching, how many are still there when the pitch starts, and at which moment do they click and buy. Each has its query, and together they draw the video second by second.
You need the player's ID, the video's duration and the pitch time. GET /players/list returns all three (id, duration, pitch_time).
Retention curve
POST /times/user_engagement returns where each viewing ended:
curl -X POST 'https://analytics.vturb.com/times/user_engagement' \
-H "X-Api-Token: $VTURB_API_TOKEN" \
-H 'X-Api-Version: v1' \
-H 'User-Agent: my-integration/1.0' \
-H 'Content-Type: application/json' \
-d '{
"player_id": "64a5c8072e6fd10009828db2",
"video_duration": 2340,
"start_date": "2026-09-01 00:00:00",
"end_date": "2026-09-07 23:59:59",
"timezone": "America/Sao_Paulo"
}'video_duration, the video's length in seconds, is required here, as are both dates. Send the real length, the duration from GET /players/list: a session whose furthest point is more than 60 seconds past video_duration is left out.
{
"grouped_timed": [
{ "timed": 0, "total_users": 412 },
{ "timed": 5, "total_users": 96 },
{ "timed": 10, "total_users": 58 }
],
"average_watched_time": 900.9,
"engagement_rate": 38.5
}- Each item of
grouped_timedcounts the sessions whose furthest point in the video was exactly that second (timed): where they stopped. Seconds that nobody stopped at are left out. - The player reports how far each viewer got in five-second steps, so
timedcomes in multiples of 5. average_watched_timeis how far a session got, on average, in seconds.engagement_rateis that average as a share ofvideo_duration:average_watched_time ÷ video_duration × 100.
The curve is how many sessions reached each second: everyone who stopped at that second or later. Add the items up from the end:
function retentionCurve(groupedTimed, videoDuration) {
const stoppedAt = new Map(groupedTimed.map(({ timed, total_users }) => [timed, total_users]));
const total = groupedTimed.reduce((sum, { total_users }) => sum + total_users, 0);
const curve = [];
let reached = total;
for (let second = 0; second <= videoDuration; second++) {
curve.push({ second, reached, percent: total ? (reached / total) * 100 : 0 });
reached -= stoppedAt.get(second) ?? 0;
}
return curve;
}A steep drop in the first seconds points to the opening; a drop right before the pitch, to the build-up that leads to it.
Pitch retention
How many sessions reached the pitch comes ready in POST /sessions/stats:
curl -X POST 'https://analytics.vturb.com/sessions/stats' \
-H "X-Api-Token: $VTURB_API_TOKEN" \
-H 'X-Api-Version: v1' \
-H 'User-Agent: my-integration/1.0' \
-H 'Content-Type: application/json' \
-d '{
"player_id": "64a5c8072e6fd10009828db2",
"start_date": "2026-09-01 00:00:00",
"end_date": "2026-09-07 23:59:59",
"timezone": "America/Sao_Paulo"
}'In the response, total_over_pitch counts the sessions that reached pitch_time, total_under_pitch those that left before it, and over_pitch_rate is the share that reached it. Without pitch_time in the body, or with 0, the API uses the one set on the player; send it, in seconds, to test another moment, such as a pitch you are about to move. When the player has no pitch time and you send none, the pitch is second 0: every session reaches it, and over_pitch_rate is 100. The other fields are the same as in the daily report.
Where they buy
POST /conversions/video_timed places each sale at the second of the video when the viewer clicked to buy:
curl -X POST 'https://analytics.vturb.com/conversions/video_timed' \
-H "X-Api-Token: $VTURB_API_TOKEN" \
-H 'X-Api-Version: v1' \
-H 'User-Agent: my-integration/1.0' \
-H 'Content-Type: application/json' \
-d '{
"player_id": "64a5c8072e6fd10009828db2",
"start_date": "2026-09-01 00:00:00",
"end_date": "2026-09-07 23:59:59",
"timezone": "America/Sao_Paulo",
"pitch_time": 1260
}'[
{
"timed": 1260,
"timed_conversions": 9,
"cumulative_conversions": 9,
"timed_amount_usd": 0,
"cumulative_amount_usd": 0,
"timed_amount_brl": 179100,
"cumulative_amount_brl": 179100,
"timed_amount_eur": 0,
"cumulative_amount_eur": 0
},
{
"timed": 1305,
"timed_conversions": 4,
"cumulative_conversions": 13,
"timed_amount_usd": 0,
"cumulative_amount_usd": 0,
"timed_amount_brl": 79600,
"cumulative_amount_brl": 258700,
"timed_amount_eur": 0,
"cumulative_amount_eur": 0
}
]timed_conversionscounts the sessions with an initial sale whose click happened attimed;cumulative_conversionsadds them up to that second.timed_amount_*sums, per currency and as an integer in cents, the amounts of every purchase clicked at that second, order bumps included;cumulative_amount_*adds them up to that second.start_dateandend_dateselect the sales by the time of the click.- With
pitch_time, a sale whose click came before the pitch is counted at the pitch second, so the first row gathers everything up to the pitch. Leave it out to see every click at its own second.
Where they click
POST /clicks/total_by_company_timed takes the player, the dates and timezone, and returns the clicks on the player's calls to action at each second:
[
{ "timed": 1260, "total_users": 31 },
{ "timed": 1261, "total_users": 12 }
]total_users is the number of clicks registered at that second.
Put it together
Plot the retention curve, mark pitch_time on it, and lay the clicks and the sales over the same seconds. The gap between the clicks and the sales after the pitch shows how much of the interest the checkout turns into sales.
Next: find out which traffic sources bring the viewers who stay.