Events Service API

The Events service provides real-time event streaming via Server-Sent Events (SSE), backed by NATS JetStream for reliable, persistent delivery. Platform services publish events to NATS JetStream, which are then streamed to authenticated clients over a long-lived HTTP connection. All events are scoped to your tenant for isolation.

Architecture

LayerTechnologyRole
PersistenceNATS JetStreamDurable message storage, at-least-once delivery, event replay
Client deliverySSE (Server-Sent Events)HTTP/1.1 long-lived stream; native browser and Node.js support
ReconnectionLast-Event-ID headerResume from a known event sequence position after disconnection

Authentication

All endpoints except /api/events/health require both of the following headers on every request:

  • X-API-Key: Your API key in the format tnz_{tenant}_{secret}
  • X-Tenant-Id: Your tenant identifier

Endpoints

MethodEndpointAuthDescription
GET/api/events/streamRequiredOpen an SSE event stream for your tenant
GET/api/events/healthPublicService health check

SSE Stream

Connect to /api/events/stream to receive a continuous stream of events for your tenant. The server keeps the HTTP connection open and pushes events as they are published to NATS JetStream. NATS JetStream guarantees at-least-once delivery and persists events so they can be replayed.

Request

GET /api/events/stream
X-API-Key: tnz_{tenant}_{secret}
X-Tenant-Id: your-tenant-id
Accept: text/event-stream

Query Parameters

ParameterTypeDescription
subjectsstringComma-separated list of NATS subject patterns to filter by (e.g. ledger.*,token.*). Omit to receive all events for your tenant.
last_event_idstringResume the stream from a specific event ID. The service will replay events published after this ID before delivering live events.

Filtering by subject

GET /api/events/stream?subjects=ledger.*,token.*
X-API-Key: tnz_{tenant}_{secret}
X-Tenant-Id: your-tenant-id

Resuming from a known position

GET /api/events/stream?last_event_id=evt_01j9x2k4m8n3p6q7r0s5t
X-API-Key: tnz_{tenant}_{secret}
X-Tenant-Id: your-tenant-id

SSE Message Format

Each SSE frame contains three fields: an id field with the unique event ID (use this as the last_event_id cursor on reconnection), an event field naming the event type using a dotted subject pattern, and a data field containing the JSON-serialised event payload.

Example: ledger transfer event

id: evt_01j9x2k4m8n3p6q7r0s5t
event: ledger.transfer
data: {
  "id": "evt_01j9x2k4m8n3p6q7r0s5t",
  "subject": "ledger.transfer",
  "tenant_id": "your-tenant-id",
  "timestamp": "2025-06-10T14:22:00Z",
  "payload": {
    "transaction_id": "txn-abc-123",
    "sender": "Alice::tenant-party",
    "receiver": "Bob::tenant-party",
    "amount": "500.00",
    "asset": "USDC",
    "status": "completed"
  }
}

Example: token mint event

id: evt_01j9x3r7q2m1n4p5s6t8u
event: token.mint
data: {
  "id": "evt_01j9x3r7q2m1n4p5s6t8u",
  "subject": "token.mint",
  "tenant_id": "your-tenant-id",
  "timestamp": "2025-06-10T14:23:10Z",
  "payload": {
    "token_id": "tok-xyz-456",
    "issuer": "Issuer::tenant-party",
    "owner": "Alice::tenant-party",
    "amount": "1000",
    "symbol": "USDC"
  }
}

Keep-alive comment

The server sends a keep-alive SSE comment periodically to prevent proxy and load-balancer timeouts. These are not events and should be ignored by your client.

: ping

Event Categories

Events are organised into categories using NATS subject prefixes. You can subscribe to an entire category with a wildcard (e.g. ledger.*) or to a specific event type (e.g. ledger.transfer).

Subject prefixCategoryExample event types
ledger.*Canton Ledgerledger.transfer.completed, ledger.party.allocated, ledger.contract.created, ledger.contract.exercised
token.*Token Servicetoken.mint, token.transfer, token.burn
wallet.*Wallet Servicewallet.created, wallet.updated
bridge.*Bridge Servicebridge.initiated, bridge.completed, bridge.failed
custody.*Custody Servicecustody.key.created, custody.signature.completed
ai.*AI Serviceai.inference.completed, ai.attestation.verified
provision.*Provision Serviceprovision.party.allocated, provision.app.deployed

