LatteStream®

Quick Start

Getting Started

Building Custom SDKs

SDKs & Libraries

JavaScript/TypeScript

Node.js / Bun / Deno

Python

In Development

Go

In Development

PHP

In Development

API Reference

WebSocket API

REST API

Webhooks

Authentication

Webhooks

Server-side event notifications for real-time applications

Webhooks allow your server to receive notifications about events happening in your LatteStream channels. This enables you to track user activity, persist messages, trigger business logic, and integrate with other systems.

Overview

LatteStream webhooks deliver HTTP POST requests to your server when specific events occur:

  • Members joining/leaving presence channels
  • Channels becoming occupied or vacated
  • Clients sending custom events on private/presence channels
  • Subscription count changes

Benefits:

  • Track user presence and activity
  • Persist chat messages to database
  • Trigger notifications (email, push, SMS)
  • Integrate with analytics platforms
  • Implement custom business logic

Webhook Events

Member Events (Presence Channels Only)

member_added

Triggered when a user joins a presence channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "member_added", "channel": "presence-room-1", "user_id": "user123" } ] }

Use Cases:

  • Send welcome message
  • Log user join time
  • Update online user count
  • Trigger presence analytics

member_removed

Triggered when a user leaves a presence channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "member_removed", "channel": "presence-room-1", "user_id": "user123" } ] }

Use Cases:

  • Log user session duration
  • Update online user count
  • Clean up user-specific data
  • Trigger offline notifications

Channel Events

channel_occupied

Triggered when the first subscriber joins a previously empty channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "channel_occupied", "channel": "chat-room-1" } ] }

Use Cases:

  • Initialize channel state
  • Load message history
  • Start recording session
  • Trigger analytics event

channel_vacated

Triggered when the last subscriber leaves a channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "channel_vacated", "channel": "chat-room-1" } ] }

Use Cases:

  • Clean up channel state
  • Stop recording session
  • Archive channel data
  • Trigger cleanup tasks

subscription_count

Triggered when the subscription count changes for a channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "subscription_count", "channel": "chat-room-1", "subscription_count": 42 } ] }

Use Cases:

  • Track channel popularity
  • Scale resources based on usage
  • Display live viewer count
  • Analytics and reporting

Client Events

client_event

Triggered when a client sends a custom event on a private or presence channel.

Payload:

{ "time_ms": 1674123456789, "events": [ { "name": "client_event", "channel": "private-chat", "event": "client-message", "data": "{\"text\":\"Hello, world!\",\"user\":\"Alice\"}", "socket_id": "123.456", "user_id": "user123" } ] }

Use Cases:

  • Persist chat messages
  • Content moderation
  • Trigger notifications to offline users
  • Real-time analytics

Note: data is a JSON string that needs to be parsed.

Webhook Configuration

Environment Variables

Configure webhooks using environment variables in your LatteStream deployment:

# Enable webhooks LATTESTREAM_WEBHOOK_ENABLED=true # Your webhook endpoint URL LATTESTREAM_WEBHOOK_URL=https://your-app.com/webhooks/lattestream # Secret key for signature verification LATTESTREAM_WEBHOOK_SECRET=your_webhook_secret_key # Comma-separated list of events to send LATTESTREAM_WEBHOOK_EVENTS=member_added,member_removed,client_event # Batching configuration LATTESTREAM_WEBHOOK_BATCH_TIMEOUT_MS=1000 LATTESTREAM_WEBHOOK_MAX_BATCH_SIZE=10 # Retry configuration LATTESTREAM_WEBHOOK_RETRY_ATTEMPTS=3

Event Filtering

Subscribe only to events you need by setting LATTESTREAM_WEBHOOK_EVENTS:

# All member events only LATTESTREAM_WEBHOOK_EVENTS=member_added,member_removed # All channel events only LATTESTREAM_WEBHOOK_EVENTS=channel_occupied,channel_vacated,subscription_count # Client events only LATTESTREAM_WEBHOOK_EVENTS=client_event # All events (not recommended - high volume) LATTESTREAM_WEBHOOK_EVENTS=member_added,member_removed,channel_occupied,channel_vacated,subscription_count,client_event

Batching

