Get Paged Clips#
Retrieve a paginated list of clips for a specific collection. This endpoint is optimized for efficient content delivery with CDN caching support.
Endpoint#
GET /api/app/clips/{collectionId}/clips/paged
Path Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
collectionId |
string | Yes | The unique identifier for the collection |
Query Parameters#
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
x-storyteller-api-key |
string | Yes | - | Your API key for authentication |
ClientPlatform |
string | Yes | - | Platform: ios, android, or web |
ClientVersion |
string | Yes | - | Must be set to 11.0.0 |
pageNumber |
integer | No | 0 |
The page number to retrieve (0-indexed) |
categoryId |
string | No | - | Filter clips by category ID |
userAttributes[name] |
string | No | - | User attributes for personalization (see below) |
See Authentication for details on authentication.
See Personalization for details on using user attributes.
Response#
Success Response#
Status Code: 200 OK
The response contains a list of clips along with metadata about the collection and pagination information.
{
"feedTitle": "Featured Moments",
"feedTitleImageUrl": "https://cdn.example.com/feeds/featured-moments-title.png",
"clips": [
{
"id": "11111111-2222-3333-4444-555555555555",
"description": "Sample clip description",
"thumbnailUrl": "https://cdn.example.com/clips/clip-001/thumbnail.jpg",
"thumbnails": {
"small": "https://cdn.example.com/clips/clip-001/thumbnail-small.jpg",
"medium": "https://cdn.example.com/clips/clip-001/thumbnail-medium.jpg",
"large": "https://cdn.example.com/clips/clip-001/thumbnail-large.jpg"
},
"playcardUrl": "https://cdn.example.com/clips/clip-001/playcard.jpg",
"ogImageUrl": "https://cdn.example.com/clips/clip-001/og-image.png",
"url": "https://cdn.example.com/clips/clip-001/video.mp4",
"categories": [
"team-example",
"player-example",
"must-see"
],
"clipCategories": [
{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"name": "Example Player",
"externalId": "player-example",
"description": "Additive plain-text category description shown in rich clip payloads.",
"placement": {
"title": "Player Stories",
"code": "player-stories"
},
"type": "player",
"displayTitle": "Example Player",
"availableForNavigation": true,
"isFollowable": true,
"action": null,
"thumbnailUrl": "https://cdn.example.com/categories/player-example.jpg",
"showInClips": true
}
],
"isLive": false,
"action": {
"type": "inApp",
"url": "app://player/player-example",
"text": "Follow Player",
"storeType": "none",
"appStoreId": null,
"playStoreBundleId": null,
"sheetId": null,
"sheetTitle": null,
"sheetSize": null,
"storyId": null,
"clipId": null,
"storyCategoryId": null,
"clipCollectionId": null,
"id": "ffffffff-1111-2222-3333-444444444444",
"iosSwipeUpUrl": "app://player/player-example",
"androidSwipeUpUrl": "app://player/player-example",
"tvosUrl": null,
"rokuUrl": null,
"fireTvUrl": null
},
"primaryAction": {
"type": "inApp",
"url": "app://player/player-example",
"text": "Follow Player",
"storeType": "none",
"appStoreId": null,
"playStoreBundleId": null,
"sheetId": null,
"sheetTitle": null,
"sheetSize": null,
"storyId": null,
"clipId": null,
"storyCategoryId": null,
"clipCollectionId": null,
"id": "ffffffff-1111-2222-3333-444444444444",
"iosSwipeUpUrl": "app://player/player-example",
"androidSwipeUpUrl": "app://player/player-example",
"tvosUrl": null,
"rokuUrl": null,
"fireTvUrl": null
},
"secondaryAction": {
"type": "none",
"url": null,
"text": null,
"storeType": "none",
"appStoreId": null,
"playStoreBundleId": null,
"sheetId": null,
"sheetTitle": null,
"sheetSize": null,
"storyId": null,
"clipId": null,
"storyCategoryId": null,
"clipCollectionId": null,
"id": null,
"iosSwipeUpUrl": null,
"androidSwipeUpUrl": null,
"tvosUrl": null,
"rokuUrl": null,
"fireTvUrl": null
},
"deepLink": "https://share.example.com/go/clip/11111111-2222-3333-4444-555555555555?collectionId=[COLLECTIONID]",
"links": {
"android": "https://share.example.com/go/clip/11111111-2222-3333-4444-555555555555?collectionId=[COLLECTIONID]",
"iosUniversal": "https://open.example.com/go/clip/11111111-2222-3333-4444-555555555555?collectionId=[COLLECTIONID]",
"webViewer": "https://share.example.com/go/clip/11111111-2222-3333-4444-555555555555?collectionId=[COLLECTIONID]"
},
"shareCountDisplay": "26",
"shareCount": 26,
"likeCountDisplay": "969",
"likeCount": 969,
"publishAt": "",
"removeAt": "",
"duration": 7,
"audiences": [],
"externalId": null,
"creationTime": "2026-02-23T05:46:20.3096378",
"sortOrder": 0,
"subtitle": {
"vtt": "https://cdn.example.com/subtitles/clip-001/en/subtitles.vtt"
},
"metadata": {},
"score": null,
"fireTvActions": null,
"enableProgressBar": true,
"secondaryActions": [],
"webActions": null,
"collectionId": "example-collection-id",
"eyebrow": null,
"publishedAt": "2026-02-23T05:55:48.9608736",
"editorialBoostAppliedAt": null,
"hasNoCaptions": true
}
],
"imageTitleIos": "",
"imageTitleAndroid": "",
"supportsNavigation": true,
"enableViewedOrdering": true,
"enableFollowing": true,
"realTimeUpdate": null,
"continuousPlayEnabled": true,
"checksum": "sample-checksum-value",
"pageSize": 50,
"showBottomBannerAd": false,
"theme": {
"behavior": {
"following": {
"emptyState": {
"title": "Get the best experience by following your favorite teams and players",
"subTitle": "Update your preferences to personalize your experience",
"action": {
"type": "inApp",
"url": "app://user/profile",
"text": "Follow Teams and Players",
"storeType": "none",
"appStoreId": "",
"playStoreBundleId": "",
"sheetId": null,
"sheetTitle": null,
"sheetSize": null,
"storyId": null,
"clipId": null,
"storyCategoryId": null,
"clipCollectionId": null,
"id": null,
"iosSwipeUpUrl": null,
"androidSwipeUpUrl": null,
"tvosUrl": null,
"rokuUrl": null,
"fireTvUrl": null
}
},
"feedTitles": {
"forYou": "For You",
"following": "Following"
},
"followableCategoryLimits": {}
},
"reloading": {
"reloadOnExit": true,
"reloadOnForeground": true
},
"player": {
"followableCategoryIcons": {
"unfollowed": "https://cdn.example.com/theme/player/unfollowed.png",
"followed": "https://cdn.example.com/theme/player/followed.png"
},
"showMuteToggle": false,
"showEyebrow": false,
"defaultMuteState": null,
"persistMuteState": false,
"clipsActionButtonCompactSize": false,
"clips": {
"showLikeCount": true,
"showShareCount": true
}
},
"tiles": {
"rectangularTile": {
"eyebrow": {
"show": false,
"textSize": 14,
"lineHeight": 18,
"textColor": null
}
},
"title": {
"show": true
}
}
},
"light": {
"following": {
"emptyState": {
"icon": "https://cdn.example.com/theme/following/empty-state-light.png"
}
},
"categoryScreen": {
"followIcons": {
"unfollowed": "https://cdn.example.com/theme/following/category-unfollowed-light.png",
"followed": "https://cdn.example.com/theme/following/category-followed-light.png"
}
}
},
"dark": {
"following": {
"emptyState": {
"icon": "https://cdn.example.com/theme/following/empty-state-dark.png"
}
},
"categoryScreen": {
"followIcons": {
"unfollowed": "https://cdn.example.com/theme/following/category-unfollowed-dark.png",
"followed": "https://cdn.example.com/theme/following/category-followed-dark.png"
}
}
},
"appearance": {
"tiles": {
"title": {
"show": true
}
}
}
},
"total": 5717
}
Response Fields#
Root Object#
| Field | Type | Description |
|---|---|---|
feedTitle |
string | Display title for the clip feed |
feedTitleImageUrl |
string | URL for the feed header image |
clips |
array\<Clip Object> | Array of clip objects |
imageTitleIos |
string | Optional iOS image title text |
imageTitleAndroid |
string | Optional Android image title text |
supportsNavigation |
boolean | Whether category navigation is enabled |
enableViewedOrdering |
boolean | Whether to sort by viewed status |
enableFollowing |
boolean | Whether following features are enabled |
realTimeUpdate |
integer | null | Real-time update interval in seconds (null if disabled) |
checksum |
string | null | Content checksum for cache validation |
theme |
object | Theme configuration object for feed/player behavior |
total |
integer | Total number of clips available in the collection |
continuousPlayEnabled |
boolean | Whether auto-play next clip is enabled |
pageSize |
integer | Number of clips returned per page |
showBottomBannerAd |
boolean | Whether the client should render the bottom banner ad slot |
Clip Object#
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Unique clip identifier |
description |
string | Clip description/title |
thumbnailUrl |
string | Primary thumbnail URL |
thumbnails |
object | Multi-resolution thumbnails (small, medium, large) |
playcardUrl |
string | Playcard image URL |
ogImageUrl |
string | Open Graph image URL |
url |
string | Clip media URL |
categories |
array\<string> | Category external IDs |
clipCategories |
array\<Clip Category Object> | Detailed category objects |
isLive |
boolean | Whether the clip is a live stream |
action |
Action Object | null | Backwards-compatible action configuration |
primaryAction |
Action Object | null | Primary action configuration |
secondaryAction |
Action Object | null | Secondary action configuration |
deepLink |
string | Share/deep link URL for the clip |
links |
Clip Links Object | Platform-specific clip links |
shareCountDisplay |
string | Formatted share count for display |
likeCount |
integer | Total like count |
likeCountDisplay |
string | Formatted like count for display |
shareCount |
integer | Total share count |
publishAt |
string | Publish timestamp string (can be empty) |
removeAt |
string | Remove timestamp string (can be empty) |
duration |
integer | Clip duration in seconds |
audiences |
array | Audience targeting payload |
externalId |
string | null | External system identifier |
creationTime |
string (ISO 8601) | Clip creation timestamp |
sortOrder |
integer | Collection sort order |
subtitle |
Subtitle Object | null | Subtitle metadata |
metadata |
object | Custom metadata key-value pairs |
score |
number | null | Ranking/relevance score when available |
fireTvActions |
array | null | Fire TV specific actions |
enableProgressBar |
boolean | Whether to show clip progress UI |
secondaryActions |
array | Additional secondary actions |
webActions |
array | null | Web-specific actions |
collectionId |
string | Collection identifier for this clip response |
eyebrow |
string | null | Eyebrow text displayed above the clip |
publishedAt |
string (ISO 8601) | Effective published timestamp |
editorialBoostAppliedAt |
string | null | Timestamp when editorial boost was applied |
hasNoCaptions |
boolean | Whether clip currently has no captions available |
Thumbnails Object#
| Field | Type | Description |
|---|---|---|
small |
string | Small thumbnail URL |
medium |
string | Medium thumbnail URL |
large |
string | Large thumbnail URL |
Clip Category Object#
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Category identifier |
name |
string | Category internal name |
externalId |
string | Category external ID |
description |
string | null | Additive plain-text category description when authored |
placement |
Clip Category Placement Object | null | Placement metadata for category presentation |
type |
string | Category type (for example player, team, other) |
displayTitle |
string | UI display title |
availableForNavigation |
boolean | Whether category is shown in category navigation |
isFollowable |
boolean | Whether users can follow this category |
action |
Action Object | null | Category-level action metadata |
thumbnailUrl |
string | Category thumbnail URL |
showInClips |
boolean | Whether category should be displayed in clip context |
Clip Category Placement Object#
| Field | Type | Description |
|---|---|---|
title |
string | Placement title |
code |
string | Placement code |
Clip Links Object#
| Field | Type | Description |
|---|---|---|
android |
string | Android link |
iosUniversal |
string | iOS universal link |
webViewer |
string | Web viewer link |
Subtitle Object#
| Field | Type | Description |
|---|---|---|
vtt |
string | URL to WebVTT subtitles |
Action Object#
| Field | Type | Description |
|---|---|---|
type |
string | Action type (for example: inApp, web, deepLink, sheet, none) |
url |
string | null | Action URL |
text |
string | null | Button text |
storeType |
string | Store fallback type |
appStoreId |
string | null | App Store app identifier |
playStoreBundleId |
string | null | Play Store bundle identifier |
sheetId |
string | null | Sheet identifier for sheet actions |
sheetTitle |
string | null | Sheet title |
sheetSize |
string | null | Sheet size configuration |
storyId |
string | null | Story ID target |
clipId |
string | null | Clip ID target |
storyCategoryId |
string | null | Story category target ID |
clipCollectionId |
string | null | Clip collection target ID |
id |
string | null | Action identifier |
iosSwipeUpUrl |
string | null | iOS-specific swipe-up URL |
androidSwipeUpUrl |
string | null | Android-specific swipe-up URL |
tvosUrl |
string | null | tvOS-specific URL |
rokuUrl |
string | null | Roku-specific URL |
fireTvUrl |
string | null | Fire TV-specific URL |
Theme Object#
| Field | Type | Description |
|---|---|---|
behavior |
object | Behavior settings (following, reloading, player, tile behavior) |
light |
object | Light-mode theme assets/settings |
dark |
object | Dark-mode theme assets/settings |
appearance |
object | Appearance overrides (for example tile title behavior) |
Empty Response#
When a collection has no clips or doesn't exist:
Status Code: 200 OK
{
"feedTitle": "",
"feedTitleImageUrl": "",
"clips": [],
"imageTitleIos": "",
"imageTitleAndroid": "",
"supportsNavigation": false,
"enableViewedOrdering": false,
"enableFollowing": false,
"realTimeUpdate": null,
"checksum": null,
"theme": {},
"total": 0,
"continuousPlayEnabled": false,
"pageSize": 0,
"showBottomBannerAd": false
}
Code Examples#
const apiKey = 'your-api-key-here';
const baseUrl = 'https://api.usestoryteller.com';
async function getPagedClips(collectionId, pageNumber = 0, categoryId = null) {
const params = new URLSearchParams({
'x-storyteller-api-key': apiKey,
'ClientPlatform': 'web',
'ClientVersion': '11.0.0',
'pageNumber': pageNumber.toString()
});
if (categoryId) {
params.set('categoryId', categoryId);
}
const response = await fetch(
`${baseUrl}/api/app/clips/${collectionId}/clips/paged?${params}`,
{ method: 'GET' }
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
}
// Usage
async function loadClips() {
try {
// Get first page
const page1 = await getPagedClips('featured-clips', 0);
console.log(`Total clips: ${page1.total}`);
console.log(`Clips on this page: ${page1.clips.length}`);
// Get next page
const page2 = await getPagedClips('featured-clips', 1);
// Filter by category
const highlights = await getPagedClips('featured-clips', 0, 'highlights');
} catch (error) {
console.error('Error loading clips:', error.message);
}
}
import requests
from typing import Optional
api_key = 'your-api-key-here'
base_url = 'https://api.usestoryteller.com'
def get_paged_clips(
collection_id: str,
page_number: int = 0,
category_id: Optional[str] = None
) -> dict:
"""
Retrieve paginated clips for a collection.
Args:
collection_id: The collection identifier
page_number: Page number (0-indexed)
category_id: Optional category filter
Returns:
Dictionary containing clips and metadata
"""
params = {
'x-storyteller-api-key': api_key,
'ClientPlatform': 'android',
'ClientVersion': '11.0.0',
'pageNumber': page_number
}
if category_id:
params['categoryId'] = category_id
response = requests.get(
f'{base_url}/api/app/clips/{collection_id}/clips/paged',
params=params,
timeout=30
)
response.raise_for_status()
return response.json()
# Usage
def load_all_clips(collection_id: str):
"""Load all clips from a collection using pagination."""
all_clips = []
page = 0
while True:
result = get_paged_clips(collection_id, page)
all_clips.extend(result['clips'])
# Break if we've collected all clips or if the current page is empty
if len(all_clips) >= result['total'] or len(result['clips']) == 0:
break
page += 1
return all_clips
# Example
clips = get_paged_clips('featured-clips', page_number=0)
print(f"Total clips available: {clips['total']}")
print(f"Clips returned: {len(clips['clips'])}")
for clip in clips['clips']:
print(f"- {clip['description']} ({clip['duration']}ms)")
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ClipsResponse
{
[JsonPropertyName("feedTitle")]
public string FeedTitle { get; set; } = "";
[JsonPropertyName("clips")]
public List<Clip> Clips { get; set; } = new();
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("continuousPlayEnabled")]
public bool ContinuousPlayEnabled { get; set; }
}
public class Clip
{
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("externalId")]
public string ExternalId { get; set; } = "";
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("duration")]
public int Duration { get; set; }
[JsonPropertyName("likeCount")]
public int LikeCount { get; set; }
[JsonPropertyName("shareCount")]
public int ShareCount { get; set; }
}
public class StorytellerClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl = "https://api.usestoryteller.com";
private readonly string _apiKey;
private readonly string _platform;
public StorytellerClient(string apiKey, string platform)
{
_apiKey = apiKey;
_platform = platform;
_httpClient = new HttpClient();
}
public async Task<ClipsResponse?> GetPagedClipsAsync(
string collectionId,
int pageNumber = 0,
string? categoryId = null)
{
var query = $"x-storyteller-api-key={_apiKey}&ClientPlatform={_platform}&ClientVersion=11.0.0&pageNumber={pageNumber}";
if (!string.IsNullOrEmpty(categoryId))
{
query += $"&categoryId={categoryId}";
}
var response = await _httpClient.GetAsync(
$"{_baseUrl}/api/app/clips/{collectionId}/clips/paged?{query}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ClipsResponse>(json);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
// Usage
using var client = new StorytellerClient("your-api-key", "ios");
var result = await client.GetPagedClipsAsync("featured-clips", pageNumber: 0);
Console.WriteLine($"Total clips: {result?.Total}");
foreach (var clip in result?.Clips ?? new List<Clip>())
{
Console.WriteLine($"- {clip.Description} ({clip.Duration}ms)");
}
# Get first page of clips
curl -X GET "https://api.usestoryteller.com/api/app/clips/featured-clips/clips/paged?x-storyteller-api-key=your-api-key-here&ClientPlatform=ios&ClientVersion=11.0.0&pageNumber=0"
# Get second page
curl -X GET "https://api.usestoryteller.com/api/app/clips/featured-clips/clips/paged?x-storyteller-api-key=your-api-key-here&ClientPlatform=ios&ClientVersion=11.0.0&pageNumber=1"
# Filter by category
curl -X GET "https://api.usestoryteller.com/api/app/clips/featured-clips/clips/paged?x-storyteller-api-key=your-api-key-here&ClientPlatform=ios&ClientVersion=11.0.0&pageNumber=0&categoryId=highlights"
Pagination#
The endpoint uses page-based pagination:
- Pages are 0-indexed (first page is
pageNumber=0) - Page size is configured per-tenant (typically 20-50 clips per page)
- Use the
totalfield to determine the total number of clips available - Continue incrementing
pageNumberuntil all clips are retrieved
Calculating Total Pages#
const pageSize = clips.length; // Clips returned on first page
const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
Caching#
Responses are cached at multiple levels for optimal performance:
- CDN Cache: Responses are cached at the edge for fast delivery
- Cache Tags: Responses are tagged by collection for targeted invalidation
Cache Behavior
When content is updated in the Storyteller CMS, caches are automatically invalidated to ensure fresh content delivery.
Error Handling#
Common Errors#
| Status Code | Description | Common Causes |
|---|---|---|
401 |
Unauthorized | Missing or invalid API key |
500 |
Internal Server Error | Server-side error |
Error Response Format#
{
"status": 401,
"title": "Unauthorized",
"detail": "Invalid or missing API key"
}
Related Documentation#
- Authentication - API key setup and usage