Skip to content

Clips API#

The Clips endpoints expose the short-form clips that are available in your tenant. Integrations use this data to populate media pickers, drive recommendation rails, or deep link back into Storyteller CMS analytics.

Endpoint Summary#

Endpoint Description
GET /api/clips Retrieve published clips with pagination, search, and sorting
GET /api/clips/{externalId} Fetch a single clip by its external identifier
GET /api/clips/by-id/{id} Fetch a single clip by its internal GUID identifier
GET /api/clips/{externalId}/details Retrieve comprehensive clip details including caption localisations
GET /api/clips/by-id/{id}/details Retrieve comprehensive clip details by internal GUID identifier

ℹ️ Clip analytics are documented separately in Clip Analytics.

List Clips (GET /api/clips)#

Query Parameters#

Parameter Type Required Default Notes
searchText string No Case-insensitive match against displayTitle
externalId string No Exact match on externalId
publishedSince string (ISO 8601) No Return clips published on or after this date/time. Accepts ISO 8601 format (e.g., 2025-01-15T00:00:00Z) or standard date formats.
currentPage integer No 1 Must be >= 1
pageSize integer No 10 Must be between 1 and 1000
skipCount integer No 0 Legacy offset-style pagination alias. When used directly on the integrations endpoint it must align to the effective page size.
maxResultCount integer No 10 Legacy offset-style page-size alias. Maximum 1000.
sort string No AlphabeticalAsc Supported values: AlphabeticalAsc, CreatedDesc, PublishedDesc

Clips returned from this endpoint are already filtered to Published status inside Storyteller.

You can page this endpoint using either currentPage + pageSize or legacy-style skipCount + maxResultCount.

In legacy mode, omitted aliases fall back to skipCount=0 and maxResultCount=10. Because integrations responses still return currentPage, skipCount must land on a page boundary for the effective page size.

Example Response#

{
  "clips": [
    {
      "id": "98226576-21a4-a1c9-efd6-3a12dc9eef9b",
      "externalId": "clip-001",
      "internalTitle": "STX Launch Teaser",
      "displayTitle": "STX Launch Teaser",
      "createdAt": "2025-01-15T10:30:00Z",
      "publishedAt": "2025-01-15T12:00:00Z",
      "duration": 32000,
      "hasAudio": true,
      "videoUrl": "https://cdn.yourtenant.net/clips/clip-001/video.mp4",
      "thumbnailUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.jpg",
      "thumbnailMediumUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-medium.jpg",
      "thumbnailLargeUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-large.jpg",
      "thumbnailWebpUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.webp",
      "playcardUrl": "https://cdn.yourtenant.net/clips/clip-001/playcard.jpg",
      "cmsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/form",
      "cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/analytics",
      "universalLinkUrl": "https://yourtenant.shar.estori.es/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
      "deepLinkUrl": "yourtenantstories://open/clip/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
      "captionRemoteUrl": "https://cdn.yourtenant.net/clips/clip-001/captions.vtt",
      "categories": [
        {
          "id": "f4af24f0-c913-4bfb-bf11-b67a8f78154f",
          "title": "Product",
          "externalId": "product",
          "type": "Other",
          "placement": "home"
        }
      ],
      "collections": [
        {
          "id": "featured-clips",
          "collectionGuid": "09a0e3ce-70e1-408c-8d88-5a1f4f367d5c",
          "internalTitle": "Featured Clips",
          "displayTitle": "Featured Clips"
        }
      ]
    }
  ],
  "pageSize": 10,
  "currentPage": 1,
  "totalPages": 5,
  "totalCount": 42
}

Response Fields#

Field Type Description
id string (GUID) Internal clip identifier
externalId string External system identifier
internalTitle string Internal title shown in the CMS
displayTitle string Consumer-facing title
createdAt string (ISO 8601) Clip creation time
publishedAt string (ISO 8601) Most recent publish timestamp (falls back to creation time)
duration integer Clip duration in milliseconds
hasAudio boolean Indicates whether the clip has audio
videoUrl string CDN-aware video URL for the clip
thumbnailUrl string Primary thumbnail
thumbnailMediumUrl string Medium thumbnail (falls back to primary)
thumbnailLargeUrl string Large thumbnail (falls back to primary)
thumbnailWebpUrl string WebP thumbnail if available
playcardUrl string Playcard image/video used in some clients
cmsUrl string Direct link to the clip’s CMS form
cmsAnalyticsUrl string Link to the clip analytics dashboard inside the CMS
universalLinkUrl string Viewer-friendly link using the tenant’s sharing domain
deepLinkUrl string Custom URL scheme deep link
captionRemoteUrl string CDN-formatted caption file if captions exist
categories array Assigned categories (see table below)
collections array Collections containing the clip

