The official .NET SDK. Targets .NET 8, depends only on System.Net.Http and
System.Text.Json, fully async with CancellationToken support on every method.
var client = new OmniSocialsClient(new OmniSocialsOptions { ApiKey = "omsk_live_..." });
Reads OMNISOCIALS_API_KEY if omitted. A missing key throws
AuthenticationException. The client owns an internal HttpClient and implements
IDisposable: create one and reuse it.
Create a post
C#
var response = await client.Posts.CreateAsync(new PostCreateParams{ Content = "New drop this Friday", Channels = new[] { "instagram", "facebook", "linkedin" }, ScheduledAt = "2026-08-01T09:00:00Z", MediaUrls = new[] { "https://example.com/teaser.jpg" },});var post = response!.Value.GetProperty("data");Console.WriteLine($"{post.GetProperty("id")} {post.GetProperty("status")}");
MediaUrls / MediaIds entries can also be objects carrying per-media alt text
(new { url, alt } / new { id, alt }, max 1500 chars), delivered to Mastodon,
Bluesky, X (photos), Pinterest, Instagram (images), and LinkedIn (images):
C#
MediaUrls = new object[] { new { url = "https://example.com/teaser.jpg", alt = "Red sneaker on a white background" } },
Publish immediately:
C#
await client.Posts.CreateAndPublishAsync(new PostCreateParams{ Content = "Going live right now", Channels = new[] { "x", "bluesky" },});
Upload media
C#
var upload = await client.Media.UploadFromUrlAsync(new MediaUploadFromUrlParams{ Url = "https://example.com/launch-video.mp4", Name = "launch-video-v2", Folder = "Campaigns",});Console.WriteLine(upload!.Value.GetProperty("data").GetProperty("id"));
Local files via factory methods:
C#
// From a pathawait client.Media.UploadAsync(MediaUploadParams.FromFile("./photos/product.jpg", name: "product-hero"));// Or from bytes / a streamvar bytes = await File.ReadAllBytesAsync("./photos/product.jpg");await client.Media.UploadAsync(MediaUploadParams.FromBytes(bytes, "product.jpg"));await using var stream = File.OpenRead("./photos/product.jpg");await client.Media.UploadAsync(MediaUploadParams.FromStream(stream, "product.jpg"));
Analytics
C#
// One post's latest per-platform metricsvar stats = await client.Analytics.PostAsync("post_id");Console.WriteLine(stats!.Value.GetProperty("data").GetProperty("platforms"));// Batch: up to 100 posts in one callvar batch = await client.Analytics.PostsAsync(new[] { "id1", "id2", "id3" });// Workspace-wide overviewvar overview = await client.Analytics.OverviewAsync(new AnalyticsOverviewParams { Period = "30d" });var totals = overview!.Value.GetProperty("data");Console.WriteLine($"{totals.GetProperty("total_impressions")} impressions");// Account-level stats (followers etc)var accountStats = await client.Analytics.AccountsAsync(new AccountAnalyticsParams { 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). Optional typed models are available
(InboxNextResponse, InboxReplyResponse, InboxDeleteMessageResponse).
C#
var page = await client.Inbox.ListConversationsAsync(new InboxConversationListParams{ Platform = "instagram", Unread = true,});var conversationId = page!.Value.GetProperty("data")[0].GetProperty("conversation_id").GetString()!;var messages = await client.Inbox.GetMessagesAsync(conversationId);await client.Inbox.ReplyAsync(conversationId, new InboxReplyParams { Text = "Thanks for reaching out!" });await client.Inbox.MarkReadAsync(conversationId);// Comment moderation: hide (hide: false unhides) or deletevar messageId = messages!.Value.GetProperty("data")[0].GetProperty("id").GetString()!;await client.Inbox.HideAsync(messageId);await client.Inbox.DeleteMessageAsync(messageId);
NextAsync hands out the next conversation that still needs an answer, with the
thread and the post it belongs to. On a comment thread, set MessageId so the reply
goes under the comment you answer and not under the newest comment on the post.
C#
var next = await client.Inbox.NextAsync();var item = next!.Value.GetProperty("data");while (item.ValueKind != JsonValueKind.Null){ var message = item.GetProperty("message"); if (!item.GetProperty("reply_window").GetProperty("open").GetBoolean()) { // An Instagram/Facebook DM past Meta's 24-hour window: answer it in the app await client.Inbox.MarkReadAsync(message.GetProperty("conversation_id").GetString()!); next = await client.Inbox.NextAsync(); item = next!.Value.GetProperty("data"); continue; } var reply = await client.Inbox.ReplyAsync(message.GetProperty("conversation_id").GetString()!, new InboxReplyParams { Text = "Thanks! DM sent.", MessageId = message.GetProperty("id").GetString(), IncludeNext = true, }); item = reply!.Value.GetProperty("next");}
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.
C#
var result = (await client.Pinterest.ListProductsAsync())!.Value;if (result.TryGetProperty("error", out var error)){ Console.Error.WriteLine(error.GetProperty("code").GetString());}else{ var productTags = result.GetProperty("products").EnumerateArray() .Take(3) .Select(product => product.GetProperty("pin_id").GetString()!) .ToArray(); await client.Posts.CreateAsync(new PostCreateParams { Content = "Our summer picks", Channels = new[] { "pinterest" }, MediaUrls = new[] { "https://example.com/summer-look.jpg" }, ScheduledAt = "2026-08-01T09:00:00Z", Pinterest = new Dictionary<string, object?> { ["board_id"] = "1234567890", ["product_tags"] = productTags, }, });}
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.ValidateProductAsync(id) checks one Pin id or Pin link
before you post. On Posts.UpdateAsync the Pinterest dictionary replaces the stored
one, so leave ["product_tags"] out to remove the tags.
Verify webhooks
ASP.NET Core minimal API:
C#
using System.Text.Json;using OmniSocials;var builder = WebApplication.CreateBuilder(args);var app = builder.Build();app.MapPost("/omnisocials/webhook", async (HttpRequest request) =>{ using var reader = new StreamReader(request.Body); var payload = await reader.ReadToEndAsync(); // raw body, do not re-serialize var signature = request.Headers["X-OmniSocials-Signature"].ToString(); var secret = Environment.GetEnvironmentVariable("OMNISOCIALS_WEBHOOK_SECRET")!; JsonElement webhookEvent; try { webhookEvent = WebhookSignature.Verify(payload, signature, secret, toleranceSeconds: 300); } catch (WebhookVerificationException) { return Results.BadRequest(); } switch (webhookEvent.GetProperty("type").GetString()) { case "post.published": var data = webhookEvent.GetProperty("data"); Console.WriteLine($"Published: {data.GetProperty("post_id")} {data.GetProperty("targets")}"); break; case "post.failed": Console.Error.WriteLine($"Failed: {webhookEvent.GetProperty("data").GetProperty("post_id")}"); break; case "post.rejected": var rejected = webhookEvent.GetProperty("data"); Console.Error.WriteLine($"Rejected: {rejected.GetProperty("post_id")} {rejected.GetProperty("approval").GetProperty("reason")}"); break; } return Results.Ok();});app.Run();
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.GetApprovalAsync(id).