API Reference
Base URL: http://localhost:8080
Authentication
When API keys are configured, all /v1/* endpoints require a Bearer token:
Authorization: Bearer your-api-key-here When no keys are configured (default), authentication is disabled. /health and /metrics are always public.
POST /v1/events
Ingest a single event. Two modes: durable (default, awaits flush) and fire-and-forget (returns immediately). Supports Idempotency-Key header to prevent duplicate creation on retries.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
durable | bool | true | When false the server enqueues the event and returns 202 Accepted immediately (no await-flush). p50 drops ~10×; events may be lost if the process crashes before the next batch flush. |
Request body
Schema is strict (deny_unknown_fields) — sending an unknown key returns 422 Unprocessable Entity.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | Model name (e.g. "gpt-4o") |
provider | string | yes | Provider key (e.g. "openai") |
usage.input_tokens | u32 | no | Input tokens |
usage.output_tokens | u32 | no | Output tokens |
usage.cache_read_input_tokens | u32 | no | Cached prompt tokens |
usage.reasoning_tokens | u32 | no | Reasoning/thinking tokens |
latency.ttft_ms | u32 | no | Time to first token (ms) |
latency.total_ms | u32 | no | End-to-end latency (ms) |
timestamp | string | i64 | no | ISO 8601 or epoch nanos |
user_id | string | no | User identifier |
api_key_id | string | no | API key for attribution |
org_id | string | no | Organization for rollups |
source | string | no | Sending system |
http_status | u16 | no | Upstream status code |
flags.streaming | bool | no | Streamed response |
flags.tool_calls | bool | no | Tool calls present |
error.kind | string | no | rate_limited, auth_failed, etc. |
metadata | object | no | Arbitrary JSON (queryable via user_tag / session_tag; capped at 64 KB). |
Response 201 Created (durable)
{
"id": "01J5XQKR4M2E3N8V7P6Y1WDCBA",
"cost_nanodollars": 6250000,
"model": "gpt-4o",
"provider": "openai"
} Response 202 Accepted (fire-and-forget, ?durable=false)
// 202 Accepted
{
"id": "01J5XQKR4M2E3N8V7P6Y1WDCBA",
"cost_nanodollars": 6250000,
"model": "gpt-4o",
"provider": "openai"
} POST /v1/events/batch
Ingest up to 10,000 events. Fire-and-forget by default. Set X-Keplor-Durable: true header to await flush confirmation for each event.
Request
{"events": [{"model": "gpt-4o", "provider": "openai", ...}, ...]} Response 201 or 207 Multi-Status
{
"results": [
{"id": "01J5X...", "cost_nanodollars": 100, ...},
{"error": "validation: model must not be empty"}
],
"accepted": 1,
"rejected": 1
} GET /v1/events
Query events with filtering and cursor-based pagination.
Query parameters
| Param | Type | Description |
|---|---|---|
user_id | string | Filter by user |
model | string | Filter by model |
provider | string | Filter by provider |
source | string | Filter by source |
from | i64 | After this epoch ns |
to | i64 | Before this epoch ns |
limit | u32 | Max results (default 50, max 1000) |
cursor | i64 | Pagination cursor |
include_archived | bool | When true the server fetches every archive manifest overlapping [from, to] for the requested user, decompresses + parses the JSONL chunks, and merges with live events sorted by (ts_ns desc, id desc). Currently uncached — each request pays the per-manifest S3 round-trip. Suitable for backfill / audit, not hot dashboards. |
Response 200 OK
{
"events": [{
"id": "01J5XQKR...",
"timestamp": 1700000000000000000,
"model": "gpt-4o",
"provider": "openai",
"usage": {"input_tokens": 500, "output_tokens": 200, ...},
"cost_nanodollars": 6250000,
"latency_total_ms": 1200,
"streaming": false
}],
"cursor": 1700000000000000000,
"has_more": false
} GET /v1/quota
Real-time cost and event count from the event table. At least one of user_id or api_key_id is required.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
user_id | string | no* | Filter by user |
api_key_id | string | no* | Filter by API key |
from | i64 | yes | Events on or after this epoch ns |
* At least one of user_id or api_key_id must be provided.
Response 200 OK
{
"cost_nanodollars": 48250000,
"event_count": 137
} GET /v1/rollups
Pre-aggregated daily rollup rows broken down by provider and model. The background rollup task refreshes every 60 seconds.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
user_id | string | no | Filter by user |
api_key_id | string | no | Filter by API key |
from | i64 | yes | Start epoch ns (converted to day boundary) |
to | i64 | yes | End epoch ns (converted to day boundary) |
limit | u32 | no | Max rows (default 100, max 1000) |
offset | u32 | no | Offset for pagination (default 0) |
Response 200 OK
{
"rollups": [{
"day": 1700006400,
"user_id": "user-123",
"api_key_id": "key-abc",
"provider": "openai",
"model": "gpt-4o",
"event_count": 42,
"error_count": 1,
"input_tokens": 21000,
"output_tokens": 8400,
"cache_read_input_tokens": 5000,
"cache_creation_input_tokens": 0,
"cost_nanodollars": 18750000
}],
"has_more": false
} GET /v1/stats
Aggregated period statistics summed from daily rollups. Optionally group by model for per-model cost breakdowns.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
user_id | string | no | Filter by user |
api_key_id | string | no | Filter by API key |
from | i64 | yes | Start epoch ns |
to | i64 | yes | End epoch ns |
provider | string | no | Filter by provider |
group_by | string | no | Set to "model" to group by provider + model |
limit | u32 | no | Max rows (default 100, max 1000) |
offset | u32 | no | Offset for pagination (default 0) |
Response 200 OK
{
"stats": [{
"provider": "openai",
"model": "gpt-4o",
"event_count": 137,
"error_count": 3,
"input_tokens": 68500,
"output_tokens": 27400,
"cache_read_input_tokens": 12000,
"cache_creation_input_tokens": 0,
"cost_nanodollars": 48250000
}],
"has_more": false
} DELETE /v1/events/:id
Delete a single event by ID.
Returns 204 No Content if deleted, 404 Not Found if the event does not exist.
DELETE /v1/events?older_than_days=N
Bulk delete events older than N days. Equivalent to keplor gc via HTTP. older_than_days must be greater than 0.
Response 200 OK
{
"events_deleted": 1234,
"segments_dropped": 3
} GET /v1/events/export
Stream all matching events as JSON Lines (application/x-ndjson). Accepts the same filter parameters as GET /v1/events but with no result-set limit. Each line is one JSON event object.
GET /health
Liveness probe. Returns 200 when healthy, 503 when degraded.
{
"status": "ok",
"version": "0.1.0",
"db": "connected",
"queue_depth": 0,
"queue_capacity": 32768,
"queue_utilization_pct": 0
} GET /metrics
Prometheus text exposition format.
# TYPE keplor_events_ingested_total counter
keplor_events_ingested_total{provider="openai",model="gpt-4o",tier="pro"} 42
# TYPE keplor_segments_total gauge
keplor_segments_total{tier="free"} 9084
# TYPE keplor_storage_bytes gauge
keplor_storage_bytes{tier="free"} 41508725
# TYPE keplor_pricing_catalog_age_seconds gauge
keplor_pricing_catalog_age_seconds 14392 Request/Response headers
| Header | Direction | Description |
|---|---|---|
Authorization | Request | Bearer <secret> (required when keys configured) |
Idempotency-Key | Request | Optional. Prevents duplicate event creation on retries. Cached for 5 min (configurable). |
X-Keplor-Durable | Request | Set to true on batch endpoint to await flush confirmation. Default: fire-and-forget. |
X-Request-Id | Both | Echoed if sent; otherwise Keplor generates a ULID and returns it. |
Retry-After | Response | Seconds until rate limit resets (returned with 429). |
Error responses
{"error": "validation: model must not be empty"} | Status | When | Retry? |
|---|---|---|
204 | Event deleted successfully | No |
400 | Validation error, bad JSON, invalid timestamp | No |
401 | Missing or invalid API key | No |
404 | Event not found (DELETE) | No |
408 | Request exceeded request_timeout_secs | Yes |
415 | Content-Type not application/json | No (fix client) |
422 | Unprocessable: malformed JSON, unknown field (schema is strict deny_unknown_fields), or unknown provider | No |
429 | Per-key rate limit exceeded | Yes (after Retry-After) |
500 | Storage failure, write timeout | Yes (with backoff) |
503 | Batch writer overloaded (back-pressure) | Yes (with backoff) |
507 | Data dir size limit exceeded | Yes (run GC or increase limit) |