Synento docs
Examples

SDK Advanced Patterns — Replay, Exports & Webhooks

Intermediate patterns for building robust applications on top of Synento's SDK.

View as Markdown

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;
}

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>`);

On this page