Skip to main content
The Togul SSE stream pushes flag and rule change events to connected clients in real time. You can use it to keep a local flag cache up to date without polling the management API.

Endpoint

GET /api/v1/stream
The response uses Content-Type: text/event-stream and stays open until you close the connection.

Authentication

Two authentication methods are supported:
MethodScopeEvents received
Bearer token (JWT)Organization-scopedAll flag and rule events across the organization
API key with sdk or stream scopeEnvironment-scopedEvents for the API key’s specific environment
Pass the API key using the X-API-Key header.

Event types

TypeTrigger
flag.createdA new flag was created
flag.updatedA flag’s name, state, or default value changed
flag.deletedA flag was deleted
rule.createdA new rule was added to a flag
rule.updatedA rule was modified
rule.deletedA rule was removed

Event payload

Each event is a JSON object sent as the data field of an SSE message:
data: {"type":"flag.updated","flag_key":"new-dashboard","environment_id":"env-uuid-here","version":0,"timestamp":1710432000}
FieldTypeDescription
typestringThe event type (e.g. flag.updated)
flag_keystringThe key of the affected flag
environment_idstringUUID of the affected environment
versionintegerMonotonically increasing version counter
timestampintegerUnix timestamp of the event

Heartbeats

The server sends a comment line every 30 seconds to keep the connection alive through proxies and load balancers:
: heartbeat
SSE clients ignore comment lines by default, so no special handling is needed.

Propagation

Events are published through an outbox-driven propagation loop. This means secondary regions consume changes asynchronously — there may be a small delay between a change in the primary region and its appearance in a secondary region’s stream.

Connecting with JavaScript

The browser’s built-in EventSource API handles SSE connections, including automatic reconnection.
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
  headers: {
    'X-API-Key': 'tgl_sdk_abc123xyz...'
  }
});

stream.onmessage = (event) => {
  const payload = JSON.parse(event.data);
  console.log('Received event:', payload.type, payload.flag_key);
};

stream.onerror = (err) => {
  console.error('Stream error, will reconnect automatically:', err);
};
EventSource in browsers does not support custom headers. For browser use, pass the API key as a query parameter if your server supports it, or use a server-side proxy that injects the header.

Connecting with Node.js

Use the eventsource npm package or the native fetch API with a readable stream.
import EventSource from 'eventsource';

const stream = new EventSource('http://localhost:8080/api/v1/stream', {
  headers: {
    'X-API-Key': 'tgl_sdk_abc123xyz...'
  }
});

stream.addEventListener('message', (event) => {
  const payload = JSON.parse(event.data);
  handleFlagEvent(payload);
});

stream.addEventListener('error', () => {
  // EventSource reconnects automatically after an error.
  // Log or alert if needed.
  console.warn('SSE connection lost, reconnecting...');
});

Handling reconnections

EventSource reconnects automatically after a dropped connection. The browser uses the Last-Event-ID header to resume from the last received event if the server supports it. For more control, implement manual reconnection with exponential backoff:
let retryDelay = 1000;

function connect() {
  const stream = new EventSource('http://localhost:8080/api/v1/stream', {
    headers: { 'X-API-Key': 'tgl_sdk_abc123xyz...' }
  });

  stream.onmessage = (event) => {
    retryDelay = 1000; // Reset on success
    handleFlagEvent(JSON.parse(event.data));
  };

  stream.onerror = () => {
    stream.close();
    setTimeout(() => {
      retryDelay = Math.min(retryDelay * 2, 30000);
      connect();
    }, retryDelay);
  };
}

connect();

Practical example: invalidating a local flag cache

A common pattern is to hold an in-memory cache of flag values and invalidate entries when the stream signals a change.
const flagCache = new Map();

async function getFlag(flagKey, context) {
  if (flagCache.has(flagKey)) {
    return flagCache.get(flagKey);
  }

  const response = await fetch('http://localhost:8080/api/v1/evaluate', {
    method: 'POST',
    headers: {
      'X-API-Key': 'tgl_sdk_abc123xyz...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      flag_key: flagKey,
      environment_key: 'production',
      context
    })
  });

  const result = await response.json();
  flagCache.set(flagKey, result.value);
  return result.value;
}

// Subscribe to the stream and invalidate cache on flag changes
const stream = new EventSource('http://localhost:8080/api/v1/stream', {
  headers: { 'X-API-Key': 'tgl_sdk_abc123xyz...' }
});

stream.onmessage = (event) => {
  const payload = JSON.parse(event.data);

  if (
    payload.type === 'flag.updated' ||
    payload.type === 'flag.deleted' ||
    payload.type === 'rule.updated' ||
    payload.type === 'rule.deleted' ||
    payload.type === 'rule.created'
  ) {
    // Invalidate the cached value so the next call re-evaluates
    flagCache.delete(payload.flag_key);
    console.log(`Cache invalidated for flag: ${payload.flag_key}`);
  }
};

Build docs developers (and LLMs) love