Skip to main content

Time-Limited Credentials

Overview

Every Feed.fm SDK is initialized with the token and secret pair that we provide to you — FMAudioPlayer.setClientToken(_:secret:) on iOS, the FeedAudioPlayer.Builder constructor on Android, new Feed.Player() in the browser. Whatever value you compile into the app or serve with your web page is the value every one of your users receives, where it can be extracted from the binary or read straight out of the page source and reused elsewhere.

To avoid handing out your permanent credentials, Feed.fm can mint additional token/secret pairs that the SDKs accept exactly like your original pair, but that automatically expire after a period of time you choose. Your own backend holds the original credentials, requests a short-lived pair when a user starts listening, and passes only that pair to the SDK. If one leaks, it stops working on its own — and you can revoke it before then.

Authentication basics

There is no SDK method for creating these credentials. Your backend creates and revokes them by making authenticated calls directly to the Feed.fm API server, using the two endpoints described below.

All API requests are made against the base URL https://feed.fm/api/v2/, so the full URL for POST /access_token is https://feed.fm/api/v2/access_token.

Credentials are passed using 'Basic' HTTP authentication as defined in RFC 2617. The Authorization header holds the base-64 encoding of the token and secret joined with a colon:

token ":" secret

For example, a token of demoToken and a secret of demoSecret produce:

Authorization: Basic ZGVtb1Rva2VuOmRlbW9TZWNyZXQ=

Both of the endpoints below must be authenticated with your original token/secret pair — not with a generated one.

Response format

Every response is a JSON object that minimally reports whether the call succeeded:

{
"success": true
}

A failed request instead returns an error object with a Feed.fm-specific error code, a human readable description, and the HTTP status code associated with the error:

{
"success": false,
"error": {
"code": 123,
"message": "human readable error for code 123",
"status": 200
}
}

If your client can't read response bodies for non-200 HTTP responses, every endpoint accepts a force200=1 parameter that makes the server always respond with HTTP 200.

Creating a credential

POST https://feed.fm/api/v2/access_token

Creates a new token/secret pair that can be used anywhere your original pair can.

Body Parameters

  • ttl_seconds

    The number of seconds the new token/secret should be valid for. This defaults to 86,400 (one day). This parameter has a maximum value of 15,552,000 (6 months).

Error Codes

In addition to the generic error codes:

Error CodeHTTP Status CodeDescription
6401Forbidden. Missing Credentials.

Response

The returned object contains a new token and secret value that is available for immediate use:

{
"success": true,
"access_token": {
"token": "123123123123123",
"secret": "464564564564564"
}
}

Example

Request a credential that is good for one hour:

curl -X POST https://feed.fm/api/v2/access_token \
-u "$FEEDFM_TOKEN:$FEEDFM_SECRET" \
-d "ttl_seconds=3600"

Revoking a credential

DELETE https://feed.fm/api/v2/access_token/:token

Revokes a token/secret that was previously created via POST /access_token. Use this when a user logs out, when a session ends early, or when you believe a credential has been compromised.

Path Parameters

  • :token

    A token value that was created via POST /access_token.

Error Codes

In addition to the generic error codes:

Error CodeHTTP Status CodeDescription
6401Forbidden. Missing Credentials.

Response

The returned object only indicates whether the deletion was successful.

{
"success": true
}

Example

curl -X DELETE https://feed.fm/api/v2/access_token/123123123123123 \
-u "$FEEDFM_TOKEN:$FEEDFM_SECRET"

Putting it together

The recommended pattern is a small endpoint on your own backend that authenticates your user however you normally would, mints a credential scoped to the length of a listening session, and returns it to the client:

// Node/Express example — runs on YOUR server, where the original
// 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/v2/access_token', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
// good for four hours
body: new URLSearchParams({ 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 the SDK 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();

Notes and recommendations

  • Keep your original token and secret on a server you control. Only generated credentials should reach client devices.
  • Pick a ttl_seconds that matches how long a session realistically lasts. Shorter lifetimes limit the value of a leaked credential; longer ones mean fewer round trips to your backend.
  • Request a fresh pair from your backend each time the app starts the player rather than persisting one on the device. Expiration is not signalled in advance, and this keeps the SDK from ever being handed a credential that has already lapsed.
  • A generated credential grants the same access as the original pair — it is time-limited, not permission-limited.
  • The client ID that identifies a listener is independent of these credentials. Swapping credentials does not reset a user's playback history; see Client ID Swapping for how that value is managed.

Detecting an expired credential

Both 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 didn't come up, but they carry no error, so they can't 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 are handled as ordinary transient request errors — retried or reported as generic playback errors — rather than as a distinct "credentials expired" signal, so don't 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.