> ## 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.
# Resolve, reopen, or edit a comment on an article
> Updates an existing thread. Send `status` to resolve or reopen it, `comment` to replace its
body, or both in one call. Omitted fields are left untouched.
**Resolving is idempotent.** Sending the same `status` twice is accepted and writes nothing the
second time, so a client that retries after a timeout cannot flip the thread back. A transition that
cannot be reached is refused with `422` rather than silently applied as something else: a thread
becomes `reopened` only after having been `resolved`, and `open` cannot be set at all.
**The two fields have different access rules**, which is why one call can succeed for one and fail for
the other:
Replies are not edited here — see
`POST /v3/projects/{projectId}/articles/{articleId}/comments/{commentId}/replies` to add one.
### Related endpoints
**Before you call this**
- `GET /v3/projects/{project_id}/articles/{article_id}/comments` — Lists the threads on the article. Supplies the `comment_id` to update, and the current `status` — which determines whether `resolved` or `reopened` is reachable.
**See also**
- `POST /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies` — Adds a reply. Editing the body here is restricted to the thread's author; anyone else should reply instead.
## OpenAPI
````json PATCH /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}
{
"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/{comment_id}": {
"patch": {
"tags": [
"Articles > Comments"
],
"summary": "Resolve, reopen, or edit a comment on an article.",
"description": "Updates an existing thread. Send `status` to resolve or reopen it, `comment` to replace its\r\nbody, or both in one call. Omitted fields are left untouched.\r\n \r\n**Resolving is idempotent.** Sending the same `status` twice is accepted and writes nothing the\r\nsecond time, so a client that retries after a timeout cannot flip the thread back. A transition that\r\ncannot be reached is refused with `422` rather than silently applied as something else: a thread\r\nbecomes `reopened` only after having been `resolved`, and `open` cannot be set at all.\r\n \r\n**The two fields have different access rules**, which is why one call can succeed for one and fail for\r\nthe other:\r\n
\r\n \r\nReplies are not edited here — see\r\n`POST /v3/projects/{projectId}/articles/{articleId}/comments/{commentId}/replies` to add one.\n\n### Related endpoints\n\n**Before you call this**\n- `GET /v3/projects/{project_id}/articles/{article_id}/comments` — Lists the threads on the article. Supplies the `comment_id` to update, and the current `status` — which determines whether `resolved` or `reopened` is reachable.\n\n**See also**\n- `POST /v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies` — Adds a reply. Editing the body here is restricted to the thread's author; anyone else should reply instead.",
"operationId": "updateArticleCommentArticleComment",
"parameters": [
{
"$ref": "#/components/parameters/ProjectId"
},
{
"$ref": "#/components/parameters/ArticleId"
},
{
"name": "comment_id",
"in": "path",
"description": "The identifier of the comment thread to update.",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d"
},
"example": "9a3b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d"
},
{
"$ref": "#/components/parameters/LangCode"
}
],
"requestBody": {
"description": "The status and/or body to apply.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateCommentRequest"
},
"examples": {
"Resolve the thread": {
"summary": "Marks the discussion settled. Safe to repeat: a thread that is already resolved is left alone and still returns 200, so a retry after a timeout cannot reopen it.",
"value": {
"status": "resolved",
"comment": null,
"mentioned_user_ids": null
}
},
"Reopen a resolved thread": {
"summary": "Returns a resolved thread to discussion. Only reachable from `resolved` — reopening a thread that was never resolved is rejected with 422, and `open` cannot be set at all.",
"value": {
"status": "reopened",
"comment": null,
"mentioned_user_ids": null
}
},
"Edit the comment body (author only)": {
"summary": "Replaces the body and its @-mentions. Restricted to the thread's original author whatever their permissions; anyone else gets 403 and should reply instead.",
"value": {
"status": null,
"comment": "
Confirmed with security — ninety days is correct.
", "mentioned_user_ids": [ "7c1e4d90-2a3b-4c5d-8e9f-0a1b2c3d4e5f" ] } }, "Edit and resolve in one call": { "summary": "Both fields applied together. Because their access rules differ, a caller who is permitted to resolve but is not the author will be refused — send the two changes separately if that applies.", "value": { "status": "resolved", "comment": "Confirmed with security — ninety days is correct.
", "mentioned_user_ids": null } } } } } }, "responses": { "200": { "description": "Comment updated, or already in the requested state.", "headers": { "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 resolved": { "summary": "`status` is now `resolved` and `resolved_by_user_id` names the user who resolved it — both read back from storage rather than echoed from the request.", "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": "resolved", "author_user_id": "7c1e4d90-2a3b-4c5d-8e9f-0a1b2c3d4e5f", "resolved_by_user_id": "9d2f5e01-3b4c-5d6e-9f0a-1b2c3d4e5f60", "mentioned_user_ids": [], "replies": [ { "id": "4b81c07e-9d35-4a62-8f10-6c27b5e0a913", "comment": "Agreed, linking it now.
", "author_user_id": "9d2f5e01-3b4c-5d6e-9f0a-1b2c3d4e5f60", "mentioned_user_ids": [], "created_at": "2026-07-29T10:02:44.12Z", "modified_at": "2026-07-29T10:02:44.12Z" } ], "replies_count": 1, "created_at": "2026-07-29T09:41:12.48Z", "modified_at": "2026-07-29T11:15:03.22Z" }, "success": true, "request_id": "0HNNG2FP7GHU5:00000003", "errors": null, "warnings": null } } } } } }, "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`, node-level access is denied, the role cannot resolve comments, or the body edit was attempted by someone other than the author.", "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, language, or comment thread not found.", "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": "The comment's status changed while this request was being processed (another update, or a resolve-all). Re-read the comment and retry if the change still applies.", "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, `status` is `open`, or the requested status cannot be reached from the thread's current state, 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": "prerequisite", "method": "GET", "path": "/v3/projects/{project_id}/articles/{article_id}/comments", "operationId": "getArticleCommentArticleComments", "description": "Lists the threads on the article. Supplies the `comment_id` to update, and the current `status` — which determines whether `resolved` or `reopened` is reachable." }, { "relationship": "related", "method": "POST", "path": "/v3/projects/{project_id}/articles/{article_id}/comments/{comment_id}/replies", "operationId": "createArticleCommentArticleCommentReply", "description": "Adds a reply. Editing the body here is restricted to the thread's author; anyone else should reply instead." } ] } } }, "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": { "UpdateCommentRequest": { "type": "object", "properties": { "status": { "allOf": [ { "$ref": "#/components/schemas/CommentStatus" } ], "description": "New resolution status: `resolved` or `reopened`. Omit to leave the status untouched.\r\n`open` is not accepted — a thread returns to discussion as `reopened`, which records that it\r\nhad been resolved once.", "nullable": true, "example": "resolved" }, "comment": { "maxLength": 3000, "minLength": 1, "type": "string", "description": "Replacement comment body. Author-only. Omit to leave the body untouched.", "nullable": true, "example": "Confirmed with security — ninety days is correct.
" }, "mentioned_user_ids": { "type": "array", "items": { "type": "string" }, "description": "Replaces the @-mentions on the comment. Only applied when `comment` is also supplied, because the\r\ntwo are stored by the same operation.", "nullable": true } }, "additionalProperties": false, "description": "Updates an existing comment thread: its resolution status, its body, or both." }, "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" }, "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 } }, "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." }, "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