跳到主要内容

Create an investigation room for each new support ticket

This example shows how to create a dedicated investigation room in ONLYOFFICE DocSpace when your helpdesk system (Zendesk / Freshdesk / Jira Service Management) reports a new ticket.

When a ticket is created, the integration:

  • receives a ticket.created webhook from the helpdesk system,
  • creates a room in DocSpace for the ticket,
  • creates a standard folder structure inside the room,
  • optionally grants access to a support team,
  • generates a room link,
  • sends the link back to the helpdesk (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.
Full example
import express from "express";

// DocSpace configuration
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.

// Business configuration
const INVESTIGATION_ROOM_TYPE = 2; // Example: collaboration room (adjust if your build uses other values)
const DEFAULT_FOLDERS = ["Logs", "Attachments", "Findings"];

// Optional: grant access to a team/user
// If you do not want this step, keep SHARE_ENTRIES empty.
const SHARE_ENTRIES: Array<{ id: string; isGroup: boolean; access: number }> = [
// { id: "SUPPORT_TEAM_ID", isGroup: true, access: 4 },
];

// DocSpace auth headers
const HEADERS: Record<string, string> = {
Accept: "application/json",
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};

// Step 0: HTTP helper
async function docspaceRequest(path: string, method: string = "GET", body: any = null) {
const url = `${API_HOST}${path}`;

try {
const res = await fetch(url, {
method,
headers: HEADERS,
body: body ? JSON.stringify(body) : undefined,
});

if (!res.ok) {
const text = await res.text();
console.error(`[ERROR] DocSpace request failed: ${method} ${url}`);
console.error(`[ERROR] Status: ${res.status}, Message: ${text}`);
return null;
}

return res.json();
} catch (err: any) {
console.error(`[ERROR] DocSpace request error: ${err?.message || err}`);
return null;
}
}

// Step 1: Parse ticket payload
function extractTicket(payload: any) {
const ticketId = String(payload?.ticketId || "").trim();
const subject = String(payload?.subject || "").trim();

if (!ticketId || !subject) return null;

return { ticketId, subject };
}

// Step 2: Create investigation room
async function createInvestigationRoom(ticketId: string, subject: string) {
const title = `Ticket #${ticketId} - ${subject}`;

const body = {
title,
roomType: INVESTIGATION_ROOM_TYPE,
};

const data = await docspaceRequest("/api/2.0/files/rooms", "POST", body);
const resp = data && typeof data === "object" ? (data.response ?? null) : null;

const roomId = Number(resp?.id ?? data?.id ?? null);
if (!Number.isFinite(roomId)) {
console.error("[ERROR] Could not read roomId from response:", data);
return null;
}

return { roomId, title };
}

// Step 3: Create default folders inside the room
async function createFolder(parentFolderId: number, title: string) {
const body = { title };
const data = await docspaceRequest(`/api/2.0/files/folder/${parentFolderId}`, "POST", body);

const resp = data && typeof data === "object" ? (data.response ?? null) : null;
const folderId = Number(resp?.id ?? data?.id ?? null);

if (!Number.isFinite(folderId)) {
console.warn(`[WARN] Folder '${title}' created, but folderId not detected. Response:`, data);
return null;
}

return folderId;
}

async function createStandardStructure(roomId: number) {
for (const folderName of DEFAULT_FOLDERS) {
await createFolder(roomId, folderName);
}
}

// Step 4: Share room with team/users (optional)
async function shareRoom(roomId: number) {
if (!SHARE_ENTRIES.length) return true;

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

return Boolean(data);
}

// Step 5: Generate a room link (do not build UI URL manually)
async function generateRoomLink(roomId: number) {
const data = await docspaceRequest(`/api/2.0/files/rooms/${roomId}/links`, "PUT", {});
const resp = data && typeof data === "object" ? (data.response ?? null) : null;

// Different builds may return different structures.
// Try common fields: link/url/href.
const url =
String(resp?.link || resp?.url || resp?.href || resp?.primaryUrl || "").trim() ||
String(data?.link || data?.url || "").trim();

if (!url) {
console.warn("[WARN] Room link was created, but URL not found in response:", data);
return null;
}

return url;
}

// Step 6: Update ticket in helpdesk (placeholder)
async function updateTicketWithRoomLink(ticketId: string, roomUrl: string) {
console.log("[HELPDESK]");
console.log(`Attach investigation room to ticket ${ticketId}`);
console.log(`Room: ${roomUrl}`);

// Replace with your helpdesk API call:
// - Zendesk: add internal note / custom field
// - Freshdesk: update ticket custom field
// - JSM: add comment / issue property
}

// Orchestration
async function handleTicketCreated(payload: any) {
const ticket = extractTicket(payload);
if (!ticket) {
console.log("[INFO] Missing ticketId or subject. Skipping.");
return;
}

const room = await createInvestigationRoom(ticket.ticketId, ticket.subject);
if (!room) return;

await createStandardStructure(room.roomId);

const shared = await shareRoom(room.roomId);
if (!shared) {
console.warn("[WARN] Room created but sharing step failed.");
}

const roomUrl = await generateRoomLink(room.roomId);
if (!roomUrl) {
console.warn("[WARN] Room created but link generation failed.");
return;
}

await updateTicketWithRoomLink(ticket.ticketId, roomUrl);

console.log("[INFO] Ticket room flow completed.");
}

// Minimal webhook receiver (helpdesk -> your backend)
const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/helpdesk/ticket-created", async (req, res) => {
try {
await handleTicketCreated(req.body);
} catch (e: any) {
console.error("[ERROR]", e?.message || e);
}
return res.status(200).json({ status: "ok" });
});