Client Examples

Browser: EventSource API

The browser-native EventSource API handles reconnection automatically and sends the Last-Event-ID header on each reconnect attempt so the server can resume delivery from the last acknowledged event. Because EventSource does not support custom request headers, route the connection through a backend proxy that injects X-API-Key and X-Tenant-Id.

// Connect via a backend proxy that injects authentication headers
const es = new EventSource('/api/events/stream?subjects=ledger.*,token.*');

// Listen for specific event types using the event name
es.addEventListener('ledger.transfer.completed', (e) => {
  const event = JSON.parse(e.data);
  console.log('Transfer completed:', event.payload.transaction_id);
});

es.addEventListener('token.mint', (e) => {
  const event = JSON.parse(e.data);
  console.log('Token minted:', event.payload.token_id, event.payload.amount);
});

// Catch-all handler for any event not matched above
es.onmessage = (e) => {
  const event = JSON.parse(e.data);
  console.log(event.subject, event.payload);
};

es.onerror = (err) => {
  // EventSource will automatically attempt to reconnect,
  // sending Last-Event-ID so the server resumes from where it left off
  console.warn('SSE connection error, reconnecting...', err);
};

// Close the connection when it is no longer needed
// es.close();

Server-side: fetch with streaming response body

In Node.js or other server-side environments you can set request headers directly, so no proxy is required. Parse the raw SSE wire format from the response body stream and store the last received event ID to support manual reconnection.

async function connectEventStream(apiKey, tenantId, subjects) {
  const url = new URL('https://events.tenzro.com/api/events/stream');
  if (subjects) url.searchParams.set('subjects', subjects);

  const response = await fetch(url.toString(), {
    headers: {
      'X-API-Key':   apiKey,
      'X-Tenant-Id': tenantId,
      'Accept':      'text/event-stream',
    },
  });

  if (!response.ok || !response.body) {
    throw new Error(`Failed to connect: ${response.status}`);
  }

  const decoder = new TextDecoder();
  let buffer = '';
  let lastEventId = null;

  for await (const chunk of response.body) {
    buffer += decoder.decode(chunk, { stream: true });

    // SSE messages are separated by double newlines
    const messages = buffer.split('\n\n');
    buffer = messages.pop() ?? '';

    for (const message of messages) {
      if (!message.trim() || message.startsWith(':')) continue; // skip keep-alive

      let id = null;
      let eventType = 'message';
      let data = '';

      for (const line of message.split('\n')) {
        if (line.startsWith('id:')) {
          id = line.slice(3).trim();
        } else if (line.startsWith('event:')) {
          eventType = line.slice(6).trim();
        } else if (line.startsWith('data:')) {
          data = line.slice(5).trim();
        }
      }

      if (id) lastEventId = id;

      if (data) {
        try {
          const event = JSON.parse(data);
          console.log(`[${eventType}]`, event.payload);
        } catch {
          // non-JSON frame, ignore
        }
      }
    }
  }

  return lastEventId; // use this to resume on reconnect
}

// Usage
await connectEventStream(
  process.env.TENZRO_API_KEY,
  process.env.TENZRO_TENANT_ID,
  'ledger.*,token.*'
);

Reconnection and event replay

NATS JetStream persists events so the service can replay them on reconnection. Pass the ID of the last event you received as last_event_id in the query string, and the server will deliver all events published after that point before resuming the live stream.

The browser EventSource API handles this automatically by sending the Last-Event-ID request header on each reconnect. When reconnecting manually with fetch, store the last id field from received SSE frames and include it as the last_event_id query parameter:

GET /api/events/stream?last_event_id=evt_01j9x2k4m8n3p6q7r0s5t
X-API-Key: tnz_{tenant}_{secret}
X-Tenant-Id: your-tenant-id

Health Check

The health endpoint requires no authentication and can be used by load balancers or monitoring systems.

GET /api/events/health

HTTP/1.1 200 OK
Content-Type: application/json

{ "status": "ok" }

Error Codes

HTTP statusDescription
401X-API-Key header is missing or malformed
403API key is invalid or does not belong to the specified tenant
400X-Tenant-Id header is missing or invalid
503NATS JetStream is unavailable; retry with exponential back-off