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

# API reference

> Every public function in the keeps SDK — exact signatures, parameters, return values, and examples.

The `keeps` SDK exposes exactly seven public functions. This page documents each
one precisely: its signature, every parameter, what it returns, and a runnable
example.

The order below follows the lifecycle of a typical integration:
[`init`](#init), [`capture`](#capture), [`context`](#context),
[`flush`](#flush), [`session`](#session), [`shutdown`](#shutdown),
[`stats`](#stats).

<Note>
  **Environment variables.** These are read by [`init`](#init) when the matching
  argument is not passed explicitly:

  * `KEEPS_API_KEY` — your ingest API key. `project_id` is derived **server-side**
    from this key; the client never sends it.
  * `KEEPS_INGEST_URL` — overrides the events endpoint (staging / self-run). If unset,
    the hosted default `https://ingest.keeps.dev/v1/events` is used.
  * `KEEPS_DISABLED` — set to `1`, `true`, or `yes` to disable the SDK entirely
    (handy in CI). Equivalent to `init(disabled=True)`.

  Legacy `LENS_*` env var names are still honored (`KEEPS_*` wins when both
  are set), and legacy `lk_*` API keys keep working forever — auth is by hash.
</Note>

***

## init

Configure the process-global client. Call once at startup. Fail-open: this never
raises into your code — a missing API key logs a single warning and disables the
SDK instead of erroring.

```python theme={null}
keeps.init(
    api_key=None,
    endpoint=None,
    *,
    user_id=None,
    disabled=None,
    flush_interval=1.0,
    max_buffer=10_000,
    transport=None,
) -> None
```

<ParamField path="api_key" type="str | None" default="None">
  Your ingest API key. Falls back to the `KEEPS_API_KEY` environment variable.
  If still empty (and not disabled), the SDK logs a warning and disables itself.
</ParamField>

<ParamField path="endpoint" type="str | None" default="None">
  The events endpoint URL. Falls back to `KEEPS_INGEST_URL`. `init` auto-appends
  `/v1/events` if the URL lacks that path. When unset, the hosted default is used
  (`https://ingest.keeps.dev/v1/events`).
</ParamField>

<ParamField path="user_id" type="str | None" default="None">
  A default `user_id` applied to every `capture()` that does not supply one
  explicitly or via [`context()`](#context).
</ParamField>

<ParamField path="disabled" type="bool | None" default="None">
  Force the SDK on or off. When `None`, the value is taken from `KEEPS_DISABLED`
  (`1`/`true`/`yes` → disabled). A disabled SDK still mints and returns
  `asset_id`s from `capture()` so your correlation logic keeps working.
</ParamField>

<ParamField path="flush_interval" type="float" default="1.0">
  Seconds between automatic background flushes of the in-memory buffer. The
  buffer also flushes immediately once 200 events accumulate.
</ParamField>

<ParamField path="max_buffer" type="int" default="10_000">
  Maximum number of events held in memory. On overflow the **newest** event is
  dropped and `stats()['dropped_overflow']` increments.
</ParamField>

<ParamField path="transport" type="Transport | None" default="None">
  Inject a custom transport, primarily for testing. Most callers leave this
  unset and the default HTTP transport is used.
</ParamField>

**Returns:** `None`.

```python theme={null}
import keeps

# Reads KEEPS_API_KEY / KEEPS_INGEST_URL from the environment if omitted.
keeps.init(api_key="kp_live_…", endpoint="https://ingest.example.com")
```

***

## capture

Record one event about an asset and return its `asset_id`. This is a
non-blocking `O(1)` append to an in-memory buffer — it returns immediately and
never raises into your code.

The first call (with `asset_id=None`) **mints** a uuid7 `asset_id` and returns
it. Pass that id back on later calls to attach more events to the **same**
generation (start → finish → annotate). Reusing `asset_id` is how correlation
works — the backend groups by it.

```python theme={null}
keeps.capture(
    asset_id=None,
    *,
    user_id=None,
    model=None,
    provider=None,
    status=None,
    inputs=None,
    outputs=None,
    error=None,
    usage=None,
    properties=None,
    ts=None,
) -> str
```

<ParamField path="asset_id" type="str | None" default="None">
  The id of the asset this event belongs to. `None` mints a fresh uuid7 and
  returns it; pass a previously-returned id to add to that same asset.
</ParamField>

<ParamField path="user_id" type="str | None" default="None">
  The end user this generation is for. Falls back to the ambient
  [`context()`](#context) value, then the `init(user_id=...)` default.
</ParamField>

<ParamField path="model" type="str | None" default="None">
  The bare provider-native model id, e.g. `"fal-ai/flux-pro/v1.1"`. Don't
  prefix the vendor into it — that's what `provider` is for.
</ParamField>

<ParamField path="provider" type="str | None" default="None">
  The vendor slug, e.g. `"fal"`, `"replicate"`, `"openai"` (lowercased
  server-side). A **dimension** for pricing and reliability breakdowns — the
  same model exists on multiple providers at different prices. It is never a
  correlation key: correlation is only the `asset_id`.
</ParamField>

<ParamField path="status" type="str | None" default="None">
  The lifecycle status, e.g. `"submitted"`, `"completed"`, `"failed"`.
  Ingestion is out-of-order tolerant: the latest terminal status wins, and
  once an asset is terminal (`completed`/`failed`/`cancelled`) a non-terminal
  event can't reopen it.
</ParamField>

<ParamField path="inputs" type="Any" default="None">
  The request inputs (typically a dict, e.g. `{"prompt": "..."}`).
</ParamField>

<ParamField path="outputs" type="Any" default="None">
  Output references — URLs and metadata. Typically a
  list of dicts, e.g. `[{"url": "https://…"}]`.
</ParamField>

<ParamField path="error" type="dict[str, Any] | None" default="None">
  A verbatim provider error payload. The SDK ships it as-is; error normalization
  happens server-side.
</ParamField>

<ParamField path="usage" type="dict[str, Any] | None" default="None">
  The provider's billing meters, verbatim, as a flat numeric map — e.g.
  `{"duration_s": 6.2, "n": 1}`. These numbers exist only in the provider's
  response at call time and are **unrecoverable later**, so capture them here.
  Non-numeric values are dropped per key; an empty result is omitted.
</ParamField>

<ParamField path="properties" type="dict[str, Any] | None" default="None">
  Group-by tags stored server-side as `Map(String, String)`. The fold is
  **per key** — for each key, the value from the latest event (by `ts`) wins —
  so you can append a partial bag without clobbering keys set on earlier events.
  Omitting properties never overwrites anything.

  Values get one canonical string encoding server-side: send `True`/`False`
  and they are stored as `'true'`/`'false'`; numbers and containers are stored
  as compact JSON. A `None` value is a **tombstone** — it clears that tag.
  A bag over its 8192-byte budget has its **values trimmed to fit** (never
  replaced with a stub key), and the cut is flagged in the event's `truncated`
  field.
</ParamField>

<ParamField path="ts" type="str | None" default="None">
  An explicit event timestamp. When omitted the SDK stamps the event itself.
</ParamField>

**Returns:** `str` — the `asset_id` for this event (always returned, even when
the SDK is disabled or uninitialized).

```python theme={null}
import keeps

# First call mints and returns the id.
asset_id = keeps.capture(
    model="fal-ai/flux-pro/v1.1",
    provider="fal",
    status="submitted",
    inputs={"prompt": "a red bicycle"},
)

# Later, reuse the same id to attach the result.
keeps.capture(
    asset_id,
    status="completed",
    outputs=[{"url": "https://cdn.example.com/out.png"}],
    usage={"n": 1, "duration_s": 6.2},   # the provider's billing meters, verbatim
)
```

<Note>
  In short-lived, serverless, or forked workers, call [`flush()`](#flush) before
  the unit of work returns — otherwise buffered events can be lost when the
  process is frozen or killed before the background flush tick. See
  [Reliability](/reliability).
</Note>

***

## context

A context manager that tags every `capture()` inside the block with ambient
`user_id` and arbitrary `properties`. Blocks nest (inner wins per key), follow
the current thread/task, and propagate into `asyncio` tasks created inside the
block.

```python theme={null}
keeps.context(*, user_id=None, **properties) -> context manager
```

<ParamField path="user_id" type="str | None" default="None">
  Ambient `user_id` applied to every `capture()` in the block that does not pass
  its own `user_id`.
</ParamField>

<ParamField path="**properties" type="Any">
  Any remaining keyword arguments become ambient `properties` tags merged into
  each `capture()` in the block. Per-call `properties` win per key.
</ParamField>

**Returns:** a context manager. It only **tags** — it does **not** flush on
exit. Fail-open: a broken context never raises.

```python theme={null}
import keeps

with keeps.context(user_id="u_42", experiment_id="exp-123", tier="pro"):
    # This capture is tagged with user_id=u_42 and the two properties.
    keeps.capture(model="flux-pro", inputs={"prompt": "a tabby cat"})
```

***

## flush

Block until buffered events are delivered (or dropped), up to `timeout` seconds.
Call this before a short-lived process exits to avoid losing in-buffer events.

```python theme={null}
keeps.flush(timeout=5.0) -> bool
```

<ParamField path="timeout" type="float | None" default="5.0">
  Maximum seconds to wait for the buffer to drain. `None` waits without a deadline.
</ParamField>

**Returns:** `bool` — `True` if the buffer drained within the timeout, `False`
if it timed out. (Also returns `True` immediately if the SDK was never
initialized.)

```python theme={null}
import keeps

keeps.capture(model="flux-pro", inputs={"prompt": "a lighthouse"}, status="completed")

# Make sure it's delivered before the worker returns.
if not keeps.flush(timeout=3.0):
    # best-effort: log and move on, never block customer code
    ...
```

***

## session

A context manager for one unit of work: it applies ambient tags like
[`context`](#context) **and guarantees a flush on exit** — the built-in form of
the flush-before-exit pattern for serverless, worker, and agentic code.

```python theme={null}
keeps.session(*, user_id=None, flush_timeout=5.0, **properties)  # context manager
```

<ParamField path="user_id" type="str | None" default="None">
  Ambient `user_id` applied to every `capture()` in the block (same resolution
  as [`context`](#context)).
</ParamField>

<ParamField path="flush_timeout" type="float | None" default="5.0">
  Seconds to wait for the exit-flush to drain. `None` waits without a deadline.
</ParamField>

<ParamField path="**properties" type="Any">
  Remaining keyword arguments become ambient `properties` tags for the block.
</ParamField>

**Behavior:** flushes on exit whether the block returns or raises. Fail-open —
it never raises on its own and does **not** swallow exceptions from inside the
block (it flushes first, then they propagate).

```python theme={null}
import keeps

def handler(event):
    with keeps.session(user_id=event["user"], job="render"):
        aid = keeps.capture(model="flux-pro", status="submitted",
                           inputs={"prompt": event["prompt"]})
        keeps.capture(aid, status="completed", outputs=[{"url": "https://…"}])
    # flushed here, before the function returns
```

***

## shutdown

Flush and then close the transport, releasing the background flush thread. Call
this on a clean application exit. An `atexit` handler also runs this on normal
interpreter exit, so explicit calls are mainly for orderly teardown.

```python theme={null}
keeps.shutdown(timeout=2.0) -> None
```

<ParamField path="timeout" type="float" default="2.0">
  Maximum seconds to wait while flushing before closing the transport.
</ParamField>

**Returns:** `None`. A no-op if the SDK was never initialized.

```python theme={null}
import keeps

# At the end of a long-running process.
keeps.shutdown(timeout=5.0)
```

***

## stats

Return a snapshot of SDK counters. Useful for monitoring delivery health and
diagnosing dropped events. Counters are cumulative for the life of the process.

```python theme={null}
keeps.stats() -> dict
```

**Returns:** `dict`. Before `init()` it contains only `{"initialized": False}`.
After `init()` it contains every key below:

<ParamField path="initialized" type="bool">
  `True` once `init()` has run. When `False`, no other keys are present.
</ParamField>

<ParamField path="disabled" type="bool">
  `True` if the SDK is disabled (no API key, `KEEPS_DISABLED`, or
  `disabled=True`). Disabled clients still mint `asset_id`s but send nothing.
</ParamField>

<ParamField path="emitted" type="int">
  Total events accepted into the buffer by `capture()`. Your offered load.
</ParamField>

<ParamField path="sent" type="int">
  Total events successfully delivered to the ingest endpoint.
</ParamField>

<ParamField path="accepted" type="int">
  Total events the server confirmed it stored, parsed from each `202` response
  body. This is the server's own count — the strongest delivery signal the SDK
  can give you.
</ParamField>

<ParamField path="rejected_by_server" type="int">
  Events the server rejected individually in the `202` body (bad timestamp,
  over-cap field, …). These were delivered but **not stored**, and retrying
  them cannot succeed. The first rejection also logs a one-time warning with
  the reason codes.
</ParamField>

<ParamField path="batches" type="int">
  Number of HTTP batches successfully POSTed. Events are batched up to 200 per
  request.
</ParamField>

<ParamField path="retries" type="int">
  Number of delivery retry attempts (exponential backoff, honoring `429
      Retry-After`). A high value signals a flaky or throttling endpoint.
</ParamField>

<ParamField path="dropped_overflow" type="int">
  Events dropped because the buffer was full (`max_buffer`). The newest event is
  dropped on overflow. A nonzero value means you are producing faster than the
  SDK can deliver — raise `max_buffer` or check connectivity.
</ParamField>

<ParamField path="dropped_send" type="int">
  Events dropped after delivery failed past the retry limit (or a non-retryable
  4xx). A nonzero value means events were permanently lost in transit.
</ParamField>

```python theme={null}
import keeps

keeps.flush()
print(keeps.stats())
# {'initialized': True, 'disabled': False, 'emitted': 12, 'sent': 12,
#  'batches': 1, 'retries': 0, 'dropped_overflow': 0, 'dropped_send': 0,
#  'accepted': 12, 'rejected_by_server': 0}
```

<Note>
  Delivery is **best-effort, not at-least-once**. `emitted - sent` (minus what is
  still buffered) tells you how many events the SDK could not deliver; the two
  `dropped_*` counters tell you why. `sent` counts what reached the server;
  `accepted` is what the server actually stored — a gap between them shows up in
  `rejected_by_server`. See [Reliability](/reliability).
</Note>
