> ## Documentation Index
> Fetch the complete documentation index at: https://www.getsoundlink.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Soundlink OAuth Integration

> Connect your application to a Soundlink organization using Authorization Code + PKCE, then keep access with Client Credentials.

Soundlink's authorization server lets your application act on behalf of a Soundlink organization. A user grants consent once; your application records the organization id and uses it for ongoing access.

<Info>
  **Issuer:** `https://api.getsoundlink.com` · **Access token lifetime:** 3600 seconds · **PKCE:** required (`S256`) · **Refresh tokens:** not issued
</Info>

## Which grant do I need?

<CardGroup cols={2}>
  <Card title="Authorization Code + PKCE" icon="user-check" href="/docs/oauth-authorization-code">
    A user signs in and approves scopes. Use this to **establish** the connection and to read identity via `userinfo`.

    Produces a token that acts as **the user**.
  </Card>

  <Card title="Client Credentials" icon="server" href="/docs/oauth-client-credentials">
    Your application authenticates as itself for an organization it already has a grant for. Use this for **ongoing, unattended** access.

    Produces a token that acts as **your application**.
  </Card>
</CardGroup>

You will need **both**. Only the consent flow creates a grant, and Client Credentials requires an existing grant — so a client configured with Client Credentials alone cannot bootstrap itself.

<Warning>
  **There are no refresh tokens.** An Authorization Code token cannot be renewed — when it expires after one hour, the user must consent again. A Client Credentials token can be re-requested at any time. Design your integration around the Client Credentials grant for anything long-running.
</Warning>

***

## Discovery

Read endpoint locations from the metadata document rather than hardcoding them.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl https://api.getsoundlink.com/.well-known/oauth-authorization-server
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const metadata = await fetch(
    "https://api.getsoundlink.com/.well-known/oauth-authorization-server",
  ).then((res) => res.json());
  ```
</CodeGroup>

```json Response theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "issuer": "https://api.getsoundlink.com",
  "authorization_endpoint": "https://api.getsoundlink.com/api/v1/oauth/authorize",
  "token_endpoint": "https://api.getsoundlink.com/api/v1/oauth/token",
  "userinfo_endpoint": "https://api.getsoundlink.com/api/v1/oauth/userinfo",
  "grant_revocation_endpoint": "https://api.getsoundlink.com/api/v1/oauth/grants/revoke",
  "scopes_supported": ["openid", "email", "campaigns:read", "campaigns:write", "metrics:read", "videos:write"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "client_credentials"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "none"]
}
```

<Note>
  The response is cacheable for 300 seconds (`Cache-Control: public, max-age=300`). The values in this document are authoritative — if they ever differ from the paths written here, follow the document.
</Note>

***

## Onboarding your client

Soundlink registers confidential clients for you. There is **no self-service or dynamic client registration**, and public clients are not supported.

Provide the following when requesting a client:

<ParamField body="name" type="string" required>
  Display name shown to users on the consent screen.
</ParamField>

<ParamField body="redirectUris" type="string[]" required>
  Every callback URL your application will use. Must be absolute **HTTPS** URLs. Matched **exactly** at authorize time — no wildcards, no path or query flexibility, no trailing-slash tolerance.
</ParamField>

<ParamField body="allowedGrantTypes" type="string[]" required>
  Request **both** `authorization_code` and `client_credentials` (see above).
</ParamField>

<ParamField body="allowedScopes" type="string[]" required>
  The maximum set your client may ever request. Any `scope` sent to `/authorize` must be a subset of this.
</ParamField>

<ParamField body="logoUrl" type="string">
  Optional. Absolute URL, shown on the consent screen.
</ParamField>

You will receive a `client_id` and a `client_secret`.

<Warning>
  **The `client_secret` is shown exactly once**, at registration. Soundlink stores only a hash and cannot recover it. Store it in a secrets manager immediately; if it is lost, the client must be re-registered.
</Warning>

<Tip>
  Because redirect URIs must be HTTPS, local development needs an HTTPS tunnel (ngrok, Cloudflare Tunnel, or similar). Register the tunnel URL as an additional redirect URI.
</Tip>

***

## Reference application

A complete, runnable implementation of everything above:

<Card title="soundlink-oauth-example" icon="github" href="https://github.com/Fan-ID/soundlink-oauth-example">
  Next.js app covering the PKCE connect flow, server-side code exchange, `userinfo`, minted Client Credentials tokens, the campaign and metrics reads, and grant revocation — with every token held server-side and never sent to the browser.
</Card>

Useful entry points:

| Concern                                          | Where                                                |
| ------------------------------------------------ | ---------------------------------------------------- |
| Building the authorize URL, PKCE, `state` cookie | `app/api/oauth/connect/route.ts`                     |
| Code exchange and reading `organization_id`      | `app/api/oauth/callback/route.ts`                    |
| Calling `userinfo`                               | `app/api/oauth/userinfo/route.ts`                    |
| Minting and caching a Client Credentials token   | `lib/oauth/token-cache.ts`, `lib/oauth/org-token.ts` |
| Listing campaigns                                | `app/api/campaigns/route.ts`                         |
| Campaign detail and metrics                      | `app/api/campaigns/[campaignId]/`                    |
| Every outbound Soundlink call in one file        | `lib/oauth/client.ts`                                |
| Grant revocation                                 | `app/api/oauth/disconnect/route.ts`                  |

***

## Known limitations

<CardGroup cols={2}>
  <Card title="No refresh tokens" icon="rotate-right">
    Authorization Code tokens cannot be renewed. Use Client Credentials for anything ongoing.
  </Card>

  <Card title="No organization discovery" icon="list">
    No endpoint lists the organizations that authorized your client. Persist `organization_id` at consent time.
  </Card>

  <Card title="No token introspection" icon="magnifying-glass">
    No introspection endpoint and no JWKS. Token validity is knowable only by using it.
  </Card>

  <Card title="Confidential clients only" icon="user-lock">
    No dynamic client registration and no public-client support.
  </Card>
</CardGroup>

## Next

<CardGroup cols={2}>
  <Card title="Authorization Code + PKCE" icon="user-check" href="/docs/oauth-authorization-code">
    Connect a user's organization step by step, then read their identity via `userinfo`.
  </Card>

  <Card title="Client Credentials" icon="server" href="/docs/oauth-client-credentials">
    Mint unattended tokens for an existing grant, and revoke a grant when done.
  </Card>

  <Card title="Scopes, endpoints & errors" icon="shield-check" href="/docs/oauth-scopes-and-errors">
    Which scope unlocks which route, error codes, and a security checklist.
  </Card>
</CardGroup>
