OmniSocialsOmniSocials
DocumentationAPI reference
Help centerSupport
  • Documentation
  • API reference
Developers
  • Quickstart
  • API reference
  • SDKs
  • MCP server
  • CLI
For AI agents
  • Docs for AI agents
  • llms.txt
  • OpenAPI spec
  • Postman collection
Product
  • OmniSocials
  • Pricing
  • Changelog
  • Help center
Company
  • Support
  • Privacy policy
  • Terms of service

© 2026 OmniSocials

OmniSocials on GitHubOmniSocials on XOmniSocials on LinkedIn
Overview
Get started
SDKs
    Node.jsPythonGoRubyJavaPHP.NETRust
CLI
MCPDocs for AI agentsWebhooks
Guides
Platforms
Integrations
Migrate
Plans and billing
GlossaryChangelogAPI reference
SDKs

Rust SDK reference: install, auth and methods

The official Rust SDK. Async-first on reqwest with rustls (no OpenSSL), typed request structs with serde, serde_json::Value responses.

This page is the reference. For an overview of the Rust SDK, see omnisocials.com/sdk/rust.

Install

TerminalTerminal
cargo add omnisocials cargo 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?;

Upload media

Rust
use omnisocials::UploadMediaFromUrlParams; let upload = client.media().upload_from_url(UploadMediaFromUrlParams { url: "https://example.com/launch-video.mp4".into(), name: Some("launch-video-v2".into()), folder: Some("Campaigns".into()), ..Default::default() }).await?; println!("{} {:?}", upload["data"]["id"], upload["compatibility"]);

Local files (multipart from bytes):

Rust
use omnisocials::UploadMediaParams; let bytes = std::fs::read("./photos/product.jpg")?; let uploaded = client.media().upload(UploadMediaParams { file: bytes, filename: "product.jpg".into(), name: Some("product-hero".into()), ..Default::default() }).await?;

Analytics

Rust
use omnisocials::{AccountAnalyticsParams, AnalyticsOverviewParams}; // One post's latest per-platform metrics let stats = client.analytics().post("post_id").await?; println!("{:?}", stats["data"]["platforms"]["instagram"]["metrics"]); // Batch: up to 100 posts in one call let batch = client.analytics().posts(&["id1", "id2", "id3"]).await?; // Workspace-wide overview let 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).

Rust
use omnisocials::{ListConversationsParams, ReplyParams}; let conversations = client.inbox().list_conversations(ListConversationsParams { platform: Some("instagram".into()), unread: Some(true), ..Default::default() }).await?; let conversation_id = conversations["data"][0]["conversation_id"].as_str().unwrap(); let messages = client.inbox().get_messages(conversation_id, Default::default()).await?; client.inbox().reply(conversation_id, ReplyParams { text: "Thanks for reaching out!".into(), ..Default::default() }).await?; client.inbox().mark_read(conversation_id).await?; // Comment moderation: hide (false unhides) or delete let message_id = messages["data"][0]["id"].as_str().unwrap(); client.inbox().hide(message_id, true).await?; client.inbox().delete_message(message_id).await?;

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.

Rust
use omnisocials::ListPinterestProductsParams; let result = client.pinterest().list_products(ListPinterestProductsParams::default()).await?; if let Some(error) = result.get("error") { eprintln!("{}: {}", error["code"], error["message"]); } else { let product_tags: Vec<&str> = result["products"] .as_array() .map(|products| products.iter().filter_map(|product| product["pin_id"].as_str()).take(3).collect()) .unwrap_or_default(); client.posts().create(CreatePostParams { content: "Our summer picks".into(), channels: Some(vec!["pinterest".into()]), media_urls: Some(vec!["https://example.com/summer-look.jpg"].into()), scheduled_at: Some("2026-08-01T09:00:00Z".into()), pinterest: Some(json!({ "board_id": "1234567890", "product_tags": product_tags })), ..Default::default() }).await?; }

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.

Rust
use omnisocials::{CreatePostParams, Error}; match client.posts().create(CreatePostParams { content: "Hi".into(), channels: Some(vec!["instagram".into()]), ..Default::default() }).await { Ok(post) => println!("created {}", post["data"]["id"]), Err(Error::RateLimit { retry_after, .. }) => { eprintln!("rate limited, retry in {:?}s", retry_after); } Err(Error::Validation { code, message, .. }) => { eprintln!("bad request ({:?}): {}", code, message); } Err(Error::Connection(err)) => { eprintln!("network problem: {err}"); } Err(err) => { eprintln!("API error {:?} ({:?}): {}", err.status(), err.code(), err); } }

Configuration

Rust
use std::time::Duration; let client = omnisocials::Client::builder() .api_key("omsk_live_...") // wins over the env var .base_url("https://api.omnisocials.com/v1") // default .timeout(Duration::from_secs(30)) // per-request timeout (default 30s) .max_retries(2) // retries on 429 / 5xx / network errors (default 2) .build()?;

Notes

  • Typed request structs with the ..Default::default() idiom throughout
  • content is a Content enum with a Content::PerPlatform(HashMap<...>) variant for per-platform text
  • Clearing nullable fields uses nested options: folder_id: Some(Some("12".into())) sets a value, Some(None) sends JSON null (move to root)
  • Responses are serde_json::Value; deserialize with serde_json::from_value if you want types
  • Source: github.com/OmniSocials/omnisocials-rust
Last modified on October 21, 2026
.NETCLI
On this page
  • Install
  • Authentication
  • Create a post
  • Upload media
  • Analytics
  • Social Inbox
  • Pinterest product tags
  • Verify webhooks
  • Errors
  • Configuration
  • Notes
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust
Rust