PHPackages                             jkudish/plume - 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. [Testing &amp; Quality](/categories/testing)
4. /
5. jkudish/plume

ActiveLibrary[Testing &amp; Quality](/categories/testing)

jkudish/plume
=============

X API v2 client for Laravel — facades, typed DTOs, test fakes, and user-scoped operations.

v1.2.0(2mo ago)2903↑153.6%MITPHPPHP ^8.2CI passing

Since Feb 22Pushed 2mo agoCompare

[ Source](https://github.com/jkudish/plume)[ Packagist](https://packagist.org/packages/jkudish/plume)[ Docs](https://github.com/jkudish/plume)[ GitHub Sponsors](https://github.com/sponsors/jkudish)[ RSS](/packages/jkudish-plume/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (7)Versions (13)Used By (0)

Plume
=====

[](#plume)

X (Twitter) API v2 client for Laravel.

 [![Plume — X API v2 client for Laravel](art/banner.jpg)](art/banner.jpg)

[![Tests](https://github.com/jkudish/plume/actions/workflows/ci.yml/badge.svg)](https://github.com/jkudish/plume/actions/workflows/ci.yml/badge.svg)[![Packagist Version](https://camo.githubusercontent.com/c15ff1bb4003bad15b8f1d5e0553d7f117ec81c94bef241f66b1d1eb0840d7f2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a6b75646973682f706c756d65)](https://camo.githubusercontent.com/c15ff1bb4003bad15b8f1d5e0553d7f117ec81c94bef241f66b1d1eb0840d7f2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a6b75646973682f706c756d65)[![Packagist Downloads](https://camo.githubusercontent.com/3f0408783c2b302af3ad0821f8a0ff5354c5598275a933a58342057398d25c8e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6a6b75646973682f706c756d65)](https://camo.githubusercontent.com/3f0408783c2b302af3ad0821f8a0ff5354c5598275a933a58342057398d25c8e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6a6b75646973682f706c756d65)[![PHP Version](https://camo.githubusercontent.com/bba163832ee885bff92385b6edafe8eab1e6d4eb851a27a84f344169110dd13e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6a6b75646973682f706c756d65)](https://camo.githubusercontent.com/bba163832ee885bff92385b6edafe8eab1e6d4eb851a27a84f344169110dd13e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6a6b75646973682f706c756d65)[![License](https://camo.githubusercontent.com/6f9b0c915307dee8cb0367cdc5ba2eb3e0328fc8842db6e792b7d3077eda71e1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6a6b75646973682f706c756d65)](https://camo.githubusercontent.com/6f9b0c915307dee8cb0367cdc5ba2eb3e0328fc8842db6e792b7d3077eda71e1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6a6b75646973682f706c756d65)[![Sponsor](https://camo.githubusercontent.com/b7a618aaa68e82876df9f1279a1105fed55318494e3b22dd59f79f170fe561bf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73706f6e736f722d2545322539392541352d656134616161)](https://github.com/sponsors/jkudish)

Plume wraps the entire X API v2 behind a clean Laravel facade. Typed DTOs, automatic pagination, user-scoped operations, OAuth token refresh, rate-limit handling, test fakes with semantic assertions, 41 artisan commands, and 15 AI tools for the Laravel AI SDK.

Install
-------

[](#install)

```
composer require jkudish/plume
php artisan vendor:publish --tag=x-config
```

Add to `.env`:

```
X_BEARER_TOKEN=your-bearer-token
X_CLIENT_ID=your-client-id
X_CLIENT_SECRET=your-client-secret
```

Quick Start
-----------

[](#quick-start)

```
use Plume\Facades\X;

// Post a tweet
$post = X::createPost('Hello from Plume!');

// Search recent tweets
$results = X::searchRecent('laravel');
foreach ($results->data as $post) {
    echo "{$post->text}\n";
}

// Get your profile
$me = X::me();
echo $me->publicMetrics->followersCount;
```

Why Plume?
----------

[](#why-plume)

Most X API libraries for PHP are either stuck on API v1.1, aren't Laravel-native, or lack the DX features that make building with the API pleasant.

FeaturePlumeabraham/twitteroauthnoweh/twitter-api-v2-phpX API v2Full coveragePartialPartialLaravel facadesYesNoNoTyped DTOsActive Record methodsArraysArraysTest fakesSemantic assertionsNoNoOAuth auto-refreshBuilt-inManualManualArtisan commands41 commandsNoNoAI tools (Laravel AI SDK)15 toolsNoNoPaginationAutomaticManualManualRate-limit handlingStructured exceptions with retry timingManualManualPlume is designed to be the canonical X API package for Laravel: typed, testable, and ready for both CLI and AI agent use.

What's Covered
--------------

[](#whats-covered)

Every endpoint in the [X API v2](https://developer.x.com/en/docs/x-api):

DomainMethods**Posts**`createPost`, `deletePost`, `getPost`, `getPosts`, `hideReply`, `unhideReply`**Timelines**`userTimeline`, `mentionsTimeline`, `homeTimeline`**Search**`searchRecent`, `searchAll`, `countRecent`, `countAll`**Users**`getUser`, `getUsers`, `getUserByUsername`, `getUsersByUsernames`, `me`, `searchUsers`**Likes**`like`, `unlike`, `likingUsers`, `likedTweets`**Retweets**`retweet`, `undoRetweet`, `retweetedBy`, `quoteTweets`**Bookmarks**`bookmark`, `removeBookmark`, `bookmarks`**Follows**`follow`, `unfollow`, `followers`, `following`**Blocks**`block`, `unblock`, `blockedUsers`**Mutes**`mute`, `unmute`, `mutedUsers`**Lists**`createList`, `updateList`, `deleteList`, `getList`, `ownedLists`, `listTweets`, `listMembers`, `listFollowers`, `addListMember`, `removeListMember`, `followList`, `unfollowList`, `pinList`, `unpinList`**Media**`uploadMedia`, `initChunkedUpload`, `appendChunk`, `finalizeUpload`, `uploadStatus`, `setMediaMetadata`All methods are fully typed with enums for field selection (`TweetField`, `UserField`, `Expansion`, etc.) and return typed DTOs (`Post`, `User`, `XList`, `PaginatedResult`).

Key Features
------------

[](#key-features)

### Typed DTOs with Active Record Methods

[](#typed-dtos-with-active-record-methods)

```
$post = X::getPost('123', tweetFields: [TweetField::PublicMetrics]);
echo $post->publicMetrics->likeCount;

// DTOs carry action methods
$post->like('user-id');
$post->reply('Nice post!');
$post->bookmark('user-id');
$post->delete();
```

### Automatic Pagination

[](#automatic-pagination)

```
$page = X::userTimeline('user-id', maxResults: 100);
while ($page !== null) {
    foreach ($page->data as $post) {
        process($post);
    }
    $page = $page->nextPage();
}
```

### User-Scoped Client

[](#user-scoped-client)

`ScopedXClient` operates on behalf of a specific user. No more passing `$userId` to every call.

```
// From credentials array or a model implementing HasXCredentials
$client = X::forUser($user);

// Inject user ID directly to skip the /me API call
$client = X::forUser($credentials)->withUser('12345');
// Or pass a User DTO
$client = X::forUser($credentials)->withUser($userDto);

// All calls auto-resolve the user ID
$client->like('tweet-id');
$client->bookmark('tweet-id');
$client->followers();
$client->userTimeline(maxResults: 20);
```

Implement `HasXCredentials` on your User model:

```
use Plume\Contracts\HasXCredentials;

class User extends Authenticatable implements HasXCredentials
{
    public function toXCredentials(): array
    {
        return [
            'access_token' => $this->x_access_token,
            'refresh_token' => $this->x_refresh_token,
            'expires_at' => $this->x_token_expires_at,
        ];
    }
}
```

### OAuth 2.0 with Auto-Refresh

[](#oauth-20-with-auto-refresh)

Plume handles token refresh automatically on 401 responses. Persist refreshed tokens with a callback:

```
// In AppServiceProvider::register()
$this->app->bind('x.token_refreshed', fn () => function (array $credentials) {
    auth()->user()->update([
        'x_access_token' => $credentials['access_token'],
        'x_refresh_token' => $credentials['refresh_token'],
        'x_token_expires_at' => $credentials['expires_at'],
    ]);
});
```

### Rate-Limit Awareness

[](#rate-limit-awareness)

Plume throws a structured `RateLimitException` on 429 responses with built-in retry timing:

```
use Plume\Exceptions\RateLimitException;

try {
    $results = X::searchRecent('laravel');
} catch (RateLimitException $e) {
    $seconds = $e->retryAfterSeconds(); // seconds until rate limit resets
    $timestamp = $e->resetTimestamp;     // unix timestamp of reset

    sleep($seconds);
    // retry...
}
```

All exceptions include rate-limit headers (`x-rate-limit-limit`, `x-rate-limit-remaining`, `x-rate-limit-reset`) when available.

### Media Upload

[](#media-upload)

```
// Simple upload
$media = X::uploadMedia('/path/to/image.jpg', 'image/jpeg');
X::createPost('Check this out!', [
    'media' => ['media_ids' => [$media['media_id']]],
]);

// Chunked upload for large files
$init = X::initChunkedUpload($totalBytes, 'video/mp4', 'tweet_video');
X::appendChunk($init['media_id'], 0, $chunkData);
X::finalizeUpload($init['media_id']);
```

### Test Fakes

[](#test-fakes)

`X::fake()` swaps the client with an in-memory fake that records all calls:

```
use Plume\Facades\X;

it('creates a post', function () {
    $fake = X::fake();

    X::createPost('Hello from tests!');

    $fake->assertPostCreated('Hello');
    $fake->assertCalledTimes('createPost', 1);
});

it('tracks interactions', function () {
    $fake = X::fake();

    X::like('user-1', 'tweet-1');
    X::follow('user-1', 'target-1');

    $fake->assertLiked('tweet-1');
    $fake->assertFollowed('target-1');
});

it('stubs return values', function () {
    $fake = X::fake();
    $fake->shouldReturn('searchRecent', new PaginatedResult(
        data: [new Post(id: '1', text: 'Stubbed')],
        resultCount: 1,
    ));

    $results = X::searchRecent('test');
    expect($results->data[0]->text)->toBe('Stubbed');
});
```

**Semantic assertions:** `assertPostCreated`, `assertPostDeleted`, `assertLiked`, `assertRetweeted`, `assertBookmarked`, `assertFollowed`, `assertBlocked`, `assertMuted`, `assertRepliedTo`, `assertSearched`, `assertNothingPosted`, `assertNothingCalled`, `assertForUserCalled`.

Artisan Commands
----------------

[](#artisan-commands)

Plume ships 41 artisan commands for full CLI access to the X API. All commands support `--format=json` for machine-readable output where applicable.

```
# Your profile
php artisan plume:me

# Post a tweet
php artisan plume:post --text="Hello from the CLI!"

# Search
php artisan plume:search "laravel" --max-results=20

# Your home timeline
php artisan plume:home --max-results=10 --format=json
```

CategoryCommands**Profile**`plume:me`**Posts**`plume:post`, `plume:get-post`, `plume:delete-post`**Search**`plume:search`**Timelines**`plume:home`, `plume:timeline`, `plume:mentions`**Users**`plume:user`**Likes**`plume:like`, `plume:unlike`, `plume:likes`**Retweets**`plume:retweet`, `plume:unretweet`**Follows**`plume:follow`, `plume:unfollow`, `plume:followers`, `plume:following`**Bookmarks**`plume:bookmark`, `plume:unbookmark`, `plume:bookmarks`**Blocks**`plume:block`, `plume:unblock`, `plume:blocked`**Mutes**`plume:mute`, `plume:unmute`, `plume:muted`**Media**`plume:upload`**Lists**`plume:lists`, `plume:lists:create`, `plume:lists:get`, `plume:lists:delete`, `plume:lists:update`, `plume:lists:members`, `plume:lists:add-member`, `plume:lists:remove-member`, `plume:lists:tweets`, `plume:lists:follow`, `plume:lists:unfollow`, `plume:lists:pin`, `plume:lists:unpin`Commands that modify state (`plume:delete-post`, `plume:unfollow`, `plume:block`, `plume:unblock`, `plume:mute`, `plume:unmute`, `plume:lists:delete`, `plume:lists:remove-member`) prompt for confirmation. Pass `--force` to skip.

AI Tools
--------

[](#ai-tools)

Plume ships 15 tools for the [Laravel AI SDK](https://github.com/laravel/ai) (requires PHP 8.4+). Install `laravel/ai` to use them:

```
composer require laravel/ai
```

Tools are tagged as `ai-tools` and implement `Laravel\Ai\Contracts\Tool`:

`plume:fetch-tweet`, `plume:post-tweet`, `plume:search`, `plume:home-timeline`, `plume:my-timeline`, `plume:mentions`, `plume:like`, `plume:retweet`, `plume:bookmark`, `plume:bookmarks`, `plume:follow`, `plume:followers`, `plume:following`, `plume:profile`, `plume:upload-media`

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

[](#requirements)

- PHP 8.2+ (AI tools require 8.4+)
- Laravel 11 or 12

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md). Run `composer test`, `composer phpstan`, and `composer lint` before submitting.

Security
--------

[](#security)

Email  to report vulnerabilities. See [SECURITY.md](SECURITY.md).

Sponsoring
----------

[](#sponsoring)

If you find Plume useful, consider [becoming a sponsor](https://github.com/sponsors/jkudish).

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance83

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

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 ~0 days

Total

3

Last Release

85d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ca20d16a7871a44d060a80ad572c90be25dd5b73212b0a839fb0474e623ee357?d=identicon)[jkudish](/maintainers/jkudish)

---

Top Contributors

[![jkudish](https://avatars.githubusercontent.com/u/260253?v=4)](https://github.com/jkudish "jkudish (25 commits)")

---

Tags

ai-toolsapi-clientlaravelphptwitterxx-apitestingapiclilaravelartisantwitterfacadedtocommandsxplumetwitter-api-v2

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/jkudish-plume/health.svg)

```
[![Health](https://phpackages.com/badges/jkudish-plume/health.svg)](https://phpackages.com/packages/jkudish-plume)
```

###  Alternatives

[larastan/larastan

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

6.4k43.5M5.2k](/packages/larastan-larastan)[timacdonald/log-fake

A drop in fake logger for testing with the Laravel framework.

4235.9M56](/packages/timacdonald-log-fake)[davidhsianturi/laravel-compass

An elegant REST assistent for the Laravel framework.

1.3k84.5k](/packages/davidhsianturi-laravel-compass)[sti3bas/laravel-scout-array-driver

Array driver for Laravel Scout

971.5M3](/packages/sti3bas-laravel-scout-array-driver)[grazulex/laravel-devtoolbox

Swiss-army artisan CLI for Laravel — Scan, inspect, debug, and explore every aspect of your Laravel application from the command line.

1535.4k](/packages/grazulex-laravel-devtoolbox)

PHPackages © 2026

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