Webhooks are batched to reduce HTTP overhead and improve performance:

  • Batch Timeout: Events are held for up to WEBHOOK_BATCH_TIMEOUT_MS (default: 1000ms)
  • Batch Size: Maximum WEBHOOK_MAX_BATCH_SIZE events per batch (default: 10)
  • Immediate Send: If batch reaches max size, sends immediately (doesn't wait for timeout)

Example batch:

{ "time_ms": 1674123456789, "events": [ { "name": "member_added", "channel": "presence-room-1", "user_id": "user123" }, { "name": "client_event", "channel": "private-chat", "event": "client-message", "data": "{\"text\":\"Hello\"}", "socket_id": "123.456" }, { "name": "member_removed", "channel": "presence-room-1", "user_id": "user456" } ] }

Security

HMAC Signature Verification

All webhooks include an HMAC-SHA256 signature for verification:

Header: X-LatteStream-Signature

Algorithm: HMAC-SHA256

Input: Raw request body (JSON string)

Secret: Your WEBHOOK_SECRET

Verification Examples

Node.js (Express):

const crypto = require('crypto'); const express = require('express'); const app = express(); app.post('/webhooks/lattestream', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-lattestream-signature']; const body = req.body; // Raw buffer // Compute HMAC signature const expectedSignature = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(body) .digest('hex'); // Verify signature if (signature !== expectedSignature) { console.error('Invalid webhook signature'); return res.status(401).send('Unauthorized'); } // Parse and process webhook const webhook = JSON.parse(body.toString()); processWebhook(webhook); res.status(200).send('OK'); });

Python (Flask):

import hmac import hashlib import json from flask import Flask, request app = Flask(__name__) WEBHOOK_SECRET = 'your_webhook_secret' @app.route('/webhooks/lattestream', methods=['POST']) def webhook(): signature = request.headers.get('X-LatteStream-Signature') body = request.get_data() # Compute HMAC signature expected_signature = hmac.new( WEBHOOK_SECRET.encode('utf-8'), body, hashlib.sha256 ).hexdigest() # Verify signature if signature != expected_signature: return 'Unauthorized', 401 # Parse and process webhook webhook = json.loads(body) process_webhook(webhook) return 'OK', 200

Go:

package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" ) const webhookSecret = "your_webhook_secret" func webhookHandler(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("X-LatteStream-Signature") body, _ := io.ReadAll(r.Body) // Compute HMAC signature mac := hmac.New(sha256.New, []byte(webhookSecret)) mac.Write(body) expectedSignature := hex.EncodeToString(mac.Sum(nil)) // Verify signature if signature != expectedSignature { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse and process webhook var webhook map[string]interface{} json.Unmarshal(body, &webhook) processWebhook(webhook) w.WriteHeader(http.StatusOK) }

PHP:

<?php $webhookSecret = 'your_webhook_secret'; $signature = $_SERVER['HTTP_X_LATTESTREAM_SIGNATURE']; $body = file_get_contents('php://input'); // Compute HMAC signature $expectedSignature = hash_hmac('sha256', $body, $webhookSecret); // Verify signature if ($signature !== $expectedSignature) { http_response_code(401); echo 'Unauthorized'; exit; } // Parse and process webhook $webhook = json_decode($body, true); processWebhook($webhook); http_response_code(200); echo 'OK';

Security Best Practices

DO:

  • Always verify HMAC signature
  • Use HTTPS for webhook endpoint
  • Use environment variables for secrets
  • Return 200 OK quickly (process async if needed)
  • Implement idempotency (handle duplicate events)
  • Rate limit webhook endpoint
  • Log webhook events for debugging

DON'T:

  • Skip signature verification
  • Use HTTP (unencrypted)
  • Hardcode webhook secret
  • Block the webhook response (timeout risk)
  • Trust webhook data without validation
  • Expose webhook endpoint publicly without auth

Implementation Examples

Persist Chat Messages

const express = require('express'); const { createClient } = require('@supabase/supabase-js'); const app = express(); const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY); app.post('/webhooks/lattestream', express.raw({ type: 'application/json' }), async (req, res) => { // Verify signature (see Security section) if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } const webhook = JSON.parse(req.body.toString()); for (const event of webhook.events) { if (event.name === 'client_event' && event.event === 'client-message') { const data = JSON.parse(event.data); // Save message to database await supabase.from('messages').insert({ channel: event.channel, user_id: event.user_id, message: data.text, created_at: new Date(webhook.time_ms) }); } } res.status(200).send('OK'); });

Track User Presence

app.post('/webhooks/lattestream', async (req, res) => { if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } const webhook = JSON.parse(req.body.toString()); for (const event of webhook.events) { if (event.name === 'member_added') { // User joined presence channel await redis.sadd(`online:${event.channel}`, event.user_id); await redis.hset(`presence:${event.user_id}`, 'joined_at', webhook.time_ms); } if (event.name === 'member_removed') { // User left presence channel await redis.srem(`online:${event.channel}`, event.user_id); const joinedAt = await redis.hget(`presence:${event.user_id}`, 'joined_at'); const duration = webhook.time_ms - parseInt(joinedAt); // Log session duration console.log(`User ${event.user_id} was online for ${duration}ms`); } } res.status(200).send('OK'); });

Send Push Notifications

const { Expo } = require('expo-server-sdk'); const expo = new Expo(); app.post('/webhooks/lattestream', async (req, res) => { if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } const webhook = JSON.parse(req.body.toString()); for (const event of webhook.events) { if (event.name === 'client_event' && event.event === 'client-message') { const data = JSON.parse(event.data); // Get offline users in channel const offlineUsers = await getOfflineUsersInChannel(event.channel); // Send push notifications const messages = offlineUsers.map(user => ({ to: user.pushToken, sound: 'default', title: `New message in ${event.channel}`, body: data.text, data: { channel: event.channel } })); await expo.sendPushNotificationsAsync(messages); } } res.status(200).send('OK'); });

Content Moderation

const Filter = require('bad-words'); const filter = new Filter(); app.post('/webhooks/lattestream', async (req, res) => { if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } const webhook = JSON.parse(req.body.toString()); for (const event of webhook.events) { if (event.name === 'client_event' && event.event === 'client-message') { const data = JSON.parse(event.data); // Check for inappropriate content if (filter.isProfane(data.text)) { // Flag message for review await flagMessage({ channel: event.channel, user_id: event.user_id, message: data.text, reason: 'profanity' }); // Optionally: kick user from channel await kickUserFromChannel(event.user_id, event.channel); } } } res.status(200).send('OK'); });

Retry Logic

If your webhook endpoint returns a non-200 status code or times out, LatteStream will retry:

Retry Configuration:

  • Attempts: Up to WEBHOOK_RETRY_ATTEMPTS (default: 3)
  • Backoff: Exponential backoff between retries
  • Timeout: 10 seconds per request

Retry Schedule:

  1. Immediate delivery attempt
  2. Retry after 1 second (if failed)
  3. Retry after 2 seconds (if failed)
  4. Retry after 4 seconds (if failed)
  5. Give up (if still failing)

Idempotency Recommendation:

Since webhooks may be retried, implement idempotency to handle duplicate events:

const processedEvents = new Set(); app.post('/webhooks/lattestream', async (req, res) => { const webhook = JSON.parse(req.body.toString()); for (const event of webhook.events) { // Generate unique event ID const eventId = `${event.name}:${event.channel}:${event.user_id}:${webhook.time_ms}`; // Skip if already processed if (processedEvents.has(eventId)) { continue; } // Process event await processEvent(event); // Mark as processed processedEvents.add(eventId); } res.status(200).send('OK'); });

Testing Webhooks

Local Testing with ngrok

1. Install ngrok:

npm install -g ngrok

2. Start your local server:

node server.js # Running on http://localhost:3000

3. Expose local server to internet:

ngrok http 3000

4. Use ngrok URL in webhook configuration:

LATTESTREAM_WEBHOOK_URL=https://abc123.ngrok.io/webhooks/lattestream

Mock Webhook for Development

// test-webhook.js const crypto = require('crypto'); const fetch = require('node-fetch'); const webhookUrl = 'http://localhost:3000/webhooks/lattestream'; const webhookSecret = 'your_webhook_secret'; const payload = { time_ms: Date.now(), events: [ { name: 'client_event', channel: 'private-chat', event: 'client-message', data: JSON.stringify({ text: 'Test message' }), socket_id: '123.456', user_id: 'user123' } ] }; const body = JSON.stringify(payload); const signature = crypto .createHmac('sha256', webhookSecret) .update(body) .digest('hex'); fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-LatteStream-Signature': signature }, body: body }) .then(res => console.log('Status:', res.status)) .catch(err => console.error('Error:', err));

Troubleshooting

Webhook Not Receiving Events

Check:

  • LATTESTREAM_WEBHOOK_ENABLED=true is set
  • LATTESTREAM_WEBHOOK_URL is correct and accessible
  • LATTESTREAM_WEBHOOK_EVENTS includes the events you expect
  • Firewall allows incoming HTTPS traffic
  • SSL certificate is valid (for HTTPS endpoints)

Signature Verification Failing

Check:

  • Using raw request body (not parsed JSON)
  • WEBHOOK_SECRET matches on both sides
  • No extra whitespace or encoding issues
  • Using lowercase hex output from HMAC

Webhook Timeout

Check:

  • Webhook endpoint responds within 10 seconds
  • Processing is async (don't block response)
  • No database deadlocks or slow queries
  • Sufficient server resources

Best Practices

Performance

  • Return 200 OK immediately
  • Process events asynchronously (queue/background job)
  • Use connection pooling for database
  • Implement caching where appropriate
  • Monitor webhook latency

Reliability

  • Implement idempotency checks
  • Use database transactions
  • Handle partial failures gracefully
  • Log all webhook events
  • Monitor webhook success rate

Security

  • Always verify HMAC signature
  • Use HTTPS only
  • Rate limit webhook endpoint
  • Validate event data before processing
  • Rotate webhook secret periodically

Related Documentation

Support


Next Steps: Implement webhook handling in your application and explore the SDK Implementation Guide.