Stories API#
Use the Stories endpoints to surface the stories that are already live in your Storyteller tenant. These APIs power external CMS dashboards, content selectors, and workflow metadata flows that need story identifiers, categories, and deep links.
Endpoint Summary#
| Endpoint | Description |
|---|---|
GET /api/stories |
Retrieve published stories with pagination, search, and publish-status filtering |
GET /api/stories/{externalId} |
Fetch a single story by its external identifier (legacy lightweight response) |
GET /api/stories/by-id/{id} |
Fetch a single story by its internal GUID identifier |
GET /api/stories/{externalId}/details |
Retrieve the full story graph including ordered pages, media assets, captions, and action links |
GET /api/stories/by-id/{id}/details |
Retrieve the full story graph by internal GUID identifier |
List Stories (GET /api/stories)#
Query Parameters#
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
searchText |
string | No | – | Case-insensitive match against the story title |
externalId |
string | No | – | Exact match on the externalId column |
publishStatus |
string (Published, Draft, Scheduled) |
No | – | Filter stories that are currently in a specific publish state |
publishedSince |
string (ISO 8601) | No | – | Return stories published on or after this date/time. Matches against story-level or category-level publish dates. 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 100 |
sort |
string | No | AlphabeticalAsc |
Supported values: AlphabeticalAsc, LastModifiedDesc, CreatedDesc, PublishedDesc |
Example Response#
{
"stories": [
{
"id": "9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"externalId": "homepage-story",
"title": "Homepage Hero",
"createdAt": "2025-02-07T11:24:39Z",
"publishedAt": "2025-02-08T09:00:00Z",
"appOpenUrl": "https://yourtenant.shar.estori.es/go/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"thumbnailUrl": "https://cdn.yourtenant.net/stories/homepage-story/thumbnail.jpg",
"iconUrl": "https://cdn.yourtenant.net/stories/homepage-story/icon-square.jpg",
"cmsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/form",
"cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/analytics",
"categories": [
{
"title": "Featured",
"externalId": "featured"
},
{
"title": "Homepage",
"externalId": "homepage"
}
]
}
],
"pageSize": 10,
"currentPage": 1,
"totalPages": 3
}
Response Fields#
| Field | Type | Description |
|---|---|---|
id |
string (GUID) | Internal story identifier – use this when a workflow expects a storyId |
externalId |
string | Friendly identifier supplied by your systems |
title |
string | Story title (may be empty if not set) |
createdAt |
string (ISO 8601) | When the story was first created |
publishedAt |
string (ISO 8601) | Most recent publish timestamp, falling back to the story-level schedule |
appOpenUrl |
string | Deep link that launches the story in Storyteller apps (page segment stripped for safety) |
thumbnailUrl |
string | CDN-formatted URL to the story thumbnail |
iconUrl |
string | CDN-formatted square icon URL |
cmsUrl |
string | Direct link to the story form inside the Storyteller CMS |
cmsAnalyticsUrl |
string | CMS analytics dashboard for the story |
categories |
array | Assigned categories (see table below) |
Category Object
| Field | Type | Description |
|---|---|---|
title |
string | Display name |
externalId |
string | External identifier for the category |
Pagination Object
| Field | Type | Description |
|---|---|---|
pageSize |
integer | Number of items returned per page |
currentPage |
integer | Current page number |
totalPages |
integer | Total available pages |
Usage Examples#
import fetch from 'node-fetch';
export async function listStories({
apiKey,
searchText,
publishStatus,
publishedSince,
currentPage = 1,
pageSize = 20
}) {
const params = new URLSearchParams({ currentPage, pageSize });
if (searchText) params.set('searchText', searchText);
if (publishStatus) params.set('publishStatus', publishStatus);
if (publishedSince) params.set('publishedSince', publishedSince);
const response = await fetch(`https://integrations.usestoryteller.com/api/stories?${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_stories(
api_key: str,
search_text: str | None = None,
publish_status: 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 publish_status:
params['publishStatus'] = publish_status
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/stories',
headers={'x-storyteller-api-key': api_key},
params=params,
timeout=30
)
response.raise_for_status()
return response.json()
using System.Net.Http.Json;
public sealed class StoryClient
{
private readonly HttpClient _httpClient;
public StoryClient(string apiKey)
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://integrations.usestoryteller.com/")
};
_httpClient.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);
}
public async Task<ListStoriesResponse> GetStoriesAsync(
string? searchText = null,
string? publishStatus = null,
DateTime? publishedSince = null,
int page = 1,
int pageSize = 20)
{
var query = new Dictionary<string, string>
{
["currentPage"] = page.ToString(),
["pageSize"] = pageSize.ToString()
};
if (!string.IsNullOrWhiteSpace(searchText)) query["searchText"] = searchText;
if (!string.IsNullOrWhiteSpace(publishStatus)) query["publishStatus"] = publishStatus;
if (publishedSince.HasValue) query["publishedSince"] = publishedSince.Value.ToString("o");
var url = QueryHelpers.AddQueryString("api/stories", query);
var response = await _httpClient.GetFromJsonAsync<ListStoriesResponse>(url);
return response ?? new ListStoriesResponse();
}
}
# Get stories published since January 15, 2025
curl -X GET "https://integrations.usestoryteller.com/api/stories?currentPage=1&pageSize=20&publishedSince=2025-01-15T00:00:00Z" \
-H "x-storyteller-api-key: YOUR_API_KEY"
Get Story by External ID (GET /api/stories/{externalId})#
This legacy endpoint returns the same structure as the list endpoint for a single story. It is retained for backward compatibility, but new integrations should use the details endpoint below when they require page structures or deep metadata.
Get Story by ID (GET /api/stories/by-id/{id})#
Fetches a single story using its internal GUID identifier. This is useful when you have the story's internal ID from another Storyteller API response (such as workflow results or analytics data) and need to retrieve the story metadata.
Path Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string (GUID) | Yes | The internal story identifier |
Example Response#
Returns the same structure as the list endpoint for a single story:
{
"id": "9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"externalId": "homepage-story",
"title": "Homepage Hero",
"createdAt": "2025-02-07T11:24:39Z",
"publishedAt": "2025-02-08T09:00:00Z",
"appOpenUrl": "https://yourtenant.shar.estori.es/go/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"thumbnailUrl": "https://cdn.yourtenant.net/stories/homepage-story/thumbnail.jpg",
"iconUrl": "https://cdn.yourtenant.net/stories/homepage-story/icon-square.jpg",
"cmsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/form",
"cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/analytics",
"categories": [
{
"title": "Featured",
"externalId": "featured"
}
]
}
Usage Examples#
import fetch from 'node-fetch';
export async function getStoryById(apiKey, storyId) {
const response = await fetch(`https://integrations.usestoryteller.com/api/stories/by-id/${storyId}`, {
headers: { 'x-storyteller-api-key': apiKey }
});
if (!response.ok) {
throw await response.json();
}
return response.json();
}
import requests
def get_story_by_id(api_key: str, story_id: str) -> dict:
response = requests.get(
f"https://integrations.usestoryteller.com/api/stories/by-id/{story_id}",
headers={'x-storyteller-api-key': api_key},
timeout=30
)
response.raise_for_status()
return response.json()
public async Task<StoryResponse?> GetStoryByIdAsync(string apiKey, Guid storyId)
{
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/stories/by-id/{storyId}");
if (!response.IsSuccessStatusCode)
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch story");
}
return await response.Content.ReadFromJsonAsync<StoryResponse>();
}
curl -X GET "https://integrations.usestoryteller.com/api/stories/by-id/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa" \
-H "x-storyteller-api-key: YOUR_API_KEY"
Story Details by External ID (GET /api/stories/{externalId}/details)#
Use the details endpoint when you need the full story composition – ordered pages, action links, captions, and CDN-aware asset URLs. The response mirrors what the Storyteller CMS surfaces in the story form.
Path Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
externalId |
string | Yes | The external story identifier |
Example Response#
{
"id": "9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"externalId": "homepage-story",
"title": "Homepage Hero",
"createdAt": "2025-02-07T11:24:39Z",
"publishedAt": "2025-02-08T09:00:00Z",
"iconUrl": "https://cdn.yourtenant.net/stories/homepage-story/icon-square.jpg",
"thumbnailUrl": "https://cdn.yourtenant.net/stories/homepage-story/thumbnail.jpg",
"appOpenUrl": "https://yourtenant.shar.estori.es/go/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa",
"cmsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/form",
"cmsAnalyticsUrl": "https://yourtenant.usestoryteller.com/stories/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/analytics",
"eyebrow": "Top Stories",
"urlNames": ["homepage-hero", "daily-top"],
"categories": [
{ "title": "Featured", "externalId": "featured" },
{ "title": "Homepage", "externalId": "homepage" }
],
"pages": [
{
"id": "4a77d2a6-75f0-4b7a-ac2d-1b1cf3f28157",
"type": "Image",
"sortOrder": 1,
"duration": 8000,
"skippable": true,
"hasAudio": false,
"assetImageUrl": "https://cdn.yourtenant.net/stories/homepage-story/pages/01/asset.jpg",
"assetVideoUrl": null,
"playcardImageUrl": "https://cdn.yourtenant.net/stories/homepage-story/pages/01/playcard.jpg",
"playcardVideoUrl": null,
"actionLink": {
"id": "e1da88ba-d38c-4ed1-b2ed-22a2cc7abcab",
"actionType": "Web",
"title": "Read the full article",
"webUrl": "https://www.example.com/articles/homepage-hero",
"iosUrl": null,
"androidUrl": null,
"tvosUrl": null,
"rokuUrl": null,
"fireTvUrl": null,
"appStoreId": null,
"playStoreBundleId": null,
"sheet": null
},
"caption": {
"remoteUrl": "https://cdn.yourtenant.net/stories/homepage-story/pages/01/captions.vtt",
"localisations": [
{
"languageCode": "es",
"remoteUrl": "https://cdn.yourtenant.net/stories/homepage-story/pages/01/captions.es.vtt"
}
]
}
},
{
"id": "c6697311-3f32-492f-81e3-5de97df7bae9",
"type": "Poll",
"sortOrder": 2,
"duration": 10000,
"skippable": false,
"hasAudio": true,
"assetImageUrl": null,
"assetVideoUrl": "https://cdn.yourtenant.net/stories/homepage-story/pages/02/video.mp4",
"playcardImageUrl": null,
"playcardVideoUrl": null,
"actionLink": null,
"caption": null
}
]
}
Page Object Reference#
| Field | Description |
|---|---|
type |
Page type (Image, Video, Poll, TriviaQuiz, etc.) |
sortOrder |
Order within the story (1-based) |
duration |
Display duration in milliseconds |
skippable |
Whether the viewer can skip before the timer elapses |
hasAudio |
Indicates that the page includes audio |
assetImageUrl / assetVideoUrl |
CDN-formatted URLs for the primary media asset |
playcardImageUrl / playcardVideoUrl |
Previews used in some clients |
actionLink |
Optional CTA metadata (see below) |
caption |
Caption resources including localisation URLs |
Action Link Object
| Field | Description |
|---|---|
actionType |
Destination type such as Web, Sheet, or Collection |
webUrl / iosUrl / androidUrl / tvosUrl / rokuUrl / fireTvUrl |
Platform-specific deep links |
sheet |
Inline sheet metadata (id, title) when applicable |
Caption Object
| Field | Description |
|---|---|
remoteUrl |
Primary caption file (usually WebVTT) |
localisations |
Optional per-language caption sources |
Usage Examples#
import fetch from 'node-fetch';
export async function getStoryDetails(apiKey, externalId) {
const response = await fetch(`https://integrations.usestoryteller.com/api/stories/${externalId}/details`, {
headers: { 'x-storyteller-api-key': apiKey }
});
if (!response.ok) {
throw await response.json();
}
return response.json();
}
import requests
def get_story_details(api_key: str, external_id: str) -> dict:
response = requests.get(
f"https://integrations.usestoryteller.com/api/stories/{external_id}/details",
headers={'x-storyteller-api-key': api_key},
timeout=30
)
response.raise_for_status()
return response.json()
public async Task<StoryDetails?> GetStoryDetailsAsync(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/stories/{externalId}/details");
if (!response.IsSuccessStatusCode)
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch story");
}
return await response.Content.ReadFromJsonAsync<StoryDetails>();
}
curl -X GET "https://integrations.usestoryteller.com/api/stories/homepage-story/details" \
-H "x-storyteller-api-key: YOUR_API_KEY"
Story Details by ID (GET /api/stories/by-id/{id}/details)#
Retrieve the full story composition using its internal GUID identifier. This endpoint provides the same comprehensive details as the external ID version, including ordered pages, action links, captions, and CDN-aware asset URLs.
Path Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string (GUID) | Yes | The internal story 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 getStoryDetailsById(apiKey, storyId) {
const response = await fetch(`https://integrations.usestoryteller.com/api/stories/by-id/${storyId}/details`, {
headers: { 'x-storyteller-api-key': apiKey }
});
if (!response.ok) {
throw await response.json();
}
return response.json();
}
import requests
def get_story_details_by_id(api_key: str, story_id: str) -> dict:
response = requests.get(
f"https://integrations.usestoryteller.com/api/stories/by-id/{story_id}/details",
headers={'x-storyteller-api-key': api_key},
timeout=30
)
response.raise_for_status()
return response.json()
public async Task<StoryDetails?> GetStoryDetailsByIdAsync(string apiKey, Guid storyId)
{
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/stories/by-id/{storyId}/details");
if (!response.IsSuccessStatusCode)
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
throw new InvalidOperationException(problem?.Detail ?? "Unable to fetch story");
}
return await response.Content.ReadFromJsonAsync<StoryDetails>();
}
curl -X GET "https://integrations.usestoryteller.com/api/stories/by-id/9d9a9098-2d9e-4d67-a0bf-0f6eb0d3f2fa/details" \
-H "x-storyteller-api-key: YOUR_API_KEY"
Best Practices#
- Prefer the details endpoint when you need page structures, media URLs, or caption metadata.
- Cache CDN URLs sensibly – the Integration API already formats relative paths, so you can safely cache for short periods.
- Use
publishStatusfilters to separateDraft,Scheduled, andPublishedcontent during editorial workflows. - Store both
idandexternalIdto bridge internal CMS references with downstream workflows. - Handle RFC 7807 errors – all error responses follow Problem Details.
Related Documentation#
- Story Analytics – Retrieve all-time viewer, engagement, and per-page statistics
- Categories API – Build category pickers to accompany the stories list
- Executing Workflows – Use
storyIdmetadata when running workflows - Clips API – Surface complementary clip content alongside stories