# Python SDK

> The statejar package on PyPI, an HTTP client for StateJar's memory API. It stores what a message says, returns only the memory a question needs as the system message for your own model, and asks before it stores an unclear amount. Your model key never reaches it.

## What it does

- **Store.** `ingest(text)` sends one message. StateJar extracts typed facts from it, refuses a value that fails its type, and stores a new version named by its handle (`shm_…`, the SHA-256 of the canonical state). No transcript is stored.
- **Recall.** `recall(query)` returns `memory_context`, the system message for the model you already use: the whole state while it is small, only the fields the question needs once it is large. With `audit=True` the disclosure is written to your audit log as an audited turn.
- **One turn.** `turn(text)` stores the message, then recalls with it, in that order, and audits the disclosure.
- **Confirm before store.** An amount with no claim attached ("i have 5000 rs") is not stored. It comes back in `confirmations` with a question, and `confirm(id, accept=True)` stores it as a new version.

StateJar never calls a model for you. The package is an HTTP client and nothing else, its one dependency is httpx, and your model key stays in your code. It also carries the [`statejar` command](/docs/cli.html), which signs a terminal in.

## Install

```bash
pip install statejar
```

You need Python 3.10 or later. PyPI serves version 0.6.0 (checked on 27 Oct 2026). With `pip install "statejar[keyring]"`, the key `statejar login` stores goes to your system's keychain when it has one, instead of a file.

## Set it up

Sign in once with `statejar login` (the [statejar command](/docs/cli.html)). On a server or in CI, put a key from the [API keys page](/api-keys) in `STATEJAR_API_KEY` instead.

| Argument | Default | What it does |
|---|---|---|
| `api_key` | `STATEJAR_API_KEY`, then the key `statejar login` stored | The developer key, sent as the `X-API-Key` header. |
| `session_tag` | `default` | The conversation. A new tag's first message builds on your newest memory in any tag. |
| `base_url` | `STATEJAR_BASE_URL`, then `https://api.statejar.com/api/v1` | The API, for example staging or a server you run. |
| `timeout` | `60` | Seconds a request may take. |

`StateJar` is also a context manager: `with StateJar(session_tag="user-42") as sj:` closes its connections at the end.

## Check it works

The first-turn example from [getting started](/docs/getting-started.html#6-remember-and-recall):

```python
from statejar import StateJar

# the key comes from `statejar login` or STATEJAR_API_KEY; one tag per conversation
sj = StateJar(session_tag="user-42")

sj.ingest("I'm Meera from Pune and I prefer email.")    # typed facts, no transcript
sj.ingest("My budget is ₹3,500.")

hit = sj.recall("What is my budget?", audit=True)       # what the question needs
print(hit["handle_used"])        # shm_… — a SHA-256 of the canonical state
print(hit["memory_context"])     # the system message for YOUR model, nothing else
```

It prints a handle, then the facts your model would get. The run below used the key `statejar login` stored:

![The first-turn example run with python: a handle, then memory_context with the name, city, contact preference and budget](/media/casts/python-first-turn.svg)

The [audit log](/audit) then lists the disclosure, with the handle and the fields it sent.

## Confirm before store

An amount with no claim attached is asked about, not stored. The question comes back in `confirmations`, and only your answer writes a version:

```python
from statejar import StateJar

sj = StateJar(session_tag="user-42")

# money without a claim ("i have 5000 rs") is not stored: it is asked about
got = sj.ingest("go goa i have 5000rs ruppes")
for c in got["confirmations"]:
    print(c["question"])                     # Is ₹5,000 your budget?
    sj.confirm(c["id"], accept=True)         # or accept=False: nothing is stored

print(sj.recall("what is my budget")["subset"])
```

It prints the question StateJar asked, answers yes, and recalls: the budget is now stored, beside the destination the same message gave.

![The confirm example run with python: the question "Is ₹5,000 your budget?", then the recalled state with the destination Goa and the budget of 5000 INR](/media/casts/python-confirm.svg)

## The audit trail

Every disclosure is in your audit log: which handle, which fields, which model, never the message. The same rows come back over the API:

```python
import httpx, os
from statejar import StateJar

sj = StateJar(session_tag="user-42")
sj.ingest("I'm Meera from Pune, I prefer email, and my budget is ₹3,500.")
sj.recall("What is my budget?", audit=True)    # audit=True writes the disclosure

# every disclosure: which handle, which fields, which model — never the text
base = os.environ.get("STATEJAR_BASE_URL", "https://api.statejar.com/api/v1").rstrip("/")
rows = httpx.get(f"{base}/audit?limit=3", headers={"X-API-Key": sj.api_key}).json()
for row in rows["entries"]:
    print(row["kind"], row["handle_used"], row["subset_keys"])
```

The newest three disclosures, each with its kind, its handle and the fields it sent. The middle row is the confirm example's recall, which did not ask for an audit: a plain recall is logged too, as `query`.

![The audit example run with python: three rows, query_audited, query and query_audited, each with its handle and the fields it sent](/media/casts/python-audit.svg)

## What it sends

- **To StateJar:** the text of each message you `ingest`, each question you `recall` with, and your answer to a confirmation. Your key goes in the `X-API-Key` header over HTTPS, with the `User-Agent` `statejar-python` and the package's version.
- **Never:** your model key, your model's replies, or anything else from your program. The package does not call a model.
- **On your machine:** nothing, apart from the key `statejar login` stored.
- **What StateJar stores:** typed facts, never the messages. Every recall is in your audit log (kind `query`, or `query_audited` with `audit=True`), and deleting your account deletes all of it.

## Troubleshooting

- **`StateJarAuthError` with "api_key is required":** there is no key. Run `statejar login`, or set `STATEJAR_API_KEY`.
- **`StateJarAuthError` (401):** the key is wrong, revoked or expired, or it belongs to another server (staging and production keys differ). `statejar whoami` says which account a stored key belongs to; `STATEJAR_API_KEY` wins over the stored key.
- **`StateJarConnectionError`:** the request never reached StateJar: DNS, a proxy, or `STATEJAR_BASE_URL` pointing somewhere that is not an API root ending in `/api/v1`. Nothing was stored, so `ingest` is safe to repeat.
- **`StateJarError` with `status_code` 429:** a limit of your plan ([what each limit counts](/docs/pricing-and-payments.html)). `body` carries the server's answer.
- **`stored` is `False` and `handle` is `None`:** not an error. The message held no fact to store, such as a greeting or a question.
- **`confirmations` is not empty:** StateJar asked instead of storing. Call `confirm(id, accept=True)`, or `accept=False`; nothing is written until you do.
