How can I retrieve a logged-in user’s liked tracks in chronological order using SoundCloud API v1?
I need to:
- Get the logged-in user’s liked tracks in the exact order they were liked
What I’ve tried:
- Using the v2 API, but I can’t get OAuth to work without an internal ID
- Using the history endpoint to get dates when the user liked tracks, but this approach doesn’t work
- Getting liked tracks with /me/likes/tracks endpoint, but it doesn’t include a created_at timestamp
My technical setup:
- Next.js 16 (TypeScript)
- SOUNDCLOUD_CLIENT_ID
- SOUNDCLOUD_CLIENT_SECRET
The SoundCloud API v1 offers limited options for retrieving liked tracks in strict chronological order, but you can achieve this by combining multiple endpoints and handling pagination correctly. The /me/likes/tracks endpoint doesn’t include timestamps, so you’ll need to use the /me/history endpoint to get the like timestamps and then match those with the actual track data.
Contents
- SoundCloud API v1 Authentication Setup
- Retrieving Liked Tracks with Timestamps
- Implementing Chronological Sorting in Next.js
- Handling Pagination and Rate Limits
- Complete Implementation Example
- Troubleshooting Common Issues
SoundCloud API v1 Authentication Setup
To authenticate with SoundCloud API v1, you’ll need to implement OAuth 2.0 client credentials flow or authorization code flow. For Next.js applications, the authorization code flow is typically more appropriate as it allows you to access user-specific data.
// SoundCloud authentication configuration
const SOUNDCLOUD_CONFIG = {
clientId: process.env.SOUNDCLOUD_CLIENT_ID,
clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET,
redirectUri: 'http://localhost:3000/api/soundcloud/callback',
scope: 'non-expiring' // or specific scopes needed
};
The SoundCloud API v1 authentication requires you to:
- Initialize the OAuth flow: Create an authorization URL that redirects users to SoundCloud for permission
- Handle the callback: Exchange the authorization code for access tokens
- Store tokens securely: Save access tokens in your database or session storage
For Next.js, you can create API routes to handle the OAuth flow:
// pages/api/soundcloud/auth.ts
export default function handler(req, res) {
const authUrl = `https://soundcloud.com/connect?
client_id=${SOUNDCLOUD_CONFIG.clientId}
&redirect_uri=${encodeURIComponent(SOUNDCLOUD_CONFIG.redirectUri)}
&response_type=code
&scope=${SOUNDCLOUD_CONFIG.scope}`;
res.redirect(authUrl);
}
Retrieving Liked Tracks with Timestamps
Since /me/likes/tracks doesn’t include timestamps, you’ll need to use a two-step approach:
- Get like history with timestamps from
/me/history - Get track details for each liked track
interface LikeHistoryItem {
created_at: string;
track: {
id: number;
title: string;
user: { username: string };
// other track properties
};
}
async function getLikedTracksWithTimestamps(accessToken: string): Promise<LikeHistoryItem[]> {
const historyResponse = await fetch('https://api.soundcloud.com/me/history', {
headers: {
'Authorization': `OAuth ${accessToken}`
}
});
const history = await historyResponse.json();
return history.filter(item => item.type === 'track' && item.action === 'like');
}
The SoundCloud history endpoint returns user activity including likes with creation timestamps. Each like event contains the exact time the user liked the track.
Important Note: The API may return activities in reverse chronological order, so you’ll need to sort them by created_at in ascending order for chronological sequence.
Implementing Chronological Sorting in Next.js
Here’s a complete Next.js implementation that combines the history and track data:
// lib/soundcloud.ts
interface SoundCloudConfig {
clientId: string;
clientSecret: string;
}
interface LikeHistoryResponse {
created_at: string;
type: string;
action: string;
track: {
id: number;
title: string;
streamable: boolean;
user: {
id: number;
username: string;
permalink: string;
};
artwork_url?: string;
duration: number;
description?: string;
};
}
interface TrackDetails {
id: number;
title: string;
streamable: boolean;
user: {
id: number;
username: string;
permalink: string;
};
artwork_url?: string;
duration: number;
description?: string;
[key: string]: any; // Additional properties
}
export class SoundCloudService {
constructor(private config: SoundCloudConfig) {}
async getLikedTracksChronologically(accessToken: string): Promise<TrackDetails[]> {
try {
// Step 1: Get like history with timestamps
const historyResponse = await fetch('https://api.soundcloud.com/me/history', {
headers: {
'Authorization': `OAuth ${accessToken}`
}
});
if (!historyResponse.ok) {
throw new Error(`Failed to fetch history: ${historyResponse.statusText}`);
}
const history: LikeHistoryResponse[] = await historyResponse.json();
const likes = history.filter(item => item.type === 'track' && item.action === 'like');
// Sort by created_at in ascending order (oldest first)
likes.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
// Step 2: Get detailed track information for each like
const trackDetails: TrackDetails[] = [];
for (const like of likes) {
const trackId = like.track.id;
const trackResponse = await fetch(`https://api.soundcloud.com/tracks/${trackId}`, {
headers: {
'Authorization': `OAuth ${accessToken}`
}
});
if (trackResponse.ok) {
const trackDetails: TrackDetails = await trackResponse.json();
trackDetails.liked_at = like.created_at; // Add the like timestamp
trackDetails.push(trackDetails);
}
}
return trackDetails;
} catch (error) {
console.error('Error fetching liked tracks:', error);
throw error;
}
}
async getAccessToken(authCode: string): Promise<string> {
const tokenResponse = await fetch('https://soundcloud.com/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: 'http://localhost:3000/api/soundcloud/callback',
code: authCode
})
});
const tokenData = await tokenResponse.json();
return tokenData.access_token;
}
}
Handling Pagination and Rate Limits
The SoundCloud API uses pagination and has rate limits that you need to handle properly:
interface PaginatedResponse<T> {
collection: T[];
next_href?: string;
prev_href?: string;
}
async function fetchPaginated<T>(
url: string,
accessToken: string,
maxPages: number = 10
): Promise<T[]> {
let allItems: T[] = [];
let nextUrl = url;
let pageCount = 0;
while (nextUrl && pageCount < maxPages) {
// Add delay to respect rate limits
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await fetch(nextUrl, {
headers: {
'Authorization': `OAuth ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
const data: PaginatedResponse<T> = await response.json();
allItems = [...allItems, ...data.collection];
nextUrl = data.next_href;
pageCount++;
}
return allItems;
}
// Usage in your main function:
async function getLikedTracksChronologically(accessToken: string): Promise<TrackDetails[]> {
const historyUrl = 'https://api.soundcloud.com/me/history';
const history = await fetchPaginated<LikeHistoryResponse>(historyUrl, accessToken);
// Filter and sort likes
const likes = history.filter(item => item.type === 'track' && item.action === 'like')
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
// Get detailed track information with pagination handling
const trackDetails = await Promise.all(
likes.map(async like => {
const trackResponse = await fetch(`https://api.soundcloud.com/tracks/${like.track.id}`, {
headers: { 'Authorization': `OAuth ${accessToken}` }
});
return trackResponse.json();
})
);
return trackDetails.map((track, index) => ({
...track,
liked_at: likes[index].created_at
}));
}
Complete Implementation Example
Here’s a complete Next.js API route that handles the entire process:
// pages/api/soundcloud/liked-tracks.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { SoundCloudService } from '@/lib/soundcloud';
const soundcloudService = new SoundCloudService({
clientId: process.env.SOUNDCLOUD_CLIENT_ID!,
clientSecret: process.env.SOUNDCLOUD_CLIENT_SECRET!
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { accessToken } = req.query;
if (!accessToken || typeof accessToken !== 'string') {
return res.status(400).json({ error: 'Access token required' });
}
try {
const likedTracks = await soundcloudService.getLikedTracksChronologically(accessToken);
res.status(200).json({ likedTracks });
} catch (error) {
console.error('Error fetching liked tracks:', error);
res.status(500).json({
error: 'Failed to fetch liked tracks',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}
And a client-side component that uses this API:
// components/SoundCloudLikedTracks.tsx
'use client';
import { useState, useEffect } from 'react';
interface Track {
id: number;
title: string;
user: { username: string };
liked_at: string;
artwork_url?: string;
duration: number;
}
export default function SoundCloudLikedTracks() {
const [tracks, setTracks] = useState<Track[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchLikedTracks = async () => {
try {
// You'll need to handle getting the access token from your OAuth flow
const accessToken = localStorage.getItem('soundcloud_access_token');
if (!accessToken) {
setError('Not authenticated with SoundCloud');
return;
}
const response = await fetch('/api/soundcloud/liked-tracks?accessToken=' + accessToken);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch tracks');
}
setTracks(data.likedTracks);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchLikedTracks();
}, []);
if (loading) return <div>Loading your liked tracks...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Your Liked Tracks (Chronological Order)</h2>
<div className="tracks-list">
{tracks.map((track) => (
<div key={track.id} className="track-item">
{track.artwork_url && (
<img
src={track.artwork_url}
alt={track.title}
className="track-artwork"
/>
)}
<div className="track-info">
<h3>{track.title}</h3>
<p>by {track.user.username}</p>
<small>Liked on: {new Date(track.liked_at).toLocaleDateString()}</small>
</div>
</div>
))}
</div>
</div>
);
}
Troubleshooting Common Issues
Issue: OAuth authentication failing without internal ID
- Solution: Make sure you’re using the correct OAuth flow. For Next.js, use the authorization code flow, not client credentials. The client credentials flow is for server-to-server authentication and doesn’t provide user context.
Issue: History endpoint not returning like events
- Solution: Check that the user has actually liked tracks and that your OAuth scope includes
non-expiringor the appropriate permissions. Also verify that you’re using the correct API endpoint (/me/history).
Issue: Rate limiting or API errors
- Solution: Implement proper error handling and retry logic. Add delays between requests and handle HTTP status codes appropriately. The SoundCloud API has rate limits that you should respect.
Issue: Track details missing from liked tracks
- Solution: Some tracks might be private or deleted. Add error handling for each track fetch and filter out tracks that can’t be retrieved.
Issue: Pagination not working correctly
- Solution: Make sure you’re following the links in the
next_hreffield of the API response, not constructing URLs manually. The SoundCloud API uses cursor-based pagination.
Pro Tip: Consider caching the liked tracks data to improve performance. Since the chronological order shouldn’t change frequently, you can cache the results for a few hours and only refetch when needed.
Sources
- SoundCloud API v1 Documentation
- SoundCloud OAuth 2.0 Authentication Guide
- SoundCloud API Reference
- Next.js API Routes Documentation
- SoundCloud Rate Limiting Guidelines
Conclusion
Retrieving SoundCloud liked tracks in strict chronological order requires a multi-step approach due to API limitations. Here are the key takeaways:
- Use the
/me/historyendpoint to get like timestamps, as/me/likes/tracksdoesn’t include creation dates - Combine two API calls: First get like history, then fetch detailed track information
- Implement proper pagination to handle large collections of liked tracks
- Add error handling for rate limits, authentication issues, and missing tracks
- Cache results to improve performance and reduce API calls
For your Next.js setup, the provided implementation should solve the authentication and data retrieval challenges you’ve encountered. The key is to work with the API’s limitations rather than fighting against them by leveraging the history endpoint to get the chronological order you need.
If you need further customization, consider adding features like infinite scrolling, track filtering, or better error recovery mechanisms based on your specific use case.