Category Object

Field Description
id Internal category GUID
title Category display name
externalId Category external identifier
type Category type (Other, Editorial, Game, Team, Priority, Player)
placement Category placement code when one is assigned

Collection Object

Field Description
id Collection internal identifier
collectionGuid Internal collection GUID
internalTitle CMS title
displayTitle Public-facing title

Pagination Object

Field Description
pageSize Number of items returned per page
currentPage Current page number
totalPages Total available pages
totalCount Exact total number of matching clips

Usage Examples#

import fetch from 'node-fetch';

export async function listClips(apiKey, { searchText, publishedSince, pageSize = 20, page = 1 } = {}) {
  const params = new URLSearchParams({ currentPage: page, pageSize });
  if (searchText) params.set('searchText', searchText);
  if (publishedSince) params.set('publishedSince', publishedSince);

  const response = await fetch(`https://integrations.usestoryteller.com/api/clips?${params}`, {
    headers: { 'x-storyteller-api-key': apiKey }
  });

  if (!response.ok) {
    const problem = await response.json();
    throw new Error(`${problem.title}: ${problem.detail}`);
  }

  return response.json();
}
import requests
from datetime import datetime

def list_clips(
    api_key: str,
    search_text: str | None = None,
    published_since: datetime | str | None = None,
    page: int = 1,
    page_size: int = 20
):
    params = {
        'currentPage': page,
        'pageSize': page_size
    }
    if search_text:
        params['searchText'] = search_text
    if published_since:
        # Convert datetime to ISO 8601 string if needed
        if isinstance(published_since, datetime):
            params['publishedSince'] = published_since.isoformat()
        else:
            params['publishedSince'] = published_since

    response = requests.get(
        'https://integrations.usestoryteller.com/api/clips',
        headers={'x-storyteller-api-key': api_key},
        params=params,
        timeout=30
    )
    response.raise_for_status()
    return response.json()
public sealed class ClipClient
{
    private readonly HttpClient _client;

    public ClipClient(string apiKey)
    {
        _client = new HttpClient { BaseAddress = new Uri("https://integrations.usestoryteller.com/") };
        _client.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);
    }

    public async Task<ListClipsResponse?> GetClipsAsync(
        int page = 1,
        int pageSize = 20,
        string? searchText = null,
        DateTime? publishedSince = null)
    {
        var query = new Dictionary<string, string>
        {
            ["currentPage"] = page.ToString(),
            ["pageSize"] = pageSize.ToString()
        };
        if (!string.IsNullOrWhiteSpace(searchText)) query["searchText"] = searchText;
        if (publishedSince.HasValue) query["publishedSince"] = publishedSince.Value.ToString("o");

        var url = QueryHelpers.AddQueryString("api/clips", query);
        return await _client.GetFromJsonAsync<ListClipsResponse>(url);
    }
}
# Get clips published since January 15, 2025
curl -X GET "https://integrations.usestoryteller.com/api/clips?currentPage=1&pageSize=20&publishedSince=2025-01-15T00:00:00Z" \
  -H "x-storyteller-api-key: YOUR_API_KEY"

Get Clip by External ID (GET /api/clips/{externalId})#

Returns the lightweight single-clip payload shown below. Use this when you already know the clip external ID and need CMS or sharing links for that clip.

Unlike GET /api/clips, these single-item endpoints do not currently include the list-only metadata compatibility fields such as categories[].id, categories[].placement, or collections[].collectionGuid.

Example#

curl -X GET "https://integrations.usestoryteller.com/api/clips/clip-001" \
  -H "x-storyteller-api-key: YOUR_API_KEY"

Get Clip by ID (GET /api/clips/by-id/{id})#

Fetches a single clip using its internal GUID identifier. This is useful when you have the clip's internal ID from another Storyteller API response (such as workflow results or analytics data) and need to retrieve the clip metadata.

Path Parameters#

Parameter Type Required Description
id string (GUID) Yes The internal clip identifier

Example Response#

Returns the lightweight single-clip payload:

