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
| Layer | Technology | Role |
|---|---|---|
| Persistence | NATS JetStream | Durable message storage, at-least-once delivery, event replay |
| Client delivery | SSE (Server-Sent Events) | HTTP/1.1 long-lived stream; native browser and Node.js support |
| Reconnection | Last-Event-ID header | Resume 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 formattnz_{tenant}_{secret}X-Tenant-Id: Your tenant identifier
Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/events/stream | Required | Open an SSE event stream for your tenant |
GET | /api/events/health | Public | Service 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-streamQuery Parameters
| Parameter | Type | Description |
|---|---|---|
subjects | string | Comma-separated list of NATS subject patterns to filter by (e.g. ledger.*,token.*). Omit to receive all events for your tenant. |
last_event_id | string | Resume 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-idResuming from a known position
GET /api/events/stream?last_event_id=evt_01j9x2k4m8n3p6q7r0s5t
X-API-Key: tnz_{tenant}_{secret}
X-Tenant-Id: your-tenant-idSSE 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.
: pingEvent 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 prefix | Category | Example event types |
|---|---|---|
ledger.* | Canton Ledger | ledger.transfer.completed, ledger.party.allocated, ledger.contract.created, ledger.contract.exercised |
token.* | Token Service | token.mint, token.transfer, token.burn |
wallet.* | Wallet Service | wallet.created, wallet.updated |
bridge.* | Bridge Service | bridge.initiated, bridge.completed, bridge.failed |
custody.* | Custody Service | custody.key.created, custody.signature.completed |
ai.* | AI Service | ai.inference.completed, ai.attestation.verified |
provision.* | Provision Service | provision.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-idHealth 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 status | Description |
|---|---|
401 | X-API-Key header is missing or malformed |
403 | API key is invalid or does not belong to the specified tenant |
400 | X-Tenant-Id header is missing or invalid |
503 | NATS JetStream is unavailable; retry with exponential back-off |