Skip to main content

Backend Integration

Your backend is the bridge between your users and Verriflo. It handles authentication, makes API calls, and keeps your Organization ID secret.

Why Backend-First?

Never call Verriflo directly from your frontend.

Your Organization ID is like an API key. If you expose it in client-side code, anyone can:

  • Create rooms on your account
  • Use up your credits
  • Impersonate your organization

Instead: Frontend → Your Backend → Verriflo

Basic Flow

┌───────────┐     ┌──────────────┐     ┌─────────────┐
│ Frontend │ ──▶ │ Your Backend │ ──▶ │ Verriflo │
│ │ │ │ │ API │
│ "Join │ │ Verify user, │ │ │
│ class" │ │ add context, │ │ Create room,│
└───────────┘ │ call API │ │ get token │
▲ └──────────────┘ └─────────────┘
│ │
│ ▼
└───────────────────┘
Return join URL

Node.js / Express

Setup

npm install express cors

Basic Server

const express = require("express");
const cors = require("cors");

const app = express();
app.use(cors());
app.use(express.json());

const VERRIFLO_ORG_ID = process.env.VERRIFLO_ORG_ID;
const VERRIFLO_API_URL = "https://api.verriflo.com";

// Join class endpoint
app.post("/api/join-room", async (req, res) => {
try {
const { roomId, participantName, participantUid, role } = req.body;

// Validate inputs
if (!roomId || !participantName || !participantUid) {
return res.status(400).json({ error: "Missing required fields" });
}

// Call Verriflo API
const response = await fetch(`${VERRIFLO_API_URL}/v1/room/${roomId}/join`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"VF-ORG-ID": VERRIFLO_ORG_ID,
},
body: JSON.stringify({
participant: {
uid: participantUid,
name: participantName,
role: role || "STUDENT",
},
}),
});

const data = await response.json();

if (data.success === true) {
res.json({
iframeUrl: data.data.iframeUrl,
token: data.data.token,
});
} else {
res.status(response.status).json({
error: data.message,
});
}
} catch (error) {
console.error("Verriflo API error:", error);
res.status(500).json({ error: "Failed to join class" });
}
});

// Get recordings endpoint
app.get("/api/recordings/:roomId", async (req, res) => {
try {
const { roomId } = req.params;

const response = await fetch(
`${VERRIFLO_API_URL}/v1/room/${roomId}/recordings/download`,
{
headers: {
"VF-ORG-ID": VERRIFLO_ORG_ID,
},
}
);

const data = await response.json();

if (data.success === true) {
res.json(data.data);
} else {
res.status(response.status).json({ error: data.message });
}
} catch (error) {
console.error("Recording fetch error:", error);
res.status(500).json({ error: "Failed to get recordings" });
}
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Python / FastAPI

Setup

pip install fastapi uvicorn httpx

Basic Server

import os
import httpx
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Restrict in production
allow_methods=["*"],
allow_headers=["*"],
)

VERRIFLO_ORG_ID = os.environ.get("VERRIFLO_ORG_ID")
VERRIFLO_API_URL = "https://api.verriflo.com"


@app.post("/api/join-room")
async def join_class(request: JoinClassRequest):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{VERRIFLO_API_URL}/v1/room/{request.room_id}/join",
headers={
"Content-Type": "application/json",
"VF-ORG-ID": VERRIFLO_ORG_ID,
},
json={
"participant": {
"uid": request.participant_uid,
"name": request.participant_name,
"role": request.role,
},
},
)

data = response.json()

if data.get("success") is True:
return {
"iframeUrl": data["data"]["iframeUrl"],
"token": data["data"]["token"],
}
else:
raise HTTPException(
status_code=response.status_code,
detail=data.get("message", "Failed to join class"),
)


@app.get("/api/recordings/{room_id}")
async def get_recordings(room_id: str):
async with httpx.AsyncClient() as client:
response = await client.get(
f"{VERRIFLO_API_URL}/v1/room/{room_id}/recordings/download",
headers={"VF-ORG-ID": VERRIFLO_ORG_ID},
)

