Download the PHP package omnisocials/omnisocials-php without Composer
On this page you can find all versions of the php package omnisocials/omnisocials-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download omnisocials/omnisocials-php
More information about omnisocials/omnisocials-php
Files in omnisocials/omnisocials-php
Package omnisocials-php
Short Description Official PHP SDK for the OmniSocials API. Schedule and publish posts to Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads, Mastodon, and Google Business from one API.
License MIT
Homepage https://omnisocials.com/sdk/php
Informations about the package omnisocials-php
OmniSocials PHP SDK
The official PHP SDK for OmniSocials: post, schedule, reply and report on 11 social platforms (Instagram, Facebook, LinkedIn, X, TikTok, YouTube, Threads, Pinterest, Bluesky, Mastodon and Google Business) from one API. Guide: omnisocials.com/sdk/php
- No Composer dependencies, built on ext-curl and ext-json (PHP >= 8.1)
- Stripe-style resource objects with array params
- Automatic retries with exponential backoff, configurable timeouts
- Rich exception classes and a webhook signature verification helper
- PSR-4 autoloading, PSR-12 code style
Installation
Quickstart
Authentication
Create an API key in the OmniSocials app under Settings -> API Keys. Keys start with omsk_live_. There are no test keys and no test mode: every key works on your real connected accounts. To try things safely, leave out scheduled_at so the post stays a draft (a draft never publishes on its own), or use a separate workspace with test accounts.
The client reads OMNISOCIALS_API_KEY from the environment, or you can pass it explicitly:
Constructing a client without a key throws an OmniSocials\Exception\AuthenticationException right away.
Configuration
Retries use exponential backoff (0.5s, 1s, 2s, ...) with jitter and honor the Retry-After header. Other 4xx responses are never retried.
Rate limits
The API allows 100 requests per minute per API key. When you exceed it, the SDK retries automatically (respecting Retry-After); if retries are exhausted it throws a RateLimitException whose getRetryAfter() method returns the seconds to wait.
Return values
Methods return the parsed response body as-is, decoded to associative arrays: single items come back as ['data' => [...]], lists as ['data' => [...], 'pagination' => [...]], and some responses carry extra sibling keys (media uploads include compatibility, PDF uploads include slides and media_ids, post creates targeting X with a URL in the text include warnings). Endpoints that respond 204 No Content (deletes) return null.
Posts
Schedule a post
Omit scheduled_at to create a draft. Use content as an array for per-platform captions:
Planned drafts
Not yet released (coming in 0.10.0). This section describes the next SDK release. The current release is 0.9.0.
Pass 'draft' => true with scheduled_at to store the post as a draft that keeps its time. It shows on the dashboard calendar at that slot with a dashed outline and the scheduler never publishes it: a person schedules it from the composer, or you do with update() and 'draft' => false. Use it when an agent proposes a slot that a human should confirm. A draft is not allowed with publish_now or approval_workflow_id (400 validation_error). On update(), 'draft' => false without scheduled_at is a 400 validation_error when the draft has no time or its time is in the past (pass a new scheduled_at then), and 'draft' => true on a scheduled post unschedules it while keeping the time.
Comment-to-DM automations
Not yet released (coming in 0.10.0). This section describes the next SDK release. The current release is 0.9.0.
Answer a keyword in an Instagram comment with a DM, once per person, with an optional public reply under the comment. Pass 'comment_dm_automation' on create() (Instagram must be in channels) and the automation is bound to that post; it starts listening once the post is live. Needs the opt-in automations:write scope on the key. $client->automations manages the automations themselves (list, get, create, update, delete) and reads their activity (listEvents, automations:read). On update(), 'comment_dm_automation' as an array replaces the post's automation and null removes it. Instagram rules: one private reply per comment, within 7 days; OmniSocials caps sends at 180 DMs an hour per account.
The events response is ['data', 'counts', 'pagination'], not the plain list: counts is the number of events per outcome (sent, skipped, failed, pending). An automation can also listen on a post published outside OmniSocials ($client->automations->create(['instagram_media_id' => ..., 'actions' => ...])), on the next post the workspace publishes ('trigger' => ['scope' => 'next_post']) or on every post ('scope' => 'any'), and on DMs and story replies ('trigger' => ['kinds' => ['dm', 'story_reply'], ...]).
Publish immediately
Per-media alt text
Every media_urls / media_ids entry accepts either a plain string or an array with an alt accessibility description (max 1500 chars). Alt text is delivered to Mastodon (media description), Bluesky (embed alt), X (photos and GIFs), Pinterest (pin alt text), Instagram (images), and LinkedIn (images). Strings and arrays can be mixed, and the same shape works in per-platform maps and thread_parts media.
Post with platform-specific options
Telegram channels and groups
Not yet released. Telegram comes with the next SDK release.
telegram is a channel or group the workspace connected in OmniSocials; the OmniSocials bot posts to it. The telegram options: silent, link_preview, pin, protect_content, spoiler and caption_above_media (booleans), up to 3 buttons ({ text, url }, not on an album or a poll), a native poll (2 to 12 options of up to 100 characters, open_period 5 to 2628000 seconds, multiple; the post text is the question, max 300 characters, no media) and first_comment (a reply under the post). Text up to 4,096 characters; with media, text over 1,024 characters is sent as a reply under the media. Albums hold up to 10 photos and videos, files up to 50 MB, no Stories. Once sent, the post's telegram.result carries the message ids and the link. Telegram gives bots no view counts: analytics carry the reactions and the poll votes.
Chained threads (X, Bluesky, Mastodon, Threads)
Provide 2 to 25 thread_parts to publish a chained thread instead of a single tweet. Each part is capped at 280 characters (25,000 for X Premium/Premium+ accounts) and can carry its own media (media_ids / media_urls). The same thread_parts shape works for bluesky (300 chars per part), mastodon (500 chars per part) and threads (Meta Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; parts after the first publish as replies to the previous part, and the Threads caption is taken from part 1).
On update, pass 'thread_parts' => null to clear thread mode (revert to a single post); leave the key out to keep the existing thread untouched. The same applies to bluesky, mastodon and threads.
Threads posts can also carry a location tag: pass 'threads' => ['location_id' => '...'] with an id from $client->locations->search($q, ['platform' => 'threads']) (see Locations below). On a multi-post thread the tag is applied to part 1, and on update 'location_id' => null clears it. Threads location tagging is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error.
X link posts use credits
X bills API posts whose text contains a URL at a premium, and OmniSocials passes that fee through as prepaid credits (20 credits per URL-containing tweet; threads billed per part with a link). When a create targets X and the text contains a URL, the response includes a top-level warnings array (a sibling of data):
From enforce_from (2026-08-14) the balance is checked at publish time, but credits are only deducted after the post successfully publishes (a failed publish is never charged). If the balance can't cover it, only the X target fails (other platforms publish normally); top up in the dashboard under Settings -> Organisation -> Billing -> Credits, then call posts->retry(). Posts without links, analytics, and media on X stay free. There is no API endpoint for credits — they are managed in the dashboard.
Separately, every scheduled X link post reserves its cost up front. posts->create(), posts->update(), and posts->publish() refuse the request with a 402 ApiException whose error code is x_credits_insufficient when reserving this post's cost would push the company's total reserved credits past its balance:
Drafts are never gated (the gate runs when a draft is scheduled or published), and posts publishing before 2026-08-14 are never gated.
List, get, update, publish, retry, delete
retry re-publishes only the platforms that failed, on the same post; platforms that already succeeded are never posted again. It is asynchronous: a 200 means the retry is queued, so poll get for the outcome. Max 3 retries per platform.
Approve or reject a post
Only works on a post with approval_status: "pending" (status: "in_approval"). Both act on behalf of the user who owns the API key, who must be a listed approver for the workflow's CURRENT step — steps approve in order, so being an approver on a later step is not enough yet (throws a PermissionDeniedException with error code forbidden). Approving the last step finalizes the post (scheduled or posting); rejecting stops the whole workflow immediately, not just the current step.
Read the approval review
getApproval returns the review of a post that went through an approval workflow: status (none, pending, approved, rejected), the workflow, who requested it and when, current_step (the step the post waits on, null when the review ended), every step with its approvers and their decisions, the rejection (by, reason, at, step; null when nobody rejected) and the comments thread, oldest first. A post without an approval workflow returns status: "none" with empty steps and comments. Read-only; needs the posts:read scope.
Recent platform posts
Fetch recent posts live from the connected platform APIs, including content published outside OmniSocials. Useful for brand-new workspaces where list() is empty. Requires the analytics:read scope. Each record includes duration_seconds (integer, nullable): the video length in whole seconds where the platform reports it — currently TikTok and YouTube; null for images and for platforms that don't expose it.
Media
Upload from a URL (recommended, up to 1GB)
Videos over 100MB are processed in the background and come back with status "processing". Every upload response includes a compatibility block listing connected platforms that would reject the file.
Upload a local file (multipart)
file is either a filesystem path or the raw file contents as a string:
Direct multipart uploads are capped at 100MB by the CDN; use uploadFromUrl() or the presigned flow below for bigger files.
Upload from base64
PDF carousels
Uploading a PDF rasterizes it into one image slide per page (max 20). The response carries slides and media_ids alongside data (the first slide). Pass ALL of media_ids, in order, to posts->create() to post the deck as a carousel (on LinkedIn a native swipeable document made from the original PDF file, which is kept so text and links stay intact; an image carousel elsewhere). To keep the PDF as ONE library item instead of one item per page, pass pdf_mode = "document": the response then has a single data item of type document (its page images in pdf.pages) and media_ids holds that one id, which expands into every page at post time.
Presigned uploads for large files (up to 1GB)
createUploadUrl() mints a one-time upload URL. POST the file to it as multipart form data (field name file) within expires_in_seconds (600s); the second request needs no auth headers because the single-use token is in the URL. The response of that second request is the created media item (or media_ids for a PDF).
Preflight compatibility check
Check a file against the workspace's connected platforms before uploading. Provide one of url, media_id, or size_bytes + mime.
List, get, rename, move, delete
Folders
Hashtag Sets
Save reusable hashtag groups and apply them to posts at create time. Uses the posts:read / posts:write scopes.
Apply a set when creating a post with hashtag_set (the set name, case-insensitive) or hashtag_set_id. The set is applied once at create time and tags already in the caption are skipped. hashtag_placement is 'caption_append' (default) or 'first_comment', and hashtag_platforms restricts the hashtags to a subset of the post's channels. Instagram's 30-hashtag cap returns error code hashtag_limit_exceeded.
Accounts
Analytics
Best times to post
The response is not wrapped in data. platform, timezone, basis, grid and recommendations sit at the top level.
Locations (Instagram and Threads place tagging)
Threads uses its own location ids (a Facebook Place ID is not a Threads location id). Pass 'platform' => 'threads' and search by keyword, or by latitude plus longitude instead of a keyword; use a result's id as threads.location_id on a post:
The Threads response is { locations: [...] } (each with nullable name, address, city, country, latitude, longitude) or { error: { code, message } } with code one of not_available, threads_not_connected, threads_reauth_required (reconnect Threads), or platform_error. Threads location tagging is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error.
Pinterest product tags
Tag products on a Pin so people can shop the items in the image. $client->pinterest->listProducts() returns the product Pins of the connected Pinterest account; pass their pin_id values (max 24) as product_tags in the pinterest options of the post. Only product Pins of your own account can be tagged; products of other merchants cannot. The tags are added right after the Pin is published. A product that Pinterest refuses never fails the post: the outcome is on the post as pinterest.product_tags_result (requested, tagged, skipped, error).
Without source the list reads the Pinterest catalog (with price, currency, availability and item_id) when the connection has catalog access, else the account's own Pins. Catalog access is given one time in the OmniSocials composer: Pinterest options, Add products, Connect catalog. 'source' => 'pins' scans up to 250 Pins per call, so products can be empty while bookmark is set; call again with ['bookmark' => $result['bookmark']]. To check one Pin id or Pin link before you post, call $client->pinterest->validateProduct('813744226420795884'). On posts->update() the pinterest array replaces the stored one, so leave product_tags out to remove the tags.
Social Inbox
DMs, comments, and mentions from Instagram, Facebook, LinkedIn, TikTok (video comments only), YouTube (video comments only), X (DMs), and Threads (comments and mentions on your Threads posts; no DMs) in one place. TikTok and YouTube replies are comments only; TikTok replies are capped at 150 characters. The list endpoints are cursor-paginated ({ next_cursor, has_more, limit }), unlike the offset-paginated lists elsewhere. The Threads inbox needs a Threads connection with the reply permission (a 401 reauth_required on reply() or hide() means reconnect Threads; an account connected before 2026-09-14 needs this once).
Work queue: what needs an answer
next() hands out the next conversation that still needs a reply (the customer's latest DM with no reply after it, or an unreplied comment/mention that is not hidden), with the whole thread and the post it belongs to (post.url, post.media_type), so a reply can be drafted from one call. DMs that can still be answered come first, then Instagram/Facebook DMs whose 24-hour window has closed (reply_window.open is false: answer those from the native app or mark them read), then comments and mentions, oldest first. Replies typed in the native apps count as answers. Only unread items are served by default, so markRead() is the durable way to skip one; exclude skips conversation ids for the current session only. Always pass 'message_id' => $message['id'] to reply() on comment threads: every comment on a post shares one conversation, and without it the reply goes under the newest comment on the post. Pass include_next => true to reply() to get the following item in the same response. listConversations(['unanswered' => true]) gives the same set as a plain list.
X DM replies use credits
X DM conversations (platform = x, type always dm) cost credits to reply to: each reply() send debits 2 prepaid credits (X's send fee, passed through at cost) before sending, auto-refunded if the send fails. Two 402 error codes are specific to reply():
insufficient_credits means the company balance can't cover the 2 credits. x_inbox_suspended means the workspace's X inbox auto-suspended after the balance hit zero; top up and re-enable it in the dashboard to resume — DMs that arrived while suspended are not recovered. Same balance and top-up flow as "X link posts use credits" above.
Webhooks
Events: post.scheduled, post.published, post.failed, post.approved (the last step of a post's approval workflow is approved) and post.rejected (an approver rejects the post; it will not publish). The two approval events carry data.approval with status, decided_by (the approver's user id) and reason (null on post.approved), and an empty data.targets.
Manage endpoints
Verify deliveries (plain PHP endpoint)
Every delivery is signed with your webhook secret. The X-OmniSocials-Signature header has the form t=<unix>,v1=<hex> where the hex value is an HMAC-SHA256 of "{timestamp}.{rawBody}". Always verify against the RAW request body:
Verify deliveries (Laravel route)
Use $request->getContent() for the raw body, and exclude the route from CSRF verification (webhooks carry no CSRF token):
Webhooks::verifySignature() uses a constant-time comparison (hash_equals), rejects timestamps older than the tolerance (replay protection), throws WebhookVerificationException on any failure, and returns the parsed event array on success.
Health
Error handling
All exceptions thrown by the SDK extend OmniSocials\Exception\OmniSocialsException. Non-2xx API responses throw an ApiException subclass exposing getStatus(), getErrorCode(), getMessage(), and the parsed getBody():
| Class | Status | Typical API codes |
|---|---|---|
ValidationException |
400 / 422 | validation_error, platform_not_connected, invalid_file_type |
AuthenticationException |
401 | unauthorized, invalid_api_key |
PermissionDeniedException |
403 | forbidden, insufficient_scope |
NotFoundException |
404 | not_found |
RateLimitException |
429 | rate_limit_exceeded (exposes getRetryAfter() seconds) |
ServerException |
>= 500 | internal_error |
ApiConnectionException |
n/a | network failure or timeout |
WebhookVerificationException |
n/a | invalid webhook signature |
API scopes
Each API key carries scopes: posts:read, posts:write, media:write, accounts:read, analytics:read, webhooks:manage, plus the opt-in automations:read / automations:write for comment-to-DM automations (not yet released in the SDK, coming in 0.10.0). A call with a missing scope throws PermissionDeniedException with code insufficient_scope.
Documentation
Full API reference and guides: https://docs.omnisocials.com
License
MIT
All versions of omnisocials-php with dependencies
ext-curl Version *
ext-json Version *