> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rivi.software/llms.txt
> Use this file to discover all available pages before exploring further.

# Public delivery

> CORS, caching, ETags, station gates, pagination, the realtime feed, and now-playing.

All **listener-facing** JSON reads share one delivery policy. Each JSON route requires a **station Bearer credential** with `listener:read` (see [Authentication](/radiotools/api-reference/authentication)). Published media bytes stay anonymous.

## HTTP rules

* `Access-Control-Allow-Origin: *` on success and errors; use `GET` and `OPTIONS` for JSON reads.
* Send **`Authorization: Bearer`** on JSON reads and GraphQL. Do not send dashboard session cookies — they are not honored on listener routes.
* `Cache-Control: no-store` with a strong SHA-256 **ETag**. Send `If-None-Match` for `304 Not Modified`.
* Unpublish, **public offline**, entitlement changes, and per-API switches apply on the **next** read.
* Poll no faster than `delivery.pollIntervalSeconds` from the station document (currently **30 seconds**).

Inactive, unknown, offline, or disabled resources return **`404 NOT_FOUND`** (same CORS headers). A resource whose station module is switched off returns **`404 MODULE_DISABLED`**.

## Station entry document

```http theme={null}
GET /api/v1/stations/{stationSlug}
```

Listener JSON projections include `contractVersion: 1`. Check that integer in your client. The `resources` object links schemas, content, schedule, programs, current broadcast, now-playing, track history, weather, traffic, and public media. Disabled APIs set their href to `null`. `resources.realtime` holds the [realtime feed](#public-realtime-feed) connection details instead of a link, or `null` when the feed is off.

The optional `profile` object carries the station's public profile for an about or contact page: tagline, description, frequency label, website and stream URLs, contact email, phone, address, and social links. Each value is `null` when the station has not set it. Calendar feeds, media bytes, and ingest outcomes use their own response shapes.

See [Requests and response handling](/radiotools/api-reference/response-handling) for runnable requests and [Field types and empty states](/radiotools/api-reference/field-types) for real values and nullability. Each HTTP operation includes curated response examples alongside its schema.

## Content collections

* Required query: `?type=<schema key>`.
* `limit` between 1 and 50 (default 25); follow `nextCursor`.
* Optional order: `sort=publishedAt` (default) or `sort=updatedAt`, with `direction=desc` (default) or `direction=asc`.
* Optional filters, up to ten, on text and choice delivery fields:
  * `filter=<field>:<value>` for an exact match
  * `filter=<field>.contains:<value>` for a case-insensitive substring
  * `filter=<field>.anyOf:<a>,<b>` for any of the listed values

Records include `publishedAt` for the current snapshot and `firstPublishedAt` for the first publication. `related.programIds` lists programs the record relates to.

To route website URLs by slug, use `GET …/content/{type}/by-slug/{slug}`. A slug the record used before still returns the record. Compare `values.slug` in the response with the requested slug and redirect to the current one when they differ.

Only **published** snapshots appear. Drafts and private fields never leak through public filters.

## Schedule and on-air

* `GET …/schedule?fromDate=&throughDate=` — at most **31 inclusive days** and 1000 broadcasts per request.
* `GET …/programs` — programs in that window, with their presenters and regular weekly `slots`.
* `GET …/broadcast/current` — live or current-window broadcast from public data.

Each broadcast has a `nature`: `live`, `prerecorded`, or `repeat`.

## Now playing and track history

These reflect **encoder observations**, not the planned rundown.

* **Now playing** — freshness (`fresh` / `stale` / `empty`); timing fields computed at response time.
* **Track history** — paginated observations; duplicate titles at different times stay distinct.

## Public realtime feed

The station document's `resources.realtime` describes the station's public feed: a WebSocket `url` and the `channel` its events use. It is `null` when the station has switched the feed off.

```json theme={null}
{
  "url": "wss://radiotools.rivi.software/api/v1/realtime/stations/station-rivier/feed",
  "channel": "databases.public.tables.public_station_feed.rows"
}
```

Connect to `url` without a token; the feed is public, so browsers can connect directly. The connection starts with the current state, then sends one event for each real change. Reconnect with backoff when the connection closes.

Each message is JSON. An event has `type: "event"` and a `data` object with `events`, `channels`, and a `payload` row. Ignore events whose `channels` don't include your `channel`. Each row carries a `station_id`, a `kind`, an `entry`, and a `data_json` frame. Keep only rows whose `station_id` matches the station document's `id`. An event name ending in `.delete` means the row was removed. A message with `type: "error"` reports a problem with the connection.

The frame kinds are:

* `schedule` — public broadcasts from today through the next seven days
* `on-air` — the live broadcast's planned rundown position, or `null`
* `now-playing` — the reported track, in the same shape as `GET …/now-playing`
* `content` — one published record; a removed row means the record was unpublished

An illustrative `data_json` schedule frame with no broadcasts:

```json theme={null}
{
  "protocol": "radiotools.public.v1",
  "stationId": "station-rivier",
  "type": "schedule",
  "data": {
    "contractVersion": 1,
    "timezone": "Europe/Amsterdam",
    "broadcasts": [],
    "truncated": false
  }
}
```

The [TypeScript SDK](/radiotools/api-reference/sdks/typescript) handles all of this in `subscribePublicFeed`, version 0.3.0 or later. It connects, reconnects, filters rows, and dispatches frames to typed handlers.

`on-air` describes the planned rundown, while `now-playing` carries reported automation metadata. Their meanings are separate. Editorial previews never appear on the feed.

## Listener push devices

Device changes use a credential with `push:register`. The provider must match the station's configured provider. A provider device token is separate from the RadioTools API credential.

```json theme={null}
{
  "token": "fictional-fcm-device-token",
  "platform": "android",
  "provider": "fcm"
}
```

Send that body to `POST …/push/devices` to register or refresh a device. `PATCH` accepts `token` plus `topics`, for example `{ "alerts": false, "content": true }`; omitted topics keep their current preference. `DELETE` accepts `token` to remove the device. The imported contract documents a `200` result without a response-body schema, so do not depend on a JSON acknowledgment.

## Public media

`/api/media/public/{stationId}/{versionId}[/{variant}]` — supports a single `Range` request; `private, no-store`; not shared-cacheable.

Image variants are `thumb` and `display`, resized to width, and `card` (16:9) and `square`, cropped around the image's focal point. Each record media item lists its rendition URLs in `variants`, plus `alt`, `width`, and `height` for accessible, responsive images. A variant is `null` when it is not available.

## Framework note

Do not rely on Next.js `fetch` segment caching for listener JSON. Revalidate in your app or poll within the documented interval.

Interactive operation docs: use the **Public listener** group in the sidebar.

## Implement refresh and removal

Keep the ETag with the body it represents. Send it on the next authorized read, and reuse that body only when the server returns `304`. Do not treat an authentication failure or `404` as a cache hit.

Use realtime feed updates as a reason to fetch current data. Revalidate after reconnecting and when content is unpublished. A locally cached story can remain visible in your own app until your consumer removes it; the RadioTools response policy does not clear your app's storage for you.

## Check a missing listener resource

Confirm the station slug, token scope, publication state, and entry-document resource link. A disabled link is `null`; do not manufacture a route for it. Weather and traffic also require the station's recorded redistribution permission.


## Related topics

- [Editorial policies](/radiotools/station-admin/editorial-policies.md)
- [Station settings](/radiotools/station-admin/station-settings.md)
- [Now-playing ingest](/radiotools/api-reference/now-playing-ingest.md)
- [List content](/radiotools/api-reference/public-listener/list-content.md)
- [Requests and response handling](/radiotools/api-reference/response-handling.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.