@rivi-software/radiotools-sdk is the complete RadioTools client. It runs anywhere fetch and Web Crypto are available: Node.js, Bun, Deno, and edge runtimes.
Install
Create a client
Set the environment variables, then:createStationClient requires stationSlug and at least one of apiToken or ingestToken, and throws when either is missing. Reads and subscribe need apiToken; reportPlayback needs ingestToken. createStationClientFromEnv(env?, overrides?) reads process.env unless you pass an environment object, which is useful on runtimes without process. stationClientOptionsFromEnv(env?) returns the same options without creating a client.
Options
Read station data
Methods resolve resource links from the station document, so the first call also loads the station. The client remembers it for later calls.
Schedule windows use
YYYY-MM-DD dates and cover 1 to 31 inclusive days. Other values throw before any request is sent. The default page size is 25, and a content page holds up to 50 records.
Sort and filter content
sort is "publishedAt" (first publication, the default) or "updatedAt" (when the live version was published). direction defaults to "desc", newest first. Reuse a nextCursor only with the same sort and direction.
filter takes up to 10 filters on text and choice fields. A record must match all of them. Write each filter as an object or as the REST string form:
Look up a record by slug
getContentBySlug(type, slug) returns the published record a slug names. A slug the record used before also resolves, so compare values.slug with the slug you asked for and redirect when they differ:
Poll politely
Realtime updates
The station’s public realtime feed pushes now-playing, on-air, schedule, and content changes.station.resources.realtime holds the feed’s WebSocket url and channel, and it is null when the feed is switched off. The feed needs no credential. See Public realtime feed.
Realtime support needs version 0.3.0 or later.
Subscribe with the client
client.subscribe(handlers) reads the station document, then subscribes to the feed. It needs apiToken for that first read.
Each connection starts with the station’s current state, so your handlers receive a snapshot before live changes arrive. When the connection closes, the SDK reconnects with backoff (1, 2, 5, 10, then 30 seconds) until you call
close(). After a reconnect, the handlers receive the current state again.
The SDK ignores rows for other stations, events on other channels, and messages that do not match protocol radiotools.public.v1. The feed URL must use wss:; plain ws: is allowed only for localhost, 127.0.0.1, and [::1].
Subscribe from a browser
Because the feed needs no credential, a browser can subscribe directly withsubscribePublicFeed. Read the station document on your server, then pass resources.realtime and the station id to the page. The station key stays on your server.
Browsers, Bun, Deno, and Node.js 22 or newer have a global
WebSocket. On older Node.js versions, pass a factory, for example with the ws package:
Upgrading from 0.2. Version 0.3.0 replaces
station.resources.socket with station.resources.realtime. subscribePublicSocket is replaced by subscribePublicFeed. subscribe no longer takes a schedule window argument, and the createSocket factory receives only the URL. PublicSocketHandlers and PublicSocketSubscription remain as deprecated aliases of PublicFeedHandlers and PublicFeedSubscription. Content cursors (nextCursor) are now opaque strings instead of UUIDs.Caching and retries
Pass an ETag cache to turn repeat reads into conditional requests. A304 response reuses the cached data.
- The cache is in memory. Create it once and reuse it for the life of the process.
- Cache keys include a hash of the station key, so rotating the key does not serve stale data across credentials.
429and503responses are retried up toretriestimes. The SDK honors a numericRetry-Afterheader (capped at 5 seconds) and otherwise backs off exponentially from 250 ms, also capped at 5 seconds.
Handle errors
API failures throwRadioToolsApiError:
See Errors and versioning for what each code means and how to recover.
The SDK also throws plain
Error for problems it detects locally: a missing token, an invalid schedule window, an unavailable resource such as a station without a published schedule, an oversized playback observation, or an unparseable error body. Responses are validated against the contract schemas, so a response that does not match throws a Zod error instead of returning unexpected data.
Report playback
Encoders and automation send now-playing observations withreportPlayback. This requires ingestToken.
- Send
artistandtitletogether, or sendstreamTitlealone. - If you omit
eventId, the SDK generates one for the call, so automatic retries of that call cannot record the same observation twice. Set your own stableeventIdwhen your source can resend the same event. - Requests over 8 KiB are rejected before they are sent.
isPlayingdefaults totrue.- By default the station comes from the client’s
stationSlug. To target a station by ID, pass{ stationId }as the second argument.
reportPlayback({ baseUrl, ingestToken, stationSlug, observation }). Pass stationSlug or stationId, not both.
outcome is one of recorded, matched, uncertain, ignored, not-playing, no-live-broadcast, on-air-not-following-rundown, or rejected. Field details are in Now-playing ingest.
Media URLs
Each published media entry carriesurl (the original), alt, width, height, duration, and variants. variants.thumb and variants.display are smaller resizes, and variants.card (16:9) and variants.square are crops around the image’s focal point. Each variant is null when it does not exist. Use them for srcset and list pages, and use width and height to reserve the aspect ratio.
publicMediaUrl(baseUrl, mediaResourcePrefix, versionId, variant?) builds an anonymous media URL from station.resources.media and a versionId. variant is "thumb", "display", "card", or "square"; omit it for the original. The SDK builds URLs only; your page or CDN fetches the bytes.
Types and schemas
The package exports a type for every response, such asSdkPublicNowPlaying and SdkPublishedRecord, plus the Zod schemas as sdkSchemas. Option and handler types include CreateStationClientOptions, ContentListOptions, ContentFilter, PublicFeedHandlers, PublicFeedSubscription, SubscribePublicFeedOptions, SdkPublicRealtimeDescriptor, PublicMediaVariant, PreviewHandoff, and PreviewEditorialOptions. Use the schemas to validate data you store or forward.
LISTENER_CONTRACT_VERSION (1) and PUBLIC_SOCKET_PROTOCOL ("radiotools.public.v1") expose the contract constants.
Preview unpublished content
client.previewEditorial(grant, input) renders unpublished editorial content with a 15-minute preview grant. Pass the grant explicitly, never from a URL. With transport: "graphql" the SDK sends the station key and the grant in X-Preview-Grant; over HTTP it sends the grant as the Bearer token. Studio session cookies are never sent.
When the station has set a draft preview address and an editor clicks Open on website, RadioTools posts the grant to that address as form fields: grant, stationId, recordId, revision, and audience. readPreviewHandoff reads them from FormData, URLSearchParams, or a plain object. The standalone previewEditorial then fetches the draft. The grant covers one revision and audience.
previewEditorial uses HTTP by default. For GraphQL, pass transport: "graphql" with stationSlug and apiToken.