{
  "id": "98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "externalId": "clip-001",
  "internalTitle": "STX Launch Teaser",
  "displayTitle": "STX Launch Teaser",
  "createdAt": "2025-01-15T10:30:00Z",
  "publishedAt": "2025-01-15T12:00:00Z",
  "duration": 32000,
  "hasAudio": true,
  "videoUrl": "https://cdn.yourtenant.net/clips/clip-001/video.mp4",
  "thumbnailUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.jpg",
  "thumbnailMediumUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-medium.jpg",
  "thumbnailLargeUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-large.jpg",
  "thumbnailWebpUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.webp",
  "playcardUrl": "https://cdn.yourtenant.net/clips/clip-001/playcard.jpg",
  "cmsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/form",
  "cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/analytics",
  "universalLinkUrl": "https://yourtenant.shar.estori.es/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "deepLinkUrl": "yourtenantstories://open/clip/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "captionRemoteUrl": "https://cdn.yourtenant.net/clips/clip-001/captions.vtt",
  "categories": [
    {
      "title": "Product",
      "externalId": "product",
      "type": "Other"
    }
  ],
  "collections": [
    {
      "id": "featured-clips",
      "internalTitle": "Featured Clips",
      "displayTitle": "Featured Clips"
    }
  ]
}

Usage Examples#

import fetch from 'node-fetch';

export async function getClipById(apiKey, clipId) {
  const response = await fetch(`https://integrations.usestoryteller.com/api/clips/by-id/${clipId}`, {
    headers: { 'x-storyteller-api-key': apiKey }
  });

  if (!response.ok) {
    throw await response.json();
  }

  return response.json();
}
import requests

def get_clip_by_id(api_key: str, clip_id: str) -> dict:
    response = requests.get(
        f"https://integrations.usestoryteller.com/api/clips/by-id/{clip_id}",
        headers={'x-storyteller-api-key': api_key},
        timeout=30
    )
    response.raise_for_status()
    return response.json()
public async Task<ClipResponse?> GetClipByIdAsync(string apiKey, Guid clipId)
{
    using var client = new HttpClient
    {
        BaseAddress = new Uri("https://integrations.usestoryteller.com/")
    };
    client.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);

    var response = await client.GetAsync($"api/clips/by-id/{clipId}");
    if (!response.IsSuccessStatusCode)
    {
        var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
        throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch clip");
    }

    return await response.Content.ReadFromJsonAsync<ClipResponse>();
}
curl -X GET "https://integrations.usestoryteller.com/api/clips/by-id/98226576-21a4-a1c9-efd6-3a12dc9eef9b" \
  -H "x-storyteller-api-key: YOUR_API_KEY"

Clip Details by External ID (GET /api/clips/{externalId}/details)#

Retrieve comprehensive clip details including caption localisations. This endpoint extends the basic clip response with additional caption localisation information, providing all language-specific caption URLs.

Path Parameters#

Parameter Type Required Description
externalId string Yes The external clip identifier

Example Response#

{
  "id": "98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "externalId": "clip-001",
  "internalTitle": "STX Launch Teaser",
  "displayTitle": "STX Launch Teaser",
  "createdAt": "2025-01-15T10:30:00Z",
  "publishedAt": "2025-01-15T12:00:00Z",
  "duration": 32000,
  "hasAudio": true,
  "videoUrl": "https://cdn.yourtenant.net/clips/clip-001/video.mp4",
  "thumbnailUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.jpg",
  "thumbnailMediumUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-medium.jpg",
  "thumbnailLargeUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail-large.jpg",
  "thumbnailWebpUrl": "https://cdn.yourtenant.net/clips/clip-001/thumbnail.webp",
  "playcardUrl": "https://cdn.yourtenant.net/clips/clip-001/playcard.jpg",
  "cmsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/form",
  "cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b/analytics",
  "universalLinkUrl": "https://yourtenant.shar.estori.es/clips/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "deepLinkUrl": "yourtenantstories://open/clip/98226576-21a4-a1c9-efd6-3a12dc9eef9b",
  "caption": {
    "remoteUrl": "https://cdn.yourtenant.net/clips/clip-001/captions.vtt",
    "localisations": [
      {
        "languageCode": "es",
        "remoteUrl": "https://cdn.yourtenant.net/clips/clip-001/captions.es.vtt"
      },
      {
        "languageCode": "fr",
        "remoteUrl": "https://cdn.yourtenant.net/clips/clip-001/captions.fr.vtt"
      }
    ]
  },
  "categories": [
    {
      "title": "Product",
      "externalId": "product",
      "type": "Other"
    }
  ],
  "collections": [
    {
      "id": "featured-clips",
      "internalTitle": "Featured Clips",
      "displayTitle": "Featured Clips"
    }
  ]
}

