> ## Documentation Index > Fetch the complete documentation index at: https://apidocs.document360.com/llms.txt > Use this file to discover all available pages before exploring further. # Add a comment to an article > Creates a new comment thread on an article's content. **This is the second of two calls.** The marker must already be in the content — update it first via `PATCH /v3/projects/{projectId}/articles/{articleId}`, then create the thread here with the same `comment_id`. The full procedure, and why the order matters, is described below. The comment id is supplied by the caller because it is the same value written into the marker, which also makes a retried request safe: repeating it returns `409` rather than creating a second thread. Every comment this endpoint creates is text-anchored, so the marker check always applies — there is no scope that skips it. The thread is created against the article's latest version in the requested language. **Access:** requires the `articles.update` permission, passes the same node-level ACL as the article itself, and requires a resolvable user identity — a machine-to-machine token with no acting user is rejected with `422`. Authorship is taken from the authenticated identity, never from the request body. ### Related endpoints **Next** - `POST /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies` — Replies to the thread just created. - `PATCH /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}` — Resolves the thread once the discussion is settled. ### This is step 2 of 2 — the marker must already be in the content Call `PATCH /v3/projects/{project_id}/articles/{article_id}` first to put the marker in the content, then this endpoint to create the thread. A comment is **two facts that must agree**: a marker in the content that highlights the annotated text, and the comment thread itself. Two endpoints write them, and both calls must carry the **same `comment_id`**. 1. **Add the marker** — `PATCH /v3/projects/{project_id}/articles/{article_id}` Send the content with the annotated text wrapped in an element carrying `data-comment-id=""`. Generate the id yourself (a GUID) — you reuse it in step 2. 2. **Create the thread** — `POST /v3/projects/{project_id}/articles/{article_id}/comments` Send that same value as `comment_id` in the body. **Order matters.** Step 2 checks the marker is present and returns `422` if it is not, so a thread can never exist without the text it annotates. The reverse is not enforced: a marker with no thread is an inert highlight, so if step 2 fails, either retry it or take the marker back out. The request examples on both operations use the same `comment_id` (`b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547`) and the same annotated text (`single sign-on`), so you can copy step 1, then step 2, and the pair works unchanged. Comments are supported only on block-editor (Editor 3.0) content; any other editor is rejected with `422`. ## OpenAPI ````json POST /v3/projects/{project_id}/articles/{article_id}/comments { "openapi": "3.0.1", "info": { "title": "Document360 Customer API", "description": "Document360 RESTful APIs let you integrate your documentation with your software — onboard readers, manage articles, automate publishing, and more.\n\nFull reference: [API Documentation](https://apidocs.document360.io/docs).\n\n## Authentication\nV3 authenticates with an **API key** — not the `api_token` used by V1/V2. Create one in the Document360 portal under **Settings → Knowledge base portal → API keys**, then click **Create API key**. (Projects that still have the older screen show two tabs; choose **Enhanced keys (v3)**, or pick **API v3** from the Create menu.)\n\nSend it in the `X-API-Key` header:\n\n```\nX-API-Key: d360_sk_...\n```\n\nThe plaintext key is shown **once**, at creation — only its hash is stored, so a lost key must be replaced rather than recovered. Each key carries a fixed portal role, content role and content-access scope, supports an optional expiry, and can be disabled or deleted at any time from the same screen. Treat it as an opaque string: it is a pre-shared secret, not an OAuth 2.0 access token, and must not be parsed.\n\nFor flows that act on behalf of a specific person, V3 also accepts an OAuth 2.0 bearer token carrying the `customerApi` scope.\n\nAPI access is part of your subscription plan. If your plan does not include it, every endpoint returns `403` with the error code `FEATURE_NOT_IN_LICENSE` — including for keys created while an earlier plan was active. Contact your Customer Success Manager or `support@document360.com` to enable it.\n\n> **Note on terminology:** V1/V2 call these *API tokens* and manage them under **Settings → Knowledge base portal → API tokens**. V3 keys are a separate credential with their own screen, roles and scope — a V1/V2 token will not authenticate a V3 request.\n\n## Rate Limits\nAll endpoints are rate-limited per API key per project. Default limits vary by plan. When rate-limited, responses include `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Use exponential backoff with jitter for optimal retry behavior.", "termsOfService": "https://document360.com/terms", "contact": { "name": "Document360 Support", "url": "https://document360.io/contact-us/", "email": "support@document360.com" }, "license": { "name": "Document360 API Terms of Use", "url": "https://document360.com/terms" }, "version": "3.0.0" }, "servers": [ { "url": "https://apihub.document360.io", "description": "Document 360 API Hub" }, { "url": "https://apihub.us.document360.io", "description": "Document360 API Hub - US data center" }, { "url": "https://apihub.{private_hosting}.document360.io", "description": "Private hosting - Please provide the subdomain name.", "variables": { "private_hosting": { "default": "domain", "description": "Sub domain for private hosting" } } } ], "security": [ { "ApiKey": [] }, { "Bearer": [ "customerApi" ] } ], "tags": [ { "name": "Projects", "description": "List and manage knowledge base projects. Export and import project documentation, and manage project-level site customization such as custom CSS and JavaScript." }, { "name": "Projects > Import & export", "description": "Start a project export or import and poll the resulting background operation for completion." }, { "name": "Workspaces", "description": "Manage workspaces and access workspace-scoped categories and articles." }, { "name": "Workspaces > AI", "description": "Query the workspace with Eddy AI and submit feedback on AI answers." }, { "name": "Articles", "description": "Create, read, update, and delete knowledge base articles and their settings, including bulk operations." }, { "name": "Articles > Publishing & workflow", "description": "Publish and unpublish articles (single and bulk) and update the article workflow status." }, { "name": "Articles > Versions", "description": "List, fork, and delete article versions." }, { "name": "Articles > Attachments", "description": "Manage the files attached to an article — list, upload, and delete article attachments." }, { "name": "Articles > Content drafts", "description": "(Internal) Stage AI-generated article content with an Accept/Reject review lifecycle — used by Eddy Copilot to preview content before committing to the live article body." }, { "name": "Articles > Labels", "description": "Read and set the labels associated with an article." }, { "name": "Articles > Comments", "description": "Read and create the comment threads authors and reviewers leave on an article's content, and reply to them. Each comment id matches the `data-comment-id` attribute of the corresponding marker in the raw article content, so adding a text-anchored comment takes two calls: update the content with the marker, then create the thread with the same id." }, { "name": "Categories", "description": "Manage the category hierarchy that organizes articles. Create, update, and delete categories and their settings, including bulk operations." }, { "name": "Categories > Page category", "description": "Read and update the content body of page-type categories, including bulk content updates." }, { "name": "Categories > Publishing & workflow", "description": "Publish and unpublish categories (single and bulk) and update the category workflow status." }, { "name": "Categories > Versions", "description": "List, fork, and delete category versions." }, { "name": "Categories > Labels", "description": "Read and set the labels associated with a category." }, { "name": "Categories > Comments", "description": "Read and create the comment threads authors and reviewers leave on a page category's content, and reply to them. Only page-type categories carry content, so other category types return `404`. Adding a text-anchored comment takes two calls: update the content with the marker, then create the thread with the same id." }, { "name": "ArticleTemplates", "description": "Manage reusable article templates — per-language article skeletons that authors and AI tools (such as Eddy Copilot) can apply when starting a new article.", "x-displayName": "Article templates" }, { "name": "Content reuse > Glossaries", "description": "Manage glossary entries — terminology definitions referenced inside articles via merge codes (`{{glossary.name}}`) and rendered as tooltips on the public KB." }, { "name": "Content reuse > Snippets", "description": "Manage reusable snippets — markdown or WYSIWYG content fragments referenced across articles via merge codes (`{{snippet.name}}`)." }, { "name": "Content reuse > Variables", "description": "Manage reusable variables — short text values referenced across articles via merge codes (`{{variable.name}}`)." }, { "name": "AiWriterStyleGuides", "description": "Retrieve AI writer style guides configured for the project — voice / tone instructions Eddy Copilot follows when generating article content. Used to let users pick a style before kicking off generation.", "x-displayName": "AI writer style guides" }, { "name": "RedirectRules", "description": "Manage article-redirection rules — 301/302 redirects from old article URLs to new locations after renames or restructures.", "x-displayName": "Redirect rules" }, { "name": "Tags", "description": "Manage tags — labels used to classify articles, categories, and files for filtering and discovery." }, { "name": "Labels", "description": "Manage labels shown on the article list and category tree. (Associating labels with a specific article or category lives under Articles and Categories.)" }, { "name": "CustomFields", "description": "Retrieve the custom field definitions configured for the project. Custom field values are read and written through the article and category settings endpoints.", "x-displayName": "Custom fields" }, { "name": "ApiReferences", "description": "Import, resync, and publish OpenAPI/Swagger specifications for API reference documentation.", "x-displayName": "API references" }, { "name": "ApiReferences > Logs", "description": "Monitor API reference import logs and inspect individual log entries." }, { "name": "Drive", "description": "Search Drive and retrieve files associated with an article, and track Drive background tasks." }, { "name": "Drive > Folders", "description": "Create, read, update, and delete Drive folders." }, { "name": "Drive > Files", "description": "Upload, copy, move, delete, and tag Drive files." }, { "name": "Drive > Image drafts", "description": "(Internal) Stage uploaded or AI-generated images (committed to Drive and associated with a staged article draft) — used by Eddy Copilot." }, { "name": "Readers", "description": "Manage reader accounts, invitations, and access scopes for private documentation." }, { "name": "Readers > Groups", "description": "Create, read, update, and delete reader groups." }, { "name": "Users", "description": "Manage users, their portal and content roles, and consolidated (effective) permissions within a project." }, { "name": "Users > Groups", "description": "Manage user groups, their portal role, and consolidated (effective) group permissions." }, { "name": "Roles", "description": "Create, read, update, and delete portal and content roles, list the users assigned to a role, and retrieve the permission dependency matrix." }, { "name": "ContentAccess", "description": "Manage per-node content-access permissions — grant, allow, deny, and inheritance overrides for users and groups against specific versions, languages, categories, or articles, plus version/language site protection.", "x-displayName": "Content access" }, { "name": "ContentAccess > Eligibility", "description": "List the readers and groups eligible to be granted content access." }, { "name": "Languages", "description": "Retrieve configured languages for a workspace." }, { "name": "Translations", "description": "Query translation status of articles across languages." }, { "name": "Operations", "description": "Track asynchronous operations — long-running tasks (bulk updates, merges, exports) accepted with `202 Accepted` and an `Operation-Location` header. Poll until the operation reaches a terminal status, honouring the `Retry-After` header." }, { "name": "Analytics > Articles", "description": "View article performance metrics including views, top articles, reader locations, and category breakdowns." }, { "name": "Analytics > Search", "description": "View search usage metrics including top keywords, no-result queries, and bounced searches." }, { "name": "Analytics > AI search", "description": "View AI-powered search metrics including usage trends, most-referenced articles, and trending topics." }, { "name": "Analytics > Feedback", "description": "View reader feedback trends across articles, including top-performing articles and feedback keywords." } ], "externalDocs": { "description": "Document360 API Documentation", "url": "https://apidocs.document360.io/docs" }, "x-tagGroups": [ { "name": "Workspace", "tags": [ "Projects", "Projects > Import & export", "Workspaces", "Workspaces > AI" ] }, { "name": "Content", "tags": [ "Articles", "Articles > Publishing & workflow", "Articles > Versions", "Articles > Attachments", "Articles > Content drafts", "Articles > Labels", "Categories", "Categories > Page category", "Categories > Publishing & workflow", "Categories > Versions", "Categories > Labels", "ArticleTemplates", "AiWriterStyleGuides", "RedirectRules", "Tags", "Labels", "ApiReferences", "ApiReferences > Logs" ] }, { "name": "Content reuse", "tags": [ "Content reuse > Glossaries", "Content reuse > Snippets", "Content reuse > Variables" ] }, { "name": "Media", "tags": [ "Drive", "Drive > Folders", "Drive > Files", "Drive > Image drafts" ] }, { "name": "Access", "tags": [ "Readers", "Readers > Groups", "Users", "Users > Groups", "Roles", "ContentAccess", "ContentAccess > Eligibility" ] }, { "name": "Localization", "tags": [ "Languages", "Translations" ] }, { "name": "Operations", "tags": [ "Operations" ] }, { "name": "Analytics", "tags": [ "Analytics > Articles", "Analytics > Search", "Analytics > AI search", "Analytics > Feedback" ] } ], "paths": { "/v3/projects/{project_id}/articles/{article_id}/comments": { "post": { "tags": [ "Articles > Comments" ], "summary": "Add a comment to an article.", "description": "Creates a new comment thread on an article's content.\r\n \r\n**This is the second of two calls.** The marker must already be in the content — update it first via\r\n`PATCH /v3/projects/{projectId}/articles/{articleId}`, then create the thread here with the same\r\n`comment_id`. The full procedure, and why the order matters, is described below.\r\n \r\nThe comment id is supplied by the caller because it is the same value written into the marker, which\r\nalso makes a retried request safe: repeating it returns `409` rather than creating a second\r\nthread. Every comment this endpoint creates is text-anchored, so the marker check always applies —\r\nthere is no scope that skips it. The thread is created against the article's latest version in the\r\nrequested language.\r\n \r\n**Access:** requires the `articles.update` permission, passes the same node-level ACL as the\r\narticle itself, and requires a resolvable user identity — a machine-to-machine token with no acting\r\nuser is rejected with `422`. Authorship is taken from the authenticated identity, never from the\r\nrequest body.\n\n### Related endpoints\n\n**Next**\n- `POST /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies` — Replies to the thread just created.\n- `PATCH /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}` — Resolves the thread once the discussion is settled.\n\n### This is step 2 of 2 — the marker must already be in the content\n\nCall `PATCH /v3/projects/{project_id}/articles/{article_id}` first to put the marker in the content, then this endpoint to create the thread.\n\nA comment is **two facts that must agree**: a marker in the content that highlights the annotated text, and\nthe comment thread itself. Two endpoints write them, and both calls must carry the **same `comment_id`**.\n\n1. **Add the marker** — `PATCH /v3/projects/{project_id}/articles/{article_id}`\n Send the content with the annotated text wrapped in an element carrying `data-comment-id=\"\"`.\n Generate the id yourself (a GUID) — you reuse it in step 2.\n2. **Create the thread** — `POST /v3/projects/{project_id}/articles/{article_id}/comments`\n Send that same value as `comment_id` in the body.\n\n**Order matters.** Step 2 checks the marker is present and returns `422` if it is not, so a thread can never\nexist without the text it annotates. The reverse is not enforced: a marker with no thread is an inert\nhighlight, so if step 2 fails, either retry it or take the marker back out.\n\nThe request examples on both operations use the same `comment_id` (`b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547`) and\nthe same annotated text (`single sign-on`), so you can copy step 1, then step 2, and\nthe pair works unchanged.\n\nComments are supported only on block-editor (Editor 3.0) content; any other editor is rejected with `422`.", "operationId": "createArticleCommentArticleComment", "parameters": [ { "$ref": "#/components/parameters/ProjectId" }, { "$ref": "#/components/parameters/ArticleId" }, { "$ref": "#/components/parameters/LangCode" } ], "requestBody": { "description": "The comment to create.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCommentRequest" }, "examples": { "Step 2 of 2 — create the comment thread": { "summary": "The second of the two calls that add a comment. It pairs with the *Step 1 of 2 — add a comment marker to the content* example on `PATCH /v3/projects/{projectId}/articles/{articleId}` (or `PATCH /v3/projects/{projectId}/categories/{categoryId}/content` for a page category) — same `comment_id` (`b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547`), so the two examples are one worked flow:\n\n1. `PATCH` the content so it contains `

