cargo add omnisocialscargo add tokio --features rt-multi-thread,macros
Authentication
Rust
let client = omnisocials::Client::new("omsk_live_...")?;
Or Client::from_env()? (reads OMNISOCIALS_API_KEY). A missing key returns
Error::Auth with code missing_api_key.
Create a post
Rust
use omnisocials::CreatePostParams;let post = client.posts().create(CreatePostParams { content: "New drop this Friday".into(), channels: Some(vec!["instagram".into(), "facebook".into(), "linkedin".into()]), scheduled_at: Some("2026-08-01T09:00:00Z".into()), media_urls: Some(vec!["https://example.com/teaser.jpg"].into()), ..Default::default()}).await?;println!("{} {}", post["data"]["id"], post["data"]["status"]);
media_urls / media_ids entries can also carry per-media alt text (max 1500
chars) via MediaEntry::Url / MediaEntry::Id, delivered to Mastodon, Bluesky,
X (photos), Pinterest, Instagram (images), and LinkedIn (images):
Rust
use omnisocials::MediaEntry;media_urls: Some(vec![MediaEntry::Url { url: "https://example.com/teaser.jpg".into(), alt: Some("Red sneaker on a white background".into()),}].into()),
Publish immediately:
Rust
client.posts().create_and_publish(CreatePostParams { content: "Going live right now".into(), channels: Some(vec!["x".into(), "bluesky".into()]), ..Default::default()}).await?;
use omnisocials::{AccountAnalyticsParams, AnalyticsOverviewParams};// One post's latest per-platform metricslet stats = client.analytics().post("post_id").await?;println!("{:?}", stats["data"]["platforms"]["instagram"]["metrics"]);// Batch: up to 100 posts in one calllet batch = client.analytics().posts(&["id1", "id2", "id3"]).await?;// Workspace-wide overviewlet overview = client.analytics().overview(AnalyticsOverviewParams { period: Some("30d".into()), ..Default::default()}).await?;println!( "{} impressions, {} engagements", overview["data"]["total_impressions"], overview["data"]["total_engagements"]);// Account-level stats (followers etc)let account_stats = client.analytics().accounts(AccountAnalyticsParams { platform: Some("instagram".into()), ..Default::default()}).await?;
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, set message_id so the reply
goes under the comment you answer and not under the newest comment on the post.
Rust
use omnisocials::NextUnansweredParams;let mut next = client.inbox().next(NextUnansweredParams::default()).await?;while !next["data"].is_null() { let message = &next["data"]["message"]; if next["data"]["reply_window"]["open"] == false { // An Instagram/Facebook DM past Meta's 24-hour window: answer it in the app client.inbox().mark_read(message["conversation_id"].as_str().unwrap()).await?; next = client.inbox().next(NextUnansweredParams::default()).await?; continue; } let reply = client.inbox().reply(message["conversation_id"].as_str().unwrap(), ReplyParams { text: "Thanks! DM sent.".into(), message_id: message["id"].as_str().map(str::to_owned), include_next: Some(true), ..Default::default() }).await?; next = serde_json::json!({ "data": reply["next"], "remaining": reply["remaining"] });}
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
object (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 value replaces the stored one, so leave
product_tags out to remove the tags.
Verify webhooks
axum handler:
Rust
use axum::body::Bytes;use axum::http::{HeaderMap, StatusCode};use axum::routing::post;use axum::Router;async fn omnisocials_webhook(headers: HeaderMap, body: Bytes) -> StatusCode { let signature = headers .get("x-omnisocials-signature") .and_then(|value| value.to_str().ok()) .unwrap_or(""); let secret = std::env::var("OMNISOCIALS_WEBHOOK_SECRET").expect("secret not set"); // `body` is the raw request bytes: exactly what the signature covers. match omnisocials::webhooks::verify_signature(&body, signature, &secret, 300) { Ok(event) => { match event["type"].as_str() { Some("post.published") => { println!("Published: {} {:?}", event["data"]["post_id"], event["data"]["targets"]); } Some("post.failed") => { eprintln!("Failed: {}", event["data"]["post_id"]); } Some("post.rejected") => { eprintln!("Rejected: {} {}", event["data"]["post_id"], event["data"]["approval"]["reason"]); } _ => {} } StatusCode::OK } Err(_) => StatusCode::BAD_REQUEST, }}let app: Router = Router::new().route("/omnisocials/webhook", post(omnisocials_webhook));
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 (null 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(id).
Errors
Everything returns Result<serde_json::Value, omnisocials::Error>. The Error
enum has variants Validation (400/422), Auth (401), PermissionDenied (403),
NotFound (404), RateLimit (429, with retry_after), Server (5xx), Api
(other non-2xx, e.g. 409 media_in_use), Connection, and WebhookVerification.
Accessors err.status(), err.code(), and err.retry_after() work across all
API variants.