app.listen(3000, () => {
console.log("Helpdesk webhook listener: http://localhost:3000/helpdesk/ticket-created");
});

Step 1: Receive the “ticket created” event

Your helpdesk system sends a webhook to your backend when a new ticket is created. The handler extracts ticketId and subject from the incoming payload and starts the DocSpace workflow.

function extractTicket(payload: any) {
const ticketId = String(payload?.ticketId || "").trim();
const subject = String(payload?.subject || "").trim();

if (!ticketId || !subject) return null;

return { ticketId, subject };
}

Step 2: Create a dedicated investigation room

The backend creates a new DocSpace room with a title that includes the ticket id and subject. It uses POST /api/2.0/files/rooms with a body like { "title": "Ticket #123 - Login error", "roomType": 2 }

async function createInvestigationRoom(ticketId: string, subject: string) {
const title = `Ticket #${ticketId} - ${subject}`;

const body = {
title,
roomType: INVESTIGATION_ROOM_TYPE,
};

const data = await docspaceRequest("/api/2.0/files/rooms", "POST", body);
const resp = data && typeof data === "object" ? (data.response ?? null) : null;

const roomId = Number(resp?.id ?? data?.id ?? null);
if (!Number.isFinite(roomId)) return null;

return { roomId, title };
}

Step 3: Create the standard folder structure inside the room

To keep investigations consistent, the script creates folders inside the new room using POST /api/2.0/files/folder/:parentFolderId where parentFolderId is the room ID.

async function createStandardStructure(roomId: number) {
for (const folderName of DEFAULT_FOLDERS) {
await createFolder(roomId, folderName);
}
}

Step 4: Configure room access (optional)

If you want only a specific team to work in the room, you can share it using PUT /api/2.0/files/rooms/:roomId/share with { "entries": [ ... ] }

If you do not need this step, keep SHARE_ENTRIES empty.

async function shareRoom(roomId: number) {
if (!SHARE_ENTRIES.length) return true;

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

return Boolean(data);
}

The script requests a DocSpace-generated room link using PUT /api/2.0/files/rooms/:roomId/links.

This avoids constructing UI links manually and ensures the returned URL is correct for your portal.

async function generateRoomLink(roomId: number) {
const data = await docspaceRequest(`/api/2.0/files/rooms/${roomId}/links`, "PUT", {});
const resp = data && typeof data === "object" ? (data.response ?? null) : null;

const url =
String(resp?.link || resp?.url || resp?.href || resp?.primaryUrl || "").trim() ||
String(data?.link || data?.url || "").trim();

return url || null;
}

After the link is generated, the integration sends it back to your helpdesk system (placeholder). In production, this step updates the ticket using your helpdesk API (custom field, internal note, etc.).

async function updateTicketWithRoomLink(ticketId: string, roomUrl: string) {
console.log("[HELPDESK]");
console.log(`Attach investigation room to ticket ${ticketId}`);
console.log(`Room: ${roomUrl}`);
}