Skip to main content

Automatic home folder creation for new users in a shared Employees room

This example shows how to automatically create a personal home folder for every new user inside an existing shared room/folder (for example, Employees), instead of creating a separate room per user.

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 EMPLOYEES_ROOM_ID = 539564;
const TEMPLATE_FOLDER_ID = 341029;
const HR_GROUP_ID = "694513";

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

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

const app = express();

// Step 1: Receive and validate the webhook request
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);

function verify_signature(secretKey, rawBody, signatureHeader) {
// If WEBHOOK_SECRET is empty, skip verification
if (!secretKey) return true;

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

const parts = signatureHeader.split("=", 2);
if (parts.length < 2 || !parts[1]) return false;

const received = parts[1].trim().toLowerCase();
const expected = crypto.createHmac("sha256", secretKey).update(rawBody).digest("hex").toLowerCase();

try {
return crypto.timingSafeEqual(Buffer.from(received, "utf8"), Buffer.from(expected, "utf8"));
} catch {
return false;
}
}

async function docspace_request(pathname, method = "GET", json_body = null) {
const url = `${API_HOST}${pathname}`;

let res;
try {
res = await fetch(url, {
method: String(method).toUpperCase(),
headers: HEADERS,
body: json_body ? JSON.stringify(json_body) : 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 get_response_node(data) {
if (data && typeof data === "object" && !Array.isArray(data) && "response" in data) {
return data.response;
}
return data;
}

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

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

function build_display_name(payload) {
const first_name = String(payload?.firstName || "").trim();
const last_name = String(payload?.lastName || "").trim();
const full = `${first_name} ${last_name}`.trim();
if (full) return full;

const user_name = payload?.userName;
if (user_name) return String(user_name);

const email = payload?.email;
if (email) return String(email);

return "New user";
}

// Step 2: Create or reuse the user's home folder
async function list_children(folder_id) {
const data = await docspace_request(`/api/2.0/files/${folder_id}`, "GET", null);
const node = get_response_node(data);

if (Array.isArray(node)) {
return node.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}

if (node && typeof node === "object" && !Array.isArray(node)) {
const items = node.files || node.folders || node.items || node.response || [];
if (Array.isArray(items)) {
return items.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}
}

if (data && typeof data === "object" && !Array.isArray(data)) {
const resp = data.response;
if (Array.isArray(resp)) {
return resp.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}
}

return [];
}

async function find_folder_by_title(parent_id, title) {
const items = await list_children(parent_id);
const title_norm = String(title || "").trim();

for (const it of items) {
const it_title = String(it.title || "").trim();
if (it_title !== title_norm) continue;

const is_folder =
it.isFolder === true ||
it.folder === true ||
String(it.type || "").toLowerCase() === "folder" ||
it.folderId !== undefined && it.folderId !== null;

if (!is_folder) continue;

const raw_id = it.id ?? it.folderId;
const n = Number(raw_id);
if (Number.isFinite(n)) return n;
}

return null;
}

async function create_folder(parent_folder_id, title) {
const payload = { title };
const data = await docspace_request(`/api/2.0/files/folder/${parent_folder_id}`, "POST", payload);
const node = get_response_node(data);

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

const raw_id = node.id ?? node.folderId;
const n = Number(raw_id);
return Number.isFinite(n) ? n : null;
}

async function get_or_create_home_folder(parent_id, folder_title) {
const existing = await find_folder_by_title(parent_id, folder_title);
if (existing !== null) {
console.log(`[INFO] Home folder already exists: "${folder_title}" (id=${existing})`);
return existing;
}

const created = await create_folder(parent_id, folder_title);
if (created !== null) {
console.log(`[INFO] Home folder created: "${folder_title}" (id=${created})`);
return created;
}

console.error(`[ERROR] Failed to create home folder: "${folder_title}"`);
return null;
}

// Step 3: Copy starter documents from the template folder
function _is_folder_item(it) {
return (
it?.isFolder === true ||
it?.folder === true ||
String(it?.type || "").toLowerCase() === "folder"
);
}

async function list_files_in_folder(folder_id) {
const items = await list_children(folder_id);
const out = [];

for (const it of items) {
if (_is_folder_item(it)) continue;
if (it && typeof it === "object" && "id" in it) out.push(it);
}

return out;
}

async function copy_file_to_folder(file_id, dest_folder_id) {
const payload = { destFolderId: dest_folder_id, destTitle: null };
const data = await docspace_request(`/api/2.0/files/file/${file_id}/copyas`, "POST", payload);
return data !== null;
}

async function copy_templates_to_home(template_folder_id, home_folder_id) {
const files = await list_files_in_folder(template_folder_id);
if (!files.length) {
console.warn(`[WARN] No files found in TEMPLATE_FOLDER_ID=${template_folder_id}. Nothing to copy.`);
return;
}

for (const f of files) {
const raw_id = f.id;
const file_id = Number(raw_id);
if (!Number.isFinite(file_id)) continue;

const ok = await copy_file_to_folder(file_id, home_folder_id);
if (ok) console.log(`[INFO] Template copied: fileId=${file_id} -> homeFolderId=${home_folder_id}`);
else console.warn(`[WARN] Failed to copy template: fileId=${file_id} -> homeFolderId=${home_folder_id}`);
}
}

// Step 4: Share the home folder with the user (and optional HR group)
async function share_home_folder(home_folder_id, user_id) {
const share = [{ shareTo: user_id, access: "ReadWrite" }];

if (HR_GROUP_ID) {
share.push({ shareTo: String(HR_GROUP_ID), access: "Read" });
}

const payload = { share };
const data = await docspace_request(`/api/2.0/files/folder/${home_folder_id}/share`, "PUT", payload);
return data !== null;
}

// Step 5: Send a welcome message (optional)
function send_welcome_email(user_payload, home_folder_id) {
const email = String(user_payload?.email || "unknown");
const first_name = String(user_payload?.firstName || "there");
const folder_link = `${API_HOST}/products/files/#/files/folder/${home_folder_id}`;

console.log("[WELCOME EMAIL]");
console.log(`To: ${email}`);
console.log("Subject: Welcome to DocSpace");
console.log(`Body: Hello ${first_name}, your personal folder has been created in /Employees.`);
console.log(`Link: ${folder_link}`);
}

async function handle_user_created(user_id, user_payload) {
const display_name = build_display_name(user_payload);
console.log(`[INFO] User created: userId=${user_id}, displayName=${display_name}`);

const home_folder_id = await get_or_create_home_folder(EMPLOYEES_ROOM_ID, display_name);
if (home_folder_id === null) {
throw new Error(`Failed to create or find home folder for userId=${user_id}`);
}

await copy_templates_to_home(TEMPLATE_FOLDER_ID, home_folder_id);

const shared = await share_home_folder(home_folder_id, user_id);
if (shared) console.log(`[INFO] Home folder shared: homeFolderId=${home_folder_id}`);
else console.warn(`[WARN] Failed to share home folder: homeFolderId=${home_folder_id}`);

send_welcome_email(user_payload, home_folder_id);
console.log("[INFO] Provisioning completed.");
}

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

app.post("/docspace/webhook", async (req, res) => {
// Step 1: Receive and validate the webhook request
const raw_body = req.rawBody;
const signature_header = req.header("x-docspace-signature-256");

if (!verify_signature(WEBHOOK_SECRET, raw_body, signature_header)) {
console.warn("[WARN] Invalid webhook signature.");
return res.status(401).send("Invalid signature");
}

const body = req.body;
if (!body || typeof body !== "object") return res.json({ status: "ok" });

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

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

const user_id = extract_user_id(payload);
if (user_id === null) {
console.log("[INFO] No user ID in payload. Skipping.");
return res.json({ status: "ok" });
}

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

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

const port = Number(process.env.PORT || 3000);
app.listen(port, "0.0.0.0", () => {
console.log(`Webhook listener: http://localhost:${port}/docspace/webhook`);
});

Step 1: Receive and validate the webhook request

When an event happens in DocSpace (for example, a user is created), DocSpace sends a request to your webhook URL.

At this step the script simply:

  • receives the request from DocSpace,
  • reads the event type (event.trigger),
  • reads the main data object (payload).

If the event is not one you are interested in, the script stops and does nothing.

app.post("/docspace/webhook", async (req, res) => {
// Step 1: Receive and validate the webhook request
const raw_body = req.rawBody;
const signature_header = req.header("x-docspace-signature-256");

if (!verify_signature(WEBHOOK_SECRET, raw_body, signature_header)) {
console.warn("[WARN] Invalid webhook signature.");
return res.status(401).send("Invalid signature");
}

const body = req.body;
if (!body || typeof body !== "object") return res.json({ status: "ok" });

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

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

const user_id = extract_user_id(payload);
if (user_id === null) {
console.log("[INFO] No user ID in payload. Skipping.");
return res.json({ status: "ok" });
}

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

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

Step 2: Create or reuse the user's home folder

A folder name is generated from the user data:

  • firstName + lastName (fallback: userName, then email)

Then the script:

  • lists items in the shared Employees location (EMPLOYEES_ROOM_ID)
  • reuses an existing folder if a folder with the same title is found
  • otherwise creates a new folder with the generated title.
async function list_children(folder_id) {
const data = await docspace_request(`/api/2.0/files/${folder_id}`, "GET", null);
const node = get_response_node(data);

if (Array.isArray(node)) {
return node.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}

if (node && typeof node === "object" && !Array.isArray(node)) {
const items = node.files || node.folders || node.items || node.response || [];
if (Array.isArray(items)) {
return items.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}
}

if (data && typeof data === "object" && !Array.isArray(data)) {
const resp = data.response;
if (Array.isArray(resp)) {
return resp.filter((x) => x && typeof x === "object" && !Array.isArray(x));
}
}

return [];
}

Step 3: Copy starter documents from the template folder

To populate the home folder with standard files, the script:

  • lists files in TEMPLATE_FOLDER_ID
  • copies each file into the home folder using the DocSpace copy endpoint.
function _is_folder_item(it) {
return (
it?.isFolder === true ||
it?.folder === true ||
String(it?.type || "").toLowerCase() === "folder"
);
}

async function list_files_in_folder(folder_id) {
const items = await list_children(folder_id);
const out = [];

for (const it of items) {
if (_is_folder_item(it)) continue;
if (it && typeof it === "object" && "id" in it) out.push(it);
}

return out;
}

Step 4: Share the home folder with the user (and optional HR group)

Access permissions are applied using the folder share endpoint. In the example:

  • the new user receives ReadWrite
  • HR_GROUP_ID (if configured) receives Read.
async function share_home_folder(home_folder_id, user_id) {
const share = [{ shareTo: user_id, access: "ReadWrite" }];

if (HR_GROUP_ID) {
share.push({ shareTo: String(HR_GROUP_ID), access: "Read" });
}

const payload = { share };
const data = await docspace_request(`/api/2.0/files/folder/${home_folder_id}/share`, "PUT", payload);
return data !== null;
}

Step 5: Send a welcome message (optional)

A placeholder welcome notification is generated containing:

  • recipient email
  • a link to the created home folder Replace this stub with SMTP/SendGrid/Slack/Teams depending on your integration.
function send_welcome_email(user_payload, home_folder_id) {
const email = String(user_payload?.email || "unknown");
const first_name = String(user_payload?.firstName || "there");
const folder_link = `${API_HOST}/products/files/#/files/folder/${home_folder_id}`;

console.log("[WELCOME EMAIL]");
console.log(`To: ${email}`);
console.log("Subject: Welcome to DocSpace");
console.log(`Body: Hello ${first_name}, your personal folder has been created in /Employees.`);
console.log(`Link: ${folder_link}`);
}