Skip to main content

Station playback

The Feed.fm API delivers individual audio files to clients, rather than a single audio stream. Playing a station is a loop of four calls: reserve a play, report that audio started, report elapsed time while it runs, report that it finished. Then go back to the top for the next song.

This page assumes a CLIENT_ID from Session and a STATION_ID from Station discovery. Every command below was run against https://feed.fm/api/v3 with the demo/demo test credentials and shows what actually came back.

The loop​

1. Reserve a play​

curl -sS -u demo:demo -X POST https://feed.fm/api/v3/play \
-H 'Content-Type: application/json' \
-d "{\"client_id\":\"$CLIENT_ID\",\"station_id\":\"$STATION_ID\"}"
{
"success": true,
"play": {
"id": "200956727084045",
"audio_file": {
"id": "123262",
"duration_in_seconds": 250,
"codec": "mp3",
"track": { "id": "15360576", "title": "All the Time" },
"release": { "id": "1618797", "title": "It's About Time" },
"artist": { "id": "1206212", "name": "Eddie Roberts' West Coast Sounds" },
"url": "https://d2pz0anq08lzl7.cloudfront.net/1543381801-43916.mp3",
"bitrate": 128
},
"station": { "id": "199835928", "name": "Station One", "uuid": "9ebd57dc-c034-4373-a424-51764406bfcb" }
}
}

By default, the returned audio file will be encoded as mp3, 128kbps; other formats and bitrates can be requested (see the full API documentation for details).

The returned play is reserved, and not considered to be playing. Multiple POST /play calls with the same parameters will return the same play object, but with an updated audio file URL that is only valid for 20 minutes.

Set PLAY_ID from this response's play.id before continuing:

export PLAY_ID="200956727084045"

Unlike CLIENT_ID and STATION_ID, PLAY_ID changes with every song. You will export it again from play.id after every POST /play call, or from the play a POST /station search returns.

Eventually POST /play may stop returning a play at all. When no song can be selected for this client and station, the response is HTTP 200 with {"success": false, "error": {"code": 9, ...}}, mnemonic noMoreMusic. That is not an error condition; it is the normal way a station ends for a client. Stop requesting from that station rather than retrying: noMoreMusic counts toward the per-client error throttle described in Credentials, so a retry loop gets the client throttled.

2. Start playback​

Call this the moment audio actually begins, not when the play was reserved.

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/start" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"
{ "success": true, "can_skip": true, "can_like": true }

This call informs the servers that the end user has begun hearing the play.

Note that a play object is no longer considered valid if a different play has been reported started since its creation. If you were to call POST /play to reserve a play for station A, followed by POST /play to reserve a play for station B, then calling POST /play/start/{play_id} for either play would invalidate the other.

Read can_skip and can_like off this response and set the UI from them. Both are advisory and both follow the license the track is actually playing under for this listener, so do not assume either is always true; see Likes, skips.

3. Report elapsed time​

Call this periodically while the song plays, not just once. Typical clients send it every 10 to 30 seconds. It is what fixes the reported duration if the listener disappears mid-song.

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/elapse" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\",\"seconds\":30}"
{ "success": true }

4. Complete the play​

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/complete" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"
{ "success": true }

Completing a play twice is safe. The second call is a no-op: it returns 200 with success: true and leaves the stored play alone. So if a complete response is lost to a dropped connection, retry it rather than assuming the report landed. This applies only to a play that completed. Completing a play you already skipped or invalidated fails with playbackComplete (code 21).

Then loop back to step 1 for the next song, until the user stops playback or that station answers noMoreMusic as described above, at which point stop requesting from it.

Song selection is the server's job​

Selection rules, including limits on consecutive songs from the same artist or album, are enforced on the server. They vary by country and by track, so no fixed numbers are published. Any play the server returns is already compliant, so a client should not implement, track, or work around these rules.

Prefetch, to prevent waiting​

Fetching audio is faster than playing it, so the way to close the gap between songs is to call POST /play for the next song right after start on the current one, then let it sit until the current song completes.

The ordering is not a style preference. Repeated POST /play calls with the same parameters, with no start or invalidate between them, return the same play again. Calling POST /play twice up front to "get ahead" returns the same play twice. Here are two calls in a row, same client, same station, nothing between them:

curl -sS -u demo:demo -X POST https://feed.fm/api/v3/play \
-H 'Content-Type: application/json' \
-d "{\"client_id\":\"$CLIENT_ID\",\"station_id\":\"$STATION_ID\"}"
{ "success": true, "play": { "id": "200948627224717", "...": "..." } }
curl -sS -u demo:demo -X POST https://feed.fm/api/v3/play \
-H 'Content-Type: application/json' \
-d "{\"client_id\":\"$CLIENT_ID\",\"station_id\":\"$STATION_ID\"}"
{ "success": true, "play": { "id": "200948627224717", "...": "..." } }

Same play.id both times. start on the current play is what releases the next one, so prefetch only works if you call start first.

Interleaved correctly, the prefetch for the next song sits between start and complete on the current one. Here are three songs in a row, with the calls in the order they were sent and the responses each returned:

POST /play                       -> 211960172540941    reserves song 1
POST /play/211960172540941/start -> can_skip, can_like song 1 audio begins
POST /play -> 211960198867405 reserves song 2
POST /play/211960172540941/elapse -> success repeats while song 1 plays
POST /play/211960172540941/complete -> success song 1 ends
POST /play/211960198867405/start -> can_skip, can_like song 2 starts with no wait
POST /play -> 211960250732237 reserves song 3
POST /play/211960198867405/elapse -> success
POST /play/211960198867405/complete -> success
POST /play/211960250732237/start -> can_skip, can_like

Three different play ids, because every POST /play came after a start. The pattern from song 2 onward is the steady state: start the song you already hold, reserve the next one, report elapsed time, complete, repeat.

You can be one play ahead, not two. A second POST /play before the reserved play starts returns the same play again, as above.

Things that catch people​

What follows is behavior that is easy to get wrong even after reading every field description.

Invalidate, do not fake​

If audio will not play, call POST /play/{id}/invalidate and reserve another play. Never call start and complete on a song nobody heard. Accurate playback reporting is a compliance obligation, and a fabricated report misstates what was played.

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/invalidate" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"
{ "success": true }

An expired URL is not an invalidate​

Signed audio URLs expire roughly 20 minutes after they are issued; the Expires query parameter on the URL is a unix timestamp you can check directly. Once it has passed, call POST /play again for a fresh URL rather than invalidating the play.

Checking for errors​

success is the field to branch on, not the HTTP status line. Four error codes arrive as HTTP 200 with success: false: noMoreMusic (9) and formatUnavailable (24) here, and skipDenied (7) and playNotActive (12) covered in Likes, skips. The full list of error codes and their HTTP statuses is in the API reference; this page does not repeat it.