Skip to main content
This guide takes you from nothing to a visible event in five minutes. You’ll install the SDK, point it at your ingest endpoint, and record one image generation — submission and completion — then confirm it landed. The whole integration is two ideas: call keeps.capture() around your own provider calls, and reuse the returned asset_id to tie every event for one generation together. There are no wrappers, no decorators, and no provider patching — you stay in control of the request path.
1

Install the SDK

The package is pure Python with stdlib-only runtime dependencies.
uv pip install -e "mvp/sdk[dev]"
pip install keeps-sdk
keeps is a placeholder package name and will change before a public release. Keep the import behind a single alias if you want a painless rename later.
2

Set your credentials

The SDK reads two environment variables. The API key authenticates you; the ingest URL is where events are delivered.
export KEEPS_API_KEY="kp_live_your_key_here"
export KEEPS_INGEST_URL="https://ingest.yourcompany.com"
project_id is derived server-side from your API key — you never send it and it is never a capture() argument.
Zero-config delivery. With no endpoint=/KEEPS_INGEST_URL, events go to the hosted ingest (https://ingest.keeps.dev). Set KEEPS_INGEST_URL (or pass endpoint=) only to target a different environment — staging, or a local stack. You may pass a bare host; init() appends /v1/events for you.
3

Initialize once at startup

Call keeps.init() a single time when your process boots. It picks up KEEPS_API_KEY and KEEPS_INGEST_URL from the environment by default.
import keeps

keeps.init()  # reads KEEPS_API_KEY and KEEPS_INGEST_URL from the environment
If you’d rather configure in code:
keeps.init(api_key="kp_live_your_key_here", endpoint="https://ingest.yourcompany.com")
If no API key is found (and you haven’t explicitly disabled the SDK), init() logs a single warning and disables itself. It never raises.
4

Capture a generation

Record the submission first. Pass asset_id=None (the default) and capture() mints a uuid7 and returns it. Hold onto that id.
aid = keeps.capture(
    model="fal-ai/flux/dev",
    status="submitted",
    inputs={"prompt": "a red bicycle on a beach at sunset", "steps": 28},
)
When the provider call returns, record completion by reusing the same asset_id. That’s what correlates the two events into one generation on the backend — same id, same generation.
keeps.capture(
    aid,
    status="completed",
    outputs={"url": "https://cdn.yourcompany.com/out/red-bicycle.png"},
)
Pass output URLs and metadata in outputs. If the provider returns the image inline, upload it to your own storage first and capture the resulting URL.
5

Flush before exit, then verify

capture() only appends to an in-memory buffer; a background thread delivers it about once a second. In a short script, serverless function, or any process that may exit before that tick, call keeps.flush() so in-buffer and in-flight events actually leave the process.
keeps.flush()  # returns True if the buffer drained, False if it timed out
Then open your dashboard. The generation appears as a single row keyed by aid, with submitted inputs and completed outputs folded together.

Complete runnable example

Copy this into a file, set the two environment variables from Step 2, and run it. It records one generation end-to-end and prints delivery counters so you can confirm events actually shipped.
import keeps

# Reads KEEPS_API_KEY and KEEPS_INGEST_URL from the environment.
keeps.init()

# 1. Submission — mint a fresh asset_id and keep it.
aid = keeps.capture(
    model="fal-ai/flux/dev",
    status="submitted",
    inputs={"prompt": "a red bicycle on a beach at sunset", "steps": 28},
)
print("asset_id:", aid)

# ... your real provider call happens here ...

# 2. Completion — reuse the SAME asset_id to correlate.
keeps.capture(
    aid,
    status="completed",
    outputs={"url": "https://cdn.yourcompany.com/out/red-bicycle.png"},
)

# 3. Flush before the process exits, then inspect delivery counters.
delivered = keeps.flush()
print("flushed:", delivered)
print("stats:", keeps.stats())
A healthy run prints emitted and sent of 2 in the stats dict. If sent is 0 while dropped_send climbs, delivery is failing — usually a wrong ingest URL or API key. If emitted is 0, the SDK disabled itself at init() (check the warning log).

”It didn’t error” is not “it was delivered”

Every public call is fail-open: capture() never raises into your code and always returns an asset_id — even if the SDK is disabled, uninitialized, or misconfigured. That’s a feature (instrumentation can never break your app), but it means a clean run tells you nothing about delivery. The source of truth is keeps.stats():
keeps.flush()
print(keeps.stats())
# {'initialized': True, 'disabled': False, 'emitted': 2, 'sent': 2,
#  'batches': 1, 'retries': 0, 'dropped_overflow': 0, 'dropped_send': 0,
#  'accepted': 2, 'rejected_by_server': 0}
Watch sent vs dropped_send, and accepted — the server’s own count of what it stored. If sent grows but accepted stays 0, check the per-event rejected reasons in the 202 body (also surfaced in stats()).

Next steps

Core concepts

Events, assets, and how reusing one asset_id merges a generation.

Recording provider calls

Patterns for fal, Replicate, and OpenAI — submit, poll, complete, fail.

Reliability

Buffering, flushing, overflow, and what to do in serverless or forked workers.

API reference

Every parameter on init, capture, context, flush, and stats.