data = response.json()

if data.get("success") is True:
return data["data"]
else:
raise HTTPException(
status_code=response.status_code,
detail=data.get("message", "Failed to get recordings"),
)


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3001)

Authentication

Before calling Verriflo, verify your user is who they say they are:

app.post("/api/create-room", async (req, res) => {
// Step 1: Verify the user
const user = await verifyToken(req.headers.authorization);

if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}

// Step 2: Check permissions
const { roomId } = req.body;
const hasAccess = await checkCourseAccess(user.id, roomId);

if (!hasAccess) {
return res.status(403).json({ error: "Not enrolled in this course" });
}

// Step 3: Now call Verriflo
// Use their verified name/uid from your database
const response = await callVerrifloJoin({
roomId,
participantName: user.name,
participantUid: user.id,
role: user.isInstructor ? "TEACHER" : "STUDENT",
});

res.json(response);
});

Role Determination

Decide roles based on your business logic:

function determineRole(user, course) {
// Course creator is the teacher
if (course.instructorId === user.id) {
return "TEACHER";
}

// Teaching assistants are moderators
if (course.assistantIds.includes(user.id)) {
return "MODERATOR";
}

// Everyone else is a student
return "STUDENT";
}

Dynamic Room IDs

Generate predictable but unique room IDs:

function generateRoomId(courseId, sessionDate) {
// Format: course-123-2024-01-15-1400
const dateStr = sessionDate.toISOString().slice(0, 10);
const timeStr = sessionDate.toTimeString().slice(0, 5).replace(":", "");
return `${courseId}-${dateStr}-${timeStr}`;
}

// Usage
const roomId = generateRoomId("course-123", new Date("2024-01-15T14:00:00"));
// Returns: 'course-123-2024-01-15-1400'

Error Handling

Map Verriflo errors to user-friendly messages:

function handleVerrifloError(status, message) {
const errorMap = {
400: "Invalid request. Please try again.",
402: "This feature requires a subscription upgrade.",
404: "Class not found. It may not have started yet.",
410: "This class has ended.",
422: "Account setup incomplete. Contact support.",
};

return {
statusCode: status >= 500 ? 500 : status,
message: errorMap[status] || "Something went wrong. Please try again.",
originalMessage: message, // For logging
};
}

Caching Tokens

If participants might rejoin quickly (network issues, page refresh), cache tokens briefly:

const tokenCache = new Map();

async function getOrCreateToken(roomId, participant) {
const cacheKey = `${roomId}:${participant.uid}`;

// Check cache (5 minute TTL)
const cached = tokenCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.data;
}

// Fetch new token
const data = await callVerrifloJoin(roomId, participant);

// Cache it
tokenCache.set(cacheKey, {
data,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
});

return data;
}

Environment Variables

Keep your secrets in environment variables:

# .env
VERRIFLO_ORG_ID=org_abc123xyz

# Never commit this file!
# Add to .gitignore

Load them properly:

// Node.js with dotenv
require("dotenv").config();
const orgId = process.env.VERRIFLO_ORG_ID;
# Python with python-dotenv
from dotenv import load_dotenv
load_dotenv()
ORG_ID = os.getenv("VERRIFLO_ORG_ID")

Logging

Log API calls for debugging (but sanitize sensitive data):

async function callVerriflo(endpoint, options) {
const startTime = Date.now();

console.log(`[Verriflo] ${options.method} ${endpoint}`, {
roomId: options.body?.roomId,
participantUid: options.body?.participant?.uid,
});

const response = await fetch(endpoint, options);
const data = await response.json();

console.log(`[Verriflo] Response in ${Date.now() - startTime}ms`, {
status: response.status,
success: data.success,
// Don't log tokens!
});

return data;
}

Next: Recordings Guide — Automate recording downloads.