Canonical page: https://docs.singlebase.io/sdk-python

# Python SDK

A small client you can copy into your backend. This is a documented implementation, not a separately published package. It sends the same API envelope for every service through one `call` method.

Python 3.10+; standard library only. Async calls run blocking HTTP I/O in a worker thread.

## Response contract

| Field | Meaning |
|---|---|
| `data` | Operation result; absent on API errors |
| `error` | API error object, or null/nil on success; `message` is the stable code |
| `meta` | Response metadata, including pagination when returned |
| `exec_time` | Gateway execution time in seconds, when returned |

API errors return in `Result.error`. Network, invalid JSON, and transport failures raise an exception or return a Go error. These small clients do not manage sessions, auto-refresh tokens, or retry requests. Pass a current user bearer token to the constructor/client only when acting as that user; omit it for trusted server/agent calls. Keep keys on the server.

## Client implementation

Save as `singlebase.py`. [Download the source](/examples/singlebase.py).

```python
import asyncio
import json
from dataclasses import dataclass, field
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

@dataclass
class Result:
    data: Any = None
    error: dict | None = None
    meta: dict = field(default_factory=dict)
    exec_time: float | None = None

class singlebaseClient:
    def __init__(self, api_key: str, api_url: str, bearer: str | None = None):
        self.api_key, self.api_url, self.bearer = api_key, api_url, bearer

    async def call(self, operation: str, payload: dict, options: dict | None = None) -> Result:
        return await asyncio.to_thread(self._call, operation, payload, options)

    def _call(self, operation, payload, options):
        headers = {"Content-Type": "application/json", "X-API-KEY": self.api_key}
        if self.bearer:
            headers["Authorization"] = "Bearer " + self.bearer
        body = {"operation": operation, "payload": payload}
        if options is not None:
            body["options"] = options
        request = Request(self.api_url, data=json.dumps(body).encode(), headers=headers, method="POST")
        try:
            response = urlopen(request, timeout=30)
        except HTTPError as exc:
            response = exc
        with response:
            result = json.load(response)
            if response.status >= 400 and not result.get("error"):
                raise RuntimeError(f"HTTP {response.status} without an API error envelope")
        return Result(result.get("data"), result.get("error"), result.get("meta") or {}, result.get("exec_time"))
```

## Example

Set `SINGLEBASE_API_URL` to the complete console endpoint and `SINGLEBASE_API_KEY` to your server or appropriately scoped agent key. The `notes` collection must exist.

```python
import asyncio
import os
from singlebase import singlebaseClient, Result

async def main():
    sbc = singlebaseClient(api_key=os.environ["SINGLEBASE_API_KEY"], api_url=os.environ["SINGLEBASE_API_URL"])
    resp: Result = await sbc.call(operation="data.query", payload={"collection": "notes", "limit": 5}, options={})
    if resp.error:
        raise RuntimeError(resp.error["message"])
    print(resp.data, resp.meta, resp.exec_time)

asyncio.run(main())
```

Use the same method with `llm.summarize` for generation or `chat.ask` for an answer, with the operation's verified payload. The `options` argument goes in the request envelope, separate from `payload`. An empty options object is valid; omit it when unused. See [API overview](/api.md) for request access and errors, and [API reference](/api-reference.md) for the operation groups.
