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.
Install the SDK
The package is pure Python with stdlib-only runtime dependencies.
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.Set your credentials
The SDK reads two environment variables. The API key authenticates you; the
ingest URL is where events are delivered.
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.Initialize once at startup
Call If you’d rather configure in code:If no API key is found (and you haven’t explicitly disabled the SDK),
keeps.init() a single time when your process boots. It picks up
KEEPS_API_KEY and KEEPS_INGEST_URL from the environment by default.init()
logs a single warning and disables itself. It never raises.Capture a generation
Record the submission first. Pass When the provider call returns, record completion by reusing the same
asset_id=None (the default) and capture()
mints a uuid7 and returns it. Hold onto that id.asset_id. That’s what correlates the two events into one generation on the
backend — same id, same generation.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.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.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.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: Watch
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():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.