Authentication
StreamerSonglist provides an OAuth based authentication mechanism adhering to parts of the OAuth2.0 protocol
Tokens
| Type | Obtained by | Description |
|---|---|---|
| OAuth2 access tokens | Authorization code flow | Authenticate a user and let your app act on their behalf, limited to the scopes they granted. This is what a third-party application uses. |
| Refresh tokens | Authorization code flow | Long-lived tokens exchanged for a new access token when the original expires. Issued only when the offline_access scope is requested. Refresh tokens rotate on each exchange — the previous one is invalidated. |
| ID tokens | Authorization code flow | Signed JWTs identifying the authenticated user. Issued when the openid scope is requested. Verify signatures against the JWKS endpoint at /.well-known/jwks.json. |
| Streamer access tokens | Settings > Access | Created by a streamer for their own channel. Full access to that one channel, no scopes, no OAuth flow to implement. |
| User access tokens | Profile > API Access | Created by a user for themselves. Reaches every channel they own or administrate, gated by their admin permissions on each. |
| Client credentials tokens | Client credentials flow | Authenticate your application itself, with no user behind it. Accepted only on GET endpoints that require no scope — see that section for the exact rule. |
Access tokens and refresh tokens are opaque — they are not JWTs and carry no readable claims. Do not attempt to parse or verify one locally; treat it as a bearer string and let the API resolve it. Only ID tokens are JWTs.
Clients
Register a client in the developer portal when your application acts on behalf of other people. There is one kind of client; it supports the authorization code flow (with PKCE and refresh tokens), which is how you obtain an access token for a user who has granted your app scopes.
You may not need a client at all. If you are building something for your own use, an access token you create yourself is simpler and involves no OAuth flow:
| What you are building | Use |
|---|---|
| An app used by streamers or viewers other than yourself | An OAuth client, authorization code flow |
| An integration for one channel you own | A streamer access token |
| An integration across every channel you own or administrate | A user access token |
Scopes
Scopes use a 3-segment dot-separated format: category.resource.permission
For example, streamer.song.read grants Read access to the Songs resource in the streamer category.
Permissions
Each resource supports two permissions:
| Code | Permission |
|---|---|
read | Read |
write | Write |
Available Resources
| Resource | Label | Wildcard Scope |
|---|---|---|
streamer.action-log | Activity Log | streamer.action-log.* |
streamer.attribute | Attributes | streamer.attribute.* |
streamer.command | Commands | streamer.command.* |
streamer.overlay | Overlays | streamer.overlay.* |
streamer.play-history | Play History | streamer.play-history.* |
streamer.queue | Queue | streamer.queue.* |
streamer.settings | Settings | streamer.settings.* |
streamer.song | Songs | streamer.song.* |
streamer.learn-list | Learn List | streamer.learn-list.* |
streamer.permit | Permits | streamer.permit.* |
streamer.token | Request Tokens | streamer.token.* |
user.preference | Preferences | user.preference.* |
user.song-request | Song Requests | user.song-request.* |
user.favorite | Favorite Songs | user.favorite.* |
user | User | user.* |
Wildcard Matching
You can request all permissions for a resource using the wildcard * as the permission segment.
For example, streamer.song.* grants both read and write access to songs.
Wildcard matching follows fosite's WildcardScopeStrategy. A * in any segment matches any value in that position:
streamer.song.*matchesstreamer.song.readandstreamer.song.writestreamer.song.readmatches onlystreamer.song.read
When requesting scopes for your application, use the wildcard form (resource.*) to request full access or individual permission names for fine-grained control.
Getting Tokens
The domain dedicated to authentication is https://id.staging.streamersonglist.com/oauth2
OpenID Connect discovery metadata is available at https://id.staging.streamersonglist.com/oauth2/.well-known/openid-configuration and the signing keys for ID tokens are published at https://id.staging.streamersonglist.com/oauth2/.well-known/jwks.json.
Supported authentication flows:
| Flow Type | Description | Token Type |
|---|---|---|
| Authorization code | A user authenticates in the browser and your server exchanges the returned code for tokens. Supports PKCE. | User access token, refresh token, ID token |
| Refresh token | Exchange a refresh token (issued alongside an access token when offline_access was granted) for a fresh token. | User access token, refresh token |
| Client credentials | Server-to-server flow authenticating the client itself, not a user. Reaches only no-scope GET endpoints. | App access token |
Client authentication at the token endpoint uses either client_secret_post (credentials in the request body — the default for clients created in this dashboard) or client_secret_basic (credentials in an HTTP Basic auth header). Both are accepted.
Authorization Code Flow
GET /oauth2/auth
| Parameter | Type | Description |
|---|---|---|
| client_id | string (required) | id generated from registered client |
| redirect_uri | URI (required) | callback uri specified when registering the client |
| response_type | string (required) | Only allowed value is code |
| scope | string (required) | space separated list of scopes. Include offline_access to receive a refresh token. Include openid to receive an ID token. |
| state | string (required) | Your unique token, generated by your application. An OAuth 2.0 opaque value used to defeat CSRF attacks. This value is echoed back in the response. |
| code_challenge | string (recommended) | PKCE challenge derived from a verifier your app generates. Required for public clients; strongly recommended for confidential clients. |
| code_challenge_method | string (recommended) | One of S256 or plain. S256 (SHA-256) is required whenever the user agent can compute it. |
| nonce | string (optional) | Random value generated by your app. When openid is requested, the same value is returned in the ID token's nonce claim for replay protection. |
| prompt | string (optional) | Set to login to force the user to re-authenticate even if they have an active session. Standard OpenID Connect parameter. |
| max_age | number (optional) | Maximum age of the user's authentication in seconds. If the user's last authentication is older than this value, they are forced to re-authenticate. |
curl "https://id.streamersonglist.com/oauth2/auth\
?client_id=<your_registered_client_id>\
&redirect_uri=<your_registered_redirect_uri>\
&response_type=code\
&state=bQxc3kvjuzTmwX4JE9rb7HvkvoUTG6\
&code_challenge=<base64url(sha256(verifier))>\
&code_challenge_method=S256\
&scope=offline_access streamer.song.* streamer.queue.*"
/* your server generates the PKCE pair and stores the verifier in the user's session */
import { createHash, randomBytes } from 'node:crypto';
const base64Url = (buf: Buffer) =>
buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const verifier = base64Url(randomBytes(32));
const challenge = base64Url(createHash('sha256').update(verifier).digest());
const params = new URLSearchParams({
client_id: '<your_registered_client_id>',
redirect_uri: '<your_registered_redirect_uri>',
response_type: 'code',
state: 'bQxc3kvjuzTmwX4JE9rb7HvkvoUTG6',
code_challenge: challenge,
code_challenge_method: 'S256',
scope: 'offline_access streamer.song.* streamer.queue.*',
});
// Redirect the user's browser to this URL:
const authorizeUrl = `https://id.streamersonglist.com/oauth2/auth?${params.toString()}`;
Redirect the user agent to the URL above. StreamerSonglist responds with a 302 to your redirect_uri carrying code and state query parameters. Verify state matches the value you sent, then exchange the code for tokens.
/* your server */
app.get('auth/callback', (req, res) => {
const code = req.query.code;
});
Exchange for Token
POST /oauth2/token with Content-Type: application/x-www-form-urlencoded
| Parameter | Type | Description |
|---|---|---|
| grant_type | string (required) | Must be authorization_code |
| code | string (required) | code value returned from the /oauth2/auth callback |
| redirect_uri | URI (required) | Must match the redirect_uri used in the authorize request |
| client_id | string (required) | id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic). |
| client_secret | string (required) | Client secret. May be omitted here when sent via HTTP Basic auth. |
| code_verifier | string (recommended) | The PKCE verifier matching the code_challenge sent in the authorize request. Required if you included code_challenge. |
curl -X POST "https://id.streamersonglist.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code_from_callback_query_param>" \
-d "redirect_uri=<your_registered_callback_uri>" \
-d "client_id=<your_client_id>" \
-d "client_secret=<your_client_secret>" \
-d "code_verifier=<pkce_verifier_from_session>"
const response = await fetch('https://id.streamersonglist.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: '<code from previous authorize callback>',
redirect_uri: `<your registered client's redirect uri>`,
client_id: `<your registered client's id>`,
client_secret: `<your registered client's secret>`,
code_verifier: `<pkce verifier from session>`,
}),
});
const data = await response.json();
const accessToken = data.access_token;
const refreshToken = data.refresh_token; // present when offline_access was granted
const idToken = data.id_token; // present when openid was granted
Example response when offline_access openid streamer.song.* streamer.queue.* was granted:
{
"access_token": "<opaque access token>",
"refresh_token": "<opaque refresh token>",
"id_token": "eyJhbG...signed.JWT",
"expires_in": 3600,
"scope": "offline_access openid streamer.song.* streamer.queue.*",
"token_type": "bearer"
}
Refresh Token Flow
When your access token expires, exchange the refresh token issued alongside it (requires the original request to have included the offline_access scope) for a new access token.
POST /oauth2/token with Content-Type: application/x-www-form-urlencoded
| Parameter | Type | Description |
|---|---|---|
| grant_type | string (required) | Must be refresh_token |
| refresh_token | string (required) | The refresh token returned from a previous token response |
| client_id | string (required) | id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic). |
| client_secret | string (required) | Client secret. May be omitted here when sent via HTTP Basic auth. |
| scope | string (optional) | Narrower subset of the originally granted scopes. Omit to receive all previously granted scopes. |
curl -X POST "https://id.streamersonglist.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=<your_refresh_token>" \
-d "client_id=<your_client_id>" \
-d "client_secret=<your_client_secret>"
Each successful refresh rotates the refresh token: the previous refresh token is invalidated and the response contains a new one. Persist the new refresh_token on every exchange.
{
"access_token": "<new opaque access token>",
"refresh_token": "<new opaque refresh token>",
"expires_in": 3600,
"scope": "offline_access openid streamer.song.* streamer.queue.*",
"token_type": "bearer"
}
Client Credentials Flow
POST /oauth2/token with Content-Type: application/x-www-form-urlencoded
| Parameter | Type | Description |
|---|---|---|
| grant_type | string (required) | Must be client_credentials |
| scope | string (required) | Space-separated list of scopes. Cannot include user-context scopes — client credentials tokens have no user subject. Note that no scope grants this token API access: the endpoints it can reach are the ones requiring none. |
| client_id | string (required) | id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic). |
| client_secret | string (required) | Client secret. May be omitted here when sent via HTTP Basic auth. |
curl -X POST "https://id.streamersonglist.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "scope=streamer.song.read streamer.queue.read" \
-d "client_id=<your_client_id>" \
-d "client_secret=<your_client_secret>"
const response = await fetch('https://id.streamersonglist.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'streamer.song.read streamer.queue.read',
client_id: `<your registered client's id>`,
client_secret: `<your registered client's secret>`,
}),
});
const data = await response.json();
const accessToken = data.access_token;
Client credentials tokens have no user subject and no refresh token — request a new token when the current one expires.
What these tokens can reach
A client credentials token authenticates your application, not a person. Most of this API resolves a user or a streamer from the token, and there is neither here — so the token is accepted on exactly the endpoints that ask for neither:
- The endpoint is a
GET. A write is never served to a client credentials token, even one requiring no scope. - Its
securityblock listsoauth2with an empty scope array. Every scope this API defines isstreamer.*oruser.*, and a client credentials token owns neither.
Anything else returns 401. Endpoints that do resolve a streamer or a user return 403 if reached another way. The authoritative list is the spec: look for operations whose security contains - oauth2: [] at /openapi.json.
In practice this covers the public catalogue reads — songs, the queue, play history, attributes, saved requests, public overlays and command metadata — the same data the public songlist pages serve. To act on behalf of a user, or to write anything, use the authorization code flow or one of the self-service tokens above.
{
"access_token": "<opaque access token>",
"expires_in": 3600,
"scope": "streamer.song.read streamer.queue.read",
"token_type": "bearer"
}
UserInfo
When an access token was issued with the openid scope, you can fetch standard OpenID Connect claims about the authenticated user.
GET /userinfo
curl -H "Authorization: Bearer <your_access_token>" \
"https://id.streamersonglist.com/userinfo"
The response includes sub (the subject identifier for the user) plus any claims corresponding to scopes granted to the token (for example, email when the email scope was granted).
Revoking Tokens
Invalidate an access token or refresh token before it expires — for example, when the user signs out or removes your app.
POST /oauth2/revoke with Content-Type: application/x-www-form-urlencoded
| Parameter | Type | Description |
|---|---|---|
| token | string (required) | The access token or refresh token to revoke |
| client_id | string (required) | id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic). |
| client_secret | string (required) | Client secret. May be omitted here when sent via HTTP Basic auth. |
curl -X POST "https://id.streamersonglist.com/oauth2/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=<access_or_refresh_token>" \
-d "client_id=<your_client_id>" \
-d "client_secret=<your_client_secret>"
Revoking a refresh token also invalidates the access tokens issued from it.
Streamer Access Tokens
Streamer access tokens are simple, database-backed tokens that grant full access to a specific streamer's data. Unlike OAuth tokens, they do not use scopes — a valid streamer token has full read and write access to that streamer's resources.
Streamer tokens are ideal when you are building a personal integration for your own channel and don't need the complexity of the OAuth flow.
You can create and manage streamer access tokens from your Settings > Access page on streamersonglist.com.
Authorization Header
Include the token in the Authorization header with the Streamer prefix:
Authorization: Streamer <token>
Examples
curl -H "Authorization: Streamer <your_token>" \
"https://api.streamersonglist.com/v2/streamers/<streamer_id>/songs"
const response = await fetch('https://api.streamersonglist.com/v2/streamers/<streamer_id>/songs', {
headers: {
Authorization: 'Streamer <your_token>',
},
});
const data = await response.json();
Streamer tokens authenticate a channel, not a person, so they cannot call user endpoints such as /users/self. Use a user access token for those.
User Access Tokens
User access tokens are database-backed tokens bound to you, rather than to a single channel. A user token can act on every channel you own or are an admin of — which makes it the right token when you administrate for other streamers and want one credential that covers all of them.
Like streamer tokens they carry no scopes. Instead, what a user token may do on a given channel is resolved per request from your admin permissions on that channel:
- On a channel you own, the token has full access.
- On a channel you administrate, the token is held to the same per-category permissions (queue, history, songs, attributes, commands, settings, overlays) you have in the web app. A read-only admin cannot write.
- Access is evaluated live. If a streamer removes you as an admin, the token loses access to that channel on the very next request — there is nothing to revoke or re-issue. Being added as an admin likewise grants access immediately, with no new token needed.
You can create and manage user access tokens from your Profile > API Access page on streamersonglist.com.
Authorization Header
Include the token in the Authorization header with the User prefix:
Authorization: User <token>
Examples
curl -H "Authorization: User <your_token>" \
"https://api.streamersonglist.com/v2/songs?streamer_id=<streamer_id>"
const response = await fetch(
'https://api.streamersonglist.com/v2/songs?streamer_id=<streamer_id>',
{
headers: {
Authorization: 'User <your_token>',
},
},
);
const data = await response.json();
Requests for a channel you neither own nor administrate return 403.
Differences from OAuth Tokens
| Feature | Streamer Access Token | User Access Token | OAuth Token |
|---|---|---|---|
| Setup | Create from settings page | Create from profile page | Register an OAuth client |
| Bound to | One streamer | One user, all channels they admin | A user, via a registered client |
| Scopes | Full access (no scopes) | No scopes — gated by admin permissions | Fine-grained scopes |
| Token format | Authorization: Streamer <tok> | Authorization: User <tok> | Authorization: Bearer <tok> |
| Best for | Personal / single-streamer use | Administrating several channels | Third-party apps, multi-user apps |
The Client-Id Header
OAuth clients should send their client id alongside every request:
Authorization: Bearer <your_token>
Client-Id: <your_client_id>
Rate limits are applied per client (and per streamer, when the request carries a streamer_id), and the limiter reads this header. Requests without it still work, but fall back to a per-token limit rather than your client's shared allowance.
The header is verified against the token: sending a Client-Id that does not belong to the presented token returns 401. Streamer and user tokens do not use this header.
Security Schemes
The OpenAPI spec declares three security schemes, one per credential type:
| Scheme | Type | Credential |
|---|---|---|
oauth2 | oauth2 (authorization code) | Authorization: Bearer <token> |
streamerToken | apiKey in header | Authorization: Streamer <token> |
userToken | apiKey in header | Authorization: User <token> |
Streamer and user tokens are described as apiKey rather than http because Streamer and User are not registered HTTP authentication schemes, even though both ride in the Authorization header. There is no separate http/bearer scheme: an OAuth2 access token is already covered by oauth2, and the scopes it must carry are attached there.
Every operation lists the schemes it accepts, and they are alternatives — any one of them satisfies the endpoint:
security:
- oauth2:
- streamer.song.read
- streamerToken: []
- userToken: []
The token schemes list no scopes because they have none. What a streamer token may do is fixed by the channel that owns it; what a user token may do is resolved per request from your admin permissions on the channel being addressed.
The rule in practice:
- Streamer-scoped endpoints accept all three credentials.
- User-scoped endpoints (those requiring a
user.*scope, such as/users/self) acceptoauth2anduserTokenonly. A streamer token is refused with403 streamer tokens are not accepted on this endpoint, because it authenticates a channel and so has no user to act as. - No-scope
GETendpoints additionally accept a client credentials token, which has no user at all.
Presenting a credential an endpoint does not list is rejected before the request reaches the handler, so it fails the same way whether or not the token would otherwise have been authorized. To see the exact list for an endpoint, read the security block for that operation in the spec at /openapi.json or /openapi.yaml.
