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

ParamTypeDefaultDescription
durablebooltrueWhen 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.

FieldTypeRequiredDescription
modelstringyesModel name (e.g. "gpt-4o")
providerstringyesProvider key (e.g. "openai")
usage.input_tokensu32noInput tokens
usage.output_tokensu32noOutput tokens
usage.cache_read_input_tokensu32noCached prompt tokens
usage.reasoning_tokensu32noReasoning/thinking tokens
latency.ttft_msu32noTime to first token (ms)
latency.total_msu32noEnd-to-end latency (ms)
timestampstring | i64noISO 8601 or epoch nanos
user_idstringnoUser identifier
api_key_idstringnoAPI key for attribution
org_idstringnoOrganization for rollups
sourcestringnoSending system
http_statusu16noUpstream status code
flags.streamingboolnoStreamed response
flags.tool_callsboolnoTool calls present
error.kindstringnorate_limited, auth_failed, etc.
metadataobjectnoArbitrary 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

ParamTypeDescription
user_idstringFilter by user
modelstringFilter by model
providerstringFilter by provider
sourcestringFilter by source
fromi64After this epoch ns
toi64Before this epoch ns
limitu32Max results (default 50, max 1000)
cursori64Pagination cursor
include_archivedboolWhen 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

ParamTypeRequiredDescription
user_idstringno*Filter by user
api_key_idstringno*Filter by API key
fromi64yesEvents 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

ParamTypeRequiredDescription
user_idstringnoFilter by user
api_key_idstringnoFilter by API key
fromi64yesStart epoch ns (converted to day boundary)
toi64yesEnd epoch ns (converted to day boundary)
limitu32noMax rows (default 100, max 1000)
offsetu32noOffset 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

ParamTypeRequiredDescription
user_idstringnoFilter by user
api_key_idstringnoFilter by API key
fromi64yesStart epoch ns
toi64yesEnd epoch ns
providerstringnoFilter by provider
group_bystringnoSet to "model" to group by provider + model
limitu32noMax rows (default 100, max 1000)
offsetu32noOffset 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

HeaderDirectionDescription
AuthorizationRequestBearer <secret> (required when keys configured)
Idempotency-KeyRequestOptional. Prevents duplicate event creation on retries. Cached for 5 min (configurable).
X-Keplor-DurableRequestSet to true on batch endpoint to await flush confirmation. Default: fire-and-forget.
X-Request-IdBothEchoed if sent; otherwise Keplor generates a ULID and returns it.
Retry-AfterResponseSeconds until rate limit resets (returned with 429).

Error responses

{"error": "validation: model must not be empty"}
StatusWhenRetry?
204Event deleted successfullyNo
400Validation error, bad JSON, invalid timestampNo
401Missing or invalid API keyNo
404Event not found (DELETE)No
408Request exceeded request_timeout_secsYes
415Content-Type not application/jsonNo (fix client)
422Unprocessable: malformed JSON, unknown field (schema is strict deny_unknown_fields), or unknown providerNo
429Per-key rate limit exceededYes (after Retry-After)
500Storage failure, write timeoutYes (with backoff)
503Batch writer overloaded (back-pressure)Yes (with backoff)
507Data dir size limit exceededYes (run GC or increase limit)