axsag/opentweet

PHP 8+ SDK for the OpenTweet API — schedule, publish and analyse X/Twitter posts.

Maintainers

Package info

github.com/Axsag/opentweet-php

pkg:composer/axsag/opentweet

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-05-07 09:16 UTC

This package is auto-updated.

Last update: 2026-05-07 09:16:52 UTC


README

A modern, fully-typed PHP 8.1+ SDK for the OpenTweet API.
Schedule, publish, and analyse X/Twitter posts from your PHP application.

PHP License

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