Skip to main content

Apps and credentials

Everything that connects your product to Feed.fm runs through a pair of credentials: a token and a secret. This page covers where those come from, how to hand them to an untrusted device safely, and which credentials to develop against before you have your own.

It applies whether you integrate with one of our SDKs or call the API directly.

Your apps​

An app is the container for the stations your product plays. When you sign up, we create two:

AppWhat it is for
ProductionYour live product. Playback here is billed and reported to rights holders.
DevelopmentTesting and development. Playback incurs no charges and reports no analytics.

Each app has its own token and secret, which you will find in the Customer Portal. The portal is also where you see the stations each app carries.

Develop against the development app. It exists so you can exercise playback repeatedly without generating charges or polluting your reporting. The development app may also hold different stations and configuration, allowing you to test against station changes before they are made to the production app.

warning

Do not ship your development credentials in your production app. Plays made with them are not reported, so you lose both your own analytics and the play reporting we pass to rights holders. Feed.fm reserves the right to revoke any app found using development credentials in production.

Consumer and access credentials​

The pair from the Customer Portal is your consumer credentials. They are long-lived and they are the keys to your app, so they belong on a server you control.

That is a problem for a client application. Whatever you compile into a mobile binary or serve in a web page reaches every one of your users, where it can be extracted and reused elsewhere.

Access credentials solve that. They are additional token and secret pairs that you mint from your consumer credentials, that work anywhere the consumer pair works, and that expire on their own. Your backend holds the consumer credentials and hands the device only a short-lived pair. If one leaks, it stops working by itself, and you can revoke it before then.

tip

An access credential is time-limited, not permission-limited. While it is valid it grants the same access your consumer pair does.

Generating an access credential​

There is no SDK method for this. Your backend calls the API directly, using your consumer credentials, and passes the result to the client.

curl -sS -u "$FEEDFM_TOKEN:$FEEDFM_SECRET" \
-X POST https://feed.fm/api/v3/access_token \
-H 'Content-Type: application/json' -d '{"ttl_seconds": 3600}'
{
"success": true,
"access_token": {
"token": "kD8vQ2mXw5tR9cLpZ3nHbY7fJ4sA6gE1",
"secret": "uT0iN8rW2yB5xM3qV7dK9pF4hC6zL1oG"
}
}

Hand that token and secret to the client exactly as you would the consumer pair. Every SDK and every API endpoint accepts it.

Three things the response does not tell you:

  • ttl_seconds defaults to 86400 (24 hours) when you omit it.
  • Values above 2592000 (30 days) are silently clamped, not rejected. The call still returns success: true and the token simply expires earlier than you asked for.
  • There is no expiry in the response. Record the lifetime you requested rather than assuming you got it.

Pick a lifetime that matches a realistic listening session, and mint a fresh pair each time your app starts the player rather than storing one on the device. Expiry is not signalled in advance.

Revoking an access credential​

curl -sS -u "$FEEDFM_TOKEN:$FEEDFM_SECRET" \
-X DELETE https://feed.fm/api/v3/access_token/kD8vQ2mXw5tR9cLpZ3nHbY7fJ4sA6gE1

Revoke when a user logs out, when a session ends early, or when you think a credential has been compromised. The request must use the consumer credentials that created the token.

For how credentials travel on an HTTP request, see REST API: Credentials.

Handing credentials to your app​

The pattern is a small endpoint on your own backend that authenticates your user however you normally would, mints a pair scoped to a listening session, and returns only that pair.

// Node/Express. Runs on YOUR server, where the consumer
// token and secret never leave.
app.post('/music-credentials', requireLoggedInUser, async (req, res) => {
const auth = Buffer.from(
`${process.env.FEEDFM_TOKEN}:${process.env.FEEDFM_SECRET}`
).toString('base64');

const response = await fetch('https://feed.fm/api/v3/access_token', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json'
},
// good for four hours
body: JSON.stringify({ ttl_seconds: 14400 })
});

const body = await response.json();

if (!body.success) {
return res.status(500).json({ error: body.error.message });
}

res.json({
token: body.access_token.token,
secret: body.access_token.secret
});
});

The client then initializes with the credentials it was handed, instead of with hardcoded values:

