Analytics APIGuides

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.

200 OK (first items)
{
  "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_timed counts 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 timed comes in multiples of 5.
  • average_watched_time is how far a session got, on average, in seconds.
  • engagement_rate is that average as a share of video_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
  }'
200 OK
[
  {
    "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_conversions counts the sessions with an initial sale whose click happened at timed; cumulative_conversions adds 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_date and end_date select 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:

200 OK
[
  { "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.

On this page