Category Analytics API#
Retrieve Story or Clip metrics grouped by Category for a selected date range. These reports use the same calculations as the corresponding CMS Category analytics and include a page of Category rows plus overall totals for the requested scope.
Endpoints#
| Report | Method and path |
|---|---|
| Clip metrics by Category | GET /api/analytics/clips/categories |
| Story metrics by Category | GET /api/analytics/stories/categories |
Both routes require a tenant Server App API key in the x-storyteller-api-key header. The key determines the tenant; there is no tenant override. Client App keys and API keys in query parameters are not accepted. See Authentication for obtaining a Server App key.
These endpoints return Category breakdowns. They do not accept Category or Collection filters, return individual content records, or include chart buckets. For individual content analytics, use Clip Analytics or Story Analytics.
Query parameters#
Both routes accept the following parameters. Supply each parameter at most once.
| Parameter | Required | Description |
|---|---|---|
startDate |
Yes | Start of the requested reporting range. Accepts an ISO date such as 2026-09-01, or an ISO timestamp such as 2026-09-01T10:30:00Z. Must identify an instant earlier than endDate. |
endDate |
Yes | Exclusive end of the requested reporting range, using the same formats as startDate. For example, startDate=2026-09-01&endDate=2026-09-08 requests 1–7 September in the effective timezone. |
timezone |
No | IANA timezone, such as Europe/London or UTC. Defaults to the tenant's analytics timezone, with America/New_York as the fallback. An explicitly empty or invalid value is rejected. |
platforms |
No | Comma-separated selection from ios, android, tvos, web, onebox, roku, and firetv. Omit it or supply all to select all platforms. Names are case-insensitive, trimmed and deduplicated. Do not combine all with other values. |
sort |
No | One of the descending metric sorts listed below, case-insensitive. Defaults to ClipViewsDesc for Clips or PageViewsDesc for Stories. |
currentPage |
No | One-based positive integer. Defaults to 1. The calculated offset, (currentPage - 1) * pageSize, must not exceed 2147483647. |
pageSize |
No | Integer from 1 to 500. Defaults to 10. |
Unknown or repeated parameters return 400. In particular, categoryId, Collection filters, tenantId, tab, skipCount, maxResultCount, and granularity are not supported. Empty platform selections or unknown platform names are also rejected.
Dates and complete hours#
Dates without a time, and timestamps without an offset, are interpreted in the effective timezone. Timestamps with Z or an explicit ±HH:mm offset identify an absolute instant. Timestamp formats require hours and minutes, with optional seconds and up to seven fractional-second digits. Encode + in query-string offsets as %2B, or use a URL encoder as in the examples below.
The API rounds both boundaries up to whole UTC hours, then caps the exclusive end at the latest complete hour. For example, a historical UTC interval from 10:15 to 12:15 becomes 11:00 to 13:00. The current incomplete hour is never included.
Use the response's range.fromUtc and range.toUtcExclusive to identify the applied reporting window. A valid input range can become empty after rounding and capping. In that case the API returns 200, an empty categories array, and zero metrics. The response preserves the applied boundaries even if the capped end is at or before the rounded start.
Metrics and sorting#
Every Category row and the totals object contain all metrics for that report, regardless of the selected sort. Metrics are integer values. Unsupported metrics, including Engagement, Watch and Overall Scores, are absent rather than returned as zero.
Clips#
| Field | Definition | Sort |
|---|---|---|
clipViews |
Clip opens plus completed loops | ClipViewsDesc |
clipLoops |
Completed loops | ClipLoopsDesc |
viewers |
Unique viewers under the CMS Clip Category viewer rules | ViewersDesc |
shares |
Share button taps | SharesDesc |
clickThroughs |
Action button taps | ClickThroughsDesc |
likes |
Clip like events | LikesDesc |
Clip Categories with eligible activity remain in the report even when their selected sort metric is zero.
Stories#
| Field | Definition | Sort |
|---|---|---|
pageViews |
Story page opens | PageViewsDesc |
viewers |
Unique viewers under the CMS Story Category viewer rules | ViewersDesc |
shares |
Share button taps | SharesDesc |
clickThroughs |
Combined swipe-up and action button taps | ClickThroughsDesc |
pollVotes |
Poll votes | PollVotesDesc |
triviaQuizAnswers |
Trivia question answers | TriviaQuizAnswersDesc |
triviaQuizCompletions |
Trivia quiz completions | TriviaQuizCompletionsDesc |
Story Categories are included only when the selected sort metric is positive. Changing sort can therefore change totalCount and the set of Category rows. Clip-only metrics are not available on the Story route, and Story-only metrics are not available on the Clip route.
Response fields#
| Field | Description |
|---|---|
range |
The applied window: fromUtc (inclusive), toUtcExclusive (exclusive), and the effective timezone. |
categories |
The requested page of Category rows. Each row includes identity fields and all metrics for its report type. |
totals |
Independently calculated metrics for the full requested scope, including eligible activity without a Category. These are not the sum of Category rows or the current page. |
currentPage |
Requested page number. |
pageSize |
Requested or default page size. |
totalCount |
Number of eligible Categories before pagination. |
totalPages |
totalCount divided by pageSize, rounded up; zero when there are no eligible Categories. |
Each Category row has these identity fields:
| Field | Description |
|---|---|
key |
Opaque row identity. Treat it as a string, not a Category ID to parse. Do not assume unresolved Categories have the same key across the Story and Clip routes. |
categoryId |
Current CMS Category GUID, or explicit null if it cannot be resolved. |
externalId |
Available Category external ID, or explicit null. |
name |
Current Category label, or a historical fallback label when current metadata cannot be resolved. |
Historical rows can remain in the response after their current Category metadata becomes unavailable. Use key to distinguish rows with identical names. If metadata is restored, a fallback identity may resolve to a current Category key.
Requests beyond the last page return an empty categories array while retaining the full totals and counts. Sorting is deterministic for unchanged data. Separate page requests do not guarantee a fixed snapshot while analytics data changes.
Overlapping Categories and historical attribution#
For Clips, each event contributes once to every distinct Category recorded on that event. Later edits to a Clip's Category membership do not move its historical activity. Available history depends on recorded Category attribution; it is not reconstructed from current memberships.
For example, Clip A has 100 views recorded against Featured and Tutorials, and Clip B has 50 views recorded against Featured and Reviews:
| Category | Clip views |
|---|---|
| Featured | 150 |
| Tutorials | 100 |
| Reviews | 50 |
The overall totals.clipViews is 150, not 300. Use the independently calculated totals for overall reporting. Viewers are deduplicated within each Category using the corresponding CMS rules; do not sum viewers across Categories either.
Request and response examples#
These examples use a local Integrations host and synthetic data. Replace the local base URL with your configured Integrations API base URL and supply a Server App key privately.
Clip Category report#
curl --get "http://localhost:7071/api/analytics/clips/categories" \
--header "x-storyteller-api-key: YOUR_SERVER_APP_API_KEY" \
--data-urlencode "startDate=2026-09-01" \
--data-urlencode "endDate=2026-09-08" \
--data-urlencode "timezone=UTC" \
--data-urlencode "platforms=ios,android" \
--data-urlencode "sort=ClipViewsDesc" \
--data-urlencode "currentPage=1" \
--data-urlencode "pageSize=1"
The first page of the overlapping-Category example could return:
{
"range": {
"fromUtc": "2026-09-01T00:00:00Z",
"toUtcExclusive": "2026-09-08T00:00:00Z",
"timezone": "UTC"
},
"categories": [
{
"key": "11111111-1111-4111-8111-111111111111",
"categoryId": "11111111-1111-4111-8111-111111111111",
"externalId": "example-featured",
"name": "Featured",
"clipViews": 150,
"clipLoops": 30,
"viewers": 80,
"shares": 12,
"clickThroughs": 8,
"likes": 20
}
],
"totals": {
"clipViews": 150,
"clipLoops": 30,
"viewers": 80,
"shares": 12,
"clickThroughs": 8,
"likes": 20
},
"currentPage": 1,
"pageSize": 1,
"totalCount": 3,
"totalPages": 3
}
Story Category report#
curl --get "http://localhost:7071/api/analytics/stories/categories" \
--header "x-storyteller-api-key: YOUR_SERVER_APP_API_KEY" \
--data-urlencode "startDate=2026-09-01" \
--data-urlencode "endDate=2026-09-08" \
--data-urlencode "timezone=UTC" \
--data-urlencode "sort=PageViewsDesc"
{
"range": {
"fromUtc": "2026-09-01T00:00:00Z",
"toUtcExclusive": "2026-09-08T00:00:00Z",
"timezone": "UTC"
},
"categories": [
{
"key": "22222222-2222-4222-8222-222222222222",
"categoryId": "22222222-2222-4222-8222-222222222222",
"externalId": "example-tutorials",
"name": "Tutorials",
"pageViews": 240,
"viewers": 100,
"shares": 12,
"clickThroughs": 18,
"pollVotes": 25,
"triviaQuizAnswers": 40,
"triviaQuizCompletions": 10
}
],
"totals": {
"pageViews": 300,
"viewers": 120,
"shares": 15,
"clickThroughs": 20,
"pollVotes": 25,
"triviaQuizAnswers": 40,
"triviaQuizCompletions": 10
},
"currentPage": 1,
"pageSize": 10,
"totalCount": 1,
"totalPages": 1
}
Here the overall totals also include eligible activity without a Category, so they exceed the single Category row.
Errors and empty reports#
| Status | Meaning |
|---|---|
200 |
Report returned, including valid empty windows or pages beyond the last result. |
400 |
Invalid request, returned as Problem Details. Check required dates, ordering of the dates, timezone, platforms, metric sort, paging limits, and unsupported or repeated parameters. |
401 |
Missing or invalid API key, a non-Server App key, or unavailable tenant context. |
500 |
Analytics could not be read. The error is sanitized; do not treat it as a successful report with zero activity. |
For CMS comparisons, use the same tenant, applied date range, timezone and platforms. Confirm range before comparing totals, and account for overlapping Categories and the Story sort-dependent row selection.