Applies to: src/backend (active WatchTower source)
Status: Accepted
Sprint: 5
Supersedes: API Contract v1
See also:
API Contract v2 documents the current Prototype 3 HTTP surface. It keeps the v1 telemetry ingestion shape, then adds the newer APIs required by the current architecture:
POST /api/beacon.Local Base URL: http://localhost:3000
Hosted Base URL: https://watchtower-course-project-g8dv.onrender.com
All JSON endpoints send CORS headers. Ingestion remains open for SDK traffic. Dashboard read routes require the current Clerk user.
There are two separate auth concerns:
| Concern | Current API behavior |
|---|---|
| Dashboard user auth | Clerk session. Protected read routes require a current user. |
| SDK event ingestion auth | Not implemented yet. POST /api/events and POST /api/beacon remain open. |
Protected routes resolve the user in this order:
Authorization: Bearer <Clerk session JWT> verified against Clerk JWKS.X-Clerk-User-Id: <clerk_user_id> only when token verification is not
configured or WATCHTOWER_TRUST_USER_HEADER=true. This mode binds to
loopback only and is forbidden when NODE_ENV=production.If a protected route cannot resolve a user, it returns 401 Unauthorized.
The server accepts the v1 event envelope and the newer Prototype 3 fields.
{
"type": "error",
"eventName": "error",
"timestamp": "2026-06-05T18:00:01.234Z",
"sessionId": "session-123",
"userId": "user_2abc",
"deployVersion": "v1.2.0",
"appName": "shopdemo",
"environment": "production",
"sdkVersion": "sdk-unknown",
"route": "/checkout",
"data": {
"message": "Cannot read properties of undefined",
"source": "checkout.js",
"line": 42,
"col": 8
}
}
Server normalization fills missing values:
| Field | Default / behavior |
|---|---|
type |
Required for a meaningful event; defaults to custom in normalization paths. |
eventName |
Derived from type when absent. |
timestamp |
Current server time when absent. |
sessionId |
unknown-session when absent. |
userId |
Verified ingest owner, DEFAULT_INGEST_OWNER_USER_ID, or null. Local loopback prototype mode also accepts payload userId for ShopDemo. |
deployVersion |
unknown when absent. |
appName |
shopdemo when absent. |
environment |
Normalized to production, staging, development, or preview. |
sdkVersion |
sdk-unknown when absent. |
route |
/ when absent. |
data |
Empty object when absent or invalid. |
receivedAt |
Server-generated ISO timestamp. |
Ingest one event or a batch of events. This is the primary SDK endpoint.
Auth: Open. If a Clerk user is present, the server stamps events with that
user. Otherwise it uses DEFAULT_INGEST_OWNER_USER_ID when configured, then
falls back to anonymous storage.
Request body - single event:
{
"type": "custom",
"timestamp": "2026-06-05T18:00:00.000Z",
"sessionId": "session-123",
"route": "/demo",
"data": { "name": "manual-verification" }
}
Request body - batch:
{
"events": [
{
"type": "pageload",
"timestamp": "2026-06-05T18:00:00.000Z",
"sessionId": "session-123",
"route": "/checkout",
"data": { "duration": 640, "ttfb": 45, "transferSize": 51200 }
},
{
"type": "error",
"timestamp": "2026-06-05T18:00:05.000Z",
"sessionId": "session-123",
"route": "/checkout",
"data": { "message": "boom", "source": "checkout.js" }
}
]
}
Response 200 OK:
{ "accepted": 2 }
Response 500 Internal Server Error:
{ "error": "Failed to store events" }
Accepted events are persisted through Supabase when configured, returned by the dashboard’s scoped polling APIs, and included in stats for their resolved owner.
Ingest events sent with the browser Beacon API. This endpoint supports unload and session-finalization telemetry where callers do not inspect a JSON response.
Auth: Open. Ownership resolution matches POST /api/events.
Request body: same as POST /api/events.
Response 204 No Content:
No response body is returned, including for malformed or ignored payloads.
Return the most recent events for the authenticated dashboard user.
Auth: Required.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
number |
Maximum events to return. Defaults to 100; clamped between 1 and the server maximum. |
Response 200 OK:
{
"events": [
{
"type": "error",
"eventName": "error",
"timestamp": "2026-06-05T18:00:01.234Z",
"sessionId": "session-123",
"userId": "user_2abc",
"deployVersion": "v1.2.0",
"appName": "shopdemo",
"environment": "production",
"sdkVersion": "sdk-unknown",
"route": "/checkout",
"data": { "message": "boom", "source": "checkout.js" },
"receivedAt": "2026-06-05T18:00:01.300Z"
}
]
}
Response 401 Unauthorized:
{ "error": "Unauthorized" }
Return dashboard aggregates for the authenticated dashboard user.
Auth: Required.
Response 200 OK:
{
"activeUsers": 3,
"totalEvents": 142,
"totalErrors": 7,
"eventsByType": { "error": 7, "pageload": 80, "custom": 12 },
"errorsByVersion": { "v1.2.0": 5, "v1.2.1": 2 },
"latencyByRoute": {
"/checkout": {
"count": 18,
"p50": 290,
"p95": 520,
"avg": 315,
"points": [
{ "duration": 290, "ttfb": 40, "timestamp": "2026-06-05T17:55:00.000Z" }
]
}
},
"averageLatency": 315,
"recentErrors": [],
"recentActivity": [],
"analytics": {
"breakdownCounts": { "performance": 80, "errors": 7, "feedback": 4, "clicks": 21 },
"feedbackBreakdown": { "1": 0, "2": 0, "3": 1, "4": 1, "5": 2 },
"feedbackTotal": 4,
"feedbackAverage": 4.25,
"customActivityTotal": 12,
"userSeries": { "labels": ["1", "2", "3"], "values": [1, 2, 3] },
"activitySeries": { "labels": ["1", "2", "3"], "values": [5, 8, 13] }
}
}
Create or update the current application user in Supabase app_users.
Auth: Required. The server derives the owner from a verified Clerk token,
or a header in loopback-only development mode. The body clerkUserId is ignored.
Request body:
{
"clerkUserId": "user_2abc",
"email": "dev@example.com",
"displayName": "Dev User",
"timezone": "America/Los_Angeles"
}
Response 200 OK:
{
"ok": true,
"user": {
"clerkUserId": "user_2abc",
"email": "dev@example.com",
"displayName": "Dev User",
"timezone": "America/Los_Angeles"
}
}
Response 401 Unauthorized:
{ "error": "Authentication required" }
Return a filtered, paginated event inspector feed for the authenticated user.
Auth: Required.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
eventName |
string |
Case-insensitive event-name contains filter. |
user |
string |
Case-insensitive user id contains filter. |
session |
string |
Case-insensitive session id contains filter. |
environment |
string |
Exact environment filter. |
sdkVersion |
string |
Case-insensitive SDK version contains filter. |
search |
string |
Case-insensitive full-text search over type, user, session, route, and data. |
dateFrom |
ISO date | Lower timestamp bound. |
dateTo |
ISO date | Upper timestamp bound. |
limit |
number |
Page size. Defaults to 80; max 500. |
cursor |
number |
Zero-based offset cursor. Defaults to 0. |
Response 200 OK:
{
"total": 5,
"cursor": 0,
"nextCursor": 2,
"events": [
{
"id": "2026-06-05T18:00:01.234Z|session-123|error|/checkout",
"type": "error",
"eventName": "error",
"userId": "user_2abc",
"sessionId": "session-123",
"environment": "production",
"sdkVersion": "sdk-unknown",
"deployVersion": "v1.2.0",
"route": "/checkout",
"timestamp": "2026-06-05T18:00:01.234Z",
"receivedAt": "2026-06-05T18:00:01.300Z",
"ingestionLatencyMs": 66,
"data": { "message": "boom" },
"raw": {}
}
]
}
Return derived developer diagnostics for the authenticated user’s event history.
Auth: Required.
Response 200 OK:
The response is a JSON object with performance, error, and activity summaries. The exact object is derived server-side from recent events. Current e2e coverage requires Navigation Timing metrics such as:
{
"performance": {
"ttfb": { "sampleCount": 1 },
"navigationFetchStartToResponse": { "sampleCount": 1 },
"resourceCount": { "sampleCount": 1 }
}
}
Execute a saved developer query against the authenticated user’s recent events.
Auth: Required.
Request body:
{ "query": "SELECT type, route, count(*) FROM events WHERE type = 'error' GROUP BY route" }
Response 200 OK:
{
"error": null,
"durationMs": 3,
"rowCount": 1,
"columns": ["type", "route", "count"],
"rows": [
{ "type": "error", "route": "/checkout", "count": 4 }
]
}
Supported syntax:
SELECT field, count(*) FROM events [WHERE field = 'value'] [GROUP BY field] [ORDER BY field] [LIMIT n]
Evaluate feature flags for an identity payload. This route is used by the Prototype 3 dashboard.
Auth: Required; the identity payload is evaluated only for a signed-in user.
Request body:
{
"userId": "user_2abc",
"email": "dev@example.com",
"environment": "production"
}
Response 200 OK:
{
"userId": "user_2abc",
"environment": "production",
"flags": {}
}
Common errors:
| Status | Example | Meaning |
|---|---|---|
400 |
{ "error": "Invalid JSON body" } |
Malformed JSON input. |
401 |
{ "error": "Authentication required" } |
Protected route without a current user. |
413 |
{ "error": "Request body too large" } |
Body exceeds the request limit. |
500 |
{ "error": "Failed to fetch events" } |
Server/store failure. |
| Version | Date | Notes |
|---|---|---|
| v2 | 2026-06-05 | Current Prototype 3 API contract using Clerk-scoped dashboard reads, Supabase-backed event storage, Beacon API ingestion, developer routes, and Render deployment. |
| v1 | 2026-05-16 | Initial prototype contract for open in-memory event ingestion and dashboard stats. |