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

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):

{
"id": "red_abc123",
"status": "compiled",
"kind": "reducer",
"discovered": [
{
"className": "MyReducers.OrderReducer",
"stateTypeName": "MyReducers.OrderState",
"kind": "reducer",
"aggregateType": "order"
}
],
"createdAt": "2026-03-15T18:30:00.000Z",
"compiledAt": "2026-03-15T18:30:05.000Z"
}

The discovered array lists all IReducer<T> implementations found in the compiled DLL. For multi-reducer DLLs, multiple entries appear with their resolved aggregateType. If compilation failed, the response includes compilationError instead of discovered.

Bind a reducer to an aggregate type.

Request:

{
"reducerId": "red_abc123",
"className": "MyReducers.OrderReducer"
}

className is optional. When omitted, the platform auto-resolves it from the discovered implementations by matching the aggregate type. In most cases, manual binding is not needed — deploying a reducer DLL auto-binds all discovered reducers to their aggregate types.

Response (200):

{
"aggregateType": "order",
"reducerId": "red_abc123",
"boundAt": "2026-03-15T18:30:00.000Z",
"message": "Reducer bound. Aggregate state catchup in progress."
}

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 'config={"notifications":{"webhook":{"url":"https://your-api.com/hooks/hapnd"}}}'

The config field accepts either {"notifications": {...}} (wrapped) or the notification config directly at the top level. Both formats work.

Response (202):

{
"success": true,
"projectionId": "proj_abc123",
"status": "compiling",
"message": "Projection uploaded successfully and queued for compilation",
"notifications": {
"webhook": {
"enabled": true,
"secret": "whsec_K7xN2bQ9mP4xR1sT8vW5yZ2a...",
"isolated": false
},
"sse": false
}
}

The notifications field is only present when notification config was provided in the upload. The secret is the active webhook secret — store this securely. The isolated field indicates whether this is a projection-specific secret or the shared tenant secret.

Check projection compilation status.

Response (200):

{
"id": "proj_abc123",
"status": "compiled",
"kind": "projection",
"discovered": [
{
"className": "MyProjections.SalesProjection",
"stateTypeName": "MyProjections.SalesStats",
"kind": "projection"
}
],
"createdAt": "2026-03-15T18:30:00.000Z",
"compiledAt": "2026-03-15T18:30:05.000Z"
}

Projection discovered entries don’t include aggregateType — projections define their own state key via ResolveKey.

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 an isolated projection. Only works for projections uploaded with isolateSecret: true.

Response (200):

{
"projectionId": "proj_abc123",
"webhook": {
"secret": "whsec_newSecret...",
"isolated": true
}
}

Returns 400 if the projection uses the shared tenant secret — use POST /webhooks/rotate instead.

Rotate the tenant-level webhook secret. Affects all projections that use the shared secret (those without isolateSecret: true).

Response (200):

{
"webhook": {
"secret": "whsec_newSecret..."
}
}

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