// Javascript SDK
var player = new Feed.Player(credentials.token, credentials.secret);
// iOS SDK
FMAudioPlayer.setClientToken(credentials.token, secret: credentials.secret)
// Android SDK
FeedAudioPlayer player =
new FeedAudioPlayer.Builder(getApplicationContext(),
credentials.token,
credentials.secret)
.build();

The client id that identifies a listener is independent of all this. Swapping credentials does not reset a user's playback history; see Client ID Swapping for how that value is managed.

Detecting a rejected credential​

Both native SDKs validate credentials when they create a session at player startup, and that is where an expired pair is reported as a distinct, identifiable error. Watch for it there, and respond by fetching a new pair from your backend and initializing the player again.

iOS​

Assign the FMAudioPlayerDelegate before calling setClientToken(_:secret:), since the session request begins immediately. A rejected credential arrives as a FeedFMError with code FeedFMErrorCodeSessionCreationFailed (1203), and the specific cause is carried in its underlying error, an NSError in the FMAPIErrorDomain with code FMErrorCodeInvalidCredentials (5):

FMAudioPlayer.shared().delegate = self
FMAudioPlayer.setClientToken(credentials.token, secret: credentials.secret)

// ...

func audioPlayerDidReceiveError(_ error: Error) {
let nsError = error as NSError

guard nsError.code == FeedFMErrorCode.sessionCreationFailed.rawValue,
let underlying = nsError.userInfo[NSUnderlyingErrorKey] as? NSError,
underlying.domain == FMAPIErrorDomain,
underlying.code == FMErrorCode.invalidCredentials.rawValue // 5
else {
return
}

// The credential was rejected. Fetch a new pair and start over.
fetchCredentials { credentials in
FMAudioPlayer.setClientToken(credentials.token, secret: credentials.secret)
}
}

whenAvailable(_:notAvailable:) and a playbackState of FMAudioPlayerPlaybackStateUnavailable also tell you the player did not come up, but they carry no error, so they cannot distinguish an expired credential from a geographic restriction or a network failure. Use the delegate for that.

Android​

A rejected credential is delivered to AvailabilityListener.onPlayerUnavailable() as a FeedFMError whose apiError is ApiErrorEnum.INVALID_CREDENTIALS (code 5):

FeedAudioPlayer.Builder(context, credentials.token, credentials.secret)
.setAvailabilityListener(object : AvailabilityListener {
override fun onPlayerAvailable(player: FeedAudioPlayer) {
// music is ready
}

override fun onPlayerUnavailable(e: Exception) {
if ((e as? FeedFMError)?.apiError == ApiErrorEnum.INVALID_CREDENTIALS) {
// The credential was rejected. Fetch a new pair and rebuild
// the player. Token and secret are only settable on the Builder.
fetchCredentials { credentials -> buildPlayer(credentials) }
}
}
})
.build()

Because the token and secret are constructor arguments to Builder, recovering means calling destroyInstance() on the existing player and building a new one.

If a credential expires mid-session​

Every API call carries the credentials, so a pair that lapses while the app is running will cause subsequent calls to fail. Those failures surface as ordinary transient request errors, retried or reported as generic playback errors, rather than as a distinct "credentials expired" signal. Do not build your recovery around catching them. Size ttl_seconds comfortably longer than a realistic listening session, and mint a fresh pair on each player startup.

Test credentials​

Use these to test out an implementation before your own credentials arrive, or to reproduce conditions that are otherwise hard to stage. Pass the same string as both the token and the secret.

CredentialWhat you get
demoTwo ordinary stations, Station One and Station Two. The general-purpose starting point.
countingOne station, Numbers, playing short clips of a voice counting. Songs end quickly, which makes playback reporting and song transitions fast to test.
crossfaderOne station, Crossfader, configured with a 3 second crossfade, for checking how your player handles the overlap between songs.
badgeoA client with no music available, for testing how your app behaves where it cannot stream.

badgeo is worth understanding before you use it. It is a valid credential, so the session request succeeds: you get HTTP 200 and success: true, with session.available set to false and a message explaining there is no music. It does not produce an authentication error. That makes it the way to test the path your app takes when a listener cannot be served, which is a real condition in production whenever someone opens your app outside a licensed territory.

Plays made with test credentials are not billed and are not reported.