A modern, fully-typed PHP 8.1+ SDK for the OpenTweet API.
Schedule, publish, and analyse X/Twitter posts from your PHP application.
| Requirement | Version |
|---|---|
| PHP | ^8.1 |
| PSR-18 HTTP client | any (Guzzle 7, Symfony HTTP Client, …) |
| PSR-17 HTTP factories | any (shipped with Guzzle / Symfony) |
composer require Axsag/opentweetThe 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/psr7use 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;
}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');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.
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$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// 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'),
);$posts = $client->posts()->createMany([
['text' => 'First tweet', 'category' => 'Tech'],
['text' => 'Second tweet', 'category' => 'General'],
]);$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',
);Only unpublished posts can be edited.
$post = $client->posts()->update(
id: 'abc123',
text: 'Updated tweet text!',
category: 'Tech',
);// 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);Requires an active OpenTweet subscription.
$post = $client->posts()->schedule(
id: 'abc123',
scheduledDate: new DateTimeImmutable('2026-03-01 10:00:00'),
);Requires an active subscription and a connected X account.
$post = $client->posts()->publish('abc123');
echo $post->xPostId; // X/Twitter post ID$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 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.
$overview = $client->analytics()->overview();
// Returns raw array: posting stats, streaks, trends, categoriesuse 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 = $client->analytics()->bestTimes();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 | 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 |
| 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 |
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) { ... }| Property | Type | Description |
|---|---|---|
$page |
int |
Current page |
$limit |
int |
Items per page |
$total |
int |
Total items |
$pages |
int |
Total pages |
Helper methods: hasNextPage(), hasPreviousPage().
composer test # Run Pest test suite
composer analyse # PHPStan static analysis (level 8)
composer cs # PSR-12 code style checkMIT © Axsag