Skip to main content

Set "Need Review" comment when a file is updated

This example demonstrates how to automatically set a file comment to Need Review whenever a file is updated in ONLYOFFICE DocSpace.

When DocSpace sends a webhook to your backend, the receiver:

  • verifies the webhook signature (optional but recommended),
  • reads the event trigger (event.trigger) and file data (payload),
  • accepts only update triggers (for example, file.updated),
  • extracts fileId from payload.id,
  • loads the file info to get a version value,
  • sets the file comment to Need Review using the DocSpace API.

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.

// Optional: webhook signature verification (recommended)
const WEBHOOK_SECRET = process.env.DOCSPACE_WEBHOOK_SECRET; // Optional. If empty, the signature check is skipped (dev only).

// Comment text to enforce on every update
const NEED_REVIEW_TEXT = "Need Review";

// Put here the exact trigger names you enabled in DocSpace Webhooks
const ALLOWED_TRIGGERS = new Set([
"file.updated",
// "file.versionCreated",
]);

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

async function docspaceRequest(path, method = "GET", body = 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().catch(() => "");
console.error(`[ERROR] DocSpace request failed: ${method} ${url}`);
console.error(`[ERROR] Status: ${res.status}, Message: ${text}`);
return null;
}

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

function getResponseNode(data) {
if (!data || typeof data !== "object") return null;
return data.response != null ? data.response : data;
}

// Step 1: Receive and verify the webhook request
function verifySignature(rawBody, signatureHeader) {
// If WEBHOOK_SECRET is empty, the signature check is skipped.
if (!WEBHOOK_SECRET) 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", WEBHOOK_SECRET)
.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 file update events and extract the file ID
function extractFileId(payload) {
const n = Number(payload?.id ?? null);
return Number.isFinite(n) ? n : null;
}

// Step 3: Read file information (confirm access + obtain version)
async function getFileInfo(fileId) {
const data = await docspaceRequest(`/api/2.0/files/file/${fileId}`, "GET");
const node = getResponseNode(data);
return node && typeof node === "object" ? node : null;
}

// Step 4: Set the comment to NEED_REVIEW_TEXT
async function setFileComment(fileId, version, comment) {
const payload = { version, comment };
const data = await docspaceRequest(`/api/2.0/files/file/${fileId}/comment`, "PUT", payload);
return Boolean(data);
}

async function handleWebhook(body) {
const { trigger, payload } = extractTriggerAndPayload(body);

if (!payload) {
console.log("[INFO] No payload object. Skipping.");
return;
}

if (!ALLOWED_TRIGGERS.has(trigger)) {
console.log(`[INFO] Trigger '${trigger}' is not handled. Skipping.`);
return;
}

const fileId = extractFileId(payload);
if (fileId == null) {
console.log("[INFO] No valid file ID in payload.id. Skipping.");
return;
}

console.log("[INFO] File update webhook received");
console.log(` trigger: ${trigger}`);
console.log(` fileId: ${fileId}`);

const fileInfo = await getFileInfo(fileId);
if (!fileInfo) {
console.warn(`[WARN] Cannot read file info for fileId=${fileId}. Skipping.`);
return;
}

const fileVersionRaw = fileInfo.version ?? fileInfo.versionNumber ?? 1;
const fileVersion = Number.isFinite(Number(fileVersionRaw)) ? Number(fileVersionRaw) : 1;

const ok = await setFileComment(fileId, fileVersion, NEED_REVIEW_TEXT);
if (ok) {
console.log(`[OK] Comment set for fileId=${fileId}: "${NEED_REVIEW_TEXT}"`);
} else {
console.error(`[ERROR] Failed to set comment for fileId=${fileId}`);
}
}

// Minimal Express webhook receiver
const app = express();

// Capture raw body for signature verification
app.use(
express.json({
limit: "2mb",
verify: (req, _res, buf) => {
req.rawBody = buf;
},
})
);

app.head("/docspace/review-comment", (_req, res) => {
res.status(200).send("");
});

app.get("/docspace/review-comment", (_req, res) => {
res.status(200).json({ status: "ok" });
});

app.post("/docspace/review-comment", async (req, res) => {
if (!req.rawBody) {
console.warn("[WARN] Missing raw body buffer. Cannot verify signature.");
return res.status(400).send("Missing raw body");
}

const signature = req.headers["x-docspace-signature-256"];

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

try {
await handleWebhook(req.body);
} catch (err) {
console.error(`[ERROR] Handler error: ${err?.message || err}`);
return res.status(500).send("Internal error");
}

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

app.listen(3000, () => {
console.log("Webhook server listening on http://localhost:3000/docspace/review-comment");
});

Step 1: Receive the webhook request

When a webhook trigger fires, DocSpace sends an HTTP POST request to your endpoint (for example, /docspace/review-comment).

At this step, the backend:

  • receives the webhook request and reads its JSON body,
  • optionally verifies that the request really comes from DocSpace using the secret key configured for the webhook.

If signature validation is enabled and the check fails, the endpoint returns 401 and stops processing the request.

Signature validation is optional. If WEBHOOK_SECRET is not set, the example skips this check and processes the webhook payload directly.

function verifySignature(rawBody, signatureHeader) {
// If WEBHOOK_SECRET is empty, the signature check is skipped.
if (!WEBHOOK_SECRET) 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", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex")
.toLowerCase();

if (received.length !== expected.length) return false;

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

Step 2: Detect file update events and extract the file ID

The webhook payload is expected to have this structure:

  • event.trigger — trigger name (for example, file.updated)
  • payload — file data
  • payload.id — file ID

Only triggers listed in ALLOWED_TRIGGERS are processed. Others are ignored.

function extractFileId(payload) {
const n = Number(payload?.id ?? null);
return Number.isFinite(n) ? n : null;
}

Step 3: Read file information

The handler calls GET /api/2.0/files/file/:fileId This confirms that the file exists and the access token can read it. The file info is also used as a fallback source for a version value.

async function getFileInfo(fileId) {
const data = await docspaceRequest(`/api/2.0/files/file/${fileId}`, "GET");
const node = getResponseNode(data);
return node && typeof node === "object" ? node : null;
}

Step 4: Set the comment to "Need Review"

For every matching update event, the script sets the file comment to NEED_REVIEW_TEXT PUT /api/2.0/files/file/:fileId/comment

async function setFileComment(fileId, version, comment) {
const payload = { version, comment };
const data = await docspaceRequest(`/api/2.0/files/file/${fileId}/comment`, "PUT", payload);
return Boolean(data);
}