Published on August 8, 2026

Adding music streaming features to a mobile app sounds straightforward until you start building. Each service has its own SDK, its own OAuth flow with mobile-specific redirect URI handling, and its own response format. Multiply that by the number of services your users expect, and you are looking at weeks of integration work per platform.
A unified music API reduces this to one REST integration that works identically in React Native and Flutter. This guide shows you how to set it up in both frameworks, with working code for auth, playlists, and favorites.
Mobile music API integration requires handling OAuth redirects through in-app browsers or deep links, managing token storage securely on-device, and adapting to each streaming service's SDK quirks. A unified API approach eliminates per-service SDK dependencies and gives mobile developers a single REST interface that works across iOS and Android with the same codebase.
Most streaming services offer native SDKs. Spotify has separate SDKs for iOS and Android. Apple Music has MusicKit for Swift. These SDKs provide deep platform integration (background playback, lock screen controls), but they come with tradeoffs:
| Factor | Native SDKs (per service) | Unified REST API |
|---|---|---|
| Setup time per service | 2-4 weeks | 1-2 days (total) |
| Services supported | 1 per SDK | 12+ with one integration |
| Cross-platform code sharing | Minimal (separate iOS/Android) | Full (same HTTP calls) |
| Maintenance burden | SDK updates per platform per service | One API version to track |
| Auth implementation | Custom per service | Single OAuth flow |
| Offline playback | Yes (SDK-managed) | No (streaming only) |
| React Native / Flutter support | Requires native modules | Works with standard HTTP |
If your app needs background playback and lock screen controls, you will eventually need native SDK integration. But for playlist management, search, favorites, library browsing, and user profile access, a REST API handles everything without native module complexity.
OAuth on mobile is different from web. Instead of HTTP redirects, you need:
musicapp://auth/callback)MusicAPI's auth flow handles the OAuth exchange server-side. Your mobile app only needs to:
No per-service OAuth client registration. No token refresh logic in your app code.
Setting up MusicAPI in React Native requires installing an HTTP client, configuring an in-app browser for OAuth, and storing auth tokens securely. The entire integration uses standard React Native libraries with no native module linking required beyond the auth browser.
You need three packages:
npm install axios react-native-inappbrowser-reborn react-native-keychain
Create a reusable API client:
// src/api/musicapi.ts
import axios from 'axios';
import * as Keychain from 'react-native-keychain';
const API_BASE = 'https://api.musicapi.com';
const client = axios.create({
baseURL: API_BASE,
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
// Attach user session token to every request
client.interceptors.request.use(async (config) => {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
config.headers['X-User-Token'] = credentials.password;
}
return config;
});
export default client;
The auth flow uses MusicAPI's authentication initialization to get a URL, opens it in the system browser, and catches the callback:
// src/auth/connectService.ts
import InAppBrowser from 'react-native-inappbrowser-reborn';
import * as Keychain from 'react-native-keychain';
import client from '../api/musicapi';
export async function connectService(serviceName: string) {
// Step 1: Get the auth URL from MusicAPI
const { data } = await client.post('/auth/initialize', {
service: serviceName,
redirectUri: 'musicapp://auth/callback',
});
// Step 2: Open the OAuth consent screen
const result = await InAppBrowser.openAuth(
data.authUrl,
'musicapp://auth/callback',
{ showTitle: true, enableUrlBarHiding: true }
);
if (result.type === 'success') {
// Step 3: Exchange the callback URL for a session token
const callbackUrl = new URL(result.url);
const { data: session } = await client.post('/auth/callback', {
code: callbackUrl.searchParams.get('code'),
service: serviceName,
});
// Step 4: Store the session token securely
await Keychain.setGenericPassword('musicapi', session.token);
return session;
}
throw new Error('Authentication cancelled');
}
Once authenticated, fetching playlists is a single API call:
// src/hooks/usePlaylists.ts
import { useState, useEffect } from 'react';
import client from '../api/musicapi';
interface Playlist {
id: string;
name: string;
trackCount: number;
service: string;
artwork_url: string;
}
export function usePlaylists(service: string) {
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPlaylists() {
try {
const { data } = await client.get(`/playlists`, {
params: { service, limit: 50 },
});
setPlaylists(data.items);
} catch (error) {
console.error('Failed to fetch playlists:', error);
} finally {
setLoading(false);
}
}
fetchPlaylists();
}, [service]);
return { playlists, loading };
}
The response format is identical whether the playlists come from Spotify, Apple Music, or any other supported service. Your component code never branches on service type.
Flutter integration follows the same pattern: an HTTP client, a browser-based OAuth flow, and secure token storage. Dart's strong typing makes the API response handling particularly clean.
Add dependencies to pubspec.yaml:
dependencies:
http: ^1.2.0
url_launcher: ^6.2.0
flutter_secure_storage: ^9.0.0
uni_links: ^0.5.1
Create the API client:
// lib/services/musicapi_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class MusicApiClient {
static const _baseUrl = 'https://api.musicapi.com';
static const _apiKey = 'YOUR_API_KEY';
final _storage = const FlutterSecureStorage();
Future<Map<String, String>> _headers() async {
final token = await _storage.read(key: 'user_token');
return {
'Authorization': 'Bearer $_apiKey',
if (token != null) 'X-User-Token': token,
'Content-Type': 'application/json',
};
}
Future<Map<String, dynamic>> get(String path,
{Map<String, String>? params}) async {
final uri = Uri.parse('$_baseUrl$path')
.replace(queryParameters: params);
final response = await http.get(uri, headers: await _headers());
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> post(String path,
Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$_baseUrl$path'),
headers: await _headers(),
body: jsonEncode(body),
);
return jsonDecode(response.body);
}
}
The authentication callback flow in Flutter uses url_launcher for the browser and uni_links to catch the redirect:
// lib/services/auth_service.dart
import 'package:url_launcher/url_launcher.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'musicapi_client.dart';
class AuthService {
final _client = MusicApiClient();
final _storage = const FlutterSecureStorage();
Future<void> connectService(String serviceName) async {
// Get the auth URL
final initResponse = await _client.post('/auth/initialize', {
'service': serviceName,
'redirectUri': 'musicapp://auth/callback',
});
// Open the consent screen
await launchUrl(
Uri.parse(initResponse['authUrl']),
mode: LaunchMode.externalApplication,
);
// Listen for the redirect
final link = await linkStream.firstWhere(
(link) => link?.startsWith('musicapp://auth/callback') ?? false,
);
// Exchange the code for a session
final uri = Uri.parse(link!);
final session = await _client.post('/auth/callback', {
'code': uri.queryParameters['code'],
'service': serviceName,
});
// Store the token
await _storage.write(key: 'user_token', value: session['token']);
}
}
Fetching favorite tracks works the same across all services:
// lib/widgets/favorites_list.dart
import 'package:flutter/material.dart';
import '../services/musicapi_client.dart';
class FavoritesList extends StatefulWidget {
final String service;
const FavoritesList({required this.service, super.key});
@override
State<FavoritesList> createState() => _FavoritesListState();
}
class _FavoritesListState extends State<FavoritesList> {
final _client = MusicApiClient();
List<Map<String, dynamic>> _tracks = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadFavorites();
}
Future<void> _loadFavorites() async {
final data = await _client.get(
'/favorites',
params: {'service': widget.service, 'limit': '50'},
);
setState(() {
_tracks = List<Map<String, dynamic>>.from(data['items']);
_loading = false;
});
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView.builder(
itemCount: _tracks.length,
itemBuilder: (context, index) {
final track = _tracks[index];
return ListTile(
leading: Image.network(track['artwork_url'], width: 48, height: 48),
title: Text(track['title']),
subtitle: Text(track['artist']),
trailing: Text(track['service']),
);
},
);
}
}
MusicAPI handles OAuth token refresh and service-specific request formatting behind the scenes. Your mobile code stays clean whether users connect Spotify, Apple Music, Tidal, or any of the other supported services.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.
Mobile apps need to handle spotty connectivity, large data sets, and battery-conscious background work. These patterns apply to both React Native and Flutter when working with the MusicAPI REST endpoints.
Cache playlist and library data locally so the app remains usable without a connection. In React Native, use AsyncStorage or MMKV. In Flutter, use shared_preferences or Hive:
// React Native: simple cache wrapper
import AsyncStorage from '@react-native-async-storage/async-storage';
async function cachedFetch(key: string, fetcher: () => Promise<any>) {
try {
const fresh = await fetcher();
await AsyncStorage.setItem(key, JSON.stringify(fresh));
return fresh;
} catch {
const cached = await AsyncStorage.getItem(key);
return cached ? JSON.parse(cached) : null;
}
}
Users with thousands of saved tracks will hit pagination limits. MusicAPI returns paginated responses with cursor-based navigation. Implement infinite scroll by tracking the pagination cursor:
async function loadMore(service: string, cursor?: string) {
const params: Record<string, string> = { service, limit: '50' };
if (cursor) params.cursor = cursor;
const { data } = await client.get('/favorites', { params });
return {
items: data.items,
nextCursor: data.pagination?.nextCursor,
hasMore: data.pagination?.hasMore ?? false,
};
}
Mobile networks are unreliable. Implement retry logic with exponential backoff and respect rate limits:
async function fetchWithRetry(fn: () => Promise<any>, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'] || 1;
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (attempt === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
No. MusicAPI uses a single API key per application. The same key works for both platforms. Manage keys in your MusicAPI dashboard.
MusicAPI is a REST API, not a native SDK. This is intentional: REST works identically across React Native, Flutter, and any other framework that can make HTTP requests. No native module linking required.
Use an in-app browser (not WebView) to open MusicAPI's auth URL. Register a custom URL scheme (yourapp://auth/callback) in your app config, and catch the redirect using deep link handlers (react-native-inappbrowser-reborn for RN, uni_links for Flutter).
Yes. MusicAPI is a standard REST API, so it works with Expo's fetch or any HTTP library. The only native module dependency is for OAuth (in-app browser), which is available in Expo via expo-web-browser and expo-linking.
MusicAPI handles token refresh automatically. If a service token expires, the API refreshes it on the next request. Your mobile app never sees expired token errors. See the authorization docs for details.
After a user connects each service, pass the service parameter to specify which one to query. You can also omit the service parameter on search endpoints to query all connected services at once.
Yes. MusicAPI applies rate limits per API key. For mobile apps, implement client-side debouncing on search and cache responses to stay well within limits.