---
updatedAt: 2026-08-24T11:36:45.000Z
agentTools:
  projectIndex: https://developers.zentag.ai/llms.txt
---

# Media library filters

Filter, count and total the media library - the same filters the Zentag Studio library uses.

`GET /media` is the unified library: clips, highlights and streams under one
shape. It accepts the same filters as the Zentag Studio media library, so a
partner UI can offer the same browsing experience.

Three endpoints share one filter vocabulary:

| Endpoint                    | Answers                                       |
| --------------------------- | --------------------------------------------- |
| `GET /media`                | Which items match?                            |
| `GET /media/stats`          | How many, how long, what average rating?      |
| `GET /media/filters/counts` | Which filter values are still worth offering? |

Send the same parameters to all three and they describe the same selection.
That is what lets you render a list, a header total, and a filter sidebar that
greys out dead ends, from one piece of UI state.

## How filters combine

Every list-valued filter is **comma-separated**. Within one parameter, values
are OR-ed; across parameters, they are AND-ed.

```
?players=p1,p2&rating=5
```

means *(player p1 OR p2) AND rated 5*.

## Filter reference

### Scope

| Parameter      | Notes                                               |
| -------------- | --------------------------------------------------- |
| `search`       | Case-insensitive substring match on title           |
| `stream_id`    | Restrict to one or more streams                     |
| `streams_only` | Return stream rows instead of the media inside them |
| `media_types`  | Subset of `clip`, `highlight`, `stream`             |

### Media attributes

| Parameter         | Notes                                 |
| ----------------- | ------------------------------------- |
| `aspect_ratio`    | e.g. `16:9,9:16`                      |
| `clip_status`     | Processing status, e.g. `COMPLETED`   |
| `download_status` | Download state                        |
| `ai_event_type`   | AI-detected event type                |
| `category`        |                                       |
| `platform`        | Destination the item was published to |
| `source`          | How the item was created              |
| `rating`          | Star ratings, e.g. `4,5`              |
| `tags`            | Tag names                             |

### Sport context

| Parameter                            | Notes                                        |
| ------------------------------------ | -------------------------------------------- |
| `players`                            | Player **tag ids**, from the `players` facet |
| `actions`                            | Action **tag ids**, from the `actions` facet |
| `competition`, `season`              | Names                                        |
| `matches`, `match_date`, `match_day` |                                              |
| `teams`, `venues`                    | Names                                        |

`players` and `actions` take ids, not names. Read them from the `id` field of
the corresponding bucket in `/media/filters/counts` and pass them straight back.

### Time and ordering

| Parameter                | Notes                                                       |
| ------------------------ | ----------------------------------------------------------- |
| `start_date`, `end_date` | Creation date range, inclusive, `YYYY-MM-DD`                |
| `sort_by`                | `latest` (default), `oldest`, `rating`, `duration`, `title` |

### Hierarchy fast-path

| Parameter                   | From facet               |
| --------------------------- | ------------------------ |
| `hierarchy_competition_ref` | `hierarchy_competitions` |
| `hierarchy_season_id`       | `hierarchy_seasons`      |
| `hierarchy_round_id`        | `hierarchy_rounds`       |
| `hierarchy_matchday_id`     | `hierarchy_matchdays`    |

These filter on structural ids rather than names. Prefer them for a drill-down
UI (competition → season → round → matchday): they are faster than the
equivalent name-based filters and immune to naming inconsistencies.

## Building a filter sidebar

`GET /media/filters/counts` returns each filter's selectable values with the
number of items behind each, computed against the filters you already applied.
Values that would return nothing are absent, so you can hide or disable them.

```json
{
  "object": "media_filter_counts",
  "aspect_ratios": [
    { "value": "16:9", "count": 812 },
    { "value": "9:16", "count": 194 }
  ],
  "players": [
    { "id": "665f...", "name": "A. Rossi", "count": 47 }
  ],
  "facet_pagination": {
    "players": { "total": 312, "has_more": true, "page": 1, "page_size": 20, "offset": 0,
                 "load_more_key": "players:2" }
  }
}
```

Note the two shapes: most facets are `{ value, count }`, while the tag-backed
`players` and `actions` are `{ id, name, count }`.

### Buckets are optional

**Read every bucket defensively.** A bucket appears only when it was computed,
and two parameters deliberately narrow that:

* `counts_bundle` computes only one named group of facets. Use it to keep
  sidebar refreshes cheap instead of recomputing everything on each click.
* `facet_load_more` returns a single bucket's next page, so the response
  contains just that one bucket.

### Paging within a facet

A facet with hundreds of values is paginated on its own. `facet_pagination`
reports the state per bucket; when `has_more` is true, send its `load_more_key`
back as `facet_load_more` to fetch the next page of that one facet without
recomputing the rest.

`facet_page_size` and `facet_page` control this directly if you would rather
drive it yourself.

### The match index

Pass `include_match_index=true` to also receive `match_index`: the matches in
the current selection, with names, dates, teams and venues. This drives a
"jump to match" control.

Each entry carries a `match_ref` - a stable, opaque handle. It is safe to store
and compare across requests, but it is not a third-party provider's match id and
cannot be resolved against one. See [Identifiers](#identifiers).

## Showing totals

`GET /media/stats` returns the totals for the current selection in one request,
which is far cheaper than paging the whole result set to count it:

```json
{
  "object": "media_stats",
  "total_items": 1204,
  "total_duration_seconds": 22344,
  "total_duration_label": "6h 12m",
  "average_rating": 4.1,
  "processing_count": 3,
  "clip_count": 1150,
  "highlight_count": 54,
  "stream_count": 12,
  "stream_media_count": 1204
}
```

## Pagination

Paginate with `cursor`, not `page`. The cursor is opaque - pass `next_cursor`
back verbatim, and don't parse it. Changing any filter or `sort_by` invalidates
the cursor; restart from the first page. See [Pagination](pagination).

## Identifiers

Every id returned here is a Zentag identifier. Zentag ingests fixtures, squads
and match data from third-party sports data providers, but provider-internal
identifiers are deliberately never exposed through this API - not in media
items, not in tag metadata, and not in facet counts.

Player tag `metadata` is therefore a closed set: `player_name`, `short_name`,
`position`, `jersey_number`, `nationality` and `roster_memberships`. Do not
write code expecting other keys to appear, and do not attempt to join Zentag ids
against another provider's data.