Skip to content

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

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.

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 }
}

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"
}
]
}

Upload reducer source code for compilation. Uses multipart/form-data.

Request:

Terminal window
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"
}

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"
}

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 the current reducer binding for an aggregate type.

Response (200):

{
"aggregateType": "order",
"reducerId": "red_abc123",
"boundAt": "2026-03-15T18:30:00.000Z"
}

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.

Upload projection source code for compilation. Uses multipart/form-data. Optionally include notification configuration.

Request:

Terminal window
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"
}

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"
}

Poll for projection state changes. Cursor-based pagination via afterSequence.

Request:

Terminal window
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"
}
]
}

Rotate the webhook secret for a projection with an isolated secret.

Response (200):

{
"projectionId": "proj_abc",
"rotatedAt": "2026-03-15T18:30:00.000Z"
}

Rotate the tenant-level webhook secret. Affects all projections using the shared secret.

Response (200):

{
"rotatedAt": "2026-03-15T18:30:00.000Z"
}

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.

Health check. Does not require authentication.

Response (200):

{
"status": "ok"
}

Validate an API key and return tenant information.

Response (200):

{
"tenantId": "tenant_abc123",
"name": "My Company",
"createdAt": "2026-01-15T10:00:00.000Z"
}