Analytics endpoint
Every session, every function call, every database query is tracked as a UsageEvent. The analytics endpoint lets you query them — either for billing reconciliation, BI dashboards, or debugging live behavior.
Endpoints
| Method | Path | Scope |
|---|---|---|
POST | /v1/:siteId/analytics/event | Written by the widget itself — you don’t call this |
GET | /v1/:siteId/analytics/events | analytics:read |
GET | /v1/:siteId/analytics/summary | analytics:read |
UsageEvent shape
type UsageEventType = | 'session_start' | 'session_end' | 'function_call' | 'db_query' | 'image_description';
interface UsageEvent { id: string; site_id: string; type: UsageEventType; duration_seconds?: number; // session_end only function_name?: string; // function_call only cost_cents?: number; // computed post-hoc metadata?: Record<string, unknown>; created_at: string;}GET /v1/:siteId/analytics/events
List raw events, paginated.
GET /v1/ab1c2d3e/analytics/events?since=2026-04-01&type=session_end&limit=100Authorization: Bearer vk_live_...Query params:
| Param | Default | Notes |
|---|---|---|
since | 30 days ago | ISO date |
until | now | ISO date |
type | all | Filter by UsageEventType |
limit | 100 | Max 1000 |
cursor | — | Opaque pagination cursor |
Response 200:
{ "success": true, "data": [ { "id": "evt_abc", "site_id": "ab1c2d3e", "type": "session_end", "duration_seconds": 124, "cost_cents": 42, "metadata": { "session_id": "sess_xyz", "function_calls": 7 }, "created_at": "2026-04-17T14:22:10.000Z" } ], "meta": { "next_cursor": "cur_xyz" }}GET /v1/:siteId/analytics/summary
Pre-aggregated metrics for dashboards.
GET /v1/ab1c2d3e/analytics/summary?period=month&bucket=dayAuthorization: Bearer vk_live_...Query params:
| Param | Values | Default |
|---|---|---|
period | day, week, month, quarter, year, custom | month |
bucket | hour, day, week | day |
since / until | ISO dates (with period=custom) | — |
Response 200:
{ "success": true, "data": { "totals": { "sessions": 1247, "minutes": 3128, "function_calls": 4821, "db_queries": 2103, "cost_cents": 18450 }, "buckets": [ { "bucket": "2026-04-01", "sessions": 38, "minutes": 94, "function_calls": 142, "db_queries": 61, "cost_cents": 561 }, ... ], "top_functions": [ { "name": "search_database", "count": 2103 }, { "name": "navigate", "count": 1234 }, { "name": "scroll_to", "count": 892 } ] }}POST /v1/:siteId/analytics/event (widget only)
The widget writes events as they happen. You don’t call this endpoint directly.
POST /v1/ab1c2d3e/analytics/eventOrigin: https://emberandoak.comContent-Type: application/jsonX-Spelo-Session: sess_xyzX-Spelo-Signature: sha256=...
{ "type": "function_call", "function_name": "navigate", "metadata": { "url": "/menu" }}Rate-limited to 600/minute per site.
Real-time streaming (beta)
For live dashboards, subscribe to events via Server-Sent Events:
GET /v1/:siteId/analytics/streamAuthorization: Bearer vk_live_...Accept: text/event-streamThe endpoint emits one SSE event per UsageEvent as it’s recorded. Useful for a live “current sessions” widget in your ops dashboard.
Cost computation
session_end.cost_centsis filled in asynchronously (~1 minute after session ends) once OpenAI’s usage webhook confirms actual token consumption.- For managed plans, this is what counts against your bundled minutes.
- For BYOK, this is informational — OpenAI bills you directly.
Retention
- Raw events: 13 months
- Aggregated summaries: indefinite
Export to your own warehouse for longer retention — see Webhooks.
Error codes
| HTTP | Code | Cause |
|---|---|---|
| 400 | invalid_params | Bad date format or unknown type |
| 401 | unauthorized | Missing / revoked API key |
| 403 | forbidden | Key lacks analytics:read scope |
| 404 | site_not_found | Unknown site_id |
See also
- Webhooks — push events to your warehouse
- Usage metering — how minutes are counted
- Plans and limits