Skip to content

HTTP API

przemek edited this page May 16, 2026 · 1 revision

HTTP API

Defaults: bound to 127.0.0.1:8080, configurable via Config.http_bind_address. Implemented in src/api/http_server.rs.

Endpoints

POST /v1/usage/batch

Spec §9.1 — batch ingest.

Request:

{
  "events": [
    {
      "event_id": "evt_01H...",
      "kind": "Usage",
      "account_id": "acc_123",
      "product_id": "ai_gateway",
      "meter_id": "tokens.input",
      "timestamp_ms": 1778954400000,
      "quantity": 1240,
      "unit": "token",
      "source": "agentcore",
      "model_id": "claude-sonnet-4",
      "dimensions": { "provider": "anthropic" },
      "ingested_at_ms": 0
    }
  ]
}

Note: ingested_at_ms is overwritten server-side; the client value is ignored.

Response:

{ "accepted": 998, "duplicates": 1, "conflicts": 0, "rejected": 1 }
  • accepted — new events that passed dedupe + made it durable.
  • duplicates — same event_id, same payload (retries).
  • conflicts — same event_id, different payload (collector bug). Logged.
  • rejected — failed validation (empty IDs, non-positive timestamp, >16 dimensions, Correction/Retraction without correction_ref).

GET /v1/accounts/{account_id}/usage

Spec §12.2 — monthly account usage.

Query params:

  • from, to — RFC 3339 timestamps. Required. Half-open [from, to).
  • group_by — comma-separated list. Supported: account_id, subscription_id, product_id, meter_id, model_id, source, unit, kind, hour_start_ms, day, or any dimension key.
  • product_id, meter_id, model_id — equality filters.
  • source"raw" (force raw scan) or "rollup" (default; uses rollup segments with raw fallback for the open-period tail).

Response:

{
  "watermark_ms": 1778950800000,
  "lines": [
    {
      "product_id": "ai_gateway",
      "meter_id": "tokens.input",
      "model_id": "claude-sonnet-4",
      "quantity": "9823000",
      "count": 4127
    }
  ]
}

quantity is serialized as a string because i128 doesn't fit safely in JSON numbers past 2^53.

watermark_ms is the upper bound (exclusive) below which every hour has been rolled up — anything above is being served from raw events.

COUNT semantics differ for the rollup source: each rollup row counts as 1, not as the number of underlying events. Use source=raw for exact event counts.

GET /v1/accounts/{account_id}/usage/events

Spec §12.3 — raw audit query.

Query params:

  • from, to — RFC 3339 timestamps. Half-open.
  • meter_id, product_id — equality filters.

Returns raw event rows (no aggregation):

{
  "events": [
    { "event_id": "evt_...", "kind": "Usage", "account_id": "acc_123", ... }
  ]
}

POST /v1/query/sql

Flexible SQL subset for investigation. Not the billing path.

{ "query": "SELECT meter_id, SUM(quantity) FROM usage_events WHERE account_id = 'acc_x' AND timestamp_ms >= 1000 AND timestamp_ms < 2000 GROUP BY meter_id" }

Supported:

  • SELECT col, ..., SUM(quantity), COUNT(*) FROM (usage_events | usage_rollup_hourly)
  • WHERE — AND-only conjunctions of =, IN (...), <, <=, >, >= (range comparisons only on timestamp_ms / hour_start_ms)
  • GROUP BY col, ...

Rejected with a specific error:

  • OR in WHERE
  • SUM(anything but quantity) / COUNT(col) / COUNT(DISTINCT ...)
  • SELECT * / aliases / HAVING / ORDER BY / LIMIT
  • Multi-statement queries
  • Anything else not explicitly listed above

POST /v1/query/json

Programmatic alternative to SQL — same plan shape as the path-based endpoints but with full filter/metric flexibility:

{
  "source": "usage_events",
  "account_id": "acc_x",
  "from": "2026-05-01T00:00:00Z",
  "to": "2026-06-01T00:00:00Z",
  "group_by": ["product_id", "meter_id"],
  "filters": { "meter_id": ["tokens.input", "tokens.output"] },
  "metrics": { "quantity": "sum" }
}

GET /health

Returns OK if the server is running.

Errors

400 — invalid from/to parsing, SQL parse error, unknown table, unsupported syntax.

500 — WAL append/sync failure, flusher channel closed.

The body of error responses includes the underlying error message verbatim.

Clone this wiki locally