# Retention, impressions and CTR

> Retention, impressions and CTR: GET /v1/insights/retention. Retention (% watched and average watch time), impressions and CTR from YouTube, and skip rate from Instagram, per post. Data the network does not provide comes as null, with the reason in retention.message. Requires the yt-analytics.readonl

Source: https://praeko.app/docs/en/api/insights/retention-impressions-and-ctr

`GET /v1/insights/retention` · minimum scope: read

Retention (% watched and average watch time), impressions and CTR from YouTube, and skip rate from Instagram, per post. Data the network does not provide comes as `null`, with the reason in `retention.message`. Requires the `yt-analytics.readonly` permission, which the YouTube connection already requests (channels connected before it was added must reauthorize once).

> **Headers** `Authorization: Bearer praeko_…` and `X-Praeko-Contract: en`. Always send both.

## Example

**curl**

```bash
curl "https://praeko.app/v1/insights/retention" \
  -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "X-Praeko-Contract: en"
```

**JavaScript**

```js
const res = await fetch("https://praeko.app/v1/insights/retention", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.PRAEKO_KEY}`,
    "X-Praeko-Contract": "en",
  },
});
console.log(res.status, await res.json());
```

**Node**

```js
// O @praeko/sdk ainda não tem um método para esta rota: use o fetch (aba JavaScript).
const res = await fetch("https://praeko.app/v1/insights/retention", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.PRAEKO_KEY}`,
    "X-Praeko-Contract": "en",
  },
});
console.log(res.status, await res.json());
```

**Python**

```python
import os
import requests

resp = requests.get(
    "https://praeko.app/v1/insights/retention",
    headers={
        "Authorization": f"Bearer {os.environ['PRAEKO_KEY']}",
        "X-Praeko-Contract": "en",
    },
)
print(resp.status_code, resp.json())
```

**PHP**

```php
<?php
$ch = curl_init('https://praeko.app/v1/insights/retention');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PRAEKO_KEY"),
        'X-Praeko-Contract: en',
    ],
]);
$resposta = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $resposta, PHP_EOL;
curl_close($ch);
```

**Go**

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://praeko.app/v1/insights/retention", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PRAEKO_KEY"))
	req.Header.Set("X-Praeko-Contract", "en")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(out))
}
```

**Ruby**

```ruby
require "net/http"
require "uri"

uri = URI("https://praeko.app/v1/insights/retention")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV['PRAEKO_KEY']}"
request["X-Praeko-Contract"] = "en"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

**C#**

```csharp
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "https://praeko.app/v1/insights/retention");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PRAEKO_KEY"));
request.Headers.Add("X-Praeko-Contract", "en");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://praeko.app/v1/insights/retention"))
            .header("Authorization", "Bearer " + System.getenv("PRAEKO_KEY"))
            .header("X-Praeko-Contract", "en")
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

**HTTPie**

```bash
http GET "https://praeko.app/v1/insights/retention" \
  "Authorization: Bearer $PRAEKO_KEY" \
  "X-Praeko-Contract: en"
```

> This route has no dedicated SDK call yet: The example uses `fetch`.

## Response

| Success status | When |
|---|---|
| `200` | Success. |

**Response 200**

```json
{
  "period": {
    "from": "2026-10-08T14:02:11.482Z",
    "to": "2026-11-07T14:02:11.482Z"
  },
  "channels": [
    {
      "channelId": "01KA3V0R5D8M2WQ7X4HT9NZ1BF",
      "network": "youtube",
      "name": "Marcone's Channel",
      "state": "ready",
      "message": "Per-video retention, impressions and CTR, through the YouTube Analytics API."
    }
  ],
  "total": 1,
  "items": [
    {
      "id": "youtube:dQw4w9WgXcQ",
      "network": "youtube",
      "channelId": "01KA3V0R5D8M2WQ7X4HT9NZ1BF",
      "title": "The test that only failed on Mondays",
      "type": "short",
      "publishedAt": "2026-11-01T12:00:00Z",
      "url": "https://youtu.be/dQw4w9WgXcQ",
      "origin": "imported",
      "views": 1240,
      "reach": null,
      "impressions": 9800,
      "ctr": 5.5,
      "avgViewPercentage": 47.5,
      "avgWatchTimeSec": 31.2,
      "watchTimeMin": 2500.5,
      "retention": {
        "state": "ready",
        "source": "youtube-analytics",
        "collectedAt": "2026-11-07T15:40:03.120Z",
        "points": 100
      }
    }
  ],
  "collection": {
    "last": "2026-11-07T14:02:11.482Z"
  }
}
```

A number the network has not delivered yet comes as `null`, never 0. `retention.state`: `ready`, `pending`, `reauthorize`, `unavailable` or `error`. `channels[].message` is free text the server writes in Portuguese; the example shows it in English.

## Errors

No errors specific to this route besides the common ones.

### Errors on any route

| Status | code | When |
|---|---|---|
| 401 | `unauthorized` | The `Authorization: Bearer …` header is missing. |
| 401 | `invalid_api_key` | The key does not exist. |
| 401 | `revoked_api_key` | The key has been revoked. |
| 401 | `expired_api_key` | The key has expired. |
| 403 | `forbidden` | The key's scope is not enough for this route (see the scope badge). |
| 429 | `rate_limited` | The per-minute request limit was exceeded. |

The body of every error is `{ "error": { "code", "message", "details"? } }`. See [all error codes](https://praeko.app/docs/en/guides/errors-and-limits).

## Try it

> **Try it**: the live test needs you to be signed in. It is switched off here.
