1
Generate PKCE values and state
Create a Store
code_verifier, derive its S256 challenge, and generate an unguessable state.codeVerifier and state server-side, bound to the user’s session — for example in a signed, httpOnly cookie with a short TTL. Never put the verifier in the browser or in a URL.2
Redirect the user to the authorization endpoint
302) to the Soundlink consent screen. This endpoint sends Cache-Control: no-store.3
The user approves, and Soundlink returns to your callback
state against the value you stored before doing anything else, and reject the request if it does not match. If the user declines, you receive ?error=access_denied&state=… instead — treat this as a normal outcome, not a failure.4
Exchange the code for a token
The request is
application/x-www-form-urlencoded. This grant authenticates with the PKCE verifier and sends no client_secret.200 Response
redirect_uri must be identical to the one used in the previous step. Authorization codes are single-use. Store the refresh_token server-side.5
Record the organization id
organization_id is a claim on the access token, so no extra request is needed to read it.This is the one value you must persist. Soundlink provides no endpoint that lists which organizations have authorized your client, so an organization whose id you have not stored is one you can no longer act for. Keep it in your own database, keyed to your own user or tenant, along with grant_id if you intend to support disconnecting.Access token claims
The user’s id for
authorization_code tokens; your client id for client_credentials tokens — those have no user.Refresh the access token
When the access token expires, trade the refresh token for a new pair.200 Response
- Every refresh rotates the token. Replace the stored refresh token with the new one. The old one stops working.
- A refresh token is valid for 30 days. Each new one gets its own 30 days.
- Never reuse a refresh token. Sending one that was already used revokes every refresh token for that grant, and the user must consent again. Two concurrent refreshes with the same token count as reuse, so refresh from one place.
- You can narrow, not widen. Send
scopeto get an access token with fewer scopes. The new refresh token keeps the original scopes. Asking for more returnsinvalid_scope. - Send
client_secretif your client has one. A wrong secret returns401 invalid_client.
400 invalid_grant. Treat it as a disconnect and send the user through consent again.
Userinfo
GET /api/v1/oauth/userinfo requires a token from the authorization_code grant with the openid scope. A Client Credentials token is rejected with 403 access_denied regardless of its scopes, because it represents an application rather than a user.cURL
200 Response
email is present only when the token carries the email scope. Responses send Cache-Control: no-store.
Failures return { "error": "…" } with a WWW-Authenticate header:
Send exactly one
Authorization header — two are rejected rather than merged.