PHPackages                             postproxy/postproxy-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. postproxy/postproxy-php

ActiveLibrary[API Development](/categories/api)

postproxy/postproxy-php
=======================

PHP client for the PostProxy API — manage social media posts, profiles, and profile groups.

v1.12.0(1w ago)0545MITPHPPHP &gt;=8.1

Since Feb 23Pushed 1w agoCompare

[ Source](https://github.com/postproxy/postproxy-php)[ Packagist](https://packagist.org/packages/postproxy/postproxy-php)[ Docs](https://postproxy.dev)[ RSS](/packages/postproxy-postproxy-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (12)Versions (15)Used By (0)

PostProxy PHP SDK
=================

[](#postproxy-php-sdk)

PHP client for the [PostProxy API](https://postproxy.dev) — manage social media posts, profiles, and profile groups.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.1
- Composer

Installation
------------

[](#installation)

```
composer require postproxy/postproxy-php
```

Quick Start
-----------

[](#quick-start)

```
use PostProxy\Client;

$client = new Client(apiKey: 'your-api-key');

// List profiles
$profiles = $client->profiles()->list();

// Create a post
$post = $client->posts()->create(
    'Hello world!',
    profiles: ['prof-1'],
);
```

Configuration
-------------

[](#configuration)

```
$client = new Client(
    apiKey: 'your-api-key',
    profileGroupId: 'pg-123',  // Default profile group for all requests
);
```

### Idempotency

[](#idempotency)

Every write method (`POST`/`PUT`/`PATCH`/`DELETE`) accepts an `idempotencyKey:`, sent as the `Idempotency-Key` header. If the connection drops before you see the response, retry with the same key and you get the original response back instead of a second post:

```
$key = bin2hex(random_bytes(16));

$post = $client->posts()->create('Hello', profiles: ['profile-id'], idempotencyKey: $key);

// Retrying the same call with the same key replays the original response.
```

Generate a fresh key per logical operation — a UUID is ideal. Keys are scoped to your account and may be up to 255 characters. The SDK never generates keys or retries for you.

SituationResultFirst request with the keyRuns normallyRetry after a successOriginal status and body replayedRetry while the first is still running`ConflictException` (409) — wait and retrySame key, different request body`ValidationException` (422)Retry after an error responseRuns normally — errors are not replayedOnly successful (`2xx`) responses are stored, so a request that failed validation or hit a quota leaves the key free — fix the payload and retry with the same key. Stored responses are kept for **24 hours**. Requests without a key are unaffected.

Resources
---------

[](#resources)

### Posts

[](#posts)

```
// List posts with filters
$result = $client->posts()->list(page: 1, perPage: 10, status: 'processed');
$result->data;    // Post[]
$result->total;   // int
$result->page;    // int
$result->perPage; // int

// Get a single post
$post = $client->posts()->get('post-id');

// Create a post
$post = $client->posts()->create(
    'Post body',
    profiles: ['prof-1', 'prof-2'],
    media: ['https://example.com/image.jpg'],
    scheduledAt: '2025-06-01T12:00:00Z',
    draft: true,
);

// Create a post with file uploads
$post = $client->posts()->create(
    'Post with uploads',
    profiles: ['prof-1'],
    mediaFiles: ['/path/to/image.jpg'],
);

// Create a thread post
$post = $client->posts()->create(
    'Thread starts here',
    profiles: ['prof-1'],
    thread: [
        ['body' => 'Second post in the thread'],
        ['body' => 'Third with media', 'media' => ['https://example.com/img.jpg']],
    ],
);
foreach ($post->thread as $child) {
    echo "{$child->id}: {$child->body}\n";
}

// Publish a draft
$post = $client->posts()->publishDraft('post-id');

// Delete a post
$result = $client->posts()->delete('post-id');

// Delete a post and also remove it from social platforms
$result = $client->posts()->delete('post-id', deleteOnPlatform: true);

// Delete from platforms only (keeps DB record). Defaults to all platforms.
$r1 = $client->posts()->deleteOnPlatform('post-id');
// Target a single network
$r2 = $client->posts()->deleteOnPlatform('post-id', network: 'twitter');
// Target a specific profile
$r3 = $client->posts()->deleteOnPlatform('post-id', profileId: 'prof-abc');
// Target a specific post profile (covers entire thread for that profile)
$r4 = $client->posts()->deleteOnPlatform('post-id', postProfileId: 'pp-abc');

// Get stats for posts
$stats = $client->posts()->stats(['post-1', 'post-2']);
foreach ($stats->data as $postId => $postStats) {
    foreach ($postStats->platforms as $platform) {
        echo "{$platform->platform}: " . count($platform->records) . " snapshots\n";
        foreach ($platform->records as $record) {
            echo "  {$record->recordedAt->format('Y-m-d')}: " . json_encode($record->stats) . "\n";
        }
    }
}

// Filter stats by profiles/networks and time range
$stats = $client->posts()->stats(
    ['post-1'],
    profiles: ['instagram', 'twitter'],
    from: '2026-02-01T00:00:00Z',
    to: '2026-02-24T00:00:00Z',
);
```

### Queues

[](#queues)

```
// List all queues
$queues = $client->queues()->list();

// Get a queue
$queue = $client->queues()->get('queue-id');

// Get next available slot
$nextSlot = $client->queues()->nextSlot('queue-id');
echo $nextSlot->nextSlot;

// Create a queue with timeslots
$queue = $client->queues()->create(
    'Morning Posts',
    'profile-group-id',
    description: 'Weekday morning content',
    timezone: 'America/New_York',
    jitter: 10,
    timeslots: [
        ['day' => 1, 'time' => '09:00'],
        ['day' => 2, 'time' => '09:00'],
        ['day' => 3, 'time' => '09:00'],
    ],
);

// Update a queue
$queue = $client->queues()->update('queue-id',
    jitter: 15,
    timeslots: [
        ['day' => 6, 'time' => '10:00'],        // add new timeslot
        ['id' => 1, '_destroy' => true],          // remove existing timeslot
    ],
);

// Pause/unpause a queue
$client->queues()->update('queue-id', enabled: false);

// Delete a queue
$client->queues()->delete('queue-id');

// Add a post to a queue
$post = $client->posts()->create(
    'This post will be scheduled by the queue',
    profiles: ['prof-1'],
    queueId: 'queue-id',
    queuePriority: 'high',
);
```

### Webhooks

[](#webhooks)

```
// List webhooks
$webhooks = $client->webhooks()->list();

// Get a webhook
$webhook = $client->webhooks()->get('wh-id');

// Create a webhook
$webhook = $client->webhooks()->create(
    'https://example.com/webhook',
    events: ['post.published', 'post.failed'],
    description: 'My webhook',
);
echo $webhook->secret;

// Update a webhook
$webhook = $client->webhooks()->update('wh-id', events: ['post.published'], enabled: false);

// Delete a webhook
$client->webhooks()->delete('wh-id');

// List deliveries
$deliveries = $client->webhooks()->deliveries('wh-id', page: 1, perPage: 10);
foreach ($deliveries->data as $d) {
    echo "{$d->eventType}: {$d->success}\n";
}
```

#### Signature verification

[](#signature-verification)

Verify incoming webhook signatures using HMAC-SHA256:

```
use PostProxy\WebhookSignature;

$isValid = WebhookSignature::verify(
    payload: $request->getContent(),
    signatureHeader: $request->headers->get('X-PostProxy-Signature'),
    secret: 'whsec_...',
);
```

#### Event types and typed payloads

[](#event-types-and-typed-payloads)

Subscribe to any of these events (or pass `["*"]` for all):

`post.processed`, `post.imported`, `platform_post.published`, `platform_post.failed`, `platform_post.failed_waiting_for_retry`, `platform_post.insights`, `profile.connected`, `profile.disconnected`, `profile.stats`, `media.failed`, `comment.created`, `profile_comment.created`, `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.edited`, `message.deleted`, `message.failed_waiting_for_retry`, `message.failed`, `reaction.received`.

`WebhookEvents::parse` validates the envelope and returns a typed `Event` — `$event->data` is the right model for the event. Direct-message events share three reusable payload shapes: the eight `message.*` events decode to `MessageEventData` (which wraps a `Message`), `reaction.received` decodes to `ReactionEventData`, and `profile_comment.created` decodes to `ProfileCommentCreatedData`:

```
use PostProxy\WebhookEvents;
use PostProxy\Types\WebhookEvents\ProfileStatsData;
use PostProxy\Types\WebhookEvents\PlatformPostData;
use PostProxy\Types\WebhookEvents\CommentCreatedData;
use PostProxy\Types\WebhookEvents\MessageEventData;
use PostProxy\Types\WebhookEvents\ReactionEventData;
use PostProxy\Types\WebhookEvents\ProfileCommentCreatedData;

$event = WebhookEvents::parse($request->getContent());
match ($event->type) {
    'profile.stats' => /** @var ProfileStatsData $d */ $d = $event->data,
    'platform_post.published' => /** @var PlatformPostData $d */ $d = $event->data,
    'comment.created' => /** @var CommentCreatedData $d */ $d = $event->data,
    'profile_comment.created' => /** @var ProfileCommentCreatedData $d */ $d = $event->data,
    'message.received', 'message.sent' => /** @var MessageEventData $d */ $d = $event->data, // $d->message is a Message
    'reaction.received' => /** @var ReactionEventData $d */ $d = $event->data,
    default => null,
};
```

### Comments

[](#comments)

```
// List comments on a post (paginated)
$comments = $client->comments()->list('post-id', profileId: 'profile-id');
foreach ($comments->data as $comment) {
    echo "{$comment->authorUsername}: {$comment->body}\n";

    // Media attachments on the comment (image/video/gif/external/file).
    foreach ($comment->attachments as $att) {
        echo "  attachment: {$att->type} -> {$att->url}\n";
    }

    // Author signals (verification, follower count, ...) when the platform provides them.
    if ($comment->metadata !== null) {
        echo "  metadata: " . json_encode($comment->metadata) . "\n";
    }

    foreach ($comment->replies as $reply) {
        echo "  {$reply->authorUsername}: {$reply->body}\n";
    }
}

// List with pagination
$comments = $client->comments()->list('post-id', profileId: 'profile-id', page: 2, perPage: 10);

// Get a single comment
$comment = $client->comments()->get('post-id', 'comment-id', profileId: 'profile-id');

// Create a comment
$comment = $client->comments()->create('post-id', profileId: 'profile-id', text: 'Great post!');

// Reply to a comment
$reply = $client->comments()->create('post-id', profileId: 'profile-id', text: 'Thanks!', parentId: 'comment-id');

// Delete a comment
$result = $client->comments()->delete('post-id', 'comment-id', profileId: 'profile-id');
echo $result->accepted; // true

// Hide / unhide a comment
$client->comments()->hide('post-id', 'comment-id', profileId: 'profile-id');
$client->comments()->unhide('post-id', 'comment-id', profileId: 'profile-id');

// Like / unlike a comment
$client->comments()->like('post-id', 'comment-id', profileId: 'profile-id');
$client->comments()->unlike('post-id', 'comment-id', profileId: 'profile-id');

// Privately reply to a comment via DM (Instagram/Facebook).
// Returns a Message, not a Comment.
$message = $client->comments()->privateReply('post-id', 'comment-id', profileId: 'profile-id', text: 'DM-ing you the details!');
echo "Reply queued as message {$message->id} in chat {$message->chatId}\n";

// Filter by when PostProxy received the comment (created_at, not posted_at).
// A bare date means that date's start of day. Applies to top-level comments —
// one in range brings its full replies array with it.
$recent = $client->comments()->list(
    'post-id',
    profileId: 'profile-id',
    from: '2026-03-25',
    to: '2026-03-26T12:00:00Z',
);
```

#### Comments across posts

[](#comments-across-posts)

`comments()->listAll()` returns comments spanning every post in the profile group in one request — the comments counterpart to `posts()->stats()`. Every filter is optional.

**This list is flat.** Unlike the per-post list, replies are not nested: every comment, top-level or reply, is its own entry linked to its parent by `parentExternalId`, so `total`counts every comment and paging is exact. Entries are `BulkComment`, which adds `postId`, `profileId`, and `platform`.

```
$all = $client->comments()->listAll(
    profiles: ['instagram', 'prof-abc'],  // profile IDs or network names, mixed
    postIds: ['post-1', 'post-2'],        // omit for every post in scope
    from: '2026-03-25',
    perPage: 50,                          // max 100
);

foreach ($all->data as $c) {
    // Each entry says where it came from, so you can act on it with the
    // post-scoped methods above.
    echo "{$c->platform} {$c->postId} {$c->profileId}: {$c->body}\n";

    if ($c->parentExternalId !== null) {
        echo "  ↳ reply to {$c->parentExternalId}\n";
    }
}

// Reply to one of them
$first = $all->data[0];
$client->comments()->create($first->postId, $first->profileId, 'Thanks!', parentId: $first->id);
```

Unknown or out-of-scope IDs in `postIds` and `profiles` are ignored rather than erroring. Results are ordered newest first by receipt time.

### Direct Messages

[](#direct-messages)

Manage one-to-one conversations (Facebook, Instagram, Telegram, Bluesky) through two resources: `chats()` for conversations and `messages()` for the messages within them.

```
// List chats for a DM-capable profile (paginated)
$chats = $client->chats()->list('profile-id', perPage: 20);
foreach ($chats->data as $chat) {
    $who = $chat->participantUsername ?? $chat->participantExternalId;
    echo "{$who}: last message at " . ($chat->lastMessageAt?->format('c') ?? 'never') . "\n";
}

// Find or create a chat with a participant
$chat = $client->chats()->create('profile-id', 'participant-external-id', participantUsername: 'jane_doe');

// Get a single chat
$chat = $client->chats()->get('chat-id');

// Archive / unarchive a chat (Bluesky only)
$client->chats()->archive('chat-id');
$client->chats()->unarchive('chat-id');

// List messages in a chat (filter by direction/status)
$messages = $client->messages()->list('chat-id', direction: 'inbound');
foreach ($messages->data as $msg) {
    echo "[{$msg->direction}] {$msg->body}\n";
    foreach ($msg->attachments as $att) {
        echo "  attachment: {$att->type} -> {$att->url}\n";
    }
    foreach ($msg->reactions as $reaction) {
        echo "  reaction: {$reaction->emoji}\n";
    }
}

// Send a text message (within the platform's messaging window)
$sent = $client->messages()->send('chat-id', body: 'Yes, we ship worldwide!');

// Send with a messaging tag (Facebook/Instagram), by hosted URL, or from a local file
$client->messages()->send('chat-id', body: 'Following up.', tag: 'HUMAN_AGENT');
$client->messages()->send('chat-id', media: ['https://cdn.example.com/photo.png']);
$client->messages()->send('chat-id', mediaFiles: ['./photo.png']);

// Get a single message
$message = $client->messages()->get('message-id');

// Edit an outbound message (Telegram only)
$client->messages()->edit('message-id', body: 'Updated answer.');

// React / unreact (Facebook & Instagram)
$client->messages()->react('message-id', reaction: 'love', emoji: '❤️');
$client->messages()->unreact('message-id');
```

### Profile comments (Google Business reviews)

[](#profile-comments-google-business-reviews)

Profile-level comments expose Google Business reviews and replies. Reviews are user-generated — the SDK lets you list/get them and reply to or delete your own replies. Reviews sync twice daily.

```
// List reviews for a profile (paginated)
$reviews = $client->profileComments()->list('profile-id');
foreach ($reviews->data as $review) {
    echo "{$review->authorUsername}: {$review->body}\n";
    foreach ($review->replies as $reply) {
        echo "  reply: {$reply->body}\n";
    }
}

// Filter by placement (location)
$reviews = $client->profileComments()->list('profile-id', placementId: 'accounts/123/locations/456');

// Get a single review
$review = $client->profileComments()->get('profile-id', 'review-id');

// Reply to a review (parentId is the review id)
$reply = $client->profileComments()->create('profile-id', parentId: 'review-id', text: 'Thanks for visiting!');

// Delete your reply
$client->profileComments()->delete('profile-id', 'reply-id');
```

### Profiles

[](#profiles)

```
// List profiles
$result = $client->profiles()->list();

// Get a single profile
$profile = $client->profiles()->get('prof-id');

// Get placements for a profile
$placements = $client->profiles()->placements('prof-id');

// Move a placement (e.g. a Facebook Page or Telegram channel) to another group
$placement = $client->profiles()->assignPlacementToGroup(
    'prof-id',
    'placement-external-id',
    'pg-other',
);
echo $placement->profileGroupId; // "pg-other"

// Ice breakers (Instagram DMs): FAQ prompts shown when a user opens a chat
$result = $client->profiles()->iceBreakers('prof-id');
foreach ($result->iceBreakers as $ib) {
    echo "{$ib->question}\n";
}

$client->profiles()->setIceBreakers('prof-id', [
    ['question' => 'What services do you offer?', 'payload' => 'services'],
    ['question' => 'What are your hours?', 'payload' => 'hours'],
]); // 1-4 items

$client->profiles()->deleteIceBreakers('prof-id');

// Delete a profile
$result = $client->profiles()->delete('prof-id');

// Profile stats timeseries — placementId required for facebook, linkedin, telegram
$stats = $client->profiles()->getProfileStats(
    'prof_li_001',
    placementId: '108520199',
    from: '2026-04-01T00:00:00Z',
);
foreach ($stats->data->records as $r) {
    echo $r->recordedAt . ': ' . $r->stats['followerCount'] . "\n";
}

// Bluesky — no placements
$bsky = $client->profiles()->getProfileStats('prof_bsky_001');
echo end($bsky->data->records)->stats['followersCount'];
```

Every stats record (post stats and profile stats alike) carries `rawStats` alongside the normalized `stats`, exposing each metric under its **original platform name**:

```
$stats = $client->posts()->stats(['post-id']);
$record = $stats->data['post-id']->platforms[0]->records[0];

echo $record->stats['impressions'];          // normalized
echo $record->rawStats['views'];             // Instagram's own name
echo $record->rawStats['impression_count'];  // Twitter/X's own name
```

LinkedIn post stats now normalize `likes`, `comments`, `shares`, and `clicks` alongside `impressions` — previously only `impressions` was normalized.

#### Post syncs &amp; backfill

[](#post-syncs--backfill)

PostProxy mirrors posts published natively on a platform into your account. Every one of those pulls is recorded as a **post sync**: the one fired when the profile connects, the recurring poll, and any backfill you start.

```
// Start a backfill — walks the feed backwards from the newest post in batches
// of 25 until it reaches `from` or the platform stops returning posts.
$sync = $client->profiles()->backfillPosts('prof-id', '2025-01-01');
echo "{$sync->id} {$sync->status}"; // "sync456def pending"

// Poll it to completion — finished when status is "completed" or "failed"
$run = $client->profiles()->postSync('prof-id', $sync->id);
echo "{$run->postsImported} of {$run->postsSeen}";

// List recent runs (kept for 30 days), newest first
$runs = $client->profiles()->postSyncs(
    'prof-id',
    trigger: 'backfill',   // connect | scheduled | backfill
    status: 'completed',   // pending | running | completed | failed
    perPage: 25,
);
```

`PostSync` propertyDescription`id`Sync identifier`profileId`Profile this run belongs to`kind`Always `posts` today`trigger``connect`, `scheduled`, or `backfill``status``pending`, `running`, `completed`, or `failed``startedAt` / `completedAt``DateTimeImmutable` or `null``postsSeen`Posts the platform returned across the run`postsImported`Posts that were **new** and got created`backfillFrom`The date floor requested; `null` for `connect`/`scheduled``oldestPostedAt`Publish date of the oldest post the run reached`error`Platform error message when `status` is `failed``createdAt``DateTimeImmutable`**How far back a backfill reaches depends on the platform's API**, not on PostProxy: where history is pageable we follow it, otherwise the run ends early with whatever it got and still reports `status === 'completed'`.

Only one backfill runs per profile at a time — starting a second throws `ConflictException`carrying the running one's id:

```
use PostProxy\Exceptions\ConflictException;

try {
    $client->profiles()->backfillPosts('prof-id', '2025-01-01');
} catch (ConflictException $e) {
    $runningId = $e->response['profile_sync_id'];
    // Poll the run that's already going.
}
```

Posts you already have are skipped, so overlapping backfills are safe. Imported posts behave exactly like ones the poll picks up (`source: "imported"`, `post.imported` webhook), but a backfill's follow-up work is queued at a lower priority so a deep run can't slow down publishing.

### Profile Groups

[](#profile-groups)

```
// List profile groups
$result = $client->profileGroups()->list();

// Get a single profile group
$group = $client->profileGroups()->get('pg-id');

// Create a profile group
$group = $client->profileGroups()->create('My Group');

// Delete a profile group
$result = $client->profileGroups()->delete('pg-id');

// Initialize an OAuth connection
$connection = $client->profileGroups()->initializeConnection(
    'pg-id',
    platform: 'instagram',
    redirectUrl: 'https://myapp.com/callback',
);
echo $connection->url; // Redirect user here

// BlueSky — app password (synchronous)
$bsky = $client->profileGroups()->connectBluesky(
    'pg-id',
    identifier: 'yourname.bsky.social',
    appPassword: 'xxxx-xxxx-xxxx-xxxx',
);
echo $bsky->profile->id;

// Telegram — bring-your-own-bot. Channels populate asynchronously; poll
// placements until non-empty.
$tg = $client->profileGroups()->connectTelegram(
    'pg-id',
    botToken: '123456789:ABCdef-GhIJklMnOpQrStUvWxYz',
);
echo $tg->nextStep;

$placements = [];
while (empty($placements)) {
    $placements = $client->profiles()->placements($tg->profile->id)->data;
    if (empty($placements)) sleep(3);
}
```

Platform Parameters
-------------------

[](#platform-parameters)

```
use PostProxy\Types\PlatformParams\PlatformParams;
use PostProxy\Types\PlatformParams\FacebookParams;
use PostProxy\Types\PlatformParams\InstagramParams;
use PostProxy\Types\PlatformParams\TelegramParams;
use PostProxy\Types\PlatformParams\BlueskyParams;

$platforms = new PlatformParams([
    'facebook' => new FacebookParams(['format' => 'post', 'first_comment' => 'Hi!']),
    'instagram' => new InstagramParams(['format' => 'reel']),
    'bluesky' => new BlueskyParams(['format' => 'post']),
    'telegram' => new TelegramParams([
        'chat_id' => '-1001234567890',
        'parse_mode' => 'HTML',
        'disable_link_preview' => true,
    ]),
]);

$post = $client->posts()->create('Hello!', profiles: ['prof-1'], platforms: $platforms);
```

### Instagram user tags

[](#instagram-user-tags)

Tag public Instagram accounts in a post — feed post, reel, or story:

```
use PostProxy\Types\PlatformParams\InstagramUserTag;

$platforms = new PlatformParams([
    'instagram' => new InstagramParams([
        'format' => 'post',
        'user_tags' => [
            new InstagramUserTag('natgeo', x: 0.5, y: 0.4),               // slide 0
            new InstagramUserTag('nasa', x: 0.2, y: 0.8, mediaIndex: 1),  // slide 1
            new InstagramUserTag('spacex', mediaIndex: 2),                // video — username only
        ],
    ]),
]);

$client->posts()->create(
    'Shot on location',
    profiles: ['ig-profile-id'],
    media: ['https://example.com/1.jpg', 'https://example.com/2.jpg', 'https://example.com/3.mp4'],
    platforms: $platforms,
);
```

- **Images require `x` and `y`** — floats `0.0`–`1.0` measured from the top-left corner.
- **Reels and video slides** are tagged by username only; coordinates are ignored and dropped.
- **Stories** accept coordinates but don't need them.
- `mediaIndex` picks the carousel slide (0-based, defaults to `0`, video slides included).
- A leading `@` on a username is stripped for you.

Coordinates outside `0.0`–`1.0`, a `mediaIndex` past the last media item, or an image tag missing `x`/`y` are rejected with a `ValidationException` naming the offending entry. Accounts that are private or have tagging turned off are silently skipped by Instagram at publish time.

Supported platforms: `facebook`, `instagram`, `tiktok`, `linkedin`, `youtube`, `twitter`, `threads`, `pinterest`, `bluesky`, `telegram`, `google_business`. Telegram requires a `chat_id` per post — list channels with `$client->profiles()->placements($profileId)`.

Twitter supports polls: pass `new TwitterParams(['format' => 'poll', 'poll_options' => ['Yes', 'No'], 'poll_duration_minutes' => 1440])` — 2-4 options (max 25 chars each), duration 5 to 10080 minutes.

#### Google Business

[](#google-business)

Google Business posts use the `googleBusiness` property on `PlatformParams` (a plain associative array). The `location_id` is the location resource path returned by `$client->profiles()->placements()`. Supported formats: `standard`, `event`, `offer`. CTA actions: `LEARN_MORE`, `BOOK`, `ORDER`, `SHOP`, `SIGN_UP`, `CALL`. Media is limited to one image (≤5 MB).

```
use PostProxy\Types\PlatformParams\PlatformParams;

$platforms = new PlatformParams([
    'google_business' => [
        'format' => 'standard',
        'location_id' => 'accounts/123/locations/456',
        'cta_action_type' => 'LEARN_MORE',
        'cta_url' => 'https://example.com',
    ],
]);
```

Error Handling
--------------

[](#error-handling)

```
use PostProxy\Exceptions\AuthenticationException;
use PostProxy\Exceptions\NotFoundException;
use PostProxy\Exceptions\ConflictException;
use PostProxy\Exceptions\ValidationException;
use PostProxy\Exceptions\BadRequestException;
use PostProxy\Exceptions\PostProxyException;

try {
    $client->posts()->get('bad-id');
} catch (AuthenticationException $e) {
    // 401
} catch (NotFoundException $e) {
    // 404
} catch (ConflictException $e) {
    // 409 — duplicate submission, a backfill already running, or an in-flight
    // Idempotency-Key. Details are in $e->response.
} catch (ValidationException $e) {
    // 422
} catch (BadRequestException $e) {
    // 400
} catch (PostProxyException $e) {
    // Other errors
    echo $e->getMessage();
    echo $e->statusCode;
    echo print_r($e->response, true);
}
```

StatusExceptionThrown for400`BadRequestException`Missing required parameters401`AuthenticationException`Invalid, missing, or insufficient API key permissions404`NotFoundException`Resource does not exist or is not accessible409`ConflictException`Duplicate submission (`duplicate_post_id`), a backfill already running (`profile_sync_id`), or an in-flight `Idempotency-Key`422`ValidationException`Validation failed429`PostProxyException`Posting rate limit reachedDevelopment
-----------

[](#development)

```
composer install
./vendor/bin/phpunit
```

License
-------

[](#license)

MIT

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance98

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~12 days

Recently: every ~21 days

Total

14

Last Release

12d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7833262476bc4d22cf65f7b64c4b2e41427015e9a4ffb72d7a558233b677981b?d=identicon)[postproxy](/maintainers/postproxy)

---

Top Contributors

[![danbaranov](https://avatars.githubusercontent.com/u/79311?v=4)](https://github.com/danbaranov "danbaranov (13 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/postproxy-postproxy-php/health.svg)

```
[![Health](https://phpackages.com/badges/postproxy-postproxy-php/health.svg)](https://phpackages.com/packages/postproxy-postproxy-php)
```

###  Alternatives

[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M49](/packages/tencentcloud-tencentcloud-sdk-php)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

353.6k](/packages/eslazarev-wildberries-sdk)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k832.6k55](/packages/neuron-core-neuron-ai)[files.com/files-php-sdk

Files.com PHP SDK

2482.9k](/packages/filescom-files-php-sdk)[volcengine/volcengine-php-sdk

119.5k](/packages/volcengine-volcengine-php-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
