Skip to main content

Automatically create a home room when a new user is added

This example shows how to automatically create a personal home room for every new user in ONLYOFFICE DocSpace using webhooks.

When DocSpace notifies your backend about a new user, the script:

  • receives and verifies the webhook request
  • checks that the trigger is supported
  • extracts user data from the payload
  • creates a personal room for the user
  • creates a default folder structure inside the room
  • copies starter documents from templates into the room
  • applies sharing rules (user gets full access, optional HR group gets read-only)
  • sends a welcome message with a room link (placeholder)

Before you start

  1. Replace https://yourportal.onlyoffice.com and YOUR_API_KEY with your actual DocSpace portal URL and API key. Ensure you have the necessary data and permissions to perform these operations.
  2. Before you can make requests to the API, you need to authenticate. Check out the Personal access tokens page to learn how to obtain and use access tokens.
  3. Expose your webhook endpoint over HTTPS in production (terminate TLS in your app or behind a reverse proxy/load balancer, and reject non-HTTPS requests). For local testing, use a secure tunnel that provides an HTTPS URL.

Webhook configuration

This example relies on DocSpace webhooks. To register and manage webhooks on your portal, see Webhooks and the Help Center instructions: https://helpcenter.onlyoffice.com/administration/docspace-webhooks.aspx.

  • Register a webhook and enable the triggers used in this example.
  • Set the payload URL to your backend endpoint.
  • Generate/set a secret key and store it on the backend as WEBHOOK_SECRET. DocSpace sends the signature in the x-docspace-signature-256 header so you can validate the request against the raw body.

Timeouts and retries

DocSpace retries failed webhook deliveries. According to the webhook docs:

  • Up to 5 attempts are made, with exponential backoff (2^attempt seconds).
  • Any successful status (any 2xx) stops retries.
  • If your endpoint returns 410 Gone, the webhook is removed from the portal.

Keep the handler fast: validate the request, enqueue background work if needed, and return quickly. Make processing idempotent because the same event may be delivered more than once.

Full example
import express from "express";
import crypto from "crypto";

const API_HOST = process.env.DOCSPACE_API_HOST; // Set DOCSPACE_API_HOST in env (recommended). For quick tests you can temporarily paste your portal URL here.
const API_KEY = process.env.DOCSPACE_API_KEY; // Set DOCSPACE_API_KEY in env (recommended). For quick tests you can temporarily paste token here.
const WEBHOOK_SECRET = process.env.DOCSPACE_WEBHOOK_SECRET; // Optional. If empty, the signature check is skipped (dev only).

const TEMPLATE_FILE_IDS = [404297, 435466];

const HR_GROUP_ID = "HR_GROUP_ID";
const HOME_ROOM_TYPE = 2;

const ALLOWED_TRIGGERS = new Set(["user.created", "user.added"]);

const HEADERS = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
};

async function docspaceRequest(path, method = "GET", jsonBody = null) {
const url = `${API_HOST}${path}`;

let res;
try {
res = await fetch(url, {
method,
headers: HEADERS,
body: jsonBody ? JSON.stringify(jsonBody) : undefined,
});
} catch (e) {
console.error(`[ERROR] Request failed: ${method} ${url} -> ${e?.message || e}`);
return null;
}

if (!res.ok) {
const text = await res.text().catch(() => "");
console.error(`[ERROR] API error: ${method} ${url} -> ${res.status} ${text}`);
return null;
}

try {
return await res.json();
} catch {
console.error(`[ERROR] Non-JSON response: ${method} ${url}`);
return null;
}
}

function getResponseNode(data) {
if (data && typeof data === "object" && !Array.isArray(data) && "response" in data) {
return data.response;
}
return data;
}