Additional Response Fields#

The details endpoint includes these additional fields beyond the basic clip response:

Field Type Description
caption object Caption details including localisations
caption.remoteUrl string Primary caption file URL (usually WebVTT)
caption.localisations array Language-specific caption files
caption.localisations[].languageCode string ISO language code (e.g., "es", "fr")
caption.localisations[].remoteUrl string CDN-formatted caption URL for the language

Usage Examples#

import fetch from 'node-fetch';

export async function getClipDetails(apiKey, externalId) {
  const response = await fetch(`https://integrations.usestoryteller.com/api/clips/${externalId}/details`, {
    headers: { 'x-storyteller-api-key': apiKey }
  });

  if (!response.ok) {
    throw await response.json();
  }

  return response.json();
}
import requests

def get_clip_details(api_key: str, external_id: str) -> dict:
    response = requests.get(
        f"https://integrations.usestoryteller.com/api/clips/{external_id}/details",
        headers={'x-storyteller-api-key': api_key},
        timeout=30
    )
    response.raise_for_status()
    return response.json()
public async Task<ClipDetails?> GetClipDetailsAsync(string apiKey, string externalId)
{
    using var client = new HttpClient
    {
        BaseAddress = new Uri("https://integrations.usestoryteller.com/")
    };
    client.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);

    var response = await client.GetAsync($"api/clips/{externalId}/details");
    if (!response.IsSuccessStatusCode)
    {
        var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
        throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch clip");
    }

    return await response.Content.ReadFromJsonAsync<ClipDetails>();
}
curl -X GET "https://integrations.usestoryteller.com/api/clips/clip-001/details" \
  -H "x-storyteller-api-key: YOUR_API_KEY"

Clip Details by ID (GET /api/clips/by-id/{id}/details)#

Retrieve comprehensive clip details using its internal GUID identifier. This endpoint provides the same detailed information as the external ID version, including caption localisations.

Path Parameters#

Parameter Type Required Description
id string (GUID) Yes The internal clip identifier

Example Response#

Returns the same structure as the external ID details endpoint. See the example response in the previous section.

Usage Examples#

import fetch from 'node-fetch';

export async function getClipDetailsById(apiKey, clipId) {
  const response = await fetch(`https://integrations.usestoryteller.com/api/clips/by-id/${clipId}/details`, {
    headers: { 'x-storyteller-api-key': apiKey }
  });

  if (!response.ok) {
    throw await response.json();
  }

  return response.json();
}
import requests

def get_clip_details_by_id(api_key: str, clip_id: str) -> dict:
    response = requests.get(
        f"https://integrations.usestoryteller.com/api/clips/by-id/{clip_id}/details",
        headers={'x-storyteller-api-key': api_key},
        timeout=30
    )
    response.raise_for_status()
    return response.json()
public async Task<ClipDetails?> GetClipDetailsByIdAsync(string apiKey, Guid clipId)
{
    using var client = new HttpClient
    {
        BaseAddress = new Uri("https://integrations.usestoryteller.com/")
    };
    client.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);

    var response = await client.GetAsync($"api/clips/by-id/{clipId}/details");
    if (!response.IsSuccessStatusCode)
    {
        var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
        throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch clip");
    }

    return await response.Content.ReadFromJsonAsync<ClipDetails>();
}
curl -X GET "https://integrations.usestoryteller.com/api/clips/by-id/98226576-21a4-a1c9-efd6-3a12dc9eef9b/details" \
  -H "x-storyteller-api-key: YOUR_API_KEY"

Error Handling#

All clip endpoints return RFC 7807 Problem Details errors. Example responses:

{
  "status": 401,
  "type": "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1",
  "title": "Unauthorized",
  "detail": "No valid API key provided",
  "instance": ""
}
{
  "status": 404,
  "type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "detail": "Clip with externalId clip-999 not found",
  "instance": ""
}

Best Practices#

  1. Serve media from Storyteller CDN – the API already resolves relative paths; use the URLs as-is to avoid hardcoding tenants.
  2. Prefer externalId filters when you are reconciling clips with your internal systems.
  3. Surface collection context so operators can see how clips are grouped inside Storyteller.
  4. Handle Problem Details responses uniformly across all content endpoints.
  5. Use Clip Analytics for engagement metrics rather than computing totals yourself – see Clip Analytics.