axsag / opentweet
PHP 8+ SDK for the OpenTweet API — schedule, publish and analyse X/Twitter posts.
Requires
- php: ^8.1
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- pestphp/pest: ^2.0
- phpstan/phpstan: ^1.10
- squizlabs/php_codesniffer: ^3.7
Suggests
- guzzlehttp/guzzle: A popular PSR-18 HTTP client (^7.0)
- symfony/http-client: Alternative PSR-18 HTTP client
README
A modern, fully-typed PHP 8.1+ SDK for the OpenTweet API.
Schedule, publish, and analyse X/Twitter posts from your PHP application.
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.1 |
| PSR-18 HTTP client | any (Guzzle 7, Symfony HTTP Client, …) |
| PSR-17 HTTP factories | any (shipped with Guzzle / Symfony) |
Installation
composer require Axsag/opentweet
The SDK is HTTP-client agnostic. Install whichever PSR-18 implementation you prefer:
# Guzzle (recommended) composer require guzzlehttp/guzzle # Or Symfony HTTP Client composer require symfony/http-client nyholm/psr7
Quick Start
use OpenTweet\OpenTweet; use OpenTweet\Enums\PostStatus; $client = new OpenTweet(apiKey: 'ot_your_api_key_here'); // Create a post and publish it immediately $post = $client->posts()->create('Hello from PHP!'); $client->posts()->publish($post->id); // Schedule a post $client->posts()->schedule($post->id, new DateTimeImmutable('2026-03-01 10:00:00')); // List all scheduled posts $collection = $client->posts()->list(status: PostStatus::Scheduled); foreach ($collection as $post) { echo "{$post->id}: {$post->text}" . PHP_EOL; }
Authentication
Pass your API key to the constructor. It is sent as a Bearer token on every request.
$client = new OpenTweet(apiKey: 'ot_your_api_key_here');
Bring Your Own HTTP Client
Pass any PSR-18 / PSR-17 implementations directly:
use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Psr7\HttpFactory; $factory = new HttpFactory(); $client = new OpenTweet( apiKey: 'ot_your_api_key_here', httpClient: new Guzzle(['timeout' => 10]), requestFactory: $factory, streamFactory: $factory, );
If you don't pass any, the SDK will auto-detect Guzzle or Symfony HTTP Client.
Posts
List posts
use OpenTweet\Enums\PostStatus; // All posts (first page, 20 per page) $collection = $client->posts()->list(); // Paginate $collection = $client->posts()->list(page: 2, limit: 50); // Filter by status $scheduled = $client->posts()->list(status: PostStatus::Scheduled); $posted = $client->posts()->list(status: PostStatus::Posted); // Iterate foreach ($collection as $post) { echo $post->text . PHP_EOL; } // Pagination metadata $collection->pagination->total; // int — total items $collection->pagination->pages; // int — total pages $collection->pagination->hasNextPage(); // bool $collection->pagination->hasPreviousPage(); // bool
Fetch a single post
$post = $client->posts()->find('abc123'); echo $post->id; // string echo $post->text; // string echo $post->category; // string echo $post->isThread; // bool echo $post->status->value; // 'draft' | 'scheduled' | 'posted' | 'failed' echo $post->scheduledDate?->format('Y-m-d H:i'); // ?string (UTC) echo $post->xPostId; // ?string — set after publication
Create a post
// Basic $post = $client->posts()->create('Hello world!'); // With category $post = $client->posts()->create('Hello world!', category: 'Tech'); // Pre-scheduled $post = $client->posts()->create( text: 'Scheduled at creation!', scheduledDate: new DateTimeImmutable('2026-03-01 10:00:00'), );
Create multiple posts at once
$posts = $client->posts()->createMany([ ['text' => 'First tweet', 'category' => 'Tech'], ['text' => 'Second tweet', 'category' => 'General'], ]);
Create a thread
$post = $client->posts()->createThread( intro: 'Thread intro — here is what I learned this week:', continuation: [ '1/ First insight…', '2/ Second insight…', '3/ Conclusion — thanks for reading!', ], category: 'Tech', );
Update a post
Only unpublished posts can be edited.
$post = $client->posts()->update( id: 'abc123', text: 'Updated tweet text!', category: 'Tech', );
Delete a post
// Delete post and its X/Twitter counterpart $client->posts()->delete('abc123'); // Delete post only (keep the X/Twitter post) $client->posts()->delete('abc123', deleteFromX: false);
Schedule a post
Requires an active OpenTweet subscription.
$post = $client->posts()->schedule( id: 'abc123', scheduledDate: new DateTimeImmutable('2026-03-01 10:00:00'), );
Publish a post immediately
Requires an active subscription and a connected X account.
$post = $client->posts()->publish('abc123'); echo $post->xPostId; // X/Twitter post ID
Batch-schedule up to 50 posts
$posts = $client->posts()->batchSchedule([ [ 'post_id' => 'abc123', 'scheduled_date' => new DateTimeImmutable('2026-03-01 10:00:00'), ], [ 'post_id' => 'def456', 'scheduled_date' => new DateTimeImmutable('2026-03-01 14:00:00'), ], ]);
Upload
Upload images (max 5 MB) or videos (max 20 MB) and receive a hosted URL.
Supported formats: JPG, PNG, GIF, WebP, MP4, MOV.
$url = $client->upload()->file('/path/to/photo.jpg'); // Use the URL when creating a post (pass it in your text or as metadata) $post = $client->posts()->create("Check this out! {$url}");
The SDK validates the file extension and size locally before making the HTTP call.
Analytics
Account overview
$overview = $client->analytics()->overview(); // Returns raw array: posting stats, streaks, trends, categories
Tweet engagement (Advanced plan)
use OpenTweet\Enums\AnalyticsPeriod; $metrics = $client->analytics()->tweets(AnalyticsPeriod::Month); // default $metrics = $client->analytics()->tweets(AnalyticsPeriod::Week); $metrics = $client->analytics()->tweets(AnalyticsPeriod::Year); $metrics = $client->analytics()->tweets(AnalyticsPeriod::All);
Best posting times
$best = $client->analytics()->bestTimes();
Error Handling
All exceptions extend OpenTweet\Exceptions\OpenTweetException.
use OpenTweet\Exceptions\AuthenticationException; use OpenTweet\Exceptions\BadRequestException; use OpenTweet\Exceptions\ForbiddenException; use OpenTweet\Exceptions\NotFoundException; use OpenTweet\Exceptions\OpenTweetException; use OpenTweet\Exceptions\RateLimitException; use OpenTweet\Exceptions\XApiException; try { $post = $client->posts()->publish('abc123'); } catch (RateLimitException $e) { // Respect the suggested back-off sleep($e->retryAfter); } catch (AuthenticationException $e) { // Invalid or missing API key } catch (ForbiddenException $e) { // Subscription required, or feature needs Advanced plan } catch (NotFoundException $e) { // Post not found or not owned by this account } catch (XApiException $e) { // X/Twitter API rejected the publish request } catch (BadRequestException $e) { // Invalid parameters sent to the API } catch (OpenTweetException $e) { // Catch-all for any other API error }
Exception reference
| Exception | HTTP status | Meaning |
|---|---|---|
BadRequestException |
400 | Invalid parameters |
AuthenticationException |
401 | Missing or invalid API key |
ForbiddenException |
403 | Subscription required / Advanced plan feature |
NotFoundException |
404 | Post not found or not owned by you |
RateLimitException |
429 | Too many requests — check $e->retryAfter |
XApiException |
502 | X/Twitter API error during publish |
Data Transfer Objects
Post
| Property | Type | Description |
|---|---|---|
$id |
string |
Unique post ID |
$text |
string |
Tweet text |
$category |
string |
User-defined category |
$isThread |
bool |
Thread starter flag |
$scheduledDate |
?DateTimeImmutable |
Scheduled UTC time |
$postedDate |
?DateTimeImmutable |
Published UTC time |
$status |
?PostStatus |
Lifecycle status enum |
$reviewStatus |
?string |
Review status string |
$xPostId |
?string |
X/Twitter post ID |
$createdAt |
DateTimeImmutable |
Record creation time |
PostCollection
Returned by Posts::list(). Implements Countable and IteratorAggregate<int, Post>.
count($collection); // int — posts on this page $collection->pagination; // Pagination DTO foreach ($collection as $post) { ... }
Pagination
| Property | Type | Description |
|---|---|---|
$page |
int |
Current page |
$limit |
int |
Items per page |
$total |
int |
Total items |
$pages |
int |
Total pages |
Helper methods: hasNextPage(), hasPreviousPage().
Testing
composer test # Run Pest test suite composer analyse # PHPStan static analysis (level 8) composer cs # PSR-12 code style check
License
MIT © Axsag