// Step 1: Receive and verify the webhook request
function verifySignature(secretKey, rawBody, signatureHeader) {
// If WEBHOOK_SECRET is empty, skip verification
if (!secretKey) return true;

if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;

const received = signatureHeader.split("=", 2)[1]?.trim()?.toLowerCase();
if (!received) return false;

const expected = crypto.createHmac("sha256", secretKey).update(rawBody).digest("hex").toLowerCase();
if (received.length !== expected.length) return false;

return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

function extractTriggerAndPayload(body) {
const trigger = String(body?.event?.trigger || "").trim();
const payload = body?.payload && typeof body.payload === "object" ? body.payload : null;
return { trigger, payload };
}

// Step 2: Detect the user event and extract user data
function extractUserId(payload) {
const raw = payload?.id;
const val = raw != null ? String(raw).trim() : "";
return val ? val : null;
}

function buildDisplayName(payload) {
const first = String(payload?.firstName || "").trim();
const last = String(payload?.lastName || "").trim();
const full = `${first} ${last}`.trim();
if (full) return full;

if (payload?.userName) return String(payload.userName);
if (payload?.email) return String(payload.email);

return "New user";
}

async function getUserInfo(userId) {
const data = await docspaceRequest(`/api/2.0/people/${encodeURIComponent(userId)}`, "GET");
const node = getResponseNode(data);
return node && typeof node === "object" && !Array.isArray(node) ? node : null;
}

// Step 3: Create a personal home room
async function createHomeRoom(displayName) {
const payload = {
title: `Home - ${displayName}`,
roomType: HOME_ROOM_TYPE,
share: [],
};

const data = await docspaceRequest("/api/2.0/files/rooms", "POST", payload);
const node = getResponseNode(data);

if (!node || typeof node !== "object") return null;

const roomIdRaw = node.id ?? node.roomId ?? null;
const roomId = Number(roomIdRaw);

return Number.isFinite(roomId) ? roomId : null;
}

// Step 4: Create a default folder structure
async function createFolder(parentFolderId, title) {
const payload = { title };
const data = await docspaceRequest(`/api/2.0/files/folder/${parentFolderId}`, "POST", payload);
const node = getResponseNode(data);

if (!node || typeof node !== "object") return null;

const folderIdRaw = node.id ?? node.folderId ?? null;
const folderId = Number(folderIdRaw);

return Number.isFinite(folderId) ? folderId : null;
}

async function createBaseFolders(roomId) {
const names = ["Documents", "Personal", "Shared with me"];

for (const title of names) {
const folderId = await createFolder(roomId, title);
if (folderId) console.log(`[INFO] Folder created: "${title}" (id=${folderId})`);
else console.warn(`[WARN] Failed to create folder: "${title}"`);
}
}

// Step 5: Copy starter documents from templates
async function copyTemplatesToRoom(roomId) {
for (const templateId of TEMPLATE_FILE_IDS) {
const payload = { destFolderId: roomId, destTitle: null };
const data = await docspaceRequest(`/api/2.0/files/file/${templateId}/copyas`, "POST", payload);

if (!data) console.warn(`[WARN] Failed to copy template ${templateId} into room ${roomId}`);
else console.log(`[INFO] Template copied: fileId=${templateId} -> roomId=${roomId}`);
}
}

// Step 6: Apply access rules (ACL)
async function applyRoomAcl(roomId, userId) {
const entries = [{ id: String(userId), isGroup: false, access: 4 }];

if (HR_GROUP_ID) {
entries.push({ id: String(HR_GROUP_ID), isGroup: true, access: 1 });
}

const payload = { entries };
const data = await docspaceRequest(`/api/2.0/files/rooms/${roomId}/share`, "PUT", payload);

if (!data) console.warn(`[WARN] Failed to apply sharing rules for room ${roomId}`);
else console.log(`[INFO] Sharing rules applied for room ${roomId}`);
}

// Step 7: Send a welcome message (placeholder)
function sendWelcomeMessage(userPayload, roomId) {
const email = String(userPayload?.email || "unknown");
const firstName = String(userPayload?.firstName || "there");
const roomLink = `${API_HOST}/products/files/rooms/${roomId}`;

console.log("[WELCOME MESSAGE]");
console.log(`To: ${email}`);
console.log("Subject: Welcome to DocSpace");
console.log(`Body: Hello ${firstName}, your personal home room has been created.`);
console.log(`Link: ${roomLink}`);
}

async function handleUserEvent(userId, payload) {
console.log(`[INFO] New user detected: userId=${userId}`);

let displayName = buildDisplayName(payload);

const userInfo = await getUserInfo(userId);
if (userInfo && typeof userInfo === "object") {
displayName = String(userInfo.displayName || userInfo.email || displayName);
}

const roomId = await createHomeRoom(displayName);
if (roomId == null) {
console.error("[ERROR] Failed to create home room.");
throw new Error("Failed to create home room.");
}

console.log(`[INFO] Home room created: roomId=${roomId}`);

await createBaseFolders(roomId);
await copyTemplatesToRoom(roomId);
await applyRoomAcl(roomId, userId);
sendWelcomeMessage(payload, roomId);

console.log("[INFO] Home room provisioning completed.");
}

const app = express();

app.use(
express.json({
limit: "2mb",
verify: (req, _res, buf) => {
req.rawBody = buf;
},
})
);

app.head("/docspace/webhook", (_req, res) => res.status(200).send(""));
app.get("/docspace/webhook", (_req, res) => res.status(200).json({ status: "ok" }));

app.post("/docspace/webhook", async (req, res) => {
const signatureHeader = req.headers["x-docspace-signature-256"];

if (!req.rawBody) {
console.warn("[WARN] Missing raw body buffer. Cannot verify signature.");
return res.status(400).send("Missing raw body");
}

if (!verifySignature(WEBHOOK_SECRET, req.rawBody, signatureHeader)) {
console.warn("[WARN] Invalid webhook signature.");
return res.status(401).send("Invalid signature");
}

const { trigger, payload } = extractTriggerAndPayload(req.body);

if (!payload) {
console.log("[INFO] No payload object. Skipping.");
return res.status(200).json({ status: "ok" });
}

if (!ALLOWED_TRIGGERS.has(trigger)) {
console.log(`[INFO] Trigger '${trigger}' is not handled. Skipping.`);
return res.status(200).json({ status: "ok" });
}

const userId = extractUserId(payload);
if (!userId) {
console.log("[INFO] No userId in payload. Skipping.");
return res.status(200).json({ status: "ok" });
}

try {
await handleUserEvent(userId, payload);
} catch (e) {
console.error("[ERROR] Handler failed:", e?.message || e);
return res.status(500).send("Internal error");
}

return res.status(200).json({ status: "ok" });
});

app.listen(3000, () => {
console.log("Webhook listener: http://localhost:3000/docspace/webhook");
});

Step 1: Receive and verify the webhook request

When a new user is added, DocSpace sends a notification to your webhook URL.

At this step, the backend:

  • accepts the incoming request from DocSpace,
  • checks that the request is authentic using the secret key configured in webhook settings,
  • reads the event data only after successful validation.

If the request cannot be validated, the server responds with 401 and stops processing it. DocSpace may also send HEAD requests to check that the webhook URL is reachable, so the endpoint should return 200 OK for such requests.

function verifySignature(secretKey, rawBody, signatureHeader) {
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;

const received = signatureHeader.split("=", 2)[1]?.trim()?.toLowerCase();
if (!received) return false;

const expected = crypto.createHmac("sha256", secretKey).update(rawBody).digest("hex").toLowerCase();
if (received.length !== expected.length) return false;

return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Step 2: Detect the user event and extract user data

The script checks the trigger name:

  • event.trigger must match one of the values in ALLOWED_TRIGGERS (for example, user.created)

Then it reads user fields from payload, including:

  • payload.id (user ID, can be a GUID string)
  • payload.firstName, payload.lastName
  • payload.email

If the trigger is not in the allowed list, the handler returns 200 OK and does nothing.

function extractUserId(payload) {
const raw = payload?.id;
const val = raw != null ? String(raw).trim() : "";
return val ? val : null;
}

Step 3: Create a personal home room

The script creates a room for the new user using POST /api/2.0/files/rooms

Fields used in this example:

  • title: Home - displayName
  • roomType: HOME_ROOM_TYPE (example: 2)
  • share: empty array

The API response returns the new room identifier (id/roomId), which is used in the next steps.

async function createHomeRoom(displayName) {
const payload = {
title: `Home - ${displayName}`,
roomType: HOME_ROOM_TYPE,
share: [],
};

const data = await docspaceRequest("/api/2.0/files/rooms", "POST", payload);
const node = getResponseNode(data);

if (!node || typeof node !== "object") return null;

const roomIdRaw = node.id ?? node.roomId ?? null;
const roomId = Number(roomIdRaw);

return Number.isFinite(roomId) ? roomId : null;
}

Step 4: Create a default folder structure

After the room is created, the script creates folders inside it using the room ID as the parent container. For each folder name, it sends POST /api/2.0/files/folder/:parentFolderId In this example, parentFolderId is the created roomId.

async function createFolder(parentFolderId, title) {
const payload = { title };
const data = await docspaceRequest(`/api/2.0/files/folder/${parentFolderId}`, "POST", payload);
const node = getResponseNode(data);

if (!node || typeof node !== "object") return null;

const folderIdRaw = node.id ?? node.folderId ?? null;
const folderId = Number(folderIdRaw);

return Number.isFinite(folderId) ? folderId : null;
}

Step 5: Copy starter documents from templates

To give each user a standard set of documents, the script copies templates into the room. For each file ID in TEMPLATE_FILE_IDS, it sends POST /api/2.0/files/file/:fileId/copyas

Body fields:

  • destFolderId: room ID
  • destTitle: null (keeps the original file name)
async function copyTemplatesToRoom(roomId) {
for (const templateId of TEMPLATE_FILE_IDS) {
const payload = { destFolderId: roomId, destTitle: null };
const data = await docspaceRequest(`/api/2.0/files/file/${templateId}/copyas`, "POST", payload);

if (!data) console.warn(`[WARN] Failed to copy template ${templateId} into room ${roomId}`);
else console.log(`[INFO] Template copied: fileId=${templateId} -> roomId=${roomId}`);
}
}

Step 6: Apply access rules (ACL)

To configure who can access the room, the script applies room sharing rules PUT /api/2.0/files/rooms/:roomId/share Entries used in this example:

  • the new user (payload.id) gets full access (access: 4)
  • an optional HR group (HR_GROUP_ID) gets read-only access (access: 1)
async function applyRoomAcl(roomId, userId) {
const entries = [{ id: String(userId), isGroup: false, access: 4 }];

if (HR_GROUP_ID) {
entries.push({ id: String(HR_GROUP_ID), isGroup: true, access: 1 });
}

const payload = { entries };
const data = await docspaceRequest(`/api/2.0/files/rooms/${roomId}/share`, "PUT", payload);

if (!data) console.warn(`[WARN] Failed to apply sharing rules for room ${roomId}`);
else console.log(`[INFO] Sharing rules applied for room ${roomId}`);
}