# soundcloud-api-ts v1.14.0 > A TypeScript client for the SoundCloud API. Zero dependencies, native fetch, full OAuth 2.1 + PKCE support via `secure.soundcloud.com`. ## Installation npm install soundcloud-api-ts ## Quick Start ```ts import { SoundCloudClient } from 'soundcloud-api-ts'; const sc = new SoundCloudClient({ clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', redirectUri: 'https://example.com/callback', // optional, needed for user auth }); // Get a client credentials token const token = await sc.auth.getClientToken(); sc.setToken(token.access_token); // Fetch a track const track = await sc.tracks.getTrack(123456); console.log(track.title); ``` ## API Reference All methods accept an optional trailing `{ token?: string }` parameter to override the stored token. Paginated responses return `SoundCloudPaginatedResponse` with `collection: T[]` and `next_href?: string`. ### Auth (sc.auth) getAuthorizationUrl(options?: { state?: string; codeChallenge?: string }): string getClientToken(): Promise getUserToken(code: string, codeVerifier?: string): Promise refreshUserToken(refreshToken: string): Promise signOut(accessToken: string): Promise > **Note (v1.13.3+)**: SoundCloud's `/oauth/token` requirements differ **per grant type**: `client_credentials` requires `Authorization: Basic` (body credentials return `401 invalid_client`), while `refresh_token` and `authorization_code` require `client_id`/`client_secret` in the request body. `getClientToken` sends Basic Auth; `refreshUserToken`/`getUserToken` send body credentials. (v1.13.2 briefly used body credentials for all grants — reversed for `client_credentials` in v1.13.3.) ### PKCE Helpers (standalone exports) generateCodeVerifier(): string generateCodeChallenge(verifier: string): Promise ### Tracks (sc.tracks) getTrack(trackId: string | number): Promise getTracks(ids: (string | number)[]): Promise // batch fetch by IDs; max 200 (throws above) getStreams(trackId: string | number): Promise getComments(trackId: string | number, limit?: number): Promise> createComment(trackId: string | number, body: string, timestamp?: number): Promise getLikes(trackId: string | number, limit?: number): Promise> getReposts(trackId: string | number, limit?: number): Promise> getRelated(trackId: string | number, limit?: number): Promise update(trackId: string | number, params: UpdateTrackParams): Promise delete(trackId: string | number): Promise ### Users (sc.users) getUser(userId: string | number): Promise getFollowers(userId: string | number, limit?: number): Promise> getFollowings(userId: string | number, limit?: number): Promise> getTracks(userId: string | number, limit?: number): Promise> getPlaylists(userId: string | number, limit?: number): Promise> getLikesTracks(userId: string | number, limit?: number, cursor?: string): Promise> getLikesPlaylists(userId: string | number, limit?: number): Promise> getWebProfiles(userId: string | number): Promise ### Playlists (sc.playlists) getPlaylist(playlistId: string | number): Promise getTracks(playlistId: string | number, limit?: number, offset?: number): Promise> getReposts(playlistId: string | number, limit?: number): Promise> create(params: CreatePlaylistParams): Promise update(playlistId: string | number, params: UpdatePlaylistParams): Promise delete(playlistId: string | number): Promise ### Search (sc.search) tracks(query: string, pageNumber?: number): Promise> users(query: string, pageNumber?: number): Promise> playlists(query: string, pageNumber?: number): Promise> ### Me — Authenticated User (sc.me) getMe(): Promise getActivities(limit?: number): Promise getActivitiesOwn(limit?: number): Promise getActivitiesTracks(limit?: number): Promise getLikesTracks(limit?: number): Promise> getLikesPlaylists(limit?: number): Promise> getFollowings(limit?: number): Promise> getFollowingsTracks(limit?: number): Promise> getFollowers(limit?: number): Promise> getTracks(limit?: number): Promise> getPlaylists(limit?: number): Promise> getConnections(): Promise // linked social accounts (user token required) follow(userUrn: string | number): Promise unfollow(userUrn: string | number): Promise ### Likes (sc.likes) likeTrack(trackId: string | number): Promise unlikeTrack(trackId: string | number): Promise likePlaylist(playlistId: string | number): Promise unlikePlaylist(playlistId: string | number): Promise ### Reposts (sc.reposts) repostTrack(trackId: string | number): Promise unrepostTrack(trackId: string | number): Promise repostPlaylist(playlistId: string | number): Promise unrepostPlaylist(playlistId: string | number): Promise ### Resolve (sc.resolve) resolveUrl(url: string): Promise ### Pagination (instance methods) sc.paginate(firstPage: () => Promise>): AsyncGenerator sc.paginateItems(firstPage: () => Promise>): AsyncGenerator sc.fetchAll(firstPage: () => Promise>, options?: { maxItems?: number }): Promise ### Client Methods sc.setToken(accessToken: string, refreshToken?: string): void sc.clearToken(): void sc.accessToken: string | undefined sc.refreshToken: string | undefined ## Common Patterns ### Client Credentials Flow (server-to-server) ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...' }); const token = await sc.auth.getClientToken(); sc.setToken(token.access_token); ``` ### User Authorization Flow (with PKCE) ```ts import { SoundCloudClient, generateCodeVerifier, generateCodeChallenge } from 'soundcloud-api-ts'; const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', redirectUri: 'https://...' }); const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const authUrl = sc.auth.getAuthorizationUrl({ state: 'random', codeChallenge: challenge }); // redirect user to authUrl, get code from callback const token = await sc.auth.getUserToken(code, verifier); sc.setToken(token.access_token, token.refresh_token); ``` ### Pagination ```ts // Iterate individual items across all pages for await (const track of sc.paginateItems(() => sc.search.tracks('lofi'))) { console.log(track.title); } // Collect all into an array (with optional limit) const all = await sc.fetchAll(() => sc.search.tracks('lofi'), { maxItems: 100 }); ``` ### Error Handling ```ts import { SoundCloudError } from 'soundcloud-api-ts'; try { await sc.tracks.getTrack(999); } catch (err) { if (err instanceof SoundCloudError) { if (err.isNotFound) console.log('Not found'); if (err.isRateLimited) console.log('Rate limited'); if (err.isUnauthorized) console.log('Bad token'); console.log(err.status, err.message); } } ``` ### Auto Token Refresh ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', onTokenRefresh: async (client) => { const newToken = await client.auth.refreshUserToken(client.refreshToken!); client.setToken(newToken.access_token, newToken.refresh_token); return newToken; }, }); ``` ### Request Telemetry ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', onRequest: (t) => console.log(`[SC] ${t.method} ${t.path} ${t.status} ${t.durationMs}ms retries=${t.retryCount}`), }); ``` `SCRequestTelemetry`: `{ method, path, durationMs, status, retryCount, error? }` — fires after every client-namespace request including pagination and retries. Not emitted for `sc.raw.*` or `auth.signOut`. ## Raw API ```ts // Call any endpoint — path templating, returns { data, status, headers } const res = await sc.raw.get('/tracks/{id}', { id: 123456 }); await sc.raw.post('/tracks/{id}/comments', { id: 123456, body: { body: 'great track' } }); await sc.raw.request({ method: 'DELETE', path: '/tracks/{id}', query: { id: 123456 } }); ``` `RawResponse`: `{ data: T, status: number, headers: Record }` — does not throw on non-2xx. ## In-Flight Deduplication & Cache ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', dedupe: true, // default: true — concurrent identical GETs share one promise cache: myCache, // optional: SoundCloudCache interface (get/set/delete) cacheTtlMs: 60000, // default: 60s per cached GET response }); ``` `SoundCloudCache`: `{ get(key): T|undefined; set(key, value, {ttlMs}): void; delete(key): void }` Applies to GETs made through the client namespaces. `sc.raw.*` and pagination `next_href` fetches are not deduped/cached. (Note: these options existed but were not wired up in v1.12.0–v1.13.4.) ## Fetch Injection & Runtime Portability ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', fetch: customFetch, // custom fetch implementation (defaults to globalThis.fetch) }); ``` No Node-only APIs. Works in Cloudflare Workers, Bun, Deno, Edge runtimes. There is no `AbortController` config option — pass a pre-wired `fetch` if you need cancellation/timeouts. ## Retry Hook ```ts const sc = new SoundCloudClient({ clientId: '...', clientSecret: '...', onRetry: (info) => console.warn(`retry #${info.attempt} — ${info.reason} delay=${info.delayMs}ms`), }); ``` `RetryInfo`: `{ attempt, delayMs, reason, status?, url }` — fires on every retry. 429 `Retry-After` header honored (capped 60s). ## OpenAPI Tooling ```bash pnpm openapi:sync # fetch SoundCloud OpenAPI spec → tools/openapi.json + openapi-operations.json pnpm openapi:coverage # compare spec operations to IMPLEMENTED_OPERATIONS in src/client/registry.ts ``` ## Auth Guide Full guide at `docs/auth-guide.md`. Key points: - **Client credentials** → public endpoints only (tracks, users, search, playlists, resolve). Cannot access /me, likes, reposts, write operations. - **User token (OAuth 2.1 + PKCE)** → required for /me, likes, reposts, create/update/delete. - **401 errors**: "invalid_client" = wrong creds; "insufficient_scope" = need user token not client creds; "invalid_token" = expired, refresh needed. ## Token Provider Interfaces ```ts import { TokenProvider, TokenStore } from 'soundcloud-api-ts'; // Implement TokenProvider to plug in any session framework (NextAuth, Clerk, Redis, etc.) interface TokenProvider { getAccessToken(): string | undefined | Promise; setTokens(accessToken: string, refreshToken?: string): void | Promise; refreshIfNeeded(client: SoundCloudClient): Promise | string; } // Simpler synchronous store interface TokenStore { getAccessToken(): string | undefined; getRefreshToken(): string | undefined; setTokens(accessToken: string, refreshToken?: string): void; clearTokens(): void; } ``` ## Related Packages - soundcloud-api-ts-next — React hooks + Next.js API route handlers. Keeps secrets server-side. https://github.com/twin-paws/soundcloud-api-ts-next - soundcloud-widget-react — React component for the SoundCloud HTML5 Widget API (embed + playback control). https://github.com/twin-paws/soundcloud-widget-react ## Full Documentation https://twin-paws.github.io/soundcloud-api-ts/