API Endpoints
All endpoints require an X-API-Key header unless otherwise noted. Requests with a JSON body must include Content-Type: application/json.
Base URL: https://hapnd-api.lightestnight.workers.dev
Event Operations
Section titled “Event Operations”POST /events
Section titled “POST /events”Append a single event to an aggregate.
Request:
{ "aggregateId": "order_123", "aggregateType": "order", "eventType": "ItemAdded", "data": { "item": "Widget", "price": 25.00 }, "expectedVersion": 5, "correlationId": "req_abc123", "causationId": "evt_xyz789", "metadata": { "userId": "user_456" }}Only aggregateId, eventType, and data are required. All other fields are optional. If aggregateType is omitted, it’s inferred from the ID.
Response (201):
{ "eventId": "evt_abc123", "aggregateId": "order_123", "aggregateType": "order", "version": 6, "timestamp": "2026-03-15T18:30:00.000Z", "state": { "total": 50.00, "items": ["Widget", "Gadget"], "itemCount": 2 }}The state field is present only if a reducer is bound to the aggregate type.
POST /events/batch
Section titled “POST /events/batch”Append multiple events atomically to a single aggregate. All events succeed or none do.
Request:
{ "aggregateId": "order_123", "aggregateType": "order", "events": [ { "eventType": "ItemAdded", "data": { "item": "Widget", "price": 25.00 } }, { "eventType": "ItemAdded", "data": { "item": "Gadget", "price": 15.00 } } ], "expectedVersion": 5, "correlationId": "req_abc123", "causationId": "evt_xyz789", "metadata": { "userId": "user_456" }}Response (201):
{ "aggregateId": "order_123", "aggregateType": "order", "startVersion": 6, "endVersion": 7, "eventIds": ["evt_abc123", "evt_def456"], "timestamp": "2026-03-15T18:30:00.000Z", "state": { "total": 65.00, "items": ["Widget", "Gadget"], "itemCount": 2 }}GET /events/{aggregateId}
Section titled “GET /events/{aggregateId}”Retrieve events for an aggregate.
Response (200):
{ "events": [ { "eventId": "evt_abc123", "aggregateId": "order_123", "eventType": "OrderPlaced", "data": { "customerId": "cust_456" }, "version": 1, "timestamp": "2026-03-15T18:00:00.000Z" } ]}Reducer Operations
Section titled “Reducer Operations”POST /reducers/upload
Section titled “POST /reducers/upload”Upload reducer source code for compilation. Uses multipart/form-data.
Request:
curl -X POST https://hapnd-api.lightestnight.workers.dev/reducers/upload \ -H "X-API-Key: your_key" \ -F "file=@reducer.zip"The zip must contain .cs source files with at least one IReducer<T> implementation.
Response (202):
{ "reducerId": "red_abc123", "status": "pending_compilation"}GET /reducers/{id}/status
Section titled “GET /reducers/{id}/status”Check compilation status and discovered implementations.
Response (200):
{ "reducerId": "red_abc123", "status": "compiled", "kind": "reducer", "compiledClassName": "MyReducers.OrderReducer", "compiledStateType": "MyReducers.OrderState", "createdAt": "2026-03-15T18:30:00.000Z", "compiledAt": "2026-03-15T18:30:05.000Z"}PUT /aggregate-types/{type}
Section titled “PUT /aggregate-types/{type}”Bind a reducer to an aggregate type.
Request:
{ "reducerId": "red_abc123"}Response (200):
{ "aggregateType": "order", "reducerId": "red_abc123", "boundAt": "2026-03-15T18:30:00.000Z"}GET /aggregate-types/{type}
Section titled “GET /aggregate-types/{type}”Get the current reducer binding for an aggregate type.
Response (200):
{ "aggregateType": "order", "reducerId": "red_abc123", "boundAt": "2026-03-15T18:30:00.000Z"}GET /aggregate-types/{type}/{id}/state
Section titled “GET /aggregate-types/{type}/{id}/state”Query the current computed state of an aggregate.
Response (200):
{ "aggregateId": "order_123", "aggregateType": "order", "version": 6, "state": { "total": 50.00, "items": ["Widget", "Gadget"], "itemCount": 2 }, "lastModified": "2026-03-15T18:30:00.000Z"}Returns 404 with NO_REDUCER_BOUND if no reducer is bound, or NOT_FOUND if the aggregate doesn’t exist.
Projection Operations
Section titled “Projection Operations”POST /projections/upload
Section titled “POST /projections/upload”Upload projection source code for compilation. Uses multipart/form-data. Optionally include notification configuration.
Request:
curl -X POST https://hapnd-api.lightestnight.workers.dev/projections/upload \ -H "X-API-Key: your_key" \ -F "file=@projection.zip" \ -F 'notificationConfig={"webhook":{"url":"https://your-api.com/hooks/hapnd"}}'Response (202):
{ "projectionId": "proj_abc123", "status": "pending_compilation"}GET /projections/{id}/status
Section titled “GET /projections/{id}/status”Check projection compilation status.
Response (200):
{ "projectionId": "proj_abc123", "status": "compiled", "kind": "projection", "compiledClassName": "MyProjections.SalesProjection", "compiledStateType": "MyProjections.SalesStats", "createdAt": "2026-03-15T18:30:00.000Z", "compiledAt": "2026-03-15T18:30:05.000Z"}GET /projections/{id}/notifications
Section titled “GET /projections/{id}/notifications”Poll for projection state changes. Cursor-based pagination via afterSequence.
Request:
curl "https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications?afterSequence=42" \ -H "X-API-Key: your_key"Response (200):
{ "notifications": [ { "sequence": 43, "projectionId": "proj_abc", "aggregateId": "order_123", "aggregateType": "order", "state": { "totalRevenue": 1500.00, "orderCount": 15 }, "timestamp": "2026-03-15T18:30:00.000Z" } ]}POST /projections/{id}/webhook/rotate
Section titled “POST /projections/{id}/webhook/rotate”Rotate the webhook secret for a projection with an isolated secret.
Response (200):
{ "projectionId": "proj_abc", "rotatedAt": "2026-03-15T18:30:00.000Z"}Webhook Operations
Section titled “Webhook Operations”POST /webhooks/rotate
Section titled “POST /webhooks/rotate”Rotate the tenant-level webhook secret. Affects all projections using the shared secret.
Response (200):
{ "rotatedAt": "2026-03-15T18:30:00.000Z"}Streaming
Section titled “Streaming”GET /projections/{id}/stream
Section titled “GET /projections/{id}/stream”WebSocket upgrade for real-time projection state changes. Authenticate via X-API-Key header on the upgrade request.
Server messages:
State change:
{ "type": "state_changed", "projectionId": "proj_abc", "aggregateId": "order_123", "aggregateType": "order", "version": 6, "state": { "totalRevenue": 1500.00 }, "sequence": 43, "timestamp": "2026-03-15T18:30:00.000Z"}Keepalive:
{ "type": "ping"}Client messages:
Pong response:
{ "type": "pong"}Resume from a specific sequence by connecting with ?afterSequence=42 on the URL.
System
Section titled “System”GET /health
Section titled “GET /health”Health check. Does not require authentication.
Response (200):
{ "status": "ok"}GET /auth/whoami
Section titled “GET /auth/whoami”Validate an API key and return tenant information.
Response (200):
{ "tenantId": "tenant_abc123", "name": "My Company", "createdAt": "2026-01-15T10:00:00.000Z"}