Symfony

The FoPost PHP client as a Symfony bundle, with autowiring, console commands, and webhook events.

fopost/symfony-bundle is a thin wrapper over the PHP SDK. Every request, retry, model, and error type lives there; the bundle wires it into Symfony: one configured client for autowiring, two console commands, and a webhook endpoint that turns deliveries into Symfony events.

Needs PHP 8.1 or newer and Symfony 6.4 LTS or 7.x.

composer require fopost/symfony-bundle

This is a 0.x release. The surface is still settling and a minor version may break something. Pin an exact version if that matters to you.

Published on Packagist. Source and issues: github.com/fopost/fopost-symfony. MIT licensed.

Configuration

With Symfony Flex the bundle registers itself. Without it, add it to config/bundles.php:

return [
    // ...
    Fopost\Symfony\FopostBundle::class => ['all' => true],
];

config/packages/fopost.yaml:

fopost:
    api_key: '%env(FOPOST_API_KEY)%'
    base_url: '%env(FOPOST_API_URL)%'
    timeout: '%env(float:FOPOST_API_TIMEOUT)%'
    max_retries: '%env(int:FOPOST_API_MAX_RETRIES)%'
    # Optional: an empty fallback leaves them null.
    default_workspace_id: '%env(default::FOPOST_WORKSPACE_ID)%'
    webhook_secret: '%env(default::FOPOST_WEBHOOK_SECRET)%'

.env:

FOPOST_API_KEY=fp_your_key_here
FOPOST_API_URL=https://api.fopost.com
FOPOST_API_TIMEOUT=30
FOPOST_API_MAX_RETRIES=3

Every key takes a literal too, so api_key: 'fp_...' works while you are trying things out. Only api_key is required.

KeyDefaultWhat it does
api_keynone, requiredYour API key. Sent as X-API-Key
base_urlthe hosted APIAPI root. A host with no path gets the versioned path appended
timeout30.0Seconds to wait for one request
max_retries3Attempts a rate limited request gets
default_workspace_idnullWorkspace the console commands use when --workspace is left off
webhook_secretnullSecret shown once when you created the webhook. Without it every delivery is rejected

Quick start

The bundle registers Fopost\Sdk\Client for autowiring, so type hint it anywhere:

use Fopost\Sdk\Client;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class PublishController extends AbstractController
{
    #[Route('/publish', methods: ['POST'])]
    public function __invoke(Client $fopost): JsonResponse
    {
        $workspace = $fopost->workspaces()->list()[0];
        $accounts = $fopost->accounts()->list($workspace->id);

        $post = $fopost->posts()->create(
            workspaceId: $workspace->id,
            content: 'Shipping today: scheduled posting straight from Symfony.',
            accounts: $accounts,
            status: 'scheduled',
            scheduleAt: new \DateTimeImmutable('+1 hour'),
        );

        return new JsonResponse(['id' => $post->id]);
    }
}

$fopost->posts(), accounts(), workspaces(), labels(), ai(), and the request() escape hatch are the full surface, documented on the PHP SDK page. The bundle adds nothing to it.

Console commands

# List the accounts connected to a workspace
php bin/console fopost:accounts --workspace ws_123

# Create a draft
php bin/console fopost:post "Shipping today" -a acc_1 -a acc_2

# Schedule it
php bin/console fopost:post "Shipping today" -a acc_1 --schedule-at 2026-09-01T09:00:00Z

# Send it now
php bin/console fopost:post "Shipping today" -a acc_1 --publish

Both commands fall back to default_workspace_id when --workspace is left off.

Receiving webhooks

Import the route the bundle ships. config/routes/fopost.yaml:

fopost:
    resource: '@FopostBundle/config/routes/webhooks.yaml'

That mounts POST /fopost/webhook. Create a webhook pointing at it and copy the secret it shows once into FOPOST_WEBHOOK_SECRET. The controller verifies the HMAC-SHA256 signature over the raw request body, answers 401 when it does not match, and dispatches a Symfony event when it does.

use Fopost\Symfony\Event\FopostPostPublishedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

final class PostPublishedListener
{
    #[AsEventListener]
    public function __invoke(FopostPostPublishedEvent $event): void
    {
        // $event->event      'post.published'
        // $event->data       the event body
        // $event->timestamp  when the API sent it
        // $event->deliveryId the X-FoPost-Delivery header
    }
}

One class per event, all extending FopostWebhookEvent:

EventClass
post.publishedFopostPostPublishedEvent
post.failedFopostPostFailedEvent
post.partially_failedFopostPostPartiallyFailedEvent
delivery.publishedFopostDeliveryPublishedEvent
delivery.failedFopostDeliveryFailedEvent
delivery.delayedFopostDeliveryDelayedEvent
account.health_changedFopostAccountHealthChangedEvent

Every delivery is dispatched twice (once under its own class, once under FopostWebhookEvent), so listening on the base class catches everything, including an event this bundle does not know yet. Listeners run inside the request, so keep them quick or hand the work to Messenger.

Errors

Every failure is a Fopost\Sdk\Exception\FopostException subclass, so one catch covers the lot:

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

try {
    $fopost->posts()->publish($postId);
} catch (ValidationException $e) {
    // 400 and 422
} catch (RateLimitException $e) {
    // retry after $e->retryAfter seconds
} catch (FopostException $e) {
    // everything else
}

Rate limited requests are retried for you, honouring Retry-After.

Testing your app

Swap the transport and nothing touches the network:

use Fopost\Sdk\Http\Transport;

// In config/services_test.yaml, or a compiler pass:
$container->getDefinition('fopost.client')->setArgument('$transport', new Reference(MyFakeTransport::class));

Next

Related documentation
  • SDKs & Integrations

    Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.

  • SDKs Overview

    Every official FoPost client, what it covers, and how to pick one.

  • TypeScript SDK

    The official TypeScript and Node.js client for the FoPost API.

  • Python SDK

    The official Python client for the FoPost API.

  • PHP SDK

    The official PHP client for the FoPost API, with no framework and no HTTP library.

  • Ruby SDK

    The official Ruby client for the FoPost API, with no runtime dependencies.

  • Go SDK

    The official Go client for the FoPost API.

  • Rust SDK

    The official Rust client for the FoPost API, async and built on reqwest.

Was this helpful?

On this page