fopost/sdk

Official PHP SDK for the FoPost API.

v0.1.0 2026-08-22 15:33 UTC

This package is auto-updated.

Last update: 2026-08-30 21:48:20 UTC


README

Packagist Version Packagist Downloads PHP Version CI License

The official PHP SDK for the FoPost API. Connect social accounts once, then compose, schedule, and publish from your own application.

Requires PHP 8.1 or newer, plus ext-curl and ext-json. Nothing else: no framework, no HTTP library.

Install

composer require fopost/sdk

Get an API key

Create a key at app.fopost.com/api-keys. The full API reference lives at fopost.com/docs.

Quickstart

<?php

require __DIR__ . '/vendor/autoload.php';

use Fopost\Sdk\Client;

$client = new Client('fp_...');           // or set FOPOST_API_KEY

$workspace = $client->workspaces()->list()[0];
$accounts = $client->accounts()->list($workspace->id);

$post = $client->posts()->create(
    workspaceId: $workspace->id,
    content: 'Hello from PHP',
    accounts: array_map(fn ($a) => $a->id, $accounts),
);

$client->posts()->publish($post->id);

The key falls back to the FOPOST_API_KEY environment variable, so new Client() works when it is set.

$client = new Client(
    apiKey: 'fp_...',
    baseUrl: 'https://api.fopost.com',  // a bare host gets /v1 appended
    timeout: 30.0,                      // seconds
    maxRetries: 3,                      // attempts, on 429 only
);

Posts

use DateTimeImmutable;

// List one page. The result iterates over its items directly.
$page = $client->posts()->list(workspaceId: $workspaceId, status: 'scheduled');
foreach ($page as $post) {
    echo $post->id, ' ', $post->status, PHP_EOL;
}
echo $page->meta->total;

// Walk every matching post, one page at a time.
foreach ($client->posts()->iterate(workspaceId: $workspaceId) as $post) {
    echo $post->text(), PHP_EOL;
}

$post = $client->posts()->get('p_123');

// Create a draft, a thread, or a scheduled post.
$draft = $client->posts()->create(
    workspaceId: $workspaceId,
    content: ['First post', 'The reply'],
    accounts: ['acc_1', 'acc_2'],
);

$scheduled = $client->posts()->create(
    workspaceId: $workspaceId,
    content: 'Going out on Monday',
    accounts: ['acc_1'],
    status: 'scheduled',
    scheduleAt: new DateTimeImmutable('2026-03-01T09:00:00Z'),
);

// Partial update: only the fields you name are sent.
$client->posts()->update($draft->id, title: 'A better title');

$client->posts()->schedule($draft->id, new DateTimeImmutable('+1 day'));
$client->posts()->unschedule($draft->id);
$client->posts()->publish($draft->id);
$client->posts()->preflight($draft->id);
$client->posts()->retry($draft->id);
$client->posts()->cancel($draft->id);
$client->posts()->delete($draft->id);

foreach ($client->posts()->deliveries($draft->id) as $delivery) {
    echo $delivery->platform, ' ', $delivery->status, ' ', $delivery->externalUrl, PHP_EOL;
}

Accounts

$accounts = $client->accounts()->list($workspaceId);
$account = $client->accounts()->get('acc_1');

$health = $client->accounts()->health('acc_1');
$client->accounts()->disconnect('acc_1');

Workspaces

$workspaces = $client->workspaces()->list();
$workspace = $client->workspaces()->get($workspaceId);

echo $workspace->name, ' ', $workspace->timezone, PHP_EOL;

Labels

$labels = $client->labels()->list($workspaceId);

$label = $client->labels()->create($workspaceId, 'Product launch', '#0070f3');
$client->labels()->update($label->id, name: 'Launch week');
$client->labels()->delete($label->id);

AI

Every AI call spends AI credits.

$balance = $client->ai()->credits();
echo $balance->creditsRemaining, ' of ', $balance->creditsTotal, PHP_EOL;

$caption = $client->ai()->generateCaption(
    currentCaption: 'new feature is live',
    platforms: ['linkedin', 'bluesky'],
    charLimit: 280,
);

$rewrite = $client->ai()->rewrite('One draft, many networks', ['linkedin', 'bluesky'], tone: 'friendly');
foreach ($rewrite->results as $variant) {
    echo $variant->platform, ': ', $variant->content, PHP_EOL;
}

$repurposed = $client->ai()->repurposeUrl('https://example.com/blog/launch', ['linkedin', 'threads']);

Errors

Every non-2xx response raises an exception under Fopost\Sdk\Exception.

Status Exception
400, 422 ValidationException
401 AuthenticationException
402 PaymentRequiredException
403 PermissionDeniedException
404 NotFoundException
429 RateLimitException
anything else ApiException

All of them extend FopostException, which carries getStatus(), getErrorCode(), getMessage(), and getBody().

use Fopost\Sdk\Exception\FopostException;
use Fopost\Sdk\Exception\RateLimitException;
use Fopost\Sdk\Exception\ValidationException;

try {
    $client->posts()->publish('p_123');
} catch (ValidationException $e) {
    print_r($e->getErrors());
} catch (RateLimitException $e) {
    echo 'retry in ', $e->getRetryAfter(), 's', PHP_EOL;
} catch (FopostException $e) {
    echo $e->getStatus(), ' ', $e->getMessage(), PHP_EOL;
}

A 429 is retried automatically, up to maxRetries attempts, waiting for the interval the API asks for in Retry-After (capped at 60 seconds). The exception is raised only when the last attempt still comes back rate limited.

Anything the SDK does not wrap yet

$body = $client->request('GET', '/some/new/endpoint', params: ['workspace_id' => $workspaceId]);

Testing your integration

The transport is an interface, so nothing has to reach the network in your test suite.

use Fopost\Sdk\Client;
use Fopost\Sdk\Http\Response;
use Fopost\Sdk\Http\Transport;

$fake = new class implements Transport {
    public function send(string $method, string $url, array $headers, ?string $body): Response
    {
        return new Response(200, [], json_encode(['data' => []]));
    }
};

$client = new Client('fop_test_key', Client::DEFAULT_BASE_URL, 30.0, 3, $fake);

Support

Questions and issues: fopost.com/contact or the issue tracker.

License

MIT. Copyright Porter Bridge, LLC.