Skip to content

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.