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.0 },
"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.0, "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.0 } },
{ "eventType": "ItemAdded", "data": { "item": "Gadget", "price": 15.0 } }
],
"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.0, "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: sk_live_your_api_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):

{
"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.0, "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: sk_live_your_api_key" \
-F "file=@projection.zip" \
-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,
"uploadId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"projectionId": null,
"status": "compiling",
"message": "Projection uploaded successfully and queued for compilation",
"notifications": {
"webhook": {
"enabled": true,
"secret": "whsec_K7xN2bQ9mP4xR1sT8vW5yZ2a...",
"isolated": false
},
"sse": false
}
}

The uploadId is a plain UUID used to track this specific deploy. The projectionId is null at upload time; the stable ID is assigned during compilation. Poll GET /projections/{uploadId}/status until status is "compiled" or type is "runtime" to retrieve the stable projectionId.

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

{
"type": "compilation",
"uploadId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"projectionId": "proj_stable_xyz789",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"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"
}

The type field is "compilation" while the upload is being processed, and "runtime" once the projection has been activated. The projectionId field is null until compilation completes; once populated, use it for all subsequent API calls (state queries, notifications, webhooks). The id field is deprecated; use uploadId instead.

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

Query the current state of a projection by its state key. The state key is the value returned by the projection’s ResolveKey method.

Authentication: API key via X-API-Key header

Path Parameters:

  • id - The stable projection ID returned after compilation (e.g. proj_abc123). This is the projectionId from the status endpoint, not the uploadId from the upload response.
  • key - The state key from ResolveKey (e.g. customer-123). URL-encode if the key contains special characters.

200 OK, Projection is active and state exists for this key:

{
"projectionId": "proj_abc123",
"stateKey": "customer-123",
"state": {
"totalOrders": 5,
"totalSpend": 249.99
},
"updatedAt": "2026-04-20T14:30:00.000Z"
}

202 Accepted, Projection exists but is still catching up on historical events:

{
"projectionId": "proj_abc123",
"status": "catching_up"
}

404 Not Found, Projection does not exist, or no state exists for the given key:

{
"error": "NOT_FOUND"
}

During catchup, the projection’s state is incomplete. The 202 response prevents consumers from reading partial state. Query again once catchup completes.

The state field contains the full projected state object, the same shape your projection’s Apply method returns.

State keys are defined by your projection’s ResolveKey method. For cross-aggregate projections, the key may differ from the source aggregate ID.

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

Path Parameters:

  • id - The stable projection ID (projectionId from the status endpoint)

Request:

Terminal window
curl "https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications?afterSequence=42" \
-H "X-API-Key: sk_live_your_api_key"

Response (200):

{
"notifications": [
{
"sequence": 43,
"projectionId": "proj_abc",
"aggregateId": "order_123",
"aggregateType": "order",
"state": { "totalRevenue": 1500.0, "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.0 },
"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"
}