PHPackages                             bjthecod3r/laravel-spotify-api-wrapper - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Search &amp; Filtering](/categories/search)
4. /
5. bjthecod3r/laravel-spotify-api-wrapper

ActiveLibrary[Search &amp; Filtering](/categories/search)

bjthecod3r/laravel-spotify-api-wrapper
======================================

A Laravel wrapper for the Spotify Web API.

0.3.0(2w ago)185↑182.4%MITPHPPHP ^8.2CI passing

Since May 10Pushed 2w agoCompare

[ Source](https://github.com/BJTheCod3r/laravel-spotify-api-wrapper)[ Packagist](https://packagist.org/packages/bjthecod3r/laravel-spotify-api-wrapper)[ RSS](/packages/bjthecod3r-laravel-spotify-api-wrapper/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (6)Versions (6)Used By (0)

 [![Laravel Spotify API Wrapper](art/logo.svg)](art/logo.svg)

Laravel Spotify API Wrapper
===========================

[](#laravel-spotify-api-wrapper)

A Laravel wrapper for the [Spotify Web API](https://developer.spotify.com/documentation/web-api). Search across tracks, albums, artists, playlists, shows, episodes, and audiobooks with a fluent facade and fully-typed responses.

Highlights
----------

[](#highlights)

- **Fluent search** for every Spotify item type, with Spotify's full filter syntax (`artist:`, `year:`, `tag:new`, `isrc:`, …) supported out of the box.
- **Typed responses.** No more reaching into nested arrays — every response is hydrated into PHP objects with public typed properties (`$track->album->name`, `$album->releaseDate` is a `Carbon` instance, etc.).
- **Pagination built in.** `Paginated` exposes `items`, `total`, `limit`, `offset`, `next`, and `previous` so you can page or drive a "Load more" button without parsing URLs.
- **Auth handled for you.** Client-credentials tokens are fetched, cached for the duration Spotify reports, and transparently refreshed on a 401.
- **Typed exceptions** mapped from Spotify's status codes — catch `RateLimitException` to read `retryAfter`, `AuthenticationException` for credential issues, etc.
- **Drop-in JSON.** Resources implement `Arrayable` + `JsonSerializable`, so `return $results;` from a controller serializes correctly.

Requirements
------------

[](#requirements)

- PHP `^8.2`
- Laravel `^11.0`, `^12.0`, or `^13.0`

Installation
------------

[](#installation)

```
composer require bjthecod3r/laravel-spotify-api-wrapper
```

Publish the config:

```
php artisan vendor:publish --tag=spotify-config
```

Add your [Spotify app](https://developer.spotify.com/dashboard) credentials to `.env`:

```
SPOTIFY_CLIENT_ID=your-client-id
SPOTIFY_CLIENT_SECRET=your-client-secret

# Optional defaults
SPOTIFY_MARKET=US
SPOTIFY_LOCALE=en_US
SPOTIFY_CACHE_STORE=redis
```

Search
------

[](#search)

### Single-type search

[](#single-type-search)

The most common case — search one type, get a typed `Paginated` back:

```
use BjTheCod3r\Spotify\Facades\Spotify;

$tracks    = Spotify::searchTracks('Doxy')->limit(20)->get();
$albums    = Spotify::searchAlbums('Kind of Blue')->market('NG')->get();
$artists   = Spotify::searchArtists('Miles Davis')->get();
$playlists = Spotify::searchPlaylists('focus')->get();
$shows     = Spotify::searchShows('how i built this')->get();
$episodes  = Spotify::searchEpisodes('startups')->includeExternalAudio()->get();
$audiobooks = Spotify::searchAudiobooks('atomic habits')->get();

foreach ($tracks->items as $track) {
    echo $track->name.' — '.$track->artists[0]->name.PHP_EOL;
}

$tracks->total;     // int
$tracks->next;      // ?string — URL for the next page
$tracks->previous;  // ?string
```

### Get a playlist

[](#get-a-playlist)

```
$playlist = Spotify::playlist('74oVZlOSwpy31tSplEWONa')
    ->market('GB')
    ->get();

$playlist->followers->total;
$playlist->tracks->items[0]->track->name;
```

Search playlists hydrate as `SimplifiedPlaylist` summaries. Direct playlist lookups hydrate as `Playlist` so `followers` and paginated `tracks.items` are only present on the endpoint that returns them.

### Get a single resource by ID

[](#get-a-single-resource-by-id)

Direct lookups exist for every searchable resource, plus user profiles. They all return a fully-typed resource (the same classes the search endpoints hydrate), and accept `->market()` where Spotify supports it.

```
$album     = Spotify::album('4aawyAB9vmqN3uQ7FjRGTy')->market('US')->get();
$artist    = Spotify::artist('0TnOYISbd1XYRBk9myaseg')->get();
$track     = Spotify::track('11dFghVXANMlKmJXsNCbNl')->market('US')->get();
$show      = Spotify::show('38bS44xjbVVZ3No3ByF1dJ')->market('US')->get();
$episode   = Spotify::episode('512ojhOuo1ktJprKbVcKyQ')->market('US')->get();
$audiobook = Spotify::audiobook('7iHfbu1YPACw6oZPAFJtqe')->market('US')->get();
$user      = Spotify::user('smedjan')->get();
```

### Multi-type search

[](#multi-type-search)

When you want several item types in one request:

```
use BjTheCod3r\Spotify\Enums\SearchType;

$results = Spotify::search('remaster track:Doxy artist:Miles Davis', [
        SearchType::Track,
        SearchType::Album,
    ])
    ->market('ES')
    ->limit(10)
    ->get();

$results->tracks->items[0]->name;          // Track::$name
$results->tracks->items[0]->album->name;   // nested Album
$results->albums->total;                   // paging total
$results->artists;                         // null — wasn't requested
```

Type strings work too if you'd rather skip the enum import:

```
Spotify::search('miles davis', ['track', 'album'])->get();
```

### Field filters

[](#field-filters)

Spotify supports inline filters in the query string. Just pass them through:

```
Spotify::searchTracks('artist:Burna Boy year:2022')->get();
Spotify::searchAlbums('tag:new')->get();
Spotify::searchTracks('isrc:USAT22003158')->get();
```

### Pagination

[](#pagination)

```
$page = Spotify::searchTracks('miles')->limit(20)->offset(0)->get();

$page->items;     // array
$page->total;     // 8462
$page->offset;    // 0
$page->next;      // 'https://api.spotify.com/v1/search?...&offset=20'
```

Typed resources
---------------

[](#typed-resources)

Every search response hydrates into objects under `BjTheCod3r\Spotify\Resources\`:

ResourceNotable fields`Track``name`, `durationMs`, `explicit`, `popularity`, `previewUrl`, `album`, `artists``Album``name`, `albumType`, `totalTracks`, `releaseDate` (Carbon), `images`, `artists``Artist``name`, `genres`, `popularity`, `images`, `followers` (`Followers` — `href`, `total`)`SimplifiedPlaylist``name`, `description`, `public`, `owner` (`User`), `tracks` (`TracksLink` — `href`, `total`), `items` (`PlaylistItemsLink`), `images``Playlist``name`, `description`, `public`, `followers`, `owner` (`User`), `tracks` (`TracksLink` — `href`, `total`, `items`), `images``Show``name`, `description`, `publisher`, `totalEpisodes`, `images``Episode``name`, `description`, `durationMs`, `releaseDate` (Carbon), `audioPreviewUrl``Audiobook``name`, `description`, `authors` (`Author[]`), `narrators` (`Narrator[]`), `publisher`, `totalChapters``Image``url`, `height`, `width``Paginated``items`, `total`, `limit`, `offset`, `next`, `previous`, `href`Date fields are real `Illuminate\Support\Carbon` instances. Spotify's date precision (`year`, `month`, `day`) is preserved on round-trip via `releaseDatePrecision`.

List fields (`items`, `artists`, `images`, `genres`, `languages`, `authors`, `narrators`, …) are `Illuminate\Support\Collection` instances, so you get the full Laravel Collection API:

```
$tracks->items
    ->filter(fn (Track $t) => $t->popularity > 50)
    ->sortByDesc('popularity')
    ->map(fn (Track $t) => $t->name);

$artist->genres->contains('jazz');
$album->artists->pluck('name');
```

Resources implement `Arrayable` + `JsonSerializable`, so this works:

```
public function index()
{
    return Spotify::searchTracks(request('q'))->get();
}
```

Laravel will serialize the `Paginated` to JSON automatically.

Error handling
--------------

[](#error-handling)

StatusException400 / 422`BjTheCod3r\Spotify\Exceptions\ValidationException`401`BjTheCod3r\Spotify\Exceptions\AuthenticationException` (after a transparent token refresh + retry)429`BjTheCod3r\Spotify\Exceptions\RateLimitException` — exposes `retryAfter` in secondsOther 4xx/5xx`BjTheCod3r\Spotify\Exceptions\ApiException`All inherit from `BjTheCod3r\Spotify\Exceptions\SpotifyException`, so you can catch broadly:

```
try {
    $tracks = Spotify::searchTracks($q)->get();
} catch (RateLimitException $e) {
    return response('Slow down', 429)->header('Retry-After', (string) $e->retryAfter);
} catch (SpotifyException $e) {
    report($e);
    return back()->with('error', 'Spotify is having a moment. Try again.');
}
```

Authentication
--------------

[](#authentication)

The package uses Spotify's **Client Credentials** grant — no user login required, suitable for any endpoint that doesn't need user context (Search, Browse, Albums, Artists, Tracks). Tokens are cached using Laravel's cache for the duration Spotify reports in `expires_in`, minus a small safety buffer, so you only hit the auth endpoint when a token actually needs refreshing.

User authentication
-------------------

[](#user-authentication)

For endpoints that act on a listener's account — their playlists, library, top items, listening history — connect their Spotify account via the **Authorization Code + PKCE** flow.

> **About playback.** The Spotify Web API does not return playable audio URLs even for authenticated users. Full playback is gated to Spotify's Web Playback SDK (browser, Premium) and the mobile SDKs. The user-auth surface here is for *reading* user data and (in a future release) *controlling* an active device, not for direct streaming.

### Setup

[](#setup)

Publish the migration that stores per-user tokens, then run it:

```
php artisan vendor:publish --tag=spotify-migrations
php artisan migrate
```

Tokens are encrypted at rest using Laravel's app key.

Set the OAuth redirect URI on your `.env` (and register the same value on the Spotify dashboard for your app):

```
SPOTIFY_REDIRECT_URI=https://your-app.test/spotify/callback
```

The package registers three opt-in routes under the `spotify` prefix (configurable):

MethodURINameGET`/spotify/connect``spotify.connect`GET`/spotify/callback``spotify.callback`POST`/spotify/disconnect``spotify.disconnect`Set `spotify.oauth.routes.enabled` to `false` (or env `SPOTIFY_OAUTH_ROUTES_ENABLED=false`) to disable them and wire your own controllers using the `Spotify::redirect()` / `Spotify::handleCallback()` helpers.

### Connecting a user

[](#connecting-a-user)

Have the authenticated user hit the `connect` route — by default it requires the `web` + `auth` middleware:

```
Connect Spotify
```

Pass extra scopes via `?scopes=playlist-modify-public,user-modify-playback-state` to merge with the configured defaults.

After consent, Spotify redirects to `/spotify/callback`. The controller exchanges the code, captures the listener's Spotify user id, persists encrypted tokens, dispatches `SpotifyConnected`, and redirects to `oauth.after_connect`.

If anything fails (state mismatch, user denied consent on Spotify, exchange error, …), the callback still redirects to `oauth.after_connect` but flashes a `spotify.oauth.error` payload onto the session so the destination can render error UX:

```
@if ($error = session('spotify.oauth.error'))

        Spotify connect failed: {{ $error['reason'] }}
        @if ($error['description']) ({{ $error['description'] }}) @endif

@endif
```

The `reason` is one of `state_mismatch`, `user_denied`, `authorize_error`, or `exchange_failed`; `description` carries the underlying Spotify error code or exception message.

### Reading user data

[](#reading-user-data)

```
use BjTheCod3r\Spotify\Facades\Spotify;

// Implicit: resolves the current user via the configured guard.
$profile      = Spotify::me()->profile()->get();
$playlists    = Spotify::me()->playlists()->limit(50)->get();
$savedTracks  = Spotify::me()->savedTracks()->market('US')->limit(50)->get();
$savedAlbums  = Spotify::me()->savedAlbums()->get();
$topTracks    = Spotify::me()->topTracks()->timeRange('short_term')->get();
$topArtists   = Spotify::me()->topArtists()->get();
$recent       = Spotify::me()->recentlyPlayed()->limit(50)->get();
$following    = Spotify::me()->followedArtists()->limit(50)->get();

// Explicit: act as a specific user id (queue workers, jobs).
Spotify::asUser($userId)->me()->playlists()->get();
```

All `me()` endpoints that return collections come back as the same `Paginated` resource the rest of the package uses. The `me/tracks`, `me/albums`, `me/shows`, `me/episodes`, and `me/player/recently-played` envelopes are unwrapped — the items collection contains the inner `Track` / `Album` / etc. directly. The `added_at` / `played_at` timestamps from those envelopes are not exposed in v0.3.

### Token refresh &amp; 401 handling

[](#token-refresh--401-handling)

Access tokens are refreshed transparently when stale. A `401` from any API call forces an out-of-band refresh and retries the original request once, so a token revoked between issuance and use is recovered automatically.

If Spotify rejects the refresh token (`invalid_grant`) — typically because the user revoked access from their Spotify settings — the stored row is deleted and `SpotifyDisconnected` is fired with `reason = invalid_grant`, so your app can prompt the user to reconnect.

Concurrent refreshes are serialised per-user via `Cache::lock`, so a fan-out of queue workers doesn't double-spend a rotating refresh token.

### Events

[](#events)

Listen for any of these to integrate with your app:

EventWhen`SpotifyConnected`After a successful callback exchange.`SpotifyTokenRefreshed`After any successful refresh-token grant.`SpotifyDisconnected`On explicit `disconnect()` or `invalid_grant` from refresh.`SpotifyConnectFailed`State mismatch, user denied consent, authorize error, exchange failure.### Disconnecting

[](#disconnecting)

```
Spotify::disconnect();              // current user via guard
Spotify::disconnect($userId);       // explicit
```

Or POST to `route('spotify.disconnect')` from a form. The default route stack includes `web` middleware, so the form must carry a CSRF token:

```

    @csrf
    Disconnect Spotify

```

### Custom token storage

[](#custom-token-storage)

The default Eloquent-backed repository covers most apps. To swap implementations (Redis, encrypted file, another DB connection), implement `BjTheCod3r\Spotify\Contracts\UserTokenRepository` and point at it:

```
// config/spotify.php
'oauth' => [
    'token_repository' => App\Spotify\RedisUserTokenRepository::class,
],
```

Testing
-------

[](#testing)

The package ships with Pest + Orchestra Testbench:

```
composer install
composer test
```

In your own application's tests, fake the HTTP layer with Laravel's standard helpers:

```
Http::fake([
    'accounts.spotify.com/*' => Http::response(['access_token' => 'x', 'token_type' => 'Bearer', 'expires_in' => 3600]),
    'api.spotify.com/v1/search*' => Http::response(['tracks' => ['items' => []]]),
]);
```

Roadmap
-------

[](#roadmap)

- Search
- Albums, Artists, Tracks (Get-by-ID)
- Episodes, Shows, Audiobooks (Get-by-ID)
- Playlists (Get-by-ID, read-only)
- Users — Authorization Code + PKCE, `me/*` reads
- Tracks (audio features / analysis)
- Browse (categories, new releases, featured playlists)
- Markets, Genres
- Player — playback control on the user's active device
- Playlist mutations (create / reorder / add-remove items)

License
-------

[](#license)

MIT.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

3

Last Release

18d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7c9d0cfc944007a872649f1e04921e82d5f0b5bc18de0bc8866a1e00b0db970e?d=identicon)[bjthecod3r](/maintainers/bjthecod3r)

---

Top Contributors

[![BJTheCod3r](https://avatars.githubusercontent.com/u/21208572?v=4)](https://github.com/BJTheCod3r "BJTheCod3r (19 commits)")

---

Tags

searchlaravelspotifymusicspotify-apispotify-web-apiaction-pattern

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/bjthecod3r-laravel-spotify-api-wrapper/health.svg)

```
[![Health](https://phpackages.com/badges/bjthecod3r-laravel-spotify-api-wrapper/health.svg)](https://phpackages.com/packages/bjthecod3r-laravel-spotify-api-wrapper)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

815320.5k3](/packages/defstudio-telegraph)[aerni/laravel-spotify

A Laravel wrapper for the Spotify Web API

207157.8k](/packages/aerni-laravel-spotify)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
