Published on July 3, 2026

Music streaming services hand out free trials and promotional offers to attract subscribers. If your app connects users to music, surfacing those offers at the right moment turns a passive listening experience into a revenue-generating partnership. The problem: every service structures its offers differently, authenticates differently, and rate-limits differently.
This post walks through the architecture, auth flows, and code patterns you need to integrate streaming music offers and free trials into your application using a single unified API.
Streaming offer integration gives your app a direct revenue channel. Music services pay referral fees and revenue shares when users convert through partner apps. Embedding trial offers keeps users inside your product longer, and subscription-tier detection lets you tailor the experience to what each user actually has access to.
The catch: building direct integrations with each streaming service means managing separate OAuth flows, parsing different response schemas for offer data, and handling per-service rate limits. A single service integration can take weeks. Supporting five or more services across their evolving APIs takes months.
That is exactly the problem a unified music API solves.
Before writing any code, it helps to understand the three-step pattern that every streaming offer integration follows:
Each streaming service implements these steps with different endpoints, different OAuth scopes, and different response shapes. A unified API normalizes all three steps behind a single interface.
Authentication is the foundation. You cannot check a user's subscription status or surface relevant offers without an active OAuth session with the target streaming service.
Here is how to set up the auth flow with MusicAPI's authentication system:
Start by redirecting the user to the streaming service's OAuth consent screen. MusicAPI handles the service-specific OAuth configuration for you:
const response = await fetch('https://api.musicapi.com/auth/init', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
service: 'spotify', // or 'apple', 'youtube', 'tidal', 'deezer', 'amazon'
callbackUrl: 'https://yourapp.com/auth/callback',
scopes: ['user-read-private', 'user-read-email']
})
});
const { authUrl } = await response.json();
// Redirect the user to authUrl
This single call replaces the per-service OAuth setup you would otherwise need to build and maintain. See the initializing authentication docs for the full parameter reference.
When the user completes OAuth consent, the streaming service redirects back to your callbackUrl. MusicAPI's authentication callback endpoint exchanges the authorization code for tokens and stores them securely:
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
const tokenResponse = await fetch('https://api.musicapi.com/auth/callback', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ code, state })
});
const { userId, service, accessToken } = await tokenResponse.json();
// Store the association between your user and their streaming account
res.redirect('/offers');
});
OAuth tokens expire. Each service sets different expiration windows and refresh mechanisms. MusicAPI handles token refresh automatically, so your offer-checking logic never breaks because of a stale token. You call the API, and it returns fresh data regardless of whether the underlying token needed a refresh.
This is where a unified API saves the most engineering time. Building token refresh logic for one service is straightforward. Maintaining it across six or more services, each with its own refresh endpoint, error codes, and edge cases, adds weeks of work that MusicAPI eliminates entirely.
Once authenticated, you can query the user's profile to determine their current subscription tier:
const profile = await fetch('https://api.musicapi.com/user/profile', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'X-User-Token': userAccessToken
}
});
const { subscription } = await profile.json();
// subscription.type: 'free', 'premium', 'family', 'student', 'trial'
// subscription.expiresAt: ISO timestamp (if applicable)
// subscription.trialEligible: boolean
This normalized response shape works across all supported music services. No per-service parsing logic needed.
With authentication and subscription detection in place, you can build the layer that fetches and displays available offers to your users.
Query the offers endpoint for the authenticated user's connected services:
const offers = await fetch('https://api.musicapi.com/offers', {
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'X-User-Token': userAccessToken
}
});
const { availableOffers } = await offers.json();
// Example response:
// [
// {
// "service": "spotify",
// "offerType": "free_trial",
// "duration": "3 months",
// "tier": "premium",
// "eligible": true,
// "activationUrl": "https://spotify.com/premium/trial?ref=..."
// },
// {
// "service": "apple",
// "offerType": "promotional",
// "duration": "1 month",
// "tier": "individual",
// "eligible": true,
// "activationUrl": "https://music.apple.com/subscribe?..."
// }
// ]
Each streaming service structures offers differently. Here is a comparison of what you can expect:
| Service | Free Trial | Student Plan | Family Plan | Promotional Offers | Trial Duration |
|---|---|---|---|---|---|
| Spotify | Yes | Yes | Yes | Seasonal | 1-3 months |
| Apple Music | Yes | Yes | Yes | Carrier bundles | 1 month |
| YouTube Music | Yes | Yes | Yes | Premium bundle | 1-3 months |
| Tidal | Yes | Yes | Yes | HiFi promotions | 30 days |
| Deezer | Yes | Yes | Yes | Regional promos | 30 days |
| Amazon Music | Yes | Yes | Yes | Prime bundles | 30-90 days |
The key challenge is that each service returns this data in a different format with different field names. A normalized response from a unified API means your frontend code renders offers the same way regardless of source.
Here is a React component pattern for displaying normalized offers:
function StreamingOffers({ offers }) {
return (
<div className="offers-grid">
{offers
.filter(offer => offer.eligible)
.map(offer => (
<div key={`${offer.service}-${offer.offerType}`} className="offer-card">
<h3>{offer.service} {offer.tier}</h3>
<p>{offer.offerType === 'free_trial' ? 'Free Trial' : 'Special Offer'}</p>
<p>{offer.duration}</p>
<a
href={offer.activationUrl}
target="_blank"
rel="noopener noreferrer"
className="activate-btn"
>
Activate Now
</a>
</div>
))}
</div>
);
}
When a user clicks through to activate an offer, the actual subscription flow happens on the streaming service's platform. Your app needs to handle what happens next.
The activationUrl from the offers endpoint sends users directly to the service's trial activation page with your referral parameters attached. This preserves attribution for revenue-share agreements.
function activateOffer(offer) {
// Track the click event before redirect
analytics.track('offer_activated', {
service: offer.service,
offerType: offer.offerType,
tier: offer.tier
});
// Open the activation URL
window.open(offer.activationUrl, '_blank');
}
After a user activates an offer, poll their subscription status to confirm conversion:
async function checkConversion(userId, service) {
const profile = await fetch(`https://api.musicapi.com/user/profile?service=${service}`, {
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'X-User-Token': userAccessToken
}
});
const { subscription } = await profile.json();
if (subscription.type !== 'free') {
analytics.track('offer_converted', {
service,
newTier: subscription.type
});
}
}
Not every user qualifies for every offer. Handle the empty state cleanly:
function getOfferMessage(offers, service) {
const serviceOffers = offers.filter(o => o.service === service && o.eligible);
if (serviceOffers.length === 0) {
return {
message: `No current offers available for ${service}.`,
action: 'browse_catalog',
actionUrl: `/catalog/${service}`
};
}
return {
message: `${serviceOffers.length} offer(s) available!`,
action: 'view_offers',
offers: serviceOffers
};
}
Shipping offer integration to production means handling the realities of scale: rate limits, caching, and error recovery.
Each streaming service enforces different rate limits. Some cap at 100 requests per minute, others at 10 requests per second. MusicAPI's rate limiting layer abstracts these differences and handles per-service throttling automatically. You set a single rate limit budget for your application, and the API distributes requests across services within safe thresholds.
Without this abstraction, you would need to build separate rate-limiting queues for each service, track different reset windows, and handle different HTTP status codes for throttled responses.
Offers do not change every second. Cache aggressively to reduce API calls and improve response times:
const OFFER_CACHE_TTL = 3600; // 1 hour in seconds
async function getCachedOffers(userId) {
const cacheKey = `offers:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const offers = await fetchOffersFromAPI(userId);
await redis.setex(cacheKey, OFFER_CACHE_TTL, JSON.stringify(offers));
return offers;
}
A one-hour TTL balances freshness with API efficiency. For most applications, offer data does not need real-time accuracy.
When a single service is down, your offer integration should not break entirely. Fetch offers per service independently and return partial results:
async function fetchAllOffers(userId, services) {
const results = await Promise.allSettled(
services.map(service =>
fetchOffers(userId, service).then(offers => ({ service, offers }))
)
);
return results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value.offers);
}
This pattern ensures users still see offers from healthy services even when one provider has an outage.
Most major services offer trial programs that can be accessed programmatically. This includes services like Spotify, Apple Music, YouTube Music, Tidal, Deezer, and Amazon Music. The specific offer types and durations vary by service and region. Check MusicAPI's supported services page for the current list.
After authenticating the user with the target streaming service, query their profile and subscription status. The response includes a trialEligible field that indicates whether the user qualifies for a free trial. Users who have previously redeemed a trial on a given service are typically ineligible.
Yes. That is the primary advantage of using a unified music API. Authenticate users with each service they want to connect, then query offers across all connected services in a single normalized response. See the supported features list for per-service capability details.
Offer integration requires scopes that grant access to the user's profile and subscription information. The exact scope names differ per service. MusicAPI's authorization system handles scope mapping automatically, so you request the capability you need (e.g., "read subscription status") rather than memorizing per-service scope strings.
Use MusicAPI's built-in rate limiting to stay within safe thresholds across all services. On the application side, cache offer responses (a one-hour TTL works well for most cases) and batch requests where possible. Avoid polling offer endpoints more frequently than your users actually need fresh data.
Yes. MusicAPI provides a single REST API that normalizes authentication, user profiles, subscription data, and music catalog operations across 10+ streaming services. Instead of building and maintaining separate integrations for each service, you connect once and access all supported endpoints through a consistent interface.
Streaming offer integration connects your users to music subscriptions they want while creating a revenue channel for your app. The hard part is not the concept. It is managing OAuth flows, token refresh, subscription schemas, and rate limits across every service you want to support.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.