Any media_urls (or media_ids) entry can also be a Hash carrying per-media
alt text (max 1500 chars), delivered to Mastodon, Bluesky, X (photos),
Pinterest, Instagram (images), and LinkedIn (images):
Ruby
media_urls: [{ "url" => "https://example.com/teaser.jpg", "alt" => "Red sneaker on a white background" }]
Publish immediately:
Ruby
post = client.posts.create_and_publish( content: "We are live!", channels: ["x", "bluesky"])
Local files: client.media.upload(file: "/path/to/image.jpg", name: "hero-shot")
(file accepts a path String/Pathname, an IO, or a raw binary String). Base64:
client.media.upload_from_base64(data: b64_string, mime_type: "image/png").
Analytics
Ruby
# One poststats = client.analytics.post("123")# Up to 100 posts in one request (avoids rate limit trouble when syncing)bulk = client.analytics.posts(["123", "124", "125"])# Workspace overviewoverview = client.analytics.overview(period: "30d")# Account-level stats (followers etc.)accounts = client.analytics.accounts(platform: "instagram")
Social Inbox
Read and answer DMs, comments and mentions from your connected accounts. The API key
needs the opt-in inbox:read / inbox:write scopes. The lists use cursor pagination
(cursor: in, pagination["next_cursor"] out).
inbox.next hands out the next conversation that still needs an answer, with the
thread and the post it belongs to. On a comment thread, pass message_id: so the reply
goes under the comment you answer and not under the newest comment on the post.
Ruby
item = client.inbox.nextwhile item["data"] message = item["data"]["message"] unless item["data"]["reply_window"]["open"] # An Instagram/Facebook DM past Meta's 24-hour window: answer it in the app client.inbox.mark_read(message["conversation_id"]) item = client.inbox.next next end reply = client.inbox.reply( message["conversation_id"], text: "Thanks! DM sent.", message_id: message["id"], include_next: true ) item = { "data" => reply["next"], "remaining" => reply["remaining"] || 0 }end
See the Social Inbox guide for the platforms, the reply rules and the error
codes.
Pinterest product tags
Tag products on a Pin so people can shop the items in the image. List the product Pins
of the connected Pinterest account, then pass up to 24 of their Pin ids 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 list answers with HTTP 200 in both cases: "products" on success, or an "error"
hash (pinterest_not_connected, pinterest_catalog_access_required, platform_error)
when it could not be read. It reads the Pinterest catalog when the connection has
catalog access (given one time in the OmniSocials composer: Pinterest options, Add
products, Connect catalog), else the account's own Pins. The tags are added right after
the Pin is published. A product that Pinterest refuses never fails the post: the post
then carries pinterest["product_tags_result"] with the tagged and the skipped Pin ids.
client.pinterest.validate_product(id) checks one Pin id or Pin link before you post.
On posts.update the pinterest: hash replaces the stored one, so leave
"product_tags" out to remove the tags.
Verify webhooks
Rails controller:
Ruby
class OmnisocialsWebhooksController < ApplicationController skip_before_action :verify_authenticity_token def create payload = request.body.read # raw body, exactly as received signature = request.headers["X-OmniSocials-Signature"] begin event = OmniSocials::Webhooks.verify( payload: payload, signature: signature, secret: ENV.fetch("OMNISOCIALS_WEBHOOK_SECRET"), tolerance: 300 ) rescue OmniSocials::WebhookVerificationError return head :bad_request end if event["type"] == "post.published" event["data"]["targets"].each do |target| Rails.logger.info "#{target["platform"]} #{target["status"]} #{target["native_post_id"]}" end elsif event["type"] == "post.rejected" Rails.logger.info "Rejected: #{event["data"]["post_id"]} #{event["data"]["approval"]["reason"]}" end head :ok endend
Events: post.scheduled, post.published, post.failed, post.approved and post.rejected.
The two approval events carry data.approval with status, decided_by (the approver's
user id) and reason (nil on post.approved). To read the full review of a post (steps,
approvers, the rejection reason and the comment thread), call client.posts.get_approval(post_id).
Errors
The base class is OmniSocials::Error. OmniSocials::APIError covers HTTP errors
with subclasses AuthenticationError (401), PermissionDeniedError (403),
NotFoundError (404), ValidationError (400/422), RateLimitError (429, with
#retry_after), and ServerError (5xx), plus APIConnectionError and
WebhookVerificationError. Each carries status, code, message, and body.
Ruby
begin client.posts.get("does-not-exist")rescue OmniSocials::NotFoundError => e puts "Gone: #{e.code} #{e.message}"rescue OmniSocials::RateLimitError => e puts "Slow down, retry in #{e.retry_after}s"rescue OmniSocials::ValidationError => e puts "Bad request: #{e.body}"rescue OmniSocials::AuthenticationError puts "Check your API key"rescue OmniSocials::APIConnectionError puts "Network problem"rescue OmniSocials::APIError => e puts "API error: #{e.status} #{e.message}"end
Configuration
Ruby
client = OmniSocials::Client.new( api_key: "omsk_live_...", timeout: 60, # seconds max_retries: 5, # retries after the first attempt base_url: "https://api.omnisocials.com/v1" # override for testing)
Notes
A fresh connection is used per request; there is nothing to close
All responses are plain Hashes with string keys
Omitting folder_id/parent_id leaves a value unchanged; passing nil explicitly moves to root/top level