Enable single sign-on before inviting readers.

`\n2. `POST` this body.\n\nThe order matters: this endpoint reads the stored content and returns `422` naming the id if the marker is absent, so a comment can never exist without the highlight it annotates. Sending the same `comment_id` twice returns `409` rather than creating a second thread, which makes a retried request safe to repeat.", "value": { "comment_id": "b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547", "comment": "

Should we link the SSO setup guide here?

", "title": "single sign-on", "mentioned_user_ids": [ "7c1e4d90-2a3b-4c5d-8e9f-0a1b2c3d4e5f" ] } }, "Minimal — no mentions": { "summary": "Only the three fields a comment needs. `title` is optional but worth sending: it is what the comment list shows as the thread heading, and the annotated text is the natural value.", "value": { "comment_id": "9f2a4b18-6c71-4e0d-8b53-1a7f4c92d8e0", "comment": "

This page needs a prerequisites section before the first procedure.

", "title": null, "mentioned_user_ids": [] } } } } } }, "responses": { "201": { "description": "Comment created successfully.", "headers": { "Location": { "description": "URL of the newly created resource.", "schema": { "type": "string", "format": "uri" } }, "X-RateLimit-Limit": { "description": "The maximum number of requests allowed in the current time window for this request's bucket. Read requests (GET/HEAD) and write requests (POST/PUT/PATCH/DELETE) have independent limits, applied per caller (API key or user) per project.", "schema": { "type": "integer", "format": "int32" } }, "X-RateLimit-Remaining": { "description": "The number of requests remaining in the current time window. When this reaches 0, subsequent requests receive a 429 response.", "schema": { "type": "integer", "format": "int32" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CommentResponseApiResponse" }, "examples": { "Comment created": { "summary": "A new open thread with no replies yet.", "value": { "data": { "id": "b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547", "title": "single sign-on", "comment": "

Should we link the SSO setup guide here?

", "commented_text": "single sign-on", "commented_text_truncated": false, "status": "open", "author_user_id": "7c1e4d90-2a3b-4c5d-8e9f-0a1b2c3d4e5f", "resolved_by_user_id": null, "mentioned_user_ids": [ "7c1e4d90-2a3b-4c5d-8e9f-0a1b2c3d4e5f" ], "replies": [], "replies_count": 0, "created_at": "2026-07-29T09:41:12.48Z", "modified_at": "2026-07-29T09:41:12.48Z" }, "success": true, "request_id": "0HNNG2FP7GHU5:00000003", "errors": null, "warnings": null } } } } }, "links": { "createArticleCommentArticleCommentReply": { "operationId": "createArticleCommentArticleCommentReply", "description": "Replies to the thread just created." }, "updateArticleCommentArticleComment": { "operationId": "updateArticleCommentArticleComment", "description": "Resolves the thread once the discussion is settled." }, "updateContentMarker": { "operationId": "updateArticleArticle", "parameters": { "project_id": "$request.path.project_id", "article_id": "$request.path.article_id" }, "description": "Step 1 — update this article's content so the annotated text carries the marker for this comment_id. Call this first; a comment whose marker is missing is rejected." } } }, "401": { "description": "Authentication token is missing or invalid.", "headers": { "WWW-Authenticate": { "description": "Indicates the authentication scheme required. Returns `Bearer` with optional `error` and `error_description` parameters per RFC 6750.", "schema": { "type": "string" } } }, "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/unauthorized" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/unauthorized` for 401 responses." }, "status": { "enum": [ 401 ], "type": "integer", "description": "HTTP status code. Always `401` for this response.", "format": "int32" } } } ] }, "examples": { "Missing or invalid token": { "summary": "Authentication token is missing or invalid.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/unauthorized", "title": "Unauthorized.", "status": 401, "detail": "The authentication token is missing or has expired.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "UNAUTHORIZED", "message": "Bearer token is missing or invalid.", "field": null, "details": null } ], "warnings": null } } } } } }, "403": { "description": "The role does not grant `articles.update`, or node-level access to the article is denied.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/forbidden" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/forbidden` for 403 responses." }, "status": { "enum": [ 403 ], "type": "integer", "description": "HTTP status code. Always `403` for this response.", "format": "int32" } } } ] }, "examples": { "Insufficient permissions": { "summary": "Insufficient permissions for this resource.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/forbidden", "title": "Forbidden.", "status": 403, "detail": "You do not have permission to perform this action.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "FORBIDDEN", "message": "Insufficient permissions for this project.", "field": null, "details": null } ], "warnings": null } } } } } }, "404": { "description": "Article not found, or the requested language does not exist on it.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/resource-not-found" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/resource-not-found` for 404 responses." }, "status": { "enum": [ 404 ], "type": "integer", "description": "HTTP status code. Always `404` for this response.", "format": "int32" } } } ] }, "examples": { "Resource not found": { "summary": "The requested resource was not found.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/resource-not-found", "title": "Not Found.", "status": 404, "detail": "The requested resource does not exist or has been deleted.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "RESOURCE_NOT_FOUND", "message": "The requested resource was not found.", "field": null, "details": null } ], "warnings": null } } } } } }, "409": { "description": "A comment with this `comment_id` already exists on this article and language, or the article has reached its comment limit.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/conflict" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/conflict` for 409 responses." }, "status": { "enum": [ 409 ], "type": "integer", "description": "HTTP status code. Always `409` for this response.", "format": "int32" } } } ] }, "examples": { "Resource conflict": { "summary": "The request conflicts with the current state of the resource.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/conflict", "title": "Conflict.", "status": 409, "detail": "The resource is in a state that conflicts with this operation.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "CONFLICT", "message": "A draft version already exists for this resource.", "field": null, "details": null } ], "warnings": null } } } } } }, "422": { "description": "The payload is invalid, the article is not block-editor content, or the content does not contain a marker for this `comment_id`, or one or more `mentioned_user_ids` are not members of this project.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/validation-error" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/validation-error` for 422 responses." }, "status": { "enum": [ 422 ], "type": "integer", "description": "HTTP status code. Always `422` for this response.", "format": "int32" } } } ] }, "examples": { "Validation failed": { "summary": "The request body contains invalid data.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/validation-error", "title": "Unprocessable Entity.", "status": 422, "detail": "One or more fields failed validation.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "VALIDATION_ERROR", "message": "This field is required.", "field": "title", "details": null } ], "warnings": null } } } } } }, "400": { "description": "The request body is malformed or contains invalid JSON.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/bad-request" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/bad-request` for 400 responses." }, "status": { "enum": [ 400 ], "type": "integer", "description": "HTTP status code. Always `400` for this response.", "format": "int32" } } } ] }, "examples": { "Malformed request body": { "summary": "The request body could not be parsed.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/bad-request", "title": "Bad Request.", "status": 400, "detail": "The request body is malformed or contains invalid JSON.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "BAD_REQUEST", "message": "Could not parse the request body. Ensure it is valid JSON.", "field": null, "details": null } ], "warnings": null } } } } } }, "429": { "description": "Rate limit exceeded. Retry after the duration specified in the Retry-After header.", "headers": { "Retry-After": { "description": "Number of seconds to wait before retrying the request. Use exponential backoff with jitter for optimal retry behavior.", "schema": { "type": "integer", "format": "int32" } }, "X-RateLimit-Limit": { "description": "The maximum number of requests allowed in the current time window for this request's bucket. Read requests (GET/HEAD) and write requests (POST/PUT/PATCH/DELETE) have independent limits, applied per caller (API key or user) per project. Read limits are typically higher than write limits.", "schema": { "type": "integer", "format": "int32" } }, "X-RateLimit-Remaining": { "description": "The number of requests remaining in the current time window. When this reaches 0, subsequent requests will receive a 429 response.", "schema": { "type": "integer", "format": "int32" } }, "X-RateLimit-Reset": { "description": "The UTC epoch timestamp (in seconds) when the current rate limit window resets.", "schema": { "type": "integer", "format": "int64" } } }, "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/too-many-requests" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/too-many-requests` for 429 responses." }, "status": { "enum": [ 429 ], "type": "integer", "description": "HTTP status code. Always `429` for this response.", "format": "int32" } } } ] }, "examples": { "Rate limit exceeded": { "summary": "Rate limit exceeded.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/too-many-requests", "title": "Too Many Requests.", "status": 429, "detail": "Rate limit exceeded. Retry after the duration specified in the Retry-After header.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "TOO_MANY_REQUESTS", "message": "Rate limit exceeded. Retry after the duration specified in the Retry-After header.", "field": null, "details": null } ], "warnings": null } } } } } }, "500": { "description": "An unexpected server error occurred.", "content": { "application/problem+json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/V3ProblemDetails" }, { "type": "object", "properties": { "type": { "enum": [ "https://apidocs.document360.io/apidocs/errors/internal-server-error" ], "type": "string", "description": "RFC 7807 error type URI. Always `https://apidocs.document360.io/apidocs/errors/internal-server-error` for 500 responses." }, "status": { "enum": [ 500 ], "type": "integer", "description": "HTTP status code. Always `500` for this response.", "format": "int32" } } } ] }, "examples": { "Unexpected server error": { "summary": "Unexpected server error.", "value": { "type": "https://apidocs.document360.io/apidocs/errors/internal-server-error", "title": "Internal Server Error.", "status": 500, "detail": "An unexpected error occurred. Please try again or contact support.", "instance": null, "trace_id": "req_abc123def456", "errors": [ { "code": "INTERNAL_SERVER_ERROR", "message": "An unexpected error occurred.", "field": null, "details": null } ], "warnings": null } } } } } } }, "security": [ { "ApiKey": [] }, { "Bearer": [ "customerApi" ] } ], "x-related-endpoints": [ { "relationship": "next", "method": "POST", "path": "/v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies", "operationId": "createArticleCommentArticleCommentReply", "description": "Replies to the thread just created." }, { "relationship": "next", "method": "PATCH", "path": "/v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}", "operationId": "updateArticleCommentArticleComment", "description": "Resolves the thread once the discussion is settled." } ] } } }, "components": { "parameters": { "ProjectId": { "name": "project_id", "in": "path", "description": "The unique identifier of the project. Retrieve project IDs from `GET /v3/projects`.", "required": true, "schema": { "type": "string", "format": "uuid", "example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d" } }, "ArticleId": { "name": "article_id", "in": "path", "description": "The unique identifier of the article. Retrieve article IDs from `GET /v3/projects/{projectId}/articles`.", "required": true, "schema": { "type": "string", "format": "uuid", "example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d" } }, "LangCode": { "name": "lang_code", "in": "query", "description": "ISO 639-1 language code (e.g., `en`, `fr`). Defaults to the project's primary language if omitted.", "schema": { "pattern": "^[a-z]{2}(-[A-Z]{2})?$", "type": "string", "format": "language-tag", "example": "en" } } }, "schemas": { "CreateCommentRequest": { "required": [ "comment", "comment_id" ], "type": "object", "properties": { "comment_id": { "maxLength": 200, "minLength": 1, "type": "string", "description": "Identifier for the new comment thread. Must match the `data-comment-id` attribute of the\r\ncorresponding marker in the content, and must be unique for this article/category and language.", "example": "b7f3c1a0-5e42-4d18-9a6c-2f8091d3e547" }, "comment": { "maxLength": 3000, "minLength": 1, "type": "string", "description": "The comment body. HTML is sanitized; disallowed markup is stripped rather than rejected.", "example": "

Should we link the SSO setup guide here?

" }, "title": { "maxLength": 300, "minLength": 0, "type": "string", "description": "Optional short title for the thread, typically the annotated text.", "nullable": true, "example": "single sign-on" }, "mentioned_user_ids": { "type": "array", "items": { "type": "string" }, "description": "Identifiers of team accounts to @-mention in the comment.", "nullable": true } }, "additionalProperties": false, "description": "Creates a new inline comment thread on an article or page category." }, "CommentResponseApiResponse": { "required": [ "data", "request_id", "success" ], "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/CommentResponse" } ], "description": "Response data payload." }, "success": { "type": "boolean", "description": "Whether the API request was successful.", "readOnly": true }, "request_id": { "minLength": 1, "type": "string", "description": "Unique identifier for request tracing and correlation.", "readOnly": true }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ApiError" }, "description": "List of errors if the request failed.", "nullable": true }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/ApiWarning" }, "description": "List of non-fatal warnings from the request.", "nullable": true } }, "additionalProperties": false, "description": "Generic API response wrapper containing typed data." }, "V3ProblemDetails": { "required": [ "status", "title", "type" ], "type": "object", "properties": { "type": { "minLength": 1, "enum": [ "https://apidocs.document360.io/apidocs/errors/bad-request", "https://apidocs.document360.io/apidocs/errors/unauthorized", "https://apidocs.document360.io/apidocs/errors/forbidden", "https://apidocs.document360.io/apidocs/errors/resource-not-found", "https://apidocs.document360.io/apidocs/errors/conflict", "https://apidocs.document360.io/apidocs/errors/validation-error", "https://apidocs.document360.io/apidocs/errors/too-many-requests", "https://apidocs.document360.io/apidocs/errors/internal-server-error" ], "type": "string", "description": "URI reference identifying the error type (links to documentation)." }, "title": { "minLength": 1, "type": "string", "description": "Short human-readable summary of the error type." }, "status": { "enum": [ 400, 401, 403, 404, 409, 422, 429, 500 ], "type": "integer", "description": "HTTP status code.", "format": "int32" }, "detail": { "type": "string", "description": "Human-readable explanation specific to this occurrence.", "nullable": true }, "instance": { "type": "string", "description": "URI of the request that generated the error.", "nullable": true }, "trace_id": { "type": "string", "description": "Request trace identifier for correlation.", "nullable": true }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ApiError" }, "description": "Structured list of specific errors (extension field).", "nullable": true }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/ApiWarning" }, "description": "Non-fatal warnings (extension field).", "nullable": true } }, "additionalProperties": false, "description": "RFC 7807 Problem Details response for V3 API errors.\r\nContent-Type: application/problem+json" }, "CommentResponse": { "required": [ "commented_text_truncated", "created_at", "id", "modified_at", "replies_count", "status", "title" ], "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier of the comment thread, and the value carried by the comment's marker in the content\r\nas `data-comment-id`.\r\n\n\r\nDo not rely on finding it in a `content_mode=raw` response. Raw mode returns the DFM markdown\r\nrepresentation, and a `` marker does not survive the conversion into it — only the\r\nEditor 3.0 `` form passes through. Use Document360.Customer.API.Models.V3.CommentResponse.CommentedText to learn what a\r\ncomment annotates rather than searching the content yourself.\r\n", "format": "uuid", "nullable": true, "readOnly": true, "example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d" }, "title": { "type": "string", "description": "Optional title/summary of the comment thread.", "nullable": true }, "comment": { "type": "string", "description": "The comment body (sanitized HTML).", "nullable": true }, "commented_text": { "type": "string", "description": "The text in the document that this comment annotates — the content inside its highlight marker — as\r\nplain text.\r\n\n\r\nRead from the document's current content on every request rather than stored with the comment, so it\r\nalways reflects what the text says now. A consequence worth knowing: edit the annotated sentence and\r\nthis value changes; it is not a record of what the text said when the comment was written.\r\n\n`null` when the annotated text cannot be determined, which is an ordinary outcome and not an\r\nerror — the marker was removed (the thread is orphaned and now annotates nothing), the document is\r\nstored in a form this cannot read, or the comment predates the marker convention. Treat it as\r\n\"unknown\", and do not assume a comment with `null` here is invalid.\r\n\n\r\nA comment may annotate a selection spanning several paragraphs; those fragments are joined with single\r\nspaces, and runs of whitespace from the document's own markup are collapsed. Long selections are\r\ntruncated — see Document360.Customer.API.Models.V3.CommentResponse.CommentedTextTruncated.\r\n", "nullable": true, "readOnly": true, "example": "single sign-on" }, "commented_text_truncated": { "type": "boolean", "description": "Whether Document360.Customer.API.Models.V3.CommentResponse.CommentedText was cut short because the annotated selection exceeded the\r\nresponse limit. When `true`, the value is a prefix of the real annotated text, not the whole of\r\nit — fetch the document content if the full span matters.", "readOnly": true }, "status": { "enum": [ "open", "resolved", "reopened" ], "type": "string", "allOf": [ { "$ref": "#/components/schemas/CommentStatus" } ], "description": "Resolution status of the thread.", "x-enumNames": [ "Open", "Resolved", "Reopened" ], "x-enum-varnames": [ "Open", "Resolved", "Reopened" ], "x-ms-enum": { "name": "CommentStatus", "modelAsString": true } }, "author_user_id": { "type": "string", "description": "Identifier of the team account that created the comment.", "nullable": true }, "resolved_by_user_id": { "type": "string", "description": "Identifier of the team account that resolved the thread. `null` when still open.", "nullable": true }, "mentioned_user_ids": { "type": "array", "items": { "type": "string" }, "description": "Identifiers of team accounts @-mentioned in the comment.", "nullable": true }, "replies": { "type": "array", "items": { "$ref": "#/components/schemas/CommentReplyResponse" }, "description": "Replies on this comment thread.", "nullable": true }, "replies_count": { "type": "integer", "description": "Total number of replies on the thread.", "format": "int32" }, "created_at": { "type": "string", "description": "UTC timestamp when the comment was created.", "format": "date-time", "readOnly": true }, "modified_at": { "type": "string", "description": "UTC timestamp when the comment was last modified.", "format": "date-time", "readOnly": true } }, "additionalProperties": false, "description": "A single inline comment thread left on an article or category by an author/reviewer, together with its\r\nreplies. The `id` matches the `data-comment-id` attribute of the comment's marker in the\r\ncontent, so a consumer editing the content can re-associate a comment with the exact text it annotates.\r\n\n\r\nTwo marker forms exist, and both are valid — which one a document uses depends on the editor that wrote\r\nit: `` for block (Advanced WYSIWYG) content, and\r\n`` for Editor 3.0. Match on the attribute\r\nrather than on the element name.\r\n" }, "ApiError": { "required": [ "code", "message" ], "type": "object", "properties": { "code": { "minLength": 1, "enum": [ "ARCHIVED_CONTENT_READ_ONLY", "ARCHIVE_ALREADY_ARCHIVED", "ARCHIVE_NOT_ARCHIVED", "BAD_REQUEST", "CONFLICT", "FEATURE_NOT_IN_LICENSE", "FORBIDDEN", "INTERNAL_SERVER_ERROR", "LICENSE_LIMIT_EXCEEDED", "OPERATION_FAILED", "PREMIUM_FEATURE_NOT_IN_LICENSE", "RESOURCE_NOT_FOUND", "TOO_MANY_REQUESTS", "UNAUTHORIZED", "UNRECOGNIZED_FIELD", "UNRECOGNIZED_FIELDS", "VALIDATION_ERROR" ], "type": "string", "description": "Machine-readable error code (e.g. VALIDATION_ERROR, RESOURCE_NOT_FOUND)." }, "message": { "minLength": 1, "type": "string", "description": "Human-readable error message." }, "field": { "type": "string", "description": "The request field that caused the error, if applicable.", "nullable": true }, "details": { "type": "string", "description": "Additional context about the error.", "nullable": true } }, "additionalProperties": false, "description": "Represents an error returned by the API." }, "ApiWarning": { "required": [ "code", "message" ], "type": "object", "properties": { "code": { "minLength": 1, "type": "string", "description": "Machine-readable warning code." }, "message": { "minLength": 1, "type": "string", "description": "Human-readable warning message." } }, "additionalProperties": false, "description": "Represents a non-fatal warning from the API." }, "CommentStatus": { "enum": [ "open", "resolved", "reopened" ], "type": "string", "description": "Resolution status of an inline comment thread.", "x-enumNames": [ "Open", "Resolved", "Reopened" ], "x-enum-varnames": [ "Open", "Resolved", "Reopened" ], "x-ms-enum": { "name": "CommentStatus", "modelAsString": true } }, "CommentReplyResponse": { "required": [ "created_at", "id", "modified_at" ], "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier of the reply.", "format": "uuid", "nullable": true, "readOnly": true, "example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d" }, "comment": { "type": "string", "description": "The reply body (sanitized HTML).", "nullable": true }, "author_user_id": { "type": "string", "description": "Identifier of the team account that created the reply.", "nullable": true }, "mentioned_user_ids": { "type": "array", "items": { "type": "string" }, "description": "Identifiers of team accounts @-mentioned in the reply.", "nullable": true }, "created_at": { "type": "string", "description": "UTC timestamp when the reply was created.", "format": "date-time", "readOnly": true }, "modified_at": { "type": "string", "description": "UTC timestamp when the reply was last modified.", "format": "date-time", "readOnly": true } }, "additionalProperties": false, "description": "A reply within an inline comment thread." } }, "securitySchemes": { "ApiKey": { "type": "apiKey", "description": "API key for machine-to-machine integrations, webhooks, and CI/CD (recommended). This is a static, pre-shared secret - treat it as an opaque string and do not parse it; it is an API key, not an OAuth 2.0 access token. Create one in the portal under **Settings > Knowledge base portal > API keys** using the **Create API key** button - this is a different credential from the V1/V2 *API token*, which lives on its own screen and will not authenticate a V3 request. The plaintext key (format `d360_sk_...`) is shown only once, at creation. Send it in the `X-API-Key` header: `X-API-Key: d360_sk_...`. Each key carries a fixed portal role, content role, and content-access scope, supports an optional expiry, and can be disabled or deleted at any time from the portal.", "name": "X-API-Key", "in": "header" }, "Bearer": { "type": "oauth2", "description": "All V3 endpoints require a Bearer token. Generate tokens in the Document360 portal under **Settings > API Tokens**. Tokens are project-scoped, require the `customerApi` scope, and do not expire by default. Tokens can be revoked at any time from the portal. Include the token in every request: `Authorization: Bearer `. Alternatively, use the Authorize button below to sign in via OAuth2 Authorization Code flow with PKCE.", "flows": { "authorizationCode": { "authorizationUrl": "https://identity.document360.io/connect/authorize", "tokenUrl": "https://identity.document360.io/connect/token", "scopes": { "openid": "OpenID Connect", "profile": "User profile", "email": "User email", "customerApi": "Document360 Customer API" }, "x-usePkce": { "disableManualConfiguration": true, "hideClientSecretInput": true } } }, "x-d360-clientId": "apiHubWebClient" } } } } ````