> ## 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 Python

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

Pure Python, no third-party dependencies, Python 3.9+. Uses `urllib` from the standard
library, so there is nothing to compile.

## Install

```bash theme={null}
pip install octavia-cms-sdk
```

## Initialize

```python theme={null}
import os
from octavia_cms_sdk import CMS

cms = CMS.init(
    os.environ["OCTAVIA_API_KEY"],
    timeout_ms=30_000,
    throw_on_error=False,
)
```

The key is sent as the `x-api-key` header on every request. There is no header
parameter, by design.

## Make a request

Resource methods are named after the route. Read operations that take filters
pass them as a single `query` dict:

```python theme={null}
res = cms.article.getAll(query={"page": 1, "limit": 10, "category": "..."})

if res.ok:
    print(res.data.article_list_item)   # a typed dataclass, not a dict
    print(res.meta)                     # pagination metadata
else:
    print(res.error["message"])
```

`res.data` is the endpoint's response model — a generated dataclass, so fields
are attributes rather than string keys:

```python theme={null}
one     = cms.article.getById("6810f2c3a1b2c3d4e5f60718")
by_slug = cms.article.getBySlug("hello-world")
hits    = cms.article.search(query={"keyword": "typescript", "limit": 5})

print(one.data.article.slug)             # the envelope's `article` key
```

<Note>
  Method names follow the route segments as generated, including the ones that
  read as verbs and methods together — `idReactionPOST`, `engagementSettingsPUT`.
  The API reference lists every operation under its route.
</Note>

## Write operations

```python theme={null}
created = cms.article.create({
    # The title and body are keyed by language code.
    "mainTitle": {"en": "Hello world"},
    "content": {"en": "<p>...</p>"},
    # category is a list of category IDs.
    "category": ["6810f2c3a1b2c3d4e5f60712"],
    "author": "6810f2c3a1b2c3d4e5f60719",
})

print(created.data.article._id)

updated = cms.article.update({
    "id": created.data.article._id,
    "mainTitle": {"en": "Updated"},
})
cms.article.deleteId(created.data.article._id)
```

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

The document comes back under a key of its own, so a single entity is
`res.data.article` / `res.data.author` / `res.data.form`, never the model
directly.

## Error handling

By default a failed call comes back as a return value:

```python theme={null}
res = cms.article.getById("does-not-exist")

if not res.ok:
    print(res.error["message"])   # human readable
```

To raise instead, opt in at construction:

```python theme={null}
from octavia_cms_sdk import ApiError

cms = CMS.init(key, throw_on_error=True)

try:
    cms.article.getById("does-not-exist")
except ApiError as err:
    print(err.status, err.payload)
```

<Warning>
  `throw_on_error=True` raises `ApiError` on any non-2xx response. Leaving it off
  and inspecting `res.ok` is the safer default for batch work.
</Warning>

## Timeouts

`timeout_ms` applies to every request. There is no separate connect/read split, and no
per-request override — set it on the client:

```python theme={null}
cms = CMS.init(key, timeout_ms=5_000)
```

## Escaping the resource wrapper

`cms.raw` is the underlying client, so an endpoint the resources do not cover is
still reachable. `request` returns the decoded envelope as-is; `request_typed`
hydrates `data` into the model you name:

```python theme={null}
from octavia_cms_sdk.models import ArticleList

res = cms.raw.request("GET", "/articles/advanceSearch", query={"keyword": "typescript", "limit": 5})

typed = cms.raw.request_typed(ArticleList, "GET", "/articles/advanceSearch", query={"keyword": "typescript", "limit": 5})
print(typed["data"].article_list_item)
```

## Resources

`article`, `author`, `category`, `subcategory`, `form`, `formSubmission`, `language`,
`report`, `ai`, `raw`.

<Note>
  The client also exposes these as `cms.raw.<name>` (plural: `articles`, `authors`,
  `categories`, `subcategories`, `forms`, `form_submissions`, `languages`, `reports`,
  `ai`). Use the `CMS` facade unless you need a method the facade does not wrap.
</Note>

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