Examples
SDK Advanced Patterns — Replay, Exports & Webhooks
Intermediate patterns for building robust applications on top of Synento's SDK.
Intermediate patterns for building robust applications on top of Synento's SDK.
Pattern 1: Session Replay Engine with Auto-Looping
import { createSynentoClient } from "@synento/node";
const synento = createSynentoClient({ apiKey: process.env.SYNENTO_API_KEY });
/**
* Replay a session by streaming its recorded events through the SDK.
*/
async function replaySession(sessionId, renderFrame) {
// Fetch manifest for track index and bounds (recommended bootstrap)
const manifestUrl = `${synento.baseUrl}/v1/sessions/${sessionId}/manifest`;
const manifestRes = await fetch(manifestUrl, {
headers: { Authorization: `Bearer ${synento.apiKey}` },
});
const { data: manifest } = await manifestRes.json();
console.log(`Session has ${manifest.participants.length} participants, ` +
`capabilities: ${manifest.capabilities.join(", ")}`);
// Walk through the session window by window
let fromTs = manifest.start_ts ?? 0;
const windowSize = 30_000; // 30 seconds per window
let eventCount = 0;
while (fromTs <= (manifest.end_ts ?? fromTs)) {
const windowUrl = `${synento.baseUrl}/v1/sessions/${sessionId}/playback?from_ts=${fromTs}&window_size=${windowSize}`;
const windowRes = await fetch(windowUrl, {
headers: { Authorization: `Bearer ${synento.apiKey}` },
});
const { data } = await windowRes.json();
if (!data.events || data.events.length === 0) break;
for (const event of data.events) {
const streamId = event.stream_id ?? "";
if (streamId.includes("/media/") && streamId.endsWith("/cam")) {
const userId = streamId.split("/media/")[1]?.split("/")[0];
const buf = Buffer.from(event.body_base64, "base64");
await renderFrame(buf, userId);
} else if (streamId.endsWith("/events") || streamId.endsWith("/director")) {
// Timeline events — useful for layout/state reduction
} else if (streamId.endsWith("/chat")) {
const msg = JSON.parse(Buffer.from(event.body_base64, "base64").toString());
console.log(`[chat] ${msg.text}`);
}
eventCount++;
if (eventCount % 100 === 0) {
console.log(`Replayed ${eventCount} events...`);
}
}
fromTs += windowSize;
await new Promise(r => setTimeout(r, 10));
}
console.log(`Replay complete: ${eventCount} total events`);
}
// Usage: render each frame to a file (useful for creating thumbnails)
await replaySession("sess_abc123", async (frameBuffer, userId) => {
// Write frame to a numbered file named after the user
const timestamp = Date.now();
console.log(`Frame for ${userId} at ${timestamp}`);
});Pattern 2: Webhook Handler with Signature Verification
import { createHmac, timingSafeEqual } from "node:crypto";
const WEBHOOK_SECRET = "your-webhook-secret-from-api"; // stored per webhook
const ALLOWED_EVENTS = new Set([
"session.created", "session.started", "session.ended",
"recording.ready", "export.completed"
]);
function verifyWebhookSignature(payload, signature) {
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest("hex");
// Constant-time comparison to prevent timing attacks
return expected === signature;
}
/**
* Example Express handler for processing webhooks.
*/
function webhookHandler(req, res) {
const signature = req.headers["x-synento-signature"];
if (!signature) {
return res.status(401).json({ error: "Missing signature" });
}
// Parse the raw body (Express needs express.raw for this)
const payload = req.body;
if (!verifyWebhookSignature(payload, signature)) {
console.warn("Invalid webhook signature from", req.ip);
return res.status(403).json({ error: "Invalid signature" });
}
const eventType = payload.type; // e.g., "session.started"
const sessionId = payload.session_id;
if (!ALLOWED_EVENTS.has(eventType)) {
console.warn(`Unknown event type: ${eventType}`);
return res.status(400).json({ error: "Unknown event type" });
}
// Process asynchronously — respond immediately
processWebhookEvent(payload).catch(err => {
console.error(`Failed to process webhook ${eventType}:`, err);
});
res.status(200).send("OK");
}
async function processWebhookEvent(payload) {
switch (payload.type) {
case "session.started":
// Session just went live — notify participants, start recording analytics...
console.log(`🟢 LIVE: ${payload.session_id} started`);
break;
case "session.ended":
// Session ended — trigger export, send email notifications...
console.log(`⏹️ ENDED: ${payload.session_id}`);
// Optionally trigger MP4 export
await triggerExport(payload.session_id);
break;
case "export.completed":
// MP4 is ready — notify the user, store download URL...
console.log(`📦 EXPORTED: ${payload.session_id}`);
break;
default:
console.log(`Event: ${payload.type} for session ${payload.session_id}`);
}
}
async function triggerExport(sessionId, opts = {}) {
const params = new URLSearchParams();
if (opts.layout) params.set("layout", opts.layout);
if (opts.orientation) params.set("orientation", opts.orientation);
if (opts.focus) params.set("focus", opts.focus);
const exportRes = await fetch(
`${process.env.SYNENTO_API_URL}/v1/sessions/${sessionId}/export?${params}`,
{ headers: { Authorization: `Bearer ${process.env.SYNENTO_API_KEY}` } }
);
if (!exportRes.ok) {
console.error(`Export enqueue failed for ${sessionId}: HTTP ${exportRes.status}`);
return;
}
const { data: { job_id } } = await exportRes.json();
// Poll until complete
while (true) {
const statusRes = await fetch(
`${process.env.SYNENTO_API_URL}/v1/exports/${job_id}`,
{ headers: { Authorization: `Bearer ${process.env.SYNENTO_API_KEY}` } }
);
const { data } = await statusRes.json();
if (data.status === "completed") {
const downloadRes = await fetch(
`${process.env.SYNENTO_API_URL}${data.download_url}`,
{ headers: { Authorization: `Bearer ${process.env.SYNENTO_API_KEY}` } }
);
return Buffer.from(await downloadRes.arrayBuffer());
}
if (data.status === "failed") {
throw new Error(data.error ?? "Export failed");
}
await new Promise(r => setTimeout(r, 2000));
}
}
module.exports = { webhookHandler };Pattern 3: Session Queue for Production Limits
When creating sessions in a loop (e.g., scheduling daily standups), respect billing limits:
/**
* Create sessions safely, respecting billing limits.
*/
async function createSessionWithLimits(name) {
// Check if we're within limits before attempting creation
const limits = await synento.getBillingLimits();
if (limits.isBlocked) {
throw new Error(`Session creation blocked: ${limits.statusMessage}`);
}
// Safe to create a session — within tier limits
return await synento.createSession({ name });
}
/**
* Process a queue of pending sessions, respecting rate limits.
*/
async function processSessionQueue(pendingSessions) {
const created = [];
for (const name of pendingSessions) {
try {
const session = await createSessionWithLimits(name);
created.push(session);
// Brief delay between sessions to avoid rate limiting
await new Promise(r => setTimeout(r, 500));
} catch (err) {
console.error(`Failed to create "${name}": ${err.message}`);
// If billing limits hit, stop the queue
if (err.statusCode === 402 || err.message.includes("blocked")) {
console.log(`Queue stopped — billing limits exceeded. ${created.length} created.`);
break;
}
// Other errors — continue with next session in queue
}
}
return created;
}Pattern 4: Expiring Link Generator for Marketing Pages
Generate shareable replay links for public sessions in marketing content:
async function createShareLink(sessionId, expiresInMinutes = 1440) { // default: 24 hours
const res = await fetch(
`${synento.baseUrl}/v1/sessions/${sessionId}/replay-links`, {
method: "POST",
headers: {
Authorization: `Bearer ${synento.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ expires_in_minutes: expiresInMinutes }),
}
);
const { data } = await res.json();
return data.link; // Shareable URL like: https://host/v1/public/sessions/{id}/replay?token=xyz
}
// Usage for a marketing page: render an iframe with the expiring link
async function getEmbeddedUrl(sessionId) {
const shareLink = await createShareLink(sessionId, 43200); // max: 30 days
return `${shareLink}&from_ts=0&window_size=15000`;
}
// In your server response:
const iframeUrl = await getEmbeddedUrl("sess_abc123");
res.send(`<iframe src="${iframeUrl}" width="640" height="360"></iframe>`);