> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Contracts

> Enterprise integration surfaces for live memory events and service clients

This page covers enterprise integration surfaces that are intentionally available to deployed clients but are not part of the generated public OpenAPI export yet.

<Note>
  The contracts below are stable for documented required fields during the v0.2 line. Nebula may add optional fields, event types, or workflow metadata without a breaking-version bump. Treat unknown fields and unknown event types as forward-compatible extensions.
</Note>

## Live memory events

Nebula exposes live memory events over:

```txt theme={null}
GET /v1/ws/memory-events?ticket=<single-use-ticket>
```

This is a WebSocket upgrade endpoint. Connect with `ws://` or `wss://`; a plain HTTP GET will not stream events.

Mint a ticket with:

```txt theme={null}
POST /v1/users/ws-ticket
```

The ticket is single-use and short-lived. The WebSocket endpoint is not shown in OpenAPI because FastAPI excludes WebSocket routes from the HTTP schema.

### Client frames

Subscribe after the socket opens:

```json theme={null}
{
  "action": "subscribe",
  "collections": ["COLLECTION_UUID"],
  "event_types": ["ingestion_progress", "memory_unconfident"],
  "last_ack_seq": 123
}
```

| Field          | Type       | Description                                                                                      |
| -------------- | ---------- | ------------------------------------------------------------------------------------------------ |
| `collections`  | `string[]` | Collection UUIDs to receive. An empty list means every collection readable by the ticket holder. |
| `event_types`  | `string[]` | Event type filter. An empty list receives every event type.                                      |
| `last_ack_seq` | `integer`  | Optional replay cursor from the last processed event.                                            |

Ack processed events:

```json theme={null}
{ "action": "ack", "seq": 124 }
```

Send a heartbeat when the client wants an application-level keepalive:

```json theme={null}
{ "action": "heartbeat" }
```

Unsubscribe without closing the socket:

```json theme={null}
{ "action": "unsubscribe" }
```

### Server frames

Successful subscribe:

```json theme={null}
{
  "action": "subscribed",
  "collections": ["COLLECTION_UUID"],
  "event_types": ["ingestion_progress"]
}
```

Event frame:

```json theme={null}
{
  "action": "event",
  "seq": 124,
  "event_id": "EVENT_UUID",
  "type": "ingestion_progress",
  "collection_id": "COLLECTION_UUID",
  "payload": {},
  "ts": "2026-05-27T18:30:00Z",
  "signature": {
    "scheme": "eddsa-detached",
    "algorithm": "Ed25519",
    "canonicalization": "json-sort-keys-v1",
    "key_id": "SIGNING_KEY_ID",
    "signed_fields": ["action", "collection_id", "event_id", "payload", "seq", "ts", "type"],
    "encoding": "base64url",
    "value": "BASE64URL_SIGNATURE"
  }
}
```

Error frames use `action: "error"` or `action: "subscribe_error"` with a `reason` string. Heartbeats receive:

```json theme={null}
{ "action": "heartbeat_ack" }
```

### Event types

Current published event types:

| Type                 | Payload                                                                                                                                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ingestion_progress` | `{ "engram_id": string, "stage": string, "user_stage": "reading" \| "structuring" \| "indexing" \| "complete" \| "failed", "current": integer, "total": integer, "message": string \| null }`                                                                                    |
| `memory_unconfident` | `{ "query": string, "reason": "no_results" \| "low_activation" \| "low_fact_confidence", "entity_count": integer, "fact_count": integer, "episode_count": integer, "source_count": integer, "avg_activation": number, "max_activation": number, "avg_fact_confidence": number }` |

A machine-readable schema is available at `/enterprise/memory-events.schema.json` in the docs bundle.

### Delivery semantics

The Postgres backend appends events to `public.memory_events`, emits `LISTEN/NOTIFY`, and replays missed events from the durable event log using `last_ack_seq`. Delivery is **at least once** within the retention window. Clients should ack after processing and deduplicate by `seq` on the WebSocket stream.

If event signing is configured, event frames include a `signature` object. Ed25519 detached signatures are the preferred stream-authenticity format; AWS KMS signing is available when private key material should remain outside the API process. AWS KMS signatures use P-256 ECDSA and encode the raw fixed-width `r || s` signature bytes as base64url.

Verification is offline and in-process. Fetch the public keyset from `GET /v1/memory-events/signing-keys`, pin the expected key material and `kid`, then verify each detached signature against the canonical JSON bytes of the same frame with `signature` removed, serialized with sorted keys and compact separators. The signed bytes include `seq` and `ts`, so consumers can detect tampering with ordering fields. To reject replay or reorder of otherwise valid old events, consumers should maintain a monotonic high-water `seq` per stream and reject events at or below the last processed sequence; timestamp policy can be applied after signature verification. Control frames are not signed.

Memory events are no longer published through the legacy SQS/API Gateway WebSocket path. Postgres is the single event backend for enterprise deployments.

### Event injection

There is no public endpoint for posting arbitrary memory events into Nebula. Smoke tests should use synthetic JSONL fixtures or trigger normal memory ingestion and subscribe to `ingestion_progress`.

Nebula has an environment-gated test-seed router for internal connector E2E tests. It is not a production ingestion or replay API.
