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

# Python SDK

> Python client for 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

Read operations that take filters take a single dictionary:

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

if res.ok:
    print(res.data)   # the envelope's `data`
    print(res.meta)   # pagination metadata
else:
    print(res.error.status, res.error.message)
```

Single-resource and search operations use the same shape:

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

## Write operations

```python theme={null}
created = cms.article.create({
    "title": "Hello world",
    "body": "...",
    "categoryId": "6810f2c3a1b2c3d4e5f60712",
    "authorId": "6810f2c3a1b2c3d4e5f60719",
})

updated = cms.article.update(created.data["id"], {"title": "Updated"})
cms.article.delete(created.data["id"])
```

## Error handling

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

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

if not res.ok:
    print(res.error.status)    # 404
    print(res.error.message)   # human readable
    print(res.error.payload)   # full API body
```

To raise a typed `ApiError` instead:

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

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

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

<Warning>
  `throw_on_error=True` raises 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 HTTP client:

```python theme={null}
res = cms.raw.request("GET", "/articles/advancedSearch", query={"body": {"minWords": 500}})
```

## Resources

`article`, `author`, `category`, `subcategory`, `form`, `form_submission`, `language`,
`tags`, `report`, `ai`, `ai_conversation`, `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>
