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. Waits for durable storage. Times out after 10 seconds. Supports Idempotency-Key header to prevent duplicate creation on retries.
Request body
| 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 |
request_body | object | no | Full request JSON (compressed) |
response_body | object | no | Full response JSON (compressed) |
error.kind | string | no | rate_limited, auth_failed, etc. |
Response 201 Created
{
"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 |
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,
"blobs_deleted": 56
} 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"} 42 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 |
422 | 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 | Database size limit exceeded | Yes (run GC or increase limit) |