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.