# .NET Social Media API SDK

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.

## Install

```bash
dotnet add package OmniSocials
```

## Authentication

```csharp
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

```csharp
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/GIFs), and Pinterest:

```csharp
MediaUrls = new object[] { new { url = "https://example.com/teaser.jpg", alt = "Red sneaker on a white background" } },
```

Publish immediately:

```csharp
await client.Posts.CreateAndPublishAsync(new PostCreateParams
{
    Content = "Going live right now",
    Channels = new[] { "x", "bluesky" },
});
```

## Upload media

```csharp
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:

```csharp
// From a path
await client.Media.UploadAsync(MediaUploadParams.FromFile("./photos/product.jpg", name: "product-hero"));

// Or from bytes / a stream
var 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

```csharp
// One post's latest per-platform metrics
var stats = await client.Analytics.PostAsync("post_id");
Console.WriteLine(stats!.Value.GetProperty("data").GetProperty("platforms"));

// Batch: up to 100 posts in one call
var batch = await client.Analytics.PostsAsync(new[] { "id1", "id2", "id3" });

// Workspace-wide overview
var 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

The .NET SDK also covers the Social Inbox with cursor pagination and optional
typed models:

```csharp
var page = await client.Inbox.ListConversationsAsync(new InboxListParams { Platform = "instagram", Unread = true });
var messages = await client.Inbox.GetMessagesAsync(conversationId);
await client.Inbox.ReplyAsync(conversationId, new InboxReplyParams { Text = "Thanks for reaching out!" });
await client.Inbox.MarkReadAsync(conversationId);
```

Requires the opt-in `inbox:read` / `inbox:write` scopes. See the
[Social Inbox guide](/inbox).

## Verify webhooks

ASP.NET Core minimal API:

```csharp
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;
    }
    return Results.Ok();
});

app.Run();
```

## Errors

All exceptions extend `OmniSocialsException`. `ApiException` exposes `Status`,
`Code`, `Message`, and `Body`. Types: `ValidationException` (400/422),
`AuthenticationException` (401), `PermissionDeniedException` (403),
`NotFoundException` (404), `RateLimitException` (429, `RetryAfter`),
`ServerException` (5xx), `ApiConnectionException`, `WebhookVerificationException`.

```csharp
try
{
    await client.Posts.CreateAsync(new PostCreateParams
    {
        Content = "Hi",
        Channels = new[] { "instagram" },
    });
}
catch (RateLimitException ex)
{
    Console.WriteLine($"Rate limited, retry in {ex.RetryAfter}s");
}
catch (ValidationException ex)
{
    Console.WriteLine($"Bad request ({ex.Code}): {ex.Message}");
}
catch (ApiConnectionException ex)
{
    Console.WriteLine($"Network problem: {ex.Message}");
}
catch (ApiException ex)
{
    Console.WriteLine($"API error {ex.Status} ({ex.Code}): {ex.Message}");
}
```

## Configuration

```csharp
var client = new OmniSocialsClient(new OmniSocialsOptions
{
    ApiKey = "omsk_live_...",
    BaseUrl = "https://api.omnisocials.com/v1", // default
    Timeout = TimeSpan.FromSeconds(30),          // per-request timeout (default 30s)
    MaxRetries = 2,                              // automatic retries on 429 / 5xx / network errors (default 2)
});
```

`Timeout` is a `TimeSpan` and applies per attempt.

## Notes

- Every method ends in `...Async` and returns `Task<JsonElement?>` (raw JSON)
- Update helpers: `MoveToRoot = true` (media) / `MoveToTopLevel = true` (folders) distinguish "leave unchanged" from "set to null"
- Dictionary entries with explicit nulls are sent on the wire; null POCO properties are omitted
- Source: [github.com/OmniSocials/omnisocials-dotnet](https://github.com/OmniSocials/omnisocials-dotnet)
