The official Rust SDK. Async-first on reqwest with rustls (no OpenSSL), typed
request structs with serde, serde_json::Value responses.
Install
Code
cargo add omnisocialscargo add tokio --features rt-multi-thread,macros
Authentication
Code
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
Code
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/GIFs), and Pinterest:
Code
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:
Code
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?;
Verify webhooks
axum handler:
Code
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"]); } _ => {} } StatusCode::OK } Err(_) => StatusCode::BAD_REQUEST, }}let app: Router = Router::new().route("/omnisocials/webhook", post(omnisocials_webhook));
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.