# soundcloud-api-ts v1.15.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', }); const token = await sc.auth.getClientToken(); sc.setToken(token.access_token, token.refresh_token); const track = await sc.tracks.getTrack(123456); ``` ## When to use this package - Backend / Node.js services that need SoundCloud data - Building CLI tools, bots, or automation around SoundCloud - Direct API access with full TypeScript types - Server-to-server integrations ## When NOT to use this package - Building a React / Next.js frontend → use `soundcloud-api-ts-next` instead (provides React hooks + secure API routes) --- ## Standalone Function Signatures All standalone functions require a token as the first parameter. ### Auth > **v1.13.3+**: Wire format differs per grant type. `getClientToken` (`client_credentials`) sends `Authorization: Basic` — body credentials return `401 invalid_client`. `refreshUserToken` (`refresh_token`) sends `client_id`/`client_secret` in the request body — SoundCloud rejects Basic Auth for this grant. ```ts function getClientToken(clientId: string, clientSecret: string): Promise function getUserToken( clientId: string, clientSecret: string, redirectUri: string, code: string, codeVerifier?: string, ): Promise function refreshUserToken( clientId: string, clientSecret: string, redirectUri: string | undefined, refreshToken: string, ): Promise function refreshToken( clientId: string, clientSecret: string, redirectUri: string | undefined, refreshToken: string, ): Promise // alias of refreshUserToken — CC and user tokens function signOut(accessToken: string): Promise function getAuthorizationUrl( clientId: string, redirectUri: string, options?: { state?: string; codeChallenge?: string }, ): string function generateCodeVerifier(): string function generateCodeChallenge(verifier: string): Promise ``` ### Users ```ts function getMe(token: string): Promise function getUser(token: string, userId: string | number): Promise function getFollowers(token: string, userId: string | number, limit?: number): Promise> function getFollowings(token: string, userId: string | number, limit?: number): Promise> function getUserTracks(token: string, userId: string | number, limit?: number): Promise> function getUserPlaylists(token: string, userId: string | number, limit?: number): Promise> function getUserLikesTracks(token: string, userId: string | number, limit?: number, cursor?: string): Promise> function getUserLikesPlaylists(token: string, userId: string | number, limit?: number): Promise> function getUserWebProfiles(token: string, userId: string | number): Promise function getRelatedUsers(token: string, userId: string | number, limit?: number): Promise> function getUserRepostsTracks(token: string, userId: string | number, limit?: number): Promise> function getUserRepostsPlaylists(token: string, userId: string | number, limit?: number): Promise> ``` ### Tracks ```ts function getTrack(token: string, trackId: string | number): Promise function getTracks(token: string, ids: (string | number)[]): Promise // max 200 IDs (throws above) function getTrackComments(token: string, trackId: string | number, limit?: number): Promise> function createTrackComment(token: string, trackId: string | number, body: string, timestamp?: number): Promise function getTrackLikes(token: string, trackId: string | number, limit?: number): Promise> function getTrackReposts(token: string, trackId: string | number, limit?: number): Promise> function getRelatedTracks(token: string, trackId: string | number, limit?: number): Promise // unwraps collection; sends access=playable function getRelatedTracksPage(token: string, trackId: string | number, options?: RelatedTracksOptions): Promise> function getTrackStreams(token: string, trackId: string | number): Promise function getTrackStreamUrl(token: string, trackId: string | number): Promise // 302 Location function getTrackPreviewUrl(token: string, trackId: string | number): Promise // 302 Location function likeTrack(token: string, trackId: string | number): Promise function unlikeTrack(token: string, trackId: string | number): Promise function uploadTrack(token: string, params: UploadTrackParams): Promise // user token; multipart function updateTrack(token: string, trackId: string | number, params: UpdateTrackParams): Promise function updateTrackStorefront(token: string, trackId: string | number, params: StorefrontUpdateParams): Promise function deleteTrack(token: string, trackId: string | number): Promise ``` ### Playlists ```ts function getPlaylist(token: string, playlistId: string | number): Promise function getPlaylistTracks(token: string, playlistId: string | number, limit?: number, offset?: number): Promise> function getPlaylistReposts(token: string, playlistId: string | number, limit?: number): Promise> function createPlaylist(token: string, params: CreatePlaylistParams): Promise function updatePlaylist(token: string, playlistId: string | number, params: UpdatePlaylistParams): Promise function deletePlaylist(token: string, playlistId: string | number): Promise ``` ### Search ```ts function searchTracks(token: string, query: string, pageNumber?: number, options?: SearchQueryOptions): Promise> // default access=playable function searchUsers(token: string, query: string, pageNumber?: number, options?: SearchQueryOptions): Promise> function searchPlaylists(token: string, query: string, pageNumber?: number, options?: SearchQueryOptions): Promise> ``` ### Me (Authenticated User) ```ts function getMeActivities(token: string, limit?: number): Promise // deprecated — prefer getMeFeed function getMeActivitiesOwn(token: string, limit?: number): Promise function getMeActivitiesTracks(token: string, limit?: number): Promise function getMeFeed(token: string, limit?: number, access?: string): Promise function getMeFeedTracks(token: string, limit?: number, access?: string): Promise function getMeRecentlyPlayedTracks(token: string, access?: string): Promise // last 25; no pagination function getMeRepostsTracks(token: string, limit?: number): Promise> function getMeRepostsPlaylists(token: string, limit?: number): Promise> function getMeLikesTracks(token: string, limit?: number): Promise> function getMeLikesPlaylists(token: string, limit?: number): Promise> function getMeFollowings(token: string, limit?: number): Promise> function getMeFollowingsTracks(token: string, limit?: number): Promise> function followUser(token: string, userUrn: string | number): Promise function unfollowUser(token: string, userUrn: string | number): Promise function getMeFollowers(token: string, limit?: number): Promise> function getMePlaylists(token: string, limit?: number): Promise> function getMeTracks(token: string, limit?: number): Promise> function getMeConnections(token: string): Promise ``` ### Likes ```ts function likeTrack(token: string, trackId: string | number): Promise function unlikeTrack(token: string, trackId: string | number): Promise function likePlaylist(token: string, playlistId: string | number): Promise function unlikePlaylist(token: string, playlistId: string | number): Promise ``` ### Reposts ```ts function repostTrack(token: string, trackId: string | number): Promise function unrepostTrack(token: string, trackId: string | number): Promise function repostPlaylist(token: string, playlistId: string | number): Promise function unrepostPlaylist(token: string, playlistId: string | number): Promise ``` ### Resolve ```ts function resolveUrl(token: string, url: string): Promise ``` ### Pagination ```ts function paginate( firstPage: () => Promise>, fetchNext: (url: string) => Promise>, ): AsyncGenerator function paginateItems( firstPage: () => Promise>, fetchNext: (url: string) => Promise>, ): AsyncGenerator function fetchAll( firstPage: () => Promise>, fetchNext: (url: string) => Promise>, options?: { maxItems?: number }, ): Promise ``` ### HTTP ```ts function scFetch(options: RequestOptions, refreshCtx?: AutoRefreshContext): Promise function scFetchUrl(url: string, token?: string, retryConfig?: RetryConfig, onRequest?: (t: SCRequestTelemetry) => void, fetchImpl?: typeof fetch, refreshCtx?: AutoRefreshContext): Promise interface RequestOptions { path: string; method: "GET" | "POST" | "PUT" | "DELETE"; token?: string; body?: Record | FormData | URLSearchParams; contentType?: string; } interface RetryConfig { maxRetries: number; retryBaseDelay: number; onDebug?: (message: string) => void; } interface SCRequestTelemetry { method: "GET" | "POST" | "PUT" | "DELETE"; path: string; durationMs: number; status: number; retryCount: number; error?: string; } ``` ### Utils ```ts function getSoundCloudWidgetUrl(trackId: string | number): string ``` --- ## SoundCloudClient Class ```ts class SoundCloudClient { constructor(config: SoundCloudClientConfig) setToken(accessToken: string, refreshToken?: string): void // one arg clears stored refresh token clearToken(): void get accessToken(): string | undefined get refreshToken(): string | undefined paginate(firstPage: () => Promise>): AsyncGenerator paginateItems(firstPage: () => Promise>): AsyncGenerator fetchAll(firstPage: () => Promise>, options?: { maxItems?: number }): Promise auth: SoundCloudClient.Auth me: SoundCloudClient.Me users: SoundCloudClient.Users tracks: SoundCloudClient.Tracks playlists: SoundCloudClient.Playlists search: SoundCloudClient.Search resolve: SoundCloudClient.Resolve likes: SoundCloudClient.Likes reposts: SoundCloudClient.Reposts } interface SoundCloudClientConfig { clientId: string; clientSecret: string; redirectUri?: string; onTokenRefresh?: (client: SoundCloudClient) => Promise; maxRetries?: number; retryBaseDelay?: number; onDebug?: (message: string) => void; onRequest?: (telemetry: SCRequestTelemetry) => void; // Runtime portability fetch?: typeof globalThis.fetch; // Deduplication & caching (GET requests via client namespaces; sc.raw is not // deduped/cached. Pagination next_href is not deduped/cached but uses the live // token, client retry config, and onTokenRefresh. Cache keys hash the token.) dedupe?: boolean; // default: true — dedupes concurrent identical GETs cache?: SoundCloudCache; // optional pluggable cache backend cacheTtlMs?: number; // default: 60000ms // Retry observability onRetry?: (info: RetryInfo) => void; } interface RetryInfo { attempt: number; delayMs: number; reason: string; status?: number; url: string; } // Token provider interfaces (src/auth/token-provider.ts) — implement to plug in any session framework interface TokenProvider { getAccessToken(): string | undefined | Promise; setTokens(accessToken: string, refreshToken?: string): void | Promise; refreshIfNeeded(client: SoundCloudClient): Promise | string; } interface TokenStore { getAccessToken(): string | undefined; getRefreshToken(): string | undefined; setTokens(accessToken: string, refreshToken?: string): void; clearTokens(): void; } // Auth guide: docs/auth-guide.md — covers all flows, 401 troubleshooting, NextAuth/Clerk bridge patterns interface SoundCloudCache { get(key: string): Promise | T | undefined; set(key: string, value: T, options: { ttlMs: number }): Promise | void; delete(key: string): Promise | void; } interface RawResponse { data: T; status: number; headers: Record; } // sc.raw — call any endpoint with path templating // sc.raw.get('/tracks/{id}', { id: 123456 }) → RawResponse // sc.raw.post(path, body?) → RawResponse // sc.raw.put(path, body?) → RawResponse // sc.raw.delete(path) → RawResponse // sc.raw.request({ method, path, query?, body?, token? }) → RawResponse interface TokenOption { token?: string; } ``` ### Client Namespace Methods All client methods accept an optional trailing `TokenOption` parameter (`{ token?: string }`) to override the stored token. #### sc.auth ```ts getAuthorizationUrl(options?: { state?: string; codeChallenge?: string }): string getClientToken(): Promise getUserToken(code: string, codeVerifier?: string): Promise refreshUserToken(refreshToken: string): Promise refreshToken(refreshToken: string): Promise // alias — CC and user tokens signOut(accessToken: string): Promise ``` > **v1.13.3+**: Token request wire format is per grant type: `client_credentials` → `Authorization: Basic` header (body credentials rejected with `401 invalid_client`); `refresh_token` / `authorization_code` → `client_id` + `client_secret` in the form body (Basic Auth rejected). Do not generalize one grant's requirements to the others. #### sc.me ```ts getMe(options?: TokenOption): Promise getActivities(limit?: number, options?: TokenOption): Promise // deprecated — prefer getFeed getActivitiesOwn(limit?: number, options?: TokenOption): Promise getActivitiesTracks(limit?: number, options?: TokenOption): Promise getFeed(limit?: number, options?: TokenOption & { access?: string }): Promise getFeedTracks(limit?: number, options?: TokenOption & { access?: string }): Promise getRecentlyPlayedTracks(options?: TokenOption & { access?: string }): Promise // last 25 getRepostsTracks(limit?: number, options?: TokenOption): Promise> getRepostsPlaylists(limit?: number, options?: TokenOption): Promise> getLikesTracks(limit?: number, options?: TokenOption): Promise> getLikesPlaylists(limit?: number, options?: TokenOption): Promise> getFollowings(limit?: number, options?: TokenOption): Promise> getFollowingsTracks(limit?: number, options?: TokenOption): Promise> follow(userUrn: string | number, options?: TokenOption): Promise unfollow(userUrn: string | number, options?: TokenOption): Promise getFollowers(limit?: number, options?: TokenOption): Promise> getPlaylists(limit?: number, options?: TokenOption): Promise> getTracks(limit?: number, options?: TokenOption): Promise> getConnections(options?: TokenOption): Promise // linked social accounts ``` #### sc.users ```ts getUser(userId: string | number, options?: TokenOption): Promise getFollowers(userId: string | number, limit?: number, options?: TokenOption): Promise> getFollowings(userId: string | number, limit?: number, options?: TokenOption): Promise> getTracks(userId: string | number, limit?: number, options?: TokenOption): Promise> getPlaylists(userId: string | number, limit?: number, options?: TokenOption): Promise> getLikesTracks(userId: string | number, limit?: number, cursor?: string, options?: TokenOption): Promise> getLikesPlaylists(userId: string | number, limit?: number, options?: TokenOption): Promise> getWebProfiles(userId: string | number, options?: TokenOption): Promise getRelated(userId: string | number, limit?: number, options?: TokenOption): Promise> getRepostsTracks(userId: string | number, limit?: number, options?: TokenOption): Promise> getRepostsPlaylists(userId: string | number, limit?: number, options?: TokenOption): Promise> ``` #### sc.tracks ```ts getTrack(trackId: string | number, options?: TokenOption): Promise getTracks(ids: (string | number)[], options?: TokenOption): Promise // batch fetch; max 200 IDs (throws above) getStreams(trackId: string | number, options?: TokenOption): Promise getComments(trackId: string | number, limit?: number, options?: TokenOption): Promise> createComment(trackId: string | number, body: string, timestamp?: number, options?: TokenOption): Promise getLikes(trackId: string | number, limit?: number, options?: TokenOption): Promise> getReposts(trackId: string | number, limit?: number, options?: TokenOption): Promise> getRelated(trackId: string | number, limit?: number, options?: TokenOption): Promise // unwraps collection; access=playable getRelatedPage(trackId: string | number, options?: TokenOption & { limit?: number; access?: string }): Promise> getStreamUrl(trackId: string | number, options?: TokenOption): Promise // 302 Location getPreviewUrl(trackId: string | number, options?: TokenOption): Promise // 302 Location upload(params: UploadTrackParams, options?: TokenOption): Promise // user token; multipart updateStorefront(trackId: string | number, params: StorefrontUpdateParams, options?: TokenOption): Promise update(trackId: string | number, params: UpdateTrackParams, options?: TokenOption): Promise delete(trackId: string | number, options?: TokenOption): Promise ``` #### sc.playlists ```ts getPlaylist(playlistId: string | number, options?: TokenOption): Promise getTracks(playlistId: string | number, limit?: number, offset?: number, options?: TokenOption): Promise> getReposts(playlistId: string | number, limit?: number, options?: TokenOption): Promise> create(params: CreatePlaylistParams, options?: TokenOption): Promise update(playlistId: string | number, params: UpdatePlaylistParams, options?: TokenOption): Promise delete(playlistId: string | number, options?: TokenOption): Promise ``` #### sc.likes ```ts likeTrack(trackId: string | number, options?: TokenOption): Promise unlikeTrack(trackId: string | number, options?: TokenOption): Promise likePlaylist(playlistId: string | number, options?: TokenOption): Promise unlikePlaylist(playlistId: string | number, options?: TokenOption): Promise ``` #### sc.reposts ```ts repostTrack(trackId: string | number, options?: TokenOption): Promise unrepostTrack(trackId: string | number, options?: TokenOption): Promise repostPlaylist(playlistId: string | number, options?: TokenOption): Promise unrepostPlaylist(playlistId: string | number, options?: TokenOption): Promise ``` #### sc.search ```ts tracks(query: string, pageNumber?: number, options?: TokenOption & SearchQueryOptions): Promise> // default access=playable users(query: string, pageNumber?: number, options?: TokenOption & SearchQueryOptions): Promise> playlists(query: string, pageNumber?: number, options?: TokenOption & SearchQueryOptions): Promise> ``` #### sc.resolve ```ts resolveUrl(url: string, options?: TokenOption): Promise ``` --- ## Errors ```ts class SoundCloudError extends Error { readonly status: number; readonly statusText: string; readonly errorCode?: string; readonly docsLink?: string; readonly errors: string[]; readonly body?: SoundCloudErrorBody; get isUnauthorized(): boolean; // 401 get isForbidden(): boolean; // 403 get isNotFound(): boolean; // 404 get isRateLimited(): boolean; // 429 get isServerError(): boolean; // 5xx get isInvalidGrant(): boolean; // OAuth invalid_grant get isPermanentAuthError(): boolean; // dead user token (not invalid_client) } interface SoundCloudErrorBody { code?: number; message?: string; status?: string; link?: string; errors?: Array<{ error_message?: string }>; error?: string | null; error_code?: string; error_description?: string; } ``` --- ## Parameter Types ```ts interface UpdateTrackParams { title?: string; description?: string; genre?: string; tag_list?: string; sharing?: "public" | "private"; } interface UploadTrackParams { title: string; asset_data: Blob | File | Uint8Array | ArrayBuffer; filename?: string; artist?: string; permalink?: string; sharing?: "public" | "private"; description?: string; genre?: string; tag_list?: string; } interface RelatedTracksOptions { limit?: number; access?: string; // default "playable" } interface SearchQueryOptions { limit?: number; // 1–200, default 10 access?: string; // track search defaults to "playable" } interface StorefrontUpdateParams { title: string; type: StorefrontType; link: string; link_title?: string; description?: string; price?: string; } type StorefrontType = "digital" | "vinyl" | "cd" | "cassette" | "apparel" | "sample_pack" | "subscription" | "live_event" | "live_stream" | "other"; interface CreatePlaylistParams { title: string; description?: string; sharing?: "public" | "private"; tracks?: { urn: string }[]; ean?: string; } interface UpdatePlaylistParams { title?: string; description?: string; sharing?: "public" | "private"; tracks?: { urn: string }[]; ean?: string; } ``` --- ## Response Types ```ts interface SoundCloudToken { access_token: string; expires_in: number; refresh_token: string; scope: string; token_type: string; } interface SoundCloudUser { id: number; urn: string; username: string; permalink: string; permalink_url: string; avatar_url: string; city: string; country: string; description: string; first_name: string; last_name: string; full_name: string; followers_count: number; followings_count: number; likes_count: number; track_count: number; playlist_count: number; reposts_count: number; plan: string; subscriptions: SoundCloudSubscription[]; created_at: string; last_modified: string; kind: string; online: boolean; uri: string; website: string | null; website_title: string | null; discogs_name: string | null; myspace_name: string | null; public_favorites_count: number; comments_count: number; } interface SoundCloudMe extends SoundCloudUser { locale: string | null; primary_email_confirmed: boolean; private_playlists_count: number; private_tracks_count: number; quota: SoundCloudQuota; upload_seconds_left: number | null; } interface SoundCloudQuota { unlimited_upload_quota: boolean; upload_seconds_used: number; upload_seconds_left: number | null; } interface SoundCloudSubscription { product: SoundCloudSubscriptionProduct; recurring?: boolean; } interface SoundCloudSubscriptionProduct { id: string; name: string; } interface SoundCloudTrack { id: number; urn: string; title: string; description: string; genre: string; duration: number; permalink_url: string; artwork_url: string; waveform_url: string; stream_url: string; user: SoundCloudUser; created_at: string; access: string; sharing: string; license: string; tag_list: string; bpm: number; comment_count: number; commentable: boolean; download_count: number; download_url: string; downloadable: boolean; embeddable_by: string; favoritings_count: number; isrc: string | null; key_signature: string | null; kind: string; label_name: string; metadata_artist: string | null; monetization_model: string | null; playback_count: number; policy: string | null; purchase_title: string; purchase_url: string; release: string | null; release_day: number; release_month: number; release_year: number; reposts_count: number; secret_uri: string | null; streamable: boolean; uri: string; user_favorite: boolean; user_playback_count: number | null; available_country_codes: string[] | null; } interface SoundCloudPlaylist { id: number; urn: string; title: string; description: string; genre: string; duration: number; permalink: string; permalink_url: string; artwork_url: string; user: SoundCloudUser; user_id: number; user_urn: string; tracks: SoundCloudTrack[]; track_count: number; created_at: string; last_modified: string; sharing: string; license: string; tag_list: string; tags: string | null; type: string; playlist_type: string; kind: string; label: SoundCloudUser | null; label_id: number | null; label_name: string; likes_count: number; downloadable: boolean; streamable: boolean; embeddable_by: string; ean: string; purchase_title: string; purchase_url: string; release: string; release_day: number; release_month: number; release_year: number; tracks_uri: string | null; uri: string; } interface SoundCloudComment { id: number; body: string; created_at: string; timestamp: number; kind: string; track_urn: string; uri: string; urn: string; user: SoundCloudCommentUser; user_urn: string; } interface SoundCloudCommentUser { avatar_url: string; followers_count: number; followings_count: number; kind: string; last_modified: string; permalink: string; permalink_url: string; reposts_count: number; uri: string; urn: string; username: string; } interface SoundCloudStreams { hls_aac_160_url?: string; hls_mp3_128_url?: string; http_mp3_128_url?: string; preview_mp3_128_url?: string; } interface SoundCloudWebProfile { created_at: string; kind: string; service: string; title: string; url: string; urn: string; username: string; } interface SoundCloudActivity { type: string; created_at: string; origin: SoundCloudTrack | SoundCloudPlaylist; } interface SoundCloudActivitiesResponse { collection: SoundCloudActivity[]; next_href: string; future_href: string; } interface SoundCloudPaginatedResponse { collection: T[]; next_href: string; } ``` --- ## CLI: sc-cli ``` sc-cli auth — Set up credentials (interactive) sc-cli login — OAuth login for user endpoints sc-cli search — Search tracks sc-cli track — Get track details sc-cli user — Get user profile sc-cli playlist — Get playlist details sc-cli play — Play a track (mpv/ffplay/afplay) sc-cli stream — Get stream URLs sc-cli resolve — Resolve a SoundCloud URL sc-cli me — Show authenticated user sc-cli likes — Show liked tracks Options: --json (raw JSON), --url (play: print URL only), --help ``` ## Related packages - soundcloud-api-ts-next — React hooks + Next.js API route handlers (built on this package). https://github.com/twin-paws/soundcloud-api-ts-next - soundcloud-widget-react — React widget. Pass `trackId` / `playlistId` (official `api.soundcloud.com` resource URL). Do not pass `getSoundCloudWidgetUrl()`. https://github.com/twin-paws/soundcloud-widget-react ## Links - npm: https://www.npmjs.com/package/soundcloud-api-ts - GitHub: https://github.com/twin-paws/soundcloud-api-ts - API Docs: https://twin-paws.github.io/soundcloud-api-ts/