> ## Documentation Index
> Fetch the complete documentation index at: https://developers.octaviatech.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Install C#

> Install the C# SDK, initialize the client, and make your first call to the Octavia AI CMS API.

Targets .NET 6+. The only SDK that lets you supply your own `HttpClient`, so it fits
into an existing DI container, Polly pipeline, or test harness without extra work.

Every method returns `CMSResponse<T>`, with `T` the model that endpoint sends — so
`GetByIdAsync` hands back `CMSResponse<ArticleWrapper>` and `res.Data.Article.Slug` is
a typed field read, not a `JsonElement` walk.

## Install

```bash theme={null}
dotnet add package Octavia.CmsSdk
```

## Initialize

```csharp theme={null}
using Octavia.CmsSDK;

var cms = CMS.Init(
    Environment.GetEnvironmentVariable("OCTAVIA_API_KEY")!,
    new CMSOptions
    {
        Timeout      = TimeSpan.FromSeconds(30),
        ThrowOnError = false,
    });
```

`CMSOptions` is init-only, so it can only be set at construction. The key is sent as
the `x-api-key` header on every request; there is no header option, by design.

## Make a request

Filters go in as a `Dictionary<string, string?>` — the client URL-encodes them:

```csharp theme={null}
var res = await cms.Article.GetAllAsync(new Dictionary<string, string?>
{
    ["page"] = "1",
    ["limit"] = "10",
    ["category"] = "6810f2c3a1b2c3d4e5f60712",
});

if (res.Ok)
{
    foreach (var item in res.Data.ArticleListItem)
    {
        Console.WriteLine(item.Slug);
    }
    Console.WriteLine(res.Data.Pagination.Total);
}
else
{
    Console.Error.WriteLine(res.Error?.Message);
}
```

Single-resource and search operations return their own model:

```csharp theme={null}
var one    = await cms.Article.GetByIdAsync("6810f2c3a1b2c3d4e5f60718");
var bySlug = await cms.Article.GetBySlugAsync("hello-world");
var hits   = await cms.Article.SearchAsync(new Dictionary<string, string?> { ["keyword"] = "typescript" });

Console.WriteLine(one.Data.Article.Slug);
Console.WriteLine(one.Data.Article.Id);
```

<Note>
  A single entity arrives under a key of its own, so it is `res.Data.Article` /
  `res.Data.Author` / `res.Data.Form`, never the model directly. Endpoints that answer
  with a free-form payload — every delete, the archive, the streaming AI calls — use
  `JsonElement` for `Data`.
</Note>

## Write operations

The body is an `object`, serialized with `System.Text.Json`, so a plain
`Dictionary<string, object>` works:

```csharp theme={null}
var created = await cms.Article.CreateAsync(new Dictionary<string, object>
{
    // The title and body are keyed by language code.
    ["mainTitle"] = new Dictionary<string, string> { ["en"] = "Hello world" },
    ["content"] = new Dictionary<string, string> { ["en"] = "<p>...</p>" },
    // category is a list of category IDs.
    ["category"] = new[] { "6810f2c3a1b2c3d4e5f60712" },
    ["author"] = "6810f2c3a1b2c3d4e5f60719",
});

var id = created.Data.Article.Id;

await cms.Article.UpdateAsync(new Dictionary<string, object>
{
    ["id"] = id,
    ["mainTitle"] = new Dictionary<string, string> { ["en"] = "Updated" },
});
await cms.Article.DeleteIdAsync(id);
```

Only `mainTitle`, `content` and `category` are required to create; `author`
defaults to the authenticated user. `UpdateAsync` takes the whole document in
its body — there is no separate id argument, so `id` is a field of that body.

## Error handling

Errors are return values, not exceptions, unless you opt in:

```csharp theme={null}
var res = await cms.Article.GetByIdAsync("does-not-exist");

if (!res.Ok)
{
    Console.WriteLine(res.Error?.StatusCode);  // 404
    Console.WriteLine(res.Error?.Message);     // human readable
}
```

`CMSError` carries only the status and the message. For the unparsed response body,
opt in to throwing:

```csharp theme={null}
var cms = CMS.Init(key, new CMSOptions { ThrowOnError = true });

try
{
    await cms.Article.GetByIdAsync("does-not-exist");
}
catch (ApiError err)
{
    Console.WriteLine(err.Status);
    Console.WriteLine(err.Payload);
}
```

<Warning>
  `ThrowOnError` throws on any non-2xx response. Leaving it off and checking `res.Ok`
  is the safer default for batch work.
</Warning>

## Timeouts

`CMSOptions.Timeout` applies to every request — it is set on the underlying
`HttpClient`, and defaults to that client's own 100 seconds when left unset. There is
no per-request override.

## Escaping the resource wrapper

`cms.Raw` is the underlying HTTP client, for an endpoint the resources do not cover.
The untyped overload hands back a `JsonElement`; the generic one names the model:

```csharp theme={null}
var res = await cms.Raw.RequestAsync("GET", "/articles/advanceSearch");

var typed = await cms.Raw.RequestAsync<ArticleList>("GET", "/articles/advanceSearch");
Console.WriteLine(typed.Data.ArticleListItem);
```

## Resources

`Article`, `Author`, `Category`, `Subcategory`, `Form`, `FormSubmission`, `Language`,
`Tag`, `Report`, `AI`, `AIConversation`, `Raw`.

<Card title="All operations" icon="list" href="/api-reference/ai-cms/articles/get-all" arrow="true">
  Browse the API reference for the full endpoint list.
</Card>
