FitRadio Partner API (2.0.0)

Download OpenAPI specification:

Music catalog discovery, playback assets, and royalty reporting for FitRadio integration partners.

Overview

The FitRadio Partner API gives approved partners access to FitRadio's licensed workout-music catalog. The content model is a simple hierarchy:

Catalog → Page → List → Mix

  1. GET /partners/catalog returns the pages (music rows) of the partner catalog.
  2. GET /partners/page/{page_id} returns the lists on one page.
  3. GET /partners/list/{list_id} returns the mixes in one list.
  4. GET /partners/mix/{mix_id} returns one mix with its playable HLS/MP3 URLs, artwork, DJ, and full track listing.
  5. POST /partners/royalty-tracking reports every track play back to FitRadio — required for licensing compliance.

Base URL

All endpoints are served from https://p.fitradio.com.

Authentication

All endpoints except the two POST /partners/session* endpoints require a session token.

  1. FitRadio issues you a client_id and client_signature out of band.
  2. Call POST /partners/session with them. The response contains a JWT session token.
  3. Send that token on every subsequent request in the Authorization header.

Important — send the raw token. The Authorization header value must be the JWT by itself, with no Bearer prefix and no other decoration:

Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOi...

A value of Bearer eyJ... is rejected with 401.

There is no X-Client-Id header anywhere in this API. Older documentation that mentioned one was wrong — the session token is the only credential sent on API calls.

Tokens expire 8 hours after issue. Any 401 response means your token is missing, malformed, expired, or was minted during standby mode (see below) — create a new session and retry. Build automatic re-authentication on 401 into your client.

Quickstart

# 1. Create a session
curl -s -X POST https://p.fitradio.com/partners/session \
  -H 'Content-Type: application/json' \
  -d '{"client_id": "YOUR_CLIENT_ID", "client_signature": "YOUR_CLIENT_SIGNATURE"}'
# → {"session":{"token":{"token":"eyJhbGciOi...","error":null}}}

TOKEN='eyJhbGciOi...'   # the inner session.token.token value, used raw

# 2. Discover content
curl -s https://p.fitradio.com/partners/catalog        -H "Authorization: $TOKEN"
curl -s https://p.fitradio.com/partners/page/71        -H "Authorization: $TOKEN"
curl -s https://p.fitradio.com/partners/list/529       -H "Authorization: $TOKEN"
curl -s https://p.fitradio.com/partners/mix/9252       -H "Authorization: $TOKEN"

# 3. Report a play (required for every track you play)
curl -s -X POST https://p.fitradio.com/partners/royalty-tracking \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "trackid": 704991,
    "playedat": 1754380800000,
    "playlength": 213,
    "sourcestream": "mix",
    "sourcedetail": "9252",
    "os": "web",
    "device": "chrome",
    "country": "US",
    "vendor": 12,
    "userid": "partner-user-42",
    "isrc": "USUM71703861",
    "endreason": 1
  }'

Data types and formats

These rules exist because getting them wrong silently corrupts royalty reporting. Please read them carefully.

Rule Detail
IDs are JSON integers Send "trackid": 704991 — never 704991.0, never "704991". Serializers that add a trailing .0 (e.g. Python floats, spreadsheet exports) break processing.
playedat is epoch milliseconds A 13-digit Unix timestamp, e.g. 1754380800000. Sending seconds (10 digits) is not rejected but corrupts play-time reporting — always milliseconds.
playlength is seconds Integer number of seconds the track actually played, e.g. 213.
Mix/track durations are strings length and time fields are HH:MM:SS strings, e.g. "00:56:13".
ISRC Every track in GET /partners/mix/{mix_id} includes its isrc (International Standard Recording Code). Echo the value back in royalty reports exactly as received — do not reformat, trim, or reconstruct it.
Country codes ISO 3166-1 alpha-2, uppercase, e.g. US, GB, DE.

Country

Catalog, page, list and mix responses are not filtered by country: every partner sees the same catalog. The country field of a royalty report is required and must be the listener's country (ISO 3166-1 alpha-2); a report without it is rejected with 400. It is never inferred from the request's IP or headers.

Requesting a page or list id that has no content returns 400 with reason: "no results for given type".

Standby mode

