Published on August 4, 2026

Music API integrations fail in ways that generic API integrations do not. A user's Spotify token expires while their Apple Music connection works fine. YouTube Music returns a quota error with a completely different format than Deezer's rate limit response. SoundCloud goes down for maintenance while every other service stays up.
When your app connects to multiple streaming services, error handling becomes a multi-dimensional problem. You need to handle failures per service, per error type, and per user, all while keeping the app functional for the features that still work.
This post covers error handling strategies built for multi-service music integrations: error categories, retry patterns, circuit breakers, and how a unified API eliminates most of the complexity.
Connecting to one music API means handling one set of errors. Connecting to twelve means handling twelve different error formats, authentication flows, and failure modes simultaneously. Each service has its own HTTP status codes, error body formats, and retry expectations.
Every streaming service returns errors differently. There is no standard format across the music API ecosystem.
| Service | Error Format | Auth Error Code | Rate Limit Code | Error Body |
|---|---|---|---|---|
| Spotify | JSON | 401 | 429 | {"error": {"status": 401, "message": "..."}} |
| Apple Music | JSON | 401 | 429 | {"errors": [{"title": "...", "detail": "..."}]} |
| YouTube Music | JSON | 401/403 | 403 (quota) | {"error": {"code": 403, "errors": [...]}} |
| Deezer | JSON | 200 (!) | 200 (!) | {"error": {"type": "OAuthException"}} |
| SoundCloud | JSON | 401 | 429 | {"errors": [{"error_message": "..."}]} |
| Tidal | JSON | 401 | 429 | {"status": 401, "subStatus": 6001} |
Notice that Deezer returns 200 OK for errors. The actual error is buried in the response body. If your error handling only checks HTTP status codes, Deezer errors slip through silently.
Four failure categories account for 95% of music API errors:
Different error types require different responses. Retrying a permanently invalid token wastes time. Failing immediately on a temporary rate limit loses data. Match your handling strategy to the error category.
When a token expires, the fix depends on whether you have a refresh token:
import requests
def handle_auth_error(user_id: str, service: str, original_request_fn):
"""Handle authentication errors with token refresh."""
try:
# Attempt token refresh
refresh_resp = requests.post(
f"{MUSICAPI_BASE}/auth/refresh",
headers=HEADERS,
json={"user_id": user_id, "service": service}
)
if refresh_resp.status_code == 200:
# Token refreshed successfully, retry original request
return original_request_fn()
else:
# Refresh failed: user needs to re-authenticate
return {
"error": "reauth_required",
"service": service,
"message": f"Please reconnect your {service} account"
}
except Exception as e:
return {"error": "auth_failure", "detail": str(e)}
When using MusicAPI's auth system, token refresh is handled automatically. You receive a clean 401 only when the user genuinely needs to re-authorize, not when a token simply expired.
Rate limit errors are temporary. The correct response is always: wait and retry. The question is how long to wait.
import time
import random
def handle_rate_limit(response, retry_fn, max_retries=3):
"""Handle 429 responses with backoff."""
retry_after = response.headers.get("Retry-After")
for attempt in range(max_retries):
if retry_after:
wait = int(retry_after)
else:
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)
result = retry_fn()
if result.status_code != 429:
return result
return response # Return last 429 if all retries exhausted
For detailed rate limit strategies across services, see MusicAPI's rate limiting documentation.
Some services return errors in formats that break standard parsing. Deezer's 200-status errors are the classic example, but other services have quirks too.
Build a service-aware error parser:
def parse_error(response, service: str) -> dict:
"""Parse errors accounting for service-specific formats."""
if response.status_code >= 400:
try:
body = response.json()
except ValueError:
return {"code": response.status_code, "message": response.text}
# Spotify format
if "error" in body and isinstance(body["error"], dict):
return {"code": body["error"].get("status"), "message": body["error"].get("message")}
# Apple Music format
if "errors" in body and isinstance(body["errors"], list):
err = body["errors"][0]
return {"code": response.status_code, "message": err.get("detail", err.get("title"))}
return {"code": response.status_code, "message": str(body)}
# Deezer: errors in 200 responses
if service == "deezer" and response.status_code == 200:
body = response.json()
if "error" in body:
return {"code": body["error"].get("code"), "message": body["error"].get("message")}
return None # No error
import requests
import time
import random
import logging
logger = logging.getLogger("musicapi")
MUSICAPI_BASE = "https://api.musicapi.com/api/v1"
HEADERS = {"Authorization": "Bearer your_api_token"}
def resilient_request(method: str, url: str, max_retries: int = 3, **kwargs):
"""Make an API request with retry logic for common failure modes."""
for attempt in range(max_retries + 1):
try:
resp = requests.request(method, url, headers=HEADERS, timeout=30, **kwargs)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 401:
logger.warning(f"Auth error on {url}: token may need refresh")
return {"error": "auth_required", "status": 401}
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
jitter = random.uniform(0, 1)
wait = retry_after + jitter
logger.info(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1})")
time.sleep(wait)
continue
if resp.status_code >= 500:
wait = (2 ** attempt) + random.uniform(0, 1)
logger.warning(f"Server error {resp.status_code}. Retrying in {wait:.1f}s")
time.sleep(wait)
continue
# Client error (4xx other than 401/429): do not retry
return {"error": "client_error", "status": resp.status_code, "body": resp.text}
except requests.exceptions.Timeout:
logger.warning(f"Timeout on {url} (attempt {attempt + 1})")
if attempt < max_retries:
time.sleep(2 ** attempt)
continue
return {"error": "timeout"}
except requests.exceptions.ConnectionError:
logger.error(f"Connection error on {url}")
return {"error": "connection_failed"}
return {"error": "max_retries_exceeded"}
Beyond individual request retries, a production music app needs system-level resilience. Circuit breakers prevent cascading failures. Graceful degradation keeps the app useful when one service is down.
A circuit breaker tracks failure rates per service. When failures exceed a threshold, the circuit "opens" and short-circuits requests to that service instead of waiting for timeouts.
import time
class CircuitBreaker:
def __init__(self, failure_threshold: int = 5, reset_timeout: int = 60):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.failure_count = 0
self.last_failure_time = 0
self.state = "closed" # closed = normal, open = blocking
def can_request(self) -> bool:
if self.state == "closed":
return True
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = "half-open"
return True
return False
def record_success(self):
self.failure_count = 0
self.state = "closed"
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
# One circuit breaker per service
breakers = {
"spotify": CircuitBreaker(),
"apple_music": CircuitBreaker(),
"youtube_music": CircuitBreaker(),
}
When a circuit breaker opens for one service, your app should still work for the others. Show users what is available and communicate clearly about what is not.
Use this music library import UX specification to translate backend failures into understandable progress, partial results and recovery actions.
def get_all_playlists(user_id: str, services: list) -> dict:
"""Fetch playlists from all services with graceful degradation."""
results = {"playlists": [], "unavailable_services": []}
for service in services:
if not breakers[service].can_request():
results["unavailable_services"].append(service)
continue
data = resilient_request("GET", f"{MUSICAPI_BASE}/users/{user_id}/playlists", params={"service": service})
if "error" in data:
breakers[service].record_failure()
results["unavailable_services"].append(service)
else:
breakers[service].record_success()
results["playlists"].extend(data.get("playlists", []))
return results
The user sees their Spotify and Apple Music playlists even if YouTube Music is temporarily down. A small notice informs them that YouTube Music data is currently unavailable.
Managing error parsing, retry logic, and circuit breakers for 12 different services is significant engineering overhead. A unified API like MusicAPI eliminates most of this complexity by normalizing errors at the API layer.
MusicAPI returns consistent error responses regardless of which upstream service failed. A Spotify 401 and a Deezer error-in-200 both arrive at your app as the same normalized error format.
| Concern | Direct Integration (12 services) | Unified API |
|---|---|---|
| Error formats to parse | 12+ different formats | 1 consistent format |
| Auth error handling | Per-service token logic | Unified auth with auto-refresh |
| Rate limit management | 12 different policies | 1 abstracted policy |
| Retry logic | Per-service backoff | Built-in retry |
| Service-specific quirks | Manual handling (Deezer 200 errors, etc.) | Normalized away |
With MusicAPI, your error handling collapses to a simple pattern:
def get_playlists_simple(user_id: str, service: str):
"""Simplified error handling with a unified API."""
resp = resilient_request("GET", f"{MUSICAPI_BASE}/users/{user_id}/playlists", params={"service": service})
if "error" in resp:
if resp.get("status") == 401:
return {"action": "reauth", "service": service}
return {"action": "retry_later", "service": service}
return resp
No service-specific error parsing. No per-service retry configuration. No Deezer 200-error special case. The unified API handles all of it.
Expired OAuth tokens (401 errors) are the most frequent issue. Tokens expire at different rates per service (Spotify: 1 hour, Tidal: 24 hours). Proper token refresh logic or using a unified API that handles refresh automatically prevents these from reaching your users.
Wait and retry. Check the Retry-After header first. If absent, use exponential backoff: wait 1 second, then 2, then 4, then 8. Add random jitter to prevent thundering herd. Cap at 5 retries, then surface the error to the user or queue the request for later.
Deezer's API returns HTTP 200 for some error responses, with the actual error information in the JSON body. This is a legacy design decision. Your error parsing needs to check the response body for an error field, not just the HTTP status code.
A circuit breaker monitors failure rates per service. When failures exceed a threshold (e.g., 5 consecutive errors), it stops sending requests to that service for a cool-down period. This prevents cascading timeouts and keeps your app responsive. You need one if your app connects to 3+ services, since one slow or down service can drag down the entire user experience.
Yes. MusicAPI normalizes error responses from all supported services into a consistent format. It handles token refresh, retries rate-limited requests with appropriate backoff, and converts service-specific error formats (like Deezer's 200-status errors) into standard HTTP error responses.
Build a mock service layer that simulates common failure modes: random 429 responses, delayed timeouts, expired tokens, and service-specific error formats. Test each error category independently. For integration testing, use a staging environment with lower rate limits to trigger real throttling behavior safely.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.