Authentication

StreamerSonglist provides an OAuth based authentication mechanism adhering to parts of the OAuth2.0 protocol

Tokens

TypeObtained byDescription
OAuth2 access tokensAuthorization code flowAuthenticate 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 tokensAuthorization code flowLong-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 tokensAuthorization code flowSigned 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 tokensSettings > AccessCreated by a streamer for their own channel. Full access to that one channel, no scopes, no OAuth flow to implement.
User access tokensProfile > API AccessCreated by a user for themselves. Reaches every channel they own or administrate, gated by their admin permissions on each.
Client credentials tokensClient credentials flowAuthenticate 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 buildingUse
An app used by streamers or viewers other than yourselfAn OAuth client, authorization code flow
An integration for one channel you ownA streamer access token
An integration across every channel you own or administrateA 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:

CodePermission
readRead
writeWrite

Available Resources

ResourceLabelWildcard Scope
streamer.action-logActivity Logstreamer.action-log.*
streamer.attributeAttributesstreamer.attribute.*
streamer.commandCommandsstreamer.command.*
streamer.overlayOverlaysstreamer.overlay.*
streamer.play-historyPlay Historystreamer.play-history.*
streamer.queueQueuestreamer.queue.*
streamer.settingsSettingsstreamer.settings.*
streamer.songSongsstreamer.song.*
streamer.learn-listLearn Liststreamer.learn-list.*
streamer.permitPermitsstreamer.permit.*
streamer.tokenRequest Tokensstreamer.token.*
user.preferencePreferencesuser.preference.*
user.song-requestSong Requestsuser.song-request.*
user.favoriteFavorite Songsuser.favorite.*
userUseruser.*

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.* matches streamer.song.read and streamer.song.write
  • streamer.song.read matches only streamer.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 TypeDescriptionToken Type
Authorization codeA user authenticates in the browser and your server exchanges the returned code for tokens. Supports PKCE.User access token, refresh token, ID token
Refresh tokenExchange a refresh token (issued alongside an access token when offline_access was granted) for a fresh token.User access token, refresh token
Client credentialsServer-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

ParameterTypeDescription
client_idstring (required)id generated from registered client
redirect_uriURI (required)callback uri specified when registering the client
response_typestring (required)Only allowed value is code
scopestring (required)space separated list of scopes. Include offline_access to receive a refresh token. Include openid to receive an ID token.
statestring (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_challengestring (recommended)PKCE challenge derived from a verifier your app generates. Required for public clients; strongly recommended for confidential clients.
code_challenge_methodstring (recommended)One of S256 or plain. S256 (SHA-256) is required whenever the user agent can compute it.
noncestring (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.
promptstring (optional)Set to login to force the user to re-authenticate even if they have an active session. Standard OpenID Connect parameter.
max_agenumber (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

ParameterTypeDescription
grant_typestring (required)Must be authorization_code
codestring (required)code value returned from the /oauth2/auth callback
redirect_uriURI (required)Must match the redirect_uri used in the authorize request
client_idstring (required)id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic).
client_secretstring (required)Client secret. May be omitted here when sent via HTTP Basic auth.
code_verifierstring (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

ParameterTypeDescription
grant_typestring (required)Must be refresh_token
refresh_tokenstring (required)The refresh token returned from a previous token response
client_idstring (required)id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic).
client_secretstring (required)Client secret. May be omitted here when sent via HTTP Basic auth.
scopestring (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

ParameterTypeDescription
grant_typestring (required)Must be client_credentials
scopestring (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_idstring (required)id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic).
client_secretstring (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 security block lists oauth2 with an empty scope array. Every scope this API defines is streamer.* or user.*, 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

ParameterTypeDescription
tokenstring (required)The access token or refresh token to revoke
client_idstring (required)id of the registered client. May be omitted here when credentials are sent via HTTP Basic auth (client_secret_basic).
client_secretstring (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

FeatureStreamer Access TokenUser Access TokenOAuth Token
SetupCreate from settings pageCreate from profile pageRegister an OAuth client
Bound toOne streamerOne user, all channels they adminA user, via a registered client
ScopesFull access (no scopes)No scopes — gated by admin permissionsFine-grained scopes
Token formatAuthorization: Streamer <tok>Authorization: User <tok>Authorization: Bearer <tok>
Best forPersonal / single-streamer useAdministrating several channelsThird-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:

SchemeTypeCredential
oauth2oauth2 (authorization code)Authorization: Bearer <token>
streamerTokenapiKey in headerAuthorization: Streamer <token>
userTokenapiKey in headerAuthorization: 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) accept oauth2 and userToken only. A streamer token is refused with 403 streamer tokens are not accepted on this endpoint, because it authenticates a channel and so has no user to act as.
  • No-scope GET endpoints 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.