When FitRadio's primary database is unavailable, the API automatically fails over to a read-only standby service so playback keeps working. What changes for you:

  • Responses include the header x-fitradio-served-by: standby (or live-bypass). Absence of the header means normal live service.
  • POST /partners/session succeeds without verifying credentials and returns a short-lived token (~15 minutes) — and the response shape flattens: session.token is the JWT string directly instead of the live {token, error} object. Extract with typeof session.token === 'string' ? session.token : session.token.token.
  • Standby-minted tokens are rejected with 401 the moment normal service resumes. This is the main reason your client must re-authenticate automatically on any 401.
  • Some catalog metadata is served from cache and may be degraded (e.g. a mix's min_bpm/max_bpm may be equal, description may be empty).
  • POST /partners/royalty-tracking accepts and acknowledges reports; queue and re-send your reports after standby ends if you require delivery guarantees.

Errors

Two error envelopes are in use:

  • Most endpoints: { "error": true, "reason": "<human-readable message>", "mix": null } (the legacy mix field is always null and can be ignored).
  • POST /partners/royalty-tracking: { "error": true, "message": "<human-readable message>" }.
  • 401 responses from the authentication layer have a plain-text body (Unauthorized), not JSON.
Status Meaning
400 Missing/invalid parameter, or no content for the given id
401 Missing, malformed, expired, or standby-minted token; invalid session credentials
404 Mix not found
500 Internal error — retry with backoff, then contact FitRadio
503 Standby failover error — retry with backoff

Authentication

Exchange your partner credentials for a session token. Tokens live 8 hours; send them raw in the Authorization header (no Bearer prefix). Re-authenticate automatically whenever you receive a 401.

Create a session token

Exchanges your client_id + client_signature for an 8-hour JWT session token.

During normal service the token is at session.token.token (note the nesting — this differs from /partners/session/with-user, where session.token is the string itself). Send it raw in the Authorization header on all other endpoints.

During standby mode the shape differs: the endpoint returns a short-lived (~15 min) token without verifying credentials, and session.token is the JWT string directly (no inner object). Handle both: typeof session.token === 'string' ? session.token : session.token.token. Standby-minted tokens stop working (401) as soon as normal service resumes.

Authorizations:
None
Request Body schema: application/json
required
client_id
required
string

Your partner client id, issued by FitRadio (10 characters).

client_signature
required
string

Your partner client signature, issued by FitRadio (40 characters). Treat it as a secret — server-side use only.

Responses

Request samples

Content type
application/json
{
  • "client_id": "a1b2c3d4e5",
  • "client_signature": "f6e5d4c3b2a1098765432109876543210fedcba9"
}

Response samples

Content type
application/json
Example
{
  • "session": {
    }
}

Create a session token bound to a partner account

Like POST /partners/session, but additionally requires the partner account email registered with FitRadio and binds the token to that partner identity. Use this variant if your agreement uses the restricted catalog (GET /partners/catalog/restricted) — the restricted endpoint filters by the partner identity carried in this token.

Note the response shape difference: here session.token is the JWT string directly, while /partners/session nests it one level deeper (session.token.token).

Authorizations:
None
Request Body schema: application/json
required
client_id
required
string

Your partner client id, issued by FitRadio (10 characters).

client_signature
required
string

Your partner client signature, issued by FitRadio (40 characters). Treat it as a secret — server-side use only.

email
required
string <email>

The partner account email registered with FitRadio for this client id.

Responses

Request samples

Content type
application/json
{
  • "client_id": "a1b2c3d4e5",
  • "client_signature": "f6e5d4c3b2a1098765432109876543210fedcba9",
}

Response samples

Content type
application/json
{
  • "session": {
    }
}

Catalog

The entry point for content discovery: the pages (music rows) of the partner catalog. Use /partners/catalog/restricted if your partner agreement scopes you to a subset of the catalog.

List catalog pages

Returns the pages (music rows) of the partner catalog — the top level of the content hierarchy. Use each entry's page_id with GET /partners/page/{page_id}.

Authorizations:
PartnerToken

Responses

Response samples

Content type
application/json
{
  • "catalogs": [
    ]
}

List catalog pages allowed for your partner account

Same shape as GET /partners/catalog, additionally filtered by the content restrictions configured for your partner account.

The partner identity is read from the session token, so call this with a token from POST /partners/session/with-user. With a plain /partners/session token (no partner identity), the response falls back to the unrestricted country catalog.

Authorizations:
PartnerToken
header Parameters
CF-IPCountry
string^[A-Za-z]{2}$
Example: US

ISO 3166-1 alpha-2 country. On production Cloudflare sets it automatically from the caller's IP; server-to-server integrations may send it explicitly. Read only by this endpoint (the partner-restriction lookup is per country); values that are not exactly two letters are ignored.

Responses

Response samples

Content type
application/json
{
  • "catalogs": [
    ]
}

Pages

A page (music row) groups related lists — e.g. a genre or featured collection. Returns the lists on the page.

Get the lists on a page

Returns the lists on one catalog page. Use each list's list_id with GET /partners/list/{list_id}.

Authorizations:
PartnerToken
path Parameters
page_id
required
integer
Example: 71

Page id from GET /partners/catalog (page_id).

Responses

Response samples

Content type
application/json
{}

Lists

A list is an ordered collection of mixes (a station or curated genre list). Returns list metadata plus lightweight mix summaries.

Get the mixes in a list

Returns one list's metadata plus lightweight summaries of its mixes. Use each mix's id with GET /partners/mix/{mix_id} to get playable URLs and the track listing.

Authorizations:
PartnerToken
path Parameters
list_id
required
integer
Example: 529

List id from GET /partners/page/{page_id} (list_id).

Responses

Response samples

Content type
application/json
{}

Genre rows

Raw items of a single music row, with the full artwork set per item.

Get the items of a music row with full artwork

Returns the raw items of a single music row with the complete artwork set per item (wide, spotlight, player, thumbnail, basic). Useful for building richer browse UIs than the catalog/page endpoints allow. Not country-filtered.

Authorizations:
PartnerToken
path Parameters
music_row_id
required
integer
Example: 46

Music row id (same id space as catalog page_id).

Responses

Response samples

Content type
application/json

Mixes

A mix is a continuous DJ mix. The detail endpoint returns everything needed for playback and compliance: HLS stream URLs (iOS/Android renditions), MP3 URL, artwork, DJ info, and the full track listing with ISRCs.

Get a mix with playback URLs and track listing

Returns everything needed to play one mix and stay license-compliant:

  • android_hls_url / ios_hls_url — ready-to-play HLS manifests per platform. hls_url mirrors the Android rendition for backwards compatibility.
  • url — the raw MP3.
  • tracks — the full track listing with each track's isrc and start time, which you need for royalty reporting.

This endpoint is not geo-gated.

During standby mode, all three HLS fields may point at the same rendition and some metadata may be degraded (equal min_bpm/max_bpm, empty description).

Authorizations:
PartnerToken
path Parameters
mix_id
required
integer
Example: 9252

Mix id from GET /partners/list/{list_id} (mixes[].id).

Responses

Response samples

Content type
application/json
{}

Royalty tracking

Report every track play. Required for licensing compliance — integrations are not approved for launch until reporting is verified end-to-end by FitRadio. Read the Data types and formats section above before implementing.

Report a track play

Report every track play — one call per track, sent when the track finishes (or playback stops). Required for licensing compliance.

Read the Data types and formats section first. The most common integration mistakes, all of which corrupt reporting silently or fail the request:

  • playedat sent in seconds instead of milliseconds
  • numeric fields serialized with a trailing .0
  • playlength, userid, or isrc omitted

isrc should be echoed from the mix's track listing; if omitted, FitRadio falls back to looking it up from trackid, but sending it explicitly is strongly preferred. Send your real per-request values — do not hardcode example payloads from this page.

Authorizations:
PartnerToken
Request Body schema: application/json
required
trackid
required
integer

Track id from the mix's track listing (tracks[].id). Must be a JSON integer — 704991, never 704991.0 or a string.

playedat
required
integer <int64>

When playback of the track started, as a Unix epoch timestamp in milliseconds (13 digits). Sending seconds corrupts reporting.

sourcestream
required
string

What kind of stream the play came from, e.g. mix.

sourcedetail
required
string

Identifier of the stream source — for mix playback, the mix id as a string.

os
required
string

Operating system / platform of the player.

device
required
string

Device or client identifier.

country
required
string

ISO 3166-1 alpha-2 country of the listener. Required: a report without it is rejected with 400; it is never inferred from the request.

vendor
required
integer

Your vendor id, assigned by FitRadio. Must be a JSON integer.

playlength
integer

How many seconds of the track actually played. Strongly recommended — required for accurate reporting.

userid
string

Stable identifier of the end listener in your system. Strongly recommended — required for per-listener compliance reporting.

uuid
string

Unique id for this play event (deduplication).

isrc
string

ISRC of the track, echoed from tracks[].isrc exactly as received (do not reformat). If omitted, FitRadio looks it up from trackid; sending it explicitly is strongly preferred.

endreason
integer
Default: 1

Numeric code for why playback of the track ended. Defaults to 1 when omitted. FitRadio provides the code list for your integration during onboarding.

cachedplay
boolean
Default: false

Whether the play was served from a local cache rather than streamed.

jumpforward
boolean
Default: false

Whether the user seeked forward during the track.

Responses

Request samples

Content type
application/json
{
  • "trackid": 704991,
  • "playedat": 1754380800000,
  • "playlength": 213,
  • "sourcestream": "mix",
  • "sourcedetail": "9252",
  • "os": "web",
  • "device": "chrome",
  • "country": "US",
  • "vendor": 12,
  • "userid": "partner-user-42",
  • "uuid": "3f6a1c2e-8f4b-4b7e-9d2a-5c8e1f0a9b3d",
  • "isrc": "USUM71703861",
  • "endreason": 1,
  • "cachedplay": false,
  • "jumpforward": false
}

Response samples

Content type
application/json
{
  • "error": false,
  • "message": "success"
}