{"openapi":"3.1.2","info":{"title":"RECOMS API","version":"v1","contact":{"name":"RECOMS","url":"https://recoms.tech"},"license":{"name":"PolyForm Perimeter 1.0.1","url":"https://polyformproject.org/licenses/perimeter/1.0.1/"},"description":"Headless comments and project-management API. Your backend owns end-user authentication and UI.\n\n## Authentication and permissions\nSend Authorization: Bearer <project-api-key> from your backend. The key selects the project. See ProjectApiKey for the complete permission list. New keys default to comments:read and comments:write; other permissions require an explicit grant. Existing keys retain their stored permissions. Management operations document their required permissions. Missing, invalid, disabled, revoked or expired keys return 401 AUTHENTICATION_REQUIRED. A valid key without the required permission returns 403 FORBIDDEN; grant that permission or use an appropriately scoped key instead of retrying unchanged. Author identity and ownership are separate checks.\n\n## Responses and errors\nSuccessful responses contain data; paginated lists also contain nextCursor. Failures use a non-2xx HTTP status and error.code/error.message; branch on code, never message text. VALIDATION_ERROR also requires nonempty error.details with location, path and rule. Every response includes X-Request-Id. Credentials and internal diagnostics are never exposed.\n\n## Submissions and retries\nMutation bodies have a 16 KiB transport limit in addition to field constraints. Comment and reaction mutations support optional Idempotency-Key replay; management mutations do not. A successful submission is not necessarily published: inspect latestRevision.status. Reads and idempotent replays do not consume write permits.\n\n## Agent metadata\nThese extensions describe behavior and client guidance. They do not enforce consent or replace server authorization. This detailed specification is the shared source of truth for documentation, SDKs and AI agents.\n\n| Extension field | Value | Meaning |\n| --- | --- | --- |\n| x-agent.effect | read | Reads state without changing product data |\n| x-agent.effect | write | Creates or changes product data |\n| x-agent.effect | destructive | Deletes or removes product data or visibility |\n| x-agent.effect | diagnostic | Calls a provider for a test; may consume provider capacity and store audit or connection-health state, but does not save a policy or comment |\n| x-agent.confirmation | none | No additional confirmation is recommended by this operation |\n| x-agent.confirmation | required | Obtain the user's authorization before executing the action |\n| x-agent.retryPolicy | safe | The read can be repeated; results may change between requests |\n| x-agent.retryPolicy | idempotency_key | Preserve the original Idempotency-Key and request after an uncertain result; obey the documented replay window and Retry-After |\n| x-agent.retryPolicy | refresh_before_retry | Refresh relevant state after an uncertain result before deciding whether to repeat; no idempotent replay is provided |\n| x-agent.permissions | Object of resource names to action arrays | Required key permissions; for example, comments: [read] requires comments:read |\n| x-error-codes | Array of error.code strings on a response | Codes applicable to that operation and HTTP status; the shared ErrorResponse contains the full public code vocabulary |\n"},"externalDocs":{"description":"RECOMS documentation and quick start","url":"https://recoms.tech/docs"},"servers":[{"url":"https://api.recoms.tech","description":"Current RECOMS deployment"}],"tags":[{"name":"Comments","description":"Submit and read comments, traverse direct replies, edit owned source and soft delete comments."},{"name":"Author state","description":"Read an externally authenticated author's latest submission and published state."},{"name":"Reactions","description":"Set, replace or remove one reaction per external user and comment."},{"name":"Management","description":"Project-scoped settings, moderation, analytics and provider controls. Required API-key permissions are documented per operation."}],"components":{"securitySchemes":{"ProjectApiKey":{"type":"http","scheme":"bearer","description":"Project-scoped server API key. Send Authorization: Bearer <project-api-key> from your backend. The key selects exactly one project; it is an opaque credential, not a JWT. Never ship it to browsers or mobile clients. Missing, invalid, disabled, revoked or expired keys return 401 AUTHENTICATION_REQUIRED. A valid key without the required permission returns 403 FORBIDDEN: grant the permission or use an appropriately scoped key, rather than treating it as an invalid credential.\n\n### Available permissions\n\n| Permission | Allowed operations |\n| --- | --- |\n| project:read | Read project context and settings |\n| project:update | Update project settings and moderation rules |\n| comments:read | Read comments, replies, author state, reactions, resource counts and moderation queues |\n| comments:write | Create, edit or delete an author's comments; set or remove reactions |\n| comments:review | Approve or reject a submitted revision through management |\n| analytics:read | Read project analytics |\n| usage:read | Read billing-period usage |\n| provider:test | Test draft moderation rules or the saved provider connection |\n| provider:configure | Test and save provider configuration |\n\nNew keys receive only comments:read and comments:write by default. Grant other permissions explicitly when needed; existing keys keep their stored permissions. Each operation documents its required permission. A valid key alone is insufficient when that permission is absent. Author identity and ownership checks still apply; comments:write does not permit impersonating another author. Project keys do not grant account management, project creation or ownership transfer."}},"schemas":{"AuthorReactionListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuthorReaction"},"maxItems":100,"description":"Page of this author's existing reactions on currently public comments; no null entries."},"nextCursor":{"type":["string","null"],"description":"Opaque token for the next page, or null when there is no continuation. Copy a non-null token into the cursor parameter using the same collection, project, filters and sort; omit cursor to start over.","examples":[null]}},"required":["data","nextCursor"]},"AuthorReaction":{"type":"object","properties":{"commentId":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"type":{"type":"string","enum":["LIKE","DISLIKE"],"description":"The supplied author's existing LIKE or DISLIKE. No entry is returned when that author has no reaction; this read never changes a reaction.","examples":["LIKE"]}},"required":["commentId","type"],"description":"An existing author reaction on a currently published, non-deleted comment in this project and resource."},"ErrorResponse":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/ApiError"}},"required":["error"],"description":"JSON error envelope. Inspect error.code and the operation's retry guidance; use X-Request-Id when reporting the problem."},"ApiError":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/OperationError"}],"description":"Machine-readable API failure. Use code and the operation's recovery guidance."},"ValidationError":{"type":"object","properties":{"code":{"type":"string","enum":["VALIDATION_ERROR"],"description":"Invalid request fields or request section. Correct the fields identified by details before retrying."},"message":{"$ref":"#/components/schemas/ErrorMessage"},"details":{"$ref":"#/components/schemas/ValidationDetails"}},"required":["code","message","details"],"additionalProperties":false,"description":"Request validation failure; details is required and contains at least one issue."},"ErrorMessage":{"type":"string","description":"Human-readable explanation suitable for diagnosis. Wording may change; use code for programmatic handling. No credentials, stack traces or raw upstream responses are included."},"ValidationDetails":{"type":"array","items":{"type":"object","properties":{"location":{"type":"string","enum":["body","query","path","header"],"description":"Request section containing the invalid field."},"path":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer","minimum":0}]},"description":"Field path relative to that request section; array indices are numbers, and an empty path refers to the section itself.","examples":[["content"]]},"rule":{"type":"string","enum":["invalid_json","invalid_type","invalid_format","invalid_value","unknown_field","min_length","max_length","min_items","max_items","minimum","maximum"],"description":"Stable validation rule. Use the field's documented schema to determine the accepted type, format, values or bounds; never parse message text."}},"required":["location","path","rule"]},"minItems":1,"description":"Field-level guidance for VALIDATION_ERROR. No submitted values or credentials are returned."},"OperationError":{"type":"object","properties":{"code":{"type":"string","enum":["INVALID_CURSOR","AUTHENTICATION_REQUIRED","FORBIDDEN","REVISION_ALREADY_REVIEWED","AUTHORSHIP_REQUIRED","COMMENT_NOT_FOUND","NOT_FOUND","METHOD_NOT_ALLOWED","PARENT_RESOURCE_MISMATCH","COMMENT_DELETED","IDEMPOTENCY_KEY_REUSED","REQUEST_BODY_TOO_LARGE","COMMENT_NOT_PUBLISHED","RATE_LIMITED","SUBMISSION_QUOTA_EXCEEDED","PROJECT_FROZEN","PROVIDER_UNAVAILABLE","PROVIDER_CONFIGURATION_CHANGED","INTERNAL_ERROR"],"description":"Machine-readable non-validation failure code; branch on this value, not message text."},"message":{"$ref":"#/components/schemas/ErrorMessage"}},"required":["code","message"],"additionalProperties":false,"description":"Non-validation failure. Use code and the operation's recovery guidance; validation details are not included."},"AuthorCommentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuthorComment"},"maxItems":100,"description":"Page of this author's live comments, roots and replies, with published content and latest revision."},"nextCursor":{"type":["string","null"],"description":"Opaque token for the next page, or null when there is no continuation. Copy a non-null token into the cursor parameter using the same collection, project, filters and sort; omit cursor to start over.","examples":[null]}},"required":["data","nextCursor"]},"AuthorComment":{"type":"object","properties":{"id":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"resourceId":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"authorUserId":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"parentId":{"type":["string","null"],"minLength":36,"maxLength":36,"format":"uuid","description":"ID of the immediate parent, or null for a root comment.","examples":[null]},"rootId":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"ID of the root comment of this discussion. Equals id for a root comment and remains the same throughout its reply tree.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"content":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Currently published Markdown source, not the latest unapproved edit. Null for a deleted comment or an author-state comment with no published revision. Non-null content follows the Markdown input constraints.","examples":["This explanation of **cursor pagination** was helpful."]},"status":{"type":"string","enum":["APPROVED","PENDING","FAILED","REJECTED","DELETED"],"description":"Comment lifecycle: APPROVED has published content; PENDING awaits review with no published version; FAILED has no published version and a required AI check failed; REJECTED has no published version and was rejected; DELETED is a soft-deleted node with null content. An edit awaiting review can coexist with APPROVED public content; inspect latestRevision in author responses.","examples":["APPROVED"]},"replyCount":{"type":"integer","minimum":0,"description":"Number of published, non-deleted direct replies, not all descendants. Only comments with an APPROVED published version count; unpublished submissions and deleted structural nodes do not. Pending, failed or rejected edits retain the previous published version and do not change this count. Minimum 0; no product-level maximum is defined.","examples":[3]},"likeCount":{"type":"integer","minimum":0,"description":"Current LIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[12]},"dislikeCount":{"type":"integer","minimum":0,"description":"Current DISLIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[1]},"reactionCount":{"type":"integer","minimum":0,"description":"Total current reactions, equal to likeCount + dislikeCount. Minimum 0; no product-level maximum is defined.","examples":[13]},"createdAt":{"type":"string","format":"date-time","description":"Time the comment was created. UTC RFC 3339 with millisecond precision; unchanged by edits.","examples":["2026-08-14T12:00:00.000Z"]},"updatedAt":{"type":"string","format":"date-time","description":"Time the comment record was last updated. UTC RFC 3339 with millisecond precision; not the timestamp of an individual AI check.","examples":["2026-08-14T12:00:00.000Z"]},"latestRevision":{"$ref":"#/components/schemas/LatestRevision"}},"required":["id","resourceId","authorUserId","parentId","rootId","content","status","replyCount","likeCount","dislikeCount","reactionCount","createdAt","updatedAt","latestRevision"],"description":"Comment state for its author: published content plus the latest submitted revision. For DELETED comments, both content fields are null. For live comments, latestRevision.content is non-null. Never expose this representation to another author."},"LatestRevision":{"type":"object","properties":{"content":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Markdown source of the latest submitted revision, even when it is not published. Null only when the comment is deleted; deleted text is never returned. Non-null content follows the Markdown input constraints.","examples":["This explanation of **cursor pagination** was helpful."]},"status":{"type":"string","enum":["APPROVED","PENDING","FAILED","REJECTED"],"description":"Moderation status of this submitted revision: APPROVED is allowed for publication; PENDING awaits human review; FAILED means required AI moderation could not produce a decision; REJECTED is not allowed for publication. FAILED is a technical failure, not a content rejection.","examples":["PENDING"]}},"required":["content","status"],"description":"Most recently submitted revision and its moderation state. This may differ from the currently published comment content."},"ResourceCommentCountsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResourceCommentCount"},"minItems":1,"maxItems":100,"description":"One count entry for each requested resource, in request order, including zeros for empty resources."}},"required":["data"]},"ResourceCommentCount":{"type":"object","properties":{"resourceId":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"commentCount":{"type":"integer","minimum":0,"description":"Number of live comments with an APPROVED published version, including roots and replies at every depth. Unpublished submissions and deleted nodes are excluded; pending edits retain their published version. Zero for an empty resource; no product maximum is defined.","examples":[42]},"rootCommentCount":{"type":"integer","minimum":0,"description":"The subset of commentCount with no parent. Zero for an empty resource; no product maximum is defined. Uses the same published, non-deleted visibility as replyCount, but counts roots across the resource rather than direct replies to a comment.","examples":[12]}},"required":["resourceId","commentCount","rootCommentCount"],"description":"Current published comment counts for one requested resource."},"CommentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicComment"},"minItems":0,"maxItems":100,"description":"Current page of results in the requested deterministic order. Can be empty; at most the requested limit of items. No total count is returned.","examples":[[]]},"nextCursor":{"type":["string","null"],"description":"Opaque token for the next page, or null when there is no continuation. Copy a non-null token into the cursor parameter using the same collection, project, filters and sort; omit cursor to start over.","examples":[null]}},"required":["data","nextCursor"]},"PublicComment":{"type":"object","properties":{"id":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"resourceId":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"authorUserId":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"parentId":{"type":["string","null"],"minLength":36,"maxLength":36,"format":"uuid","description":"ID of the immediate parent, or null for a root comment.","examples":[null]},"rootId":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"ID of the root comment of this discussion. Equals id for a root comment and remains the same throughout its reply tree.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"content":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Published Markdown source, or null for a deleted structural node. Pending edits are never returned by public reads.","examples":["This explanation of **cursor pagination** was helpful."]},"status":{"type":"string","enum":["APPROVED","DELETED"],"description":"Public visibility state: APPROVED exposes the published Markdown; DELETED preserves a structural node with null content. Pending, failed and rejected submissions without an approved version are excluded from public reads.","examples":["APPROVED"]},"replyCount":{"type":"integer","minimum":0,"description":"Number of published, non-deleted direct replies, not all descendants. Only comments with an APPROVED published version count; unpublished submissions and deleted structural nodes do not. Pending, failed or rejected edits retain the previous published version and do not change this count. Minimum 0; no product-level maximum is defined.","examples":[3]},"likeCount":{"type":"integer","minimum":0,"description":"Current LIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[12]},"dislikeCount":{"type":"integer","minimum":0,"description":"Current DISLIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[1]},"reactionCount":{"type":"integer","minimum":0,"description":"Total current reactions, equal to likeCount + dislikeCount. Minimum 0; no product-level maximum is defined.","examples":[13]},"createdAt":{"type":"string","format":"date-time","description":"Time the comment was created. UTC RFC 3339 with millisecond precision; unchanged by edits.","examples":["2026-08-14T12:00:00.000Z"]},"updatedAt":{"type":"string","format":"date-time","description":"Time the comment record was last updated. UTC RFC 3339 with millisecond precision; not the timestamp of an individual AI check.","examples":["2026-08-14T12:00:00.000Z"]}},"required":["id","resourceId","authorUserId","parentId","rootId","content","status","replyCount","likeCount","dislikeCount","reactionCount","createdAt","updatedAt"],"description":"Publicly readable comment or deleted node. Inspect the author-state representation separately for unpublished revisions."},"AuthorCommentResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AuthorComment"}},"required":["data"],"description":"Successful response containing a single result in data."},"CreateCommentRequest":{"type":"object","properties":{"resourceId":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"authorUserId":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"content":{"type":"string","minLength":1,"maxLength":2000,"description":"Markdown source, preserved without trimming. Must contain non-whitespace text and at most 2,000 Unicode code points. U+0000 and unpaired UTF-16 surrogates are forbidden. Render Markdown safely in your application's UI.","examples":["This explanation of **cursor pagination** was helpful."]},"parentId":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"Optional ID of the immediate parent comment. Omit it to create a root comment; null is not accepted. The parent must be in the same project and resource, and have a published version or be a deleted node. Replies can be nested.","examples":["0198a6ec-9e00-7000-8000-000000000001"]}},"required":["resourceId","authorUserId","content"],"additionalProperties":false,"description":"Submit a root comment or direct reply using the authenticated end user's stable external ID. Unknown properties are rejected with VALIDATION_ERROR; send only documented fields."},"CommentResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicComment"}},"required":["data"],"description":"Successful response containing a single result in data."},"EditCommentRequest":{"type":"object","properties":{"authorUserId":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"content":{"type":"string","minLength":1,"maxLength":2000,"description":"Markdown source, preserved without trimming. Must contain non-whitespace text and at most 2,000 Unicode code points. U+0000 and unpaired UTF-16 surrogates are forbidden. Render Markdown safely in your application's UI.","examples":["This explanation of **cursor pagination** was helpful."]}},"required":["authorUserId","content"],"additionalProperties":false,"description":"Submit replacement Markdown as a new revision of a comment owned by authorUserId. This replaces the entire source, not a partial text patch. Unknown properties are rejected with VALIDATION_ERROR."},"DeleteCommentResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DeletedComment"}},"required":["data"],"description":"Successful response containing a single result in data."},"DeletedComment":{"type":"object","properties":{"id":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"status":{"type":"string","enum":["DELETED"],"description":"Confirms soft deletion. The comment remains a structural node; no deleted text is included.","examples":["DELETED"]}},"required":["id","status"],"description":"Confirmation of successful soft deletion."},"ReactionResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReactionResult"}},"required":["data"],"description":"Successful response containing a single result in data."},"ReactionResult":{"type":"object","properties":{"commentId":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"type":{"type":["string","null"],"enum":["LIKE","DISLIKE",null],"description":"The supplied user's current reaction: LIKE, DISLIKE, or null after removal. Other users' reactions are unchanged.","examples":["LIKE"]},"likeCount":{"type":"integer","minimum":0,"description":"Current LIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[12]},"dislikeCount":{"type":"integer","minimum":0,"description":"Current DISLIKE reaction count. Minimum 0; no product-level maximum is defined.","examples":[1]},"reactionCount":{"type":"integer","minimum":0,"description":"Total current reactions, equal to likeCount + dislikeCount. Minimum 0; no product-level maximum is defined.","examples":[13]}},"required":["commentId","type","likeCount","dislikeCount","reactionCount"],"description":"Current reaction for the supplied user and updated aggregate counters for the comment."},"SetReactionRequest":{"type":"object","properties":{"authorUserId":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"type":{"type":"string","enum":["LIKE","DISLIKE"],"description":"LIKE expresses a positive reaction; DISLIKE expresses a negative reaction. Each external user has at most one reaction per comment; setting the other type replaces it.","examples":["LIKE"]}},"required":["authorUserId","type"],"additionalProperties":false,"description":"Set or replace the authenticated end user's reaction to a published comment. Unknown properties are rejected with VALIDATION_ERROR."},"ProjectContext":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/Project"},"access":{"$ref":"#/components/schemas/ProjectKeyAccess"},"defaultModerationPolicy":{"type":"string","description":"Recommended default policy used when resetting project moderation rules."}},"required":["project","access","defaultModerationPolicy"],"description":"Current project settings and the authenticated key's access."},"Project":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"name":{"type":"string","description":"Project display name."},"slug":{"type":"string","description":"Project slug. Use the project ID for API scope; the slug is not an authentication credential."},"createdAt":{"type":"string","format":"date-time","description":"Event timestamp in RFC 3339 UTC format."},"moderationMode":{"$ref":"#/components/schemas/ModerationMode"},"moderationPolicy":{"type":"string","description":"Current saved moderation policy. Reading it with a project key requires project:read."},"analyticsTimezone":{"type":"string","description":"IANA timezone used to calculate analytics."},"billingAccount":{"$ref":"#/components/schemas/ProjectBillingAccount"},"aiConfiguration":{"$ref":"#/components/schemas/ProjectAiConfiguration"}},"required":["id","name","slug","createdAt","moderationMode","moderationPolicy","analyticsTimezone","billingAccount","aiConfiguration"],"description":"Project settings, moderation rules and non-secret billing/provider state. Reading project context requires project:read."},"ModerationMode":{"type":"string","enum":["OFF","MANUAL","AI","HYBRID"],"description":"OFF publishes immediately; MANUAL queues review; AI applies automated decisions; HYBRID queues suspected violations for manual review."},"ProjectBillingAccount":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"planCode":{"type":"string","enum":["FREE","PRO","ENTERPRISE"],"description":"Current project plan; entitlements are scoped to this project."},"accessState":{"type":"string","enum":["ACTIVE","FROZEN"],"description":"ACTIVE permits activity within entitlements; FROZEN preserves reads and removals while blocking new submissions and reactions."},"usageAnchorAt":{"type":"string","format":"date-time","description":"UTC anchor used to calculate this project's billing periods."}},"required":["id","planCode","accessState","usageAnchorAt"],"description":"Project billing state, or null when no billing account exists."},"ProjectAiConfiguration":{"type":["object","null"],"properties":{"source":{"type":"string","enum":["MANAGED","CUSTOM_OPENAI_COMPATIBLE"],"description":"Service-managed provider or the project's custom OpenAI-compatible provider."},"availability":{"type":"string","enum":["NOT_CONFIGURED","CONNECTED","UNAVAILABLE"],"description":"Stored provider connection state, distinct from moderation outcomes."},"baseUrl":{"type":["string","null"],"description":"Custom provider API base URL, or null when not configured."},"model":{"type":["string","null"],"description":"Configured provider model identifier, or null when not configured."},"hasCredential":{"type":"boolean","description":"Whether a provider credential is stored; the credential is never returned."},"credentialReadable":{"type":"boolean","description":"Whether the API can use the stored credential."}},"required":["source","availability","baseUrl","model","hasCredential","credentialReadable"],"description":"Non-secret provider settings, or null when no configuration is stored."},"ProjectKeyAccess":{"type":"object","properties":{"keyId":{"type":"string","format":"uuid","description":"Identifier of the authenticated project API key, not the secret key value.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"permissions":{"type":"array","items":{"type":"string","minLength":1,"description":"Granted permission in resource:action form."},"description":"Effective permissions of the authenticated key, sorted and without duplicates. Compare these with each operation's required permission. Rights can change after this response; the API checks every request independently."}},"required":["keyId","permissions"],"description":"Authenticated project API key and its effective permissions."},"ProjectReviewQueues":{"type":"object","properties":{"pending":{"type":"integer","minimum":0,"description":"Latest revisions waiting for manual review; zero for roles without moderation access."},"failed":{"type":"integer","minimum":0,"description":"Latest revisions with a failed automated check; zero for roles without moderation access."}},"required":["pending","failed"],"description":"Operation result; inspect its fields as documented by this endpoint."},"ProjectComments":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectCommentListRow"},"description":"Matching comments in deterministic order; empty when no rows match."},"nextCursor":{"type":["string","null"],"description":"Opaque continuation cursor for identical filters and sort; null at the end of the list."},"hasComments":{"type":"boolean","description":"Whether the project contains any comment nodes, independent of the current filters."},"queues":{"allOf":[{"$ref":"#/components/schemas/ProjectReviewQueues"},{"description":"Moderation queue sizes for the project; hidden from read-only roles."}]}},"required":["data","nextCursor","hasComments","queues"]},"ProjectCommentListRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"resourceId":{"type":"string","description":"Opaque integrator resource identifier, scoped to the project."},"authorUserId":{"type":"string","description":"Stable external author identifier supplied by the integrator."},"parentId":{"type":["string","null"],"format":"uuid","description":"Direct parent identifier; null for a root comment.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"rootId":{"type":"string","format":"uuid","description":"Identifier of the thread's root comment.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"latestRevision":{"$ref":"#/components/schemas/ProjectCommentRevision"},"status":{"type":"string","enum":["APPROVED","PENDING","FAILED","REJECTED","DELETED"],"description":"Moderation state derived from the latest revision, or DELETED for a removed node."},"replyCount":{"type":"integer","minimum":0,"description":"Currently published, non-deleted direct replies."},"likeCount":{"type":"integer","minimum":0,"description":"Current LIKE reactions."},"dislikeCount":{"type":"integer","minimum":0,"description":"Current DISLIKE reactions."},"reactionCount":{"type":"integer","minimum":0,"description":"Sum of current LIKE and DISLIKE reactions."},"createdAt":{"type":"string","format":"date-time","description":"Event timestamp in RFC 3339 UTC format."},"updatedAt":{"type":"string","format":"date-time","description":"Event timestamp in RFC 3339 UTC format."}},"required":["id","resourceId","authorUserId","parentId","rootId","latestRevision","status","replyCount","likeCount","dislikeCount","reactionCount","createdAt","updatedAt"]},"ProjectCommentRevision":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"content":{"type":["string","null"],"description":"Markdown submitted for this revision; null when its content has been removed."},"status":{"type":"string","enum":["APPROVED","PENDING","FAILED","REJECTED"],"description":"Revision decision: published, awaiting review, technical check failure, or rejected."},"sequence":{"type":"integer","minimum":1,"description":"Monotonically increasing revision number within the comment; starts at one."},"createdAt":{"type":"string","format":"date-time","description":"Event timestamp in RFC 3339 UTC format."},"reviewedAt":{"type":["string","null"],"format":"date-time","description":"Decision timestamp in UTC, or null before a decision is recorded."},"reviewedByUserId":{"type":["string","null"],"format":"uuid","description":"Verified RECOMS moderator account ID, or null for decisions without account attribution.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"reviewedBy":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"name":{"type":"string","description":"Verified moderator's current display name."},"image":{"type":["string","null"],"description":"Verified moderator's current avatar URL, or null."}},"required":["id","name","image"],"description":"Current moderator profile, or null when no profile is available."},"aiProvider":{"type":["string","null"],"description":"Provider used for the automated check, or null when no check occurred."},"aiModel":{"type":["string","null"],"description":"Model used for the automated check, or null when no check occurred."},"aiOutcome":{"type":["string","null"],"enum":["ALLOWED","DISALLOWED","FAILED",null],"description":"Automated check outcome; null when no check occurred. It does not replace the revision's final decision."},"aiReason":{"type":["string","null"],"description":"Policy violation explanation returned by the automated check, or null."},"aiFailureReason":{"type":["string","null"],"description":"Safe technical failure category, or null when the check did not fail."},"aiCheckedAt":{"type":["string","null"],"format":"date-time","description":"Automated check timestamp in UTC, or null when no check occurred."}},"required":["id","content","status","sequence","createdAt","reviewedAt","reviewedByUserId","reviewedBy","aiProvider","aiModel","aiOutcome","aiReason","aiFailureReason","aiCheckedAt"],"description":"Latest submitted revision, including drafts when the caller has moderation access; null if absent."},"ProjectCommentStatus":{"type":"string","enum":["ALL","APPROVED","PENDING","FAILED","REJECTED","DELETED"],"default":"ALL","description":"Filter by latest revision decision or deleted node. ALL and unpublished states require moderation access."},"ProjectCommentSort":{"type":"string","enum":["NEWEST","OLDEST","REACTIONS","REPLIES"],"default":"NEWEST","description":"Creation time descending or ascending, total reactions descending, or published direct reply count descending; identifier breaks ties."},"ProjectActionResult":{"type":"object","properties":{"message":{"type":"string","description":"Safe user-facing result or recovery guidance; omitted when no message is needed."}},"additionalProperties":false,"description":"Operation result; inspect its fields as documented by this endpoint."},"ReviewComment":{"type":"object","properties":{"revisionId":{"type":"string","format":"uuid","description":"Latest PENDING or FAILED revision being reviewed; refresh if another decision has already been recorded. Copy the revision ID, not the comment ID.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"decision":{"type":"string","enum":["APPROVED","REJECTED"],"description":"Publish this revision or reject it while retaining any previously approved public version."}},"required":["revisionId","decision"],"additionalProperties":false,"examples":[{"revisionId":"0198a6ec-9e00-7000-8000-000000000003","decision":"APPROVED"}]},"ProjectSettingsResult":{"type":"object","properties":{"message":{"type":"string","description":"Safe user-facing result or recovery guidance; omitted when no message is needed."},"savedPolicy":{"type":"string","description":"Normalized policy after a successful save; omitted for other operations."}},"additionalProperties":false,"description":"Operation result; inspect its fields as documented by this endpoint."},"UpdateProjectSettings":{"type":"object","properties":{"moderationMode":{"$ref":"#/components/schemas/ModerationMode"},"moderationPolicy":{"type":"string","minLength":1,"maxLength":10000,"description":"Non-empty policy replacing the current policy after trimming; omit to preserve it."},"analyticsTimezone":{"type":"string","minLength":1,"maxLength":100,"description":"Valid IANA timezone used for calendar analytics; omit to preserve it."}},"additionalProperties":false,"examples":[{"moderationMode":"HYBRID","analyticsTimezone":"Europe/Moscow"}]},"ProjectAnalytics":{"type":"object","properties":{"analytics":{"$ref":"#/components/schemas/DiscussionAnalytics"},"moderation":{"$ref":"#/components/schemas/ModerationAnalytics"},"summary":{"$ref":"#/components/schemas/ProjectCommentModerationSummary"},"planUsage":{"$ref":"#/components/schemas/ProjectUsage"}},"required":["analytics","moderation","summary","planUsage"],"description":"Operation result; inspect its fields as documented by this endpoint."},"DiscussionAnalytics":{"type":"object","properties":{"publishedComments":{"type":"integer","minimum":0,"description":"First publications in the range, including roots and replies; edits do not add publications."},"contributors":{"type":"integer","minimum":0,"description":"Distinct external authors with a first publication in the range, including reply authors."},"replyRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Reply first publications divided by all first publications, as a percentage; null without publications."},"threadsWithReplies":{"type":["integer","null"],"minimum":0,"description":"Threads with published replies in the range, or null when unavailable under current entitlements."},"range":{"type":"object","properties":{"startAt":{"type":"string","format":"date-time","description":"Inclusive UTC start of the requested calendar range."},"endAt":{"type":"string","format":"date-time","description":"Exclusive UTC end of the requested calendar range."},"bucket":{"$ref":"#/components/schemas/AnalyticsBucket"},"preset":{"$ref":"#/components/schemas/AnalyticsRangePreset"}},"required":["startAt","endAt","bucket","preset"],"description":"Resolved calendar range in UTC; endAt is exclusive and current-day ranges stop at the request time."},"series":{"type":"array","items":{"type":"object","properties":{"bucketStartAt":{"type":"string","format":"date-time","description":"UTC start of the chart interval aligned to the project's timezone."},"rootComments":{"type":"integer","minimum":0,"description":"Root comment first publications in this interval."},"replies":{"type":"integer","minimum":0,"description":"Reply first publications in this interval."},"contributors":{"type":"integer","minimum":0,"description":"Distinct external authors publishing in this interval."},"newContributors":{"type":["integer","null"],"minimum":0,"description":"Authors whose first project publication occurs in this interval; null when segmentation is unavailable."},"returningContributors":{"type":["integer","null"],"minimum":0,"description":"Publishing authors with an earlier project publication; null when segmentation is unavailable."}},"required":["bucketStartAt","rootComments","replies","contributors","newContributors","returningContributors"]},"description":"Publication activity grouped by the selected calendar interval in the project timezone."},"topResources":{"type":"array","items":{"type":"object","properties":{"resourceId":{"type":"string","description":"Opaque integrator resource identifier."},"publishedComments":{"type":"integer","minimum":0,"description":"First publications on this resource during the range, including replies."},"contributors":{"type":"integer","minimum":0,"description":"Distinct authors publishing on this resource during the range."},"replyRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Reply share of first publications on this resource, as a percentage; null without publications."}},"required":["resourceId","publishedComments","contributors","replyRate"]},"description":"Resources ranked by first publications in the selected range, including replies. Empty when no resources have publications."},"topAuthors":{"type":"array","items":{"type":"object","properties":{"authorUserId":{"type":"string","description":"Stable external author identifier supplied by the integrator."},"publishedComments":{"type":"integer","minimum":0,"description":"First publications by this author during the range, including replies."}},"required":["authorUserId","publishedComments"]},"description":"Authors ranked by first publication count; external profile data is not stored here."},"comparison":{"type":["object","null"],"properties":{"publishedComments":{"type":"integer","minimum":0,"description":"First publications in the range, including roots and replies; edits do not add publications."},"contributors":{"type":"integer","minimum":0,"description":"Distinct external authors with a first publication in the range, including reply authors."},"replyRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Reply first publications divided by all first publications, as a percentage; null without publications."},"threadsWithReplies":{"type":["integer","null"],"minimum":0,"description":"Threads with published replies in the range, or null when unavailable under current entitlements."}},"required":["publishedComments","contributors","replyRate","threadsWithReplies"],"description":"Discussion totals for the preceding comparable period, or null when unavailable."},"contributorSegmentationAvailable":{"type":"boolean","description":"Whether new and returning contributor metrics are included by the project's entitlements."}},"required":["publishedComments","contributors","replyRate","threadsWithReplies","range","series","topResources","topAuthors","comparison","contributorSegmentationAvailable"],"description":"First publication activity for the requested range."},"AnalyticsBucket":{"type":"string","enum":["HOUR","DAY","WEEK","MONTH"],"description":"Calendar interval selected for this analytics range."},"AnalyticsRangePreset":{"type":"string","enum":["TODAY","LAST_7_DAYS","LAST_30_DAYS","LAST_90_DAYS","LAST_12_MONTHS","ALL_TIME","CUSTOM"],"description":"Calendar preset in the project's analytics timezone; CUSTOM requires both custom dates. Available history follows project entitlements."},"ModerationAnalytics":{"type":"object","properties":{"aiModerationAttempts":{"type":"integer","minimum":0,"description":"Automated checks on submitted revisions in the range; provider configuration and policy tests are excluded."},"autoResolvedRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Share of automated checks that produced a final decision without manual review; null without eligible checks."},"aiFailureRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Share of automated checks with a technical failure; null without checks."},"medianManualReviewTimeMs":{"type":["number","null"],"minimum":0,"description":"Median elapsed time from submission to manual review in milliseconds; null without eligible reviews."},"moderatorAgreementRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Share of eligible manual reviews agreeing with the automated outcome; null without a comparable sample."},"moderatorAgreementSampleSize":{"type":"integer","minimum":0,"description":"Manual decisions with an automated outcome available for agreement comparison."},"outcomes":{"type":"array","items":{"type":"object","properties":{"bucketStartAt":{"type":"string","format":"date-time","description":"Event timestamp in RFC 3339 UTC format."},"allowed":{"type":"integer","minimum":0,"description":"Automated checks allowing the text in this interval."},"disallowed":{"type":"integer","minimum":0,"description":"Automated checks detecting a violation in this interval."},"failed":{"type":"integer","minimum":0,"description":"Automated checks failing technically in this interval."}},"required":["bucketStartAt","allowed","disallowed","failed"]},"description":"Automated revision checks grouped by calendar interval; configuration and draft-policy tests are excluded."}},"required":["aiModerationAttempts","autoResolvedRate","aiFailureRate","medianManualReviewTimeMs","moderatorAgreementRate","moderatorAgreementSampleSize","outcomes"],"description":"Automated checks and manual review metrics for the requested range."},"ProjectCommentModerationSummary":{"type":"object","properties":{"approved":{"type":"integer","minimum":0,"description":"Unique comments with an approval decision in the range, across automated and manual decisions."},"blocked":{"type":"integer","minimum":0,"description":"Unique comments with a rejection decision in the range, across automated and manual decisions."},"blockedRate":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Blocked unique comments divided by approved plus blocked unique comments, as a percentage; null without decisions."},"comparison":{"type":["object","null"],"properties":{"approved":{"type":"integer","minimum":0,"description":"Unique comments with an approval decision in the range, across automated and manual decisions."},"blocked":{"type":"integer","minimum":0,"description":"Unique comments with a rejection decision in the range, across automated and manual decisions."}},"required":["approved","blocked"],"description":"Counts for the preceding comparable period, or null when unavailable."},"series":{"type":"array","items":{"type":"object","properties":{"approved":{"type":"integer","minimum":0,"description":"Unique comments with an approval decision in the range, across automated and manual decisions."},"blocked":{"type":"integer","minimum":0,"description":"Unique comments with a rejection decision in the range, across automated and manual decisions."},"bucketStartAt":{"type":"string","format":"date-time","description":"UTC start of this chart interval."}},"required":["approved","blocked","bucketStartAt"]},"description":"Decision counts grouped by analytics interval."}},"required":["approved","blocked","blockedRate","comparison","series"],"description":"Unique comment decision counts for the requested range."},"ProjectUsage":{"type":["object","null"],"properties":{"consumed":{"type":"integer","minimum":0,"description":"Comment submissions charged to the project's current billing period."},"included":{"type":"integer","minimum":0,"description":"Included submission allowance for the project's current plan."},"period":{"type":["object","null"],"properties":{"startAt":{"type":"string","format":"date-time","description":"Inclusive billing period start in UTC."},"endAt":{"type":"string","format":"date-time","description":"Exclusive billing period end in UTC."}},"required":["startAt","endAt"],"description":"Current billing interval, or null when unavailable."}},"required":["consumed","included","period"],"description":"Current billing period usage, independent of the selected analytics range; null without billing state."},"ProjectPolicyTestResult":{"type":"object","properties":{"message":{"type":"string","description":"Safe user-facing result or recovery guidance; omitted when no message is needed."},"policyTest":{"$ref":"#/components/schemas/PolicyTestResult"}},"required":["policyTest"],"additionalProperties":false,"description":"Operation result; inspect its fields as documented by this endpoint."},"PolicyTestResult":{"oneOf":[{"$ref":"#/components/schemas/AllowedPolicyTest"},{"$ref":"#/components/schemas/DisallowedPolicyTest"}],"discriminator":{"propertyName":"outcome","mapping":{"ALLOWED":"#/components/schemas/AllowedPolicyTest","DISALLOWED":"#/components/schemas/DisallowedPolicyTest"}},"description":"Ephemeral test outcome; no comment or policy changes are persisted by the test."},"AllowedPolicyTest":{"type":"object","properties":{"outcome":{"type":"string","enum":["ALLOWED"],"description":"The example complies with the draft policy; no comment is published."}},"required":["outcome"]},"DisallowedPolicyTest":{"type":"object","properties":{"outcome":{"type":"string","enum":["DISALLOWED"],"description":"The example violates the draft policy; this is a successful test result, not an HTTP failure."},"reason":{"type":"string","description":"Human-readable explanation of the example's policy violation, not chain-of-thought."}},"required":["outcome","reason"]},"TestModerationPolicy":{"type":"object","properties":{"moderationPolicy":{"type":"string","minLength":1,"maxLength":10000,"description":"Draft moderation policy used only for this test; it is not saved."},"exampleComment":{"type":"string","minLength":1,"maxLength":2000,"description":"Markdown source, preserved without trimming. Must contain non-whitespace text and at most 2,000 Unicode code points. U+0000 and unpaired UTF-16 surrogates are forbidden. Render Markdown safely in your application's UI.","examples":["This explanation of **cursor pagination** was helpful."]}},"required":["moderationPolicy","exampleComment"],"additionalProperties":false},"ConfigureProvider":{"type":"object","properties":{"baseUrl":{"type":"string","minLength":1,"maxLength":2048,"pattern":"^[hH][tT][tT][pP][sS]:\\/\\/","description":"OpenAI-compatible HTTPS API base URL, 1–2,048 Unicode code points after trimming; for example https://api.openai.com/v1. Do not embed a username or password. Private, loopback and otherwise unsafe destinations are blocked by outbound safety checks, including DNS resolution and redirects. A valid URL is not proof that the provider can be reached.","examples":["https://api.openai.com/v1"]},"model":{"type":"string","minLength":1,"maxLength":256,"description":"Provider model identifier, 1–256 Unicode code points after trimming; for example gpt-4.1-mini.","examples":["gpt-4.1-mini"]},"credential":{"type":"string","maxLength":4096,"description":"Provider API credential, at most 4,096 Unicode code points. Required and non-blank for replace intent. With preserve, an existing credential is retained; on first configuration this value may supply the initial credential. With remove it is ignored. It is never echoed in responses."},"credentialIntent":{"type":"string","enum":["preserve","replace","remove"],"default":"preserve","description":"Preserve the current credential, replace it with credential, or remove it for a provider without authentication."}},"required":["baseUrl","model"],"additionalProperties":false},"EmptyProjectAction":{"type":"object","properties":{},"additionalProperties":false,"description":"Send an empty JSON object. Unknown properties are rejected.","examples":[{}]}},"parameters":{"ResourceIdQueryParameter":{"schema":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"required":true,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","name":"resourceId","in":"query"},"CursorQueryParameter":{"schema":{"type":"string","minLength":1,"description":"Opaque continuation token returned as nextCursor by the same collection. Omit it for the first page. Pass it unchanged with the same project, collection, filters and sort. Do not decode, construct or reuse it for another query; incompatible tokens return INVALID_CURSOR. No fixed maximum token length is defined."},"required":false,"description":"Opaque continuation token returned as nextCursor by the same collection. Omit it for the first page. Pass it unchanged with the same project, collection, filters and sort. Do not decode, construct or reuse it for another query; incompatible tokens return INVALID_CURSOR. No fixed maximum token length is defined.","name":"cursor","in":"query"},"AuthorUserIdHeaderParameter":{"schema":{"type":"string","minLength":1,"maxLength":512,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","examples":["usr_42"]},"required":true,"description":"Stable external user identifier assigned by your backend after authenticating the end user. Required for every author; anonymous authors are not supported. Accepts 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. This is not a RECOMS dashboard account ID or a display name.","name":"X-Author-User-Id","in":"header"},"LimitQueryParameter":{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum number of comments to return, from 1 to 100 inclusive. Defaults to 20 when omitted. A page may contain fewer items; nextCursor determines whether another page exists.","examples":[20]},"required":false,"description":"Maximum number of comments to return, from 1 to 100 inclusive. Defaults to 20 when omitted. A page may contain fewer items; nextCursor determines whether another page exists.","name":"limit","in":"query"},"IdempotencyKeyHeaderParameter":{"schema":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"Optional client-generated UUID for one mutation. A successful committed result is replayed for 24 hours for the same project, author, method, path, key and request. Reusing the key with different input returns 409 IDEMPOTENCY_KEY_REUSED. Failed requests do not reserve the key. Preserve this key when retrying after an uncertain network outcome.","examples":["0198a6ec-9e00-7000-8000-000000000010"]},"required":false,"description":"Optional client-generated UUID for one mutation. A successful committed result is replayed for 24 hours for the same project, author, method, path, key and request. Reusing the key with different input returns 409 IDEMPOTENCY_KEY_REUSED. Failed requests do not reserve the key. Preserve this key when retrying after an uncertain network outcome.","name":"Idempotency-Key","in":"header"},"CommentIdPathParameter":{"schema":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"required":true,"description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","name":"commentId","in":"path"}},"headers":{"RequestIdHeader":{"schema":{"type":"string","format":"uuid","description":"Unique identifier for this HTTP request, including failures and idempotent replays. Include it when reporting a problem; it is not the Idempotency-Key.","examples":["0198a6ec-9e00-7000-8000-000000000099"]},"required":true,"description":"Unique identifier for this HTTP request, including failures and idempotent replays. Include it when reporting a problem; it is not the Idempotency-Key."},"CommentWriteRetryAfterHeader":{"schema":{"type":"string","pattern":"^[0-9]+$","description":"Whole seconds to wait before retrying. Wait at least this long, then retry the same action with its original Idempotency-Key where applicable. The delay reflects the refusing project and author write buckets.","examples":["2"]},"required":true,"description":"Whole seconds to wait before retrying. Wait at least this long, then retry the same action with its original Idempotency-Key where applicable. The delay reflects the refusing project and author write buckets."},"AllowedCommentMethodsHeader":{"schema":{"type":"string","description":"Comma-separated methods currently supported by this path.","examples":["GET, HEAD, PATCH, DELETE"]},"required":true,"description":"Comma-separated methods currently supported by this path."},"AllowedReactionMethodsHeader":{"schema":{"type":"string","description":"Comma-separated methods currently supported by this path.","examples":["PUT, DELETE"]},"required":true,"description":"Comma-separated methods currently supported by this path."},"ProviderTestRetryAfterHeader":{"schema":{"type":"string","pattern":"^[0-9]+$","description":"Whole seconds to wait before the next provider test.","examples":["30"]},"required":true,"description":"Whole seconds to wait before the next provider test."}},"responses":{"InvalidRequestResponse":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.\n\nINVALID_CURSOR: The cursor does not match this collection or query. Restart without cursor and keep the same filters while paginating.","x-error-codes":["VALIDATION_ERROR","INVALID_CURSOR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"query","path":["resourceId"],"rule":"invalid_type"}]}}},"INVALID_CURSOR":{"summary":"INVALID_CURSOR","value":{"error":{"code":"INVALID_CURSOR","message":"The cursor does not match this collection or query. Restart without cursor and keep the same filters while paginating."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiAuthenticationRequiredResponse":{"description":"AUTHENTICATION_REQUIRED: A valid project API key is required. Supply an active key for the intended project from your backend.","x-error-codes":["AUTHENTICATION_REQUIRED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"AUTHENTICATION_REQUIRED":{"summary":"AUTHENTICATION_REQUIRED","value":{"error":{"code":"AUTHENTICATION_REQUIRED","message":"A valid project API key is required. Supply an active key for the intended project from your backend."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiForbiddenResponse":{"description":"FORBIDDEN: The API key is valid but lacks the required permission. Grant that permission or use an appropriately scoped key. Do not retry unchanged or treat this as an invalid credential.","x-error-codes":["FORBIDDEN"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"FORBIDDEN":{"summary":"FORBIDDEN","value":{"error":{"code":"FORBIDDEN","message":"The API key is valid but lacks the required permission. Grant that permission or use an appropriately scoped key. Do not retry unchanged or treat this as an invalid credential."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiInternalErrorResponse":{"description":"INTERNAL_ERROR: An unexpected internal failure occurred. Use X-Request-Id for diagnosis. If retrying a mutation after an uncertain outcome, preserve its Idempotency-Key.","x-error-codes":["INTERNAL_ERROR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"INTERNAL_ERROR":{"summary":"INTERNAL_ERROR","value":{"error":{"code":"INTERNAL_ERROR","message":"An unexpected internal failure occurred. Use X-Request-Id for diagnosis. If retrying a mutation after an uncertain outcome, preserve its Idempotency-Key."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"SubmissionAccessDeniedResponse":{"description":"PROJECT_FROZEN: The project is frozen. The owner must activate Free or upgrade to Pro before new comments, edits or reactions are accepted. Reads and removals remain available.\n\nSUBMISSION_QUOTA_EXCEEDED: The project cannot accept another submission under its current quota or spend cap. The owner must adjust billing access or wait for renewed quota; no submission or AI call was created.","x-error-codes":["PROJECT_FROZEN","SUBMISSION_QUOTA_EXCEEDED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"PROJECT_FROZEN":{"summary":"PROJECT_FROZEN","value":{"error":{"code":"PROJECT_FROZEN","message":"The project is frozen. The owner must activate Free or upgrade to Pro before new comments, edits or reactions are accepted. Reads and removals remain available."}}},"SUBMISSION_QUOTA_EXCEEDED":{"summary":"SUBMISSION_QUOTA_EXCEEDED","value":{"error":{"code":"SUBMISSION_QUOTA_EXCEEDED","message":"The project cannot accept another submission under its current quota or spend cap. The owner must adjust billing access or wait for renewed quota; no submission or AI call was created."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentNotFoundResponse":{"description":"COMMENT_NOT_FOUND: The comment was not found in this project or is not publicly accessible for this operation. Check the project key and comment ID.","x-error-codes":["COMMENT_NOT_FOUND"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"COMMENT_NOT_FOUND":{"summary":"COMMENT_NOT_FOUND","value":{"error":{"code":"COMMENT_NOT_FOUND","message":"The comment was not found in this project or is not publicly accessible for this operation. Check the project key and comment ID."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiRequestBodyTooLargeResponse":{"description":"REQUEST_BODY_TOO_LARGE: The request body exceeds 16 KiB (16,384 bytes). Reduce the body before retrying.","x-error-codes":["REQUEST_BODY_TOO_LARGE"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"REQUEST_BODY_TOO_LARGE":{"summary":"REQUEST_BODY_TOO_LARGE","value":{"error":{"code":"REQUEST_BODY_TOO_LARGE","message":"The request body exceeds 16 KiB (16,384 bytes). Reduce the body before retrying."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiRateLimitedResponse":{"description":"RATE_LIMITED: The project or author write bucket is exhausted. Wait for Retry-After seconds before retrying with the same Idempotency-Key.","x-error-codes":["RATE_LIMITED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"},"Retry-After":{"$ref":"#/components/headers/CommentWriteRetryAfterHeader"}},"content":{"application/json":{"examples":{"RATE_LIMITED":{"summary":"RATE_LIMITED","value":{"error":{"code":"RATE_LIMITED","message":"The project or author write bucket is exhausted. Wait for Retry-After seconds before retrying with the same Idempotency-Key."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiPathCommentIdValidationErrorResponse":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.","x-error-codes":["VALIDATION_ERROR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"path","path":["commentId"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentApiBodyAuthorUserIdValidationErrorResponse":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.","x-error-codes":["VALIDATION_ERROR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"body","path":["authorUserId"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentAccessDeniedResponse":{"description":"FORBIDDEN: The API key is valid but lacks the required permission. Grant that permission or use an appropriately scoped key. Do not retry unchanged or treat this as an invalid credential.\n\nAUTHORSHIP_REQUIRED: The supplied author does not own this comment. Use the authenticated owner's external ID; do not impersonate another user.","x-error-codes":["FORBIDDEN","AUTHORSHIP_REQUIRED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"FORBIDDEN":{"summary":"FORBIDDEN","value":{"error":{"code":"FORBIDDEN","message":"The API key is valid but lacks the required permission. Grant that permission or use an appropriately scoped key. Do not retry unchanged or treat this as an invalid credential."}}},"AUTHORSHIP_REQUIRED":{"summary":"AUTHORSHIP_REQUIRED","value":{"error":{"code":"AUTHORSHIP_REQUIRED","message":"The supplied author does not own this comment. Use the authenticated owner's external ID; do not impersonate another user."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentMethodNotAllowedResponse":{"x-error-codes":["METHOD_NOT_ALLOWED"],"description":"METHOD_NOT_ALLOWED: The path exists but does not support the requested HTTP method. Choose an operation documented for this path; do not retry the same method.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"},"Allow":{"$ref":"#/components/headers/AllowedCommentMethodsHeader"}},"content":{"application/json":{"example":{"error":{"code":"METHOD_NOT_ALLOWED","message":"This route does not support the requested HTTP method."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentMutationConflictResponse":{"description":"COMMENT_DELETED: The comment is already deleted. This operation cannot modify it; refresh its state.\n\nIDEMPOTENCY_KEY_REUSED: This Idempotency-Key was used with different input. Restore the original request or use a new key for a genuinely different action.","x-error-codes":["COMMENT_DELETED","IDEMPOTENCY_KEY_REUSED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"COMMENT_DELETED":{"summary":"COMMENT_DELETED","value":{"error":{"code":"COMMENT_DELETED","message":"The comment is already deleted. This operation cannot modify it; refresh its state."}}},"IDEMPOTENCY_KEY_REUSED":{"summary":"IDEMPOTENCY_KEY_REUSED","value":{"error":{"code":"IDEMPOTENCY_KEY_REUSED","message":"This Idempotency-Key was used with different input. Restore the original request or use a new key for a genuinely different action."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ReactionMethodNotAllowedResponse":{"x-error-codes":["METHOD_NOT_ALLOWED"],"description":"METHOD_NOT_ALLOWED: The path exists but does not support the requested HTTP method. Choose an operation documented for this path; do not retry the same method.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"},"Allow":{"$ref":"#/components/headers/AllowedReactionMethodsHeader"}},"content":{"application/json":{"example":{"error":{"code":"METHOD_NOT_ALLOWED","message":"This route does not support the requested HTTP method."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"CommentNotPublishedResponse":{"description":"COMMENT_NOT_PUBLISHED: The comment has no approved public version. Wait for moderation before changing reactions.","x-error-codes":["COMMENT_NOT_PUBLISHED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"COMMENT_NOT_PUBLISHED":{"summary":"COMMENT_NOT_PUBLISHED","value":{"error":{"code":"COMMENT_NOT_PUBLISHED","message":"The comment has no approved public version. Wait for moderation before changing reactions."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ManagementAuthenticationRequiredResponse":{"x-error-codes":["AUTHENTICATION_REQUIRED"],"description":"AUTHENTICATION_REQUIRED: provide an active, valid project API key. Missing permissions on a valid key return 403 FORBIDDEN instead.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"AUTHENTICATION_REQUIRED":{"summary":"AUTHENTICATION_REQUIRED","value":{"error":{"code":"AUTHENTICATION_REQUIRED","message":"AUTHENTICATION_REQUIRED: provide an active, valid project API key. Missing permissions on a valid key return 403 FORBIDDEN instead."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ManagementForbiddenResponse":{"x-error-codes":["FORBIDDEN"],"description":"FORBIDDEN: the requested operation or project is outside the key's access. Check configured permissions.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"FORBIDDEN":{"summary":"FORBIDDEN","value":{"error":{"code":"FORBIDDEN","message":"FORBIDDEN: the requested operation or project is outside the key's access. Check configured permissions."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFoundResponse":{"x-error-codes":["NOT_FOUND"],"description":"NOT_FOUND: the key's project no longer exists, or the requested comment is outside this project. Refresh project context and use identifiers from this scope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"error":{"code":"NOT_FOUND","message":"The requested project or comment is not available."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ManagementInternalErrorResponse":{"x-error-codes":["INTERNAL_ERROR"],"description":"INTERNAL_ERROR: unexpected failure. Use X-Request-Id for diagnosis; refresh state before retrying an uncertain mutation. Management does not replay Idempotency-Key.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"error":{"code":"INTERNAL_ERROR","message":"Refresh state or contact support with X-Request-Id."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"OperationCompletedResponse":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectActionResult"}},"required":["data"]}}}},"ManagementRequestBodyTooLargeResponse":{"x-error-codes":["REQUEST_BODY_TOO_LARGE"],"description":"REQUEST_BODY_TOO_LARGE: the complete JSON body exceeds 16 KiB. Shorten the request.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"REQUEST_BODY_TOO_LARGE":{"summary":"REQUEST_BODY_TOO_LARGE","value":{"error":{"code":"REQUEST_BODY_TOO_LARGE","message":"REQUEST_BODY_TOO_LARGE: the complete JSON body exceeds 16 KiB. Shorten the request."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ProviderConfigurationChangedResponse":{"x-error-codes":["PROVIDER_CONFIGURATION_CHANGED"],"description":"PROVIDER_CONFIGURATION_CHANGED: settings changed during the test. Refresh before trying again.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"PROVIDER_CONFIGURATION_CHANGED":{"summary":"PROVIDER_CONFIGURATION_CHANGED","value":{"error":{"code":"PROVIDER_CONFIGURATION_CHANGED","message":"PROVIDER_CONFIGURATION_CHANGED: settings changed during the test. Refresh before trying again."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ManagementRateLimitedResponse":{"x-error-codes":["RATE_LIMITED"],"description":"RATE_LIMITED: no provider request was sent. Wait Retry-After seconds before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"},"Retry-After":{"$ref":"#/components/headers/ProviderTestRetryAfterHeader"}},"content":{"application/json":{"examples":{"RATE_LIMITED":{"summary":"RATE_LIMITED","value":{"error":{"code":"RATE_LIMITED","message":"RATE_LIMITED: no provider request was sent. Wait Retry-After seconds before retrying."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ProviderUnavailableResponse":{"x-error-codes":["PROVIDER_UNAVAILABLE"],"description":"PROVIDER_UNAVAILABLE: no usable provider result. Check configuration and connection; no moderation decision was made.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"PROVIDER_UNAVAILABLE":{"summary":"PROVIDER_UNAVAILABLE","value":{"error":{"code":"PROVIDER_UNAVAILABLE","message":"PROVIDER_UNAVAILABLE: no usable provider result. Check configuration and connection; no moderation decision was made."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/v1/me/reactions":{"get":{"operationId":"listAuthorReactions","tags":["Author state"],"security":[{"ProjectApiKey":[]}],"summary":"List the viewer's reactions within a resource","description":"Restore liked/disliked UI state after a reload without writing or persisting reaction state in your own database. Your backend authenticates the user before supplying X-Author-User-Id. Returns only that user's existing reactions on APPROVED, non-deleted comments in the key's project and required resource; pending edits preserve existing public visibility. Optionally repeat commentId (maximum 100) to restrict to rendered comments. Ordered by reaction creation time descending, then reaction ID descending. limit defaults to 20 (1–100). Continue using nextCursor with the same project, author, resource and commentId filter set; null ends the list. Read-only, available in frozen projects, consumes no write permits; no separate read rate limiter is applied.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"$ref":"#/components/parameters/ResourceIdQueryParameter"},{"schema":{"type":"array","items":{"type":"string","minLength":36,"maxLength":36,"format":"uuid","description":"RECOMS-generated UUIDv7 identifying a comment in the authenticated project. Copy the identifier from a response; UUID input syntax is validated.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"minItems":1,"maxItems":100,"description":"Optional rendered comment IDs. Repeat commentId in the query (not comma-separated); at most 100 values. Omit to read all of the author's public reactions in this resource. Comments without a reaction are absent.","examples":[["0198a6ec-9e00-7000-8000-000000000001"]]},"required":false,"description":"Optional rendered comment IDs. Repeat commentId in the query (not comma-separated); at most 100 values. Omit to read all of the author's public reactions in this resource. Comments without a reaction are absent.","name":"commentId","in":"query","style":"form","explode":true},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum reactions per page, 1–100 inclusive; defaults to 20. Use nextCursor to continue.","examples":[20]},"required":false,"description":"Maximum reactions per page, 1–100 inclusive; defaults to 20. Use nextCursor to continue.","name":"limit","in":"query"},{"$ref":"#/components/parameters/CursorQueryParameter"},{"$ref":"#/components/parameters/AuthorUserIdHeaderParameter"}],"responses":{"200":{"description":"Viewer's existing reactions; unreached or unreacted comments have no entry.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"page":{"summary":"Viewer liked the rendered comment","value":{"data":[{"commentId":"0198a6ec-9e00-7000-8000-000000000001","type":"LIKE"}],"nextCursor":null}},"empty":{"summary":"No matching public reactions","value":{"data":[],"nextCursor":null}}},"schema":{"$ref":"#/components/schemas/AuthorReactionListResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequestResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/me/comments":{"get":{"operationId":"listAuthorComments","tags":["Author state"],"security":[{"ProjectApiKey":[]}],"summary":"List the author's live submissions and published versions","description":"Build a my-comments page or recover submissions whose IDs were lost, including comments awaiting review, without duplicating comment state in your database. Your backend authenticates the author supplied in X-Author-User-Id; the project key and author scope every result. Roots and replies are included, deleted comments are excluded. Optional resourceId narrows the resource; repeat latestRevisionStatus to match the latest submission only, including pending edits of already published comments. Returns the existing AuthorComment representation: published content plus latestRevision. NEWEST (default) orders comment creation time and ID descending; OLDEST orders both ascending. limit defaults to 20 (1–100). Copy nextCursor with unchanged project, author, filters and sort; null ends the list. Filter order is immaterial and limit may change; pagination is not a snapshot across requests. Read-only, available in frozen projects, consumes no write permits; no separate read rate limiter is applied.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":512,"description":"Optional opaque resource filter; omit to list across this project. May contain slashes. Accepts 1–512 Unicode code points.","examples":["article:postgresql-cursors"]},"required":false,"description":"Optional opaque resource filter; omit to list across this project. May contain slashes. Accepts 1–512 Unicode code points.","name":"resourceId","in":"query"},{"schema":{"type":"array","items":{"type":"string","enum":["APPROVED","PENDING","FAILED","REJECTED"],"description":"Moderation status of this submitted revision: APPROVED is allowed for publication; PENDING awaits human review; FAILED means required AI moderation could not produce a decision; REJECTED is not allowed for publication. FAILED is a technical failure, not a content rejection.","examples":["PENDING"]},"minItems":1,"description":"Optional latest-submission states, passed as repeated query parameters. PENDING awaits review, FAILED needs review after technical failure, REJECTED was declined, APPROVED was published. Filters the latest revision only, so pending edits of approved comments are included. Omit to include every live submission state.","examples":[["PENDING","FAILED","REJECTED"]]},"required":false,"description":"Optional latest-submission states, passed as repeated query parameters. PENDING awaits review, FAILED needs review after technical failure, REJECTED was declined, APPROVED was published. Filters the latest revision only, so pending edits of approved comments are included. Omit to include every live submission state.","name":"latestRevisionStatus","in":"query","style":"form","explode":true},{"schema":{"type":"string","enum":["NEWEST","OLDEST"],"default":"NEWEST","description":"NEWEST (default): creation time descending, then ID descending. OLDEST: creation time ascending, then ID ascending. Edits do not change creation time.","examples":["NEWEST"]},"required":false,"description":"NEWEST (default): creation time descending, then ID descending. OLDEST: creation time ascending, then ID ascending. Edits do not change creation time.","name":"sort","in":"query"},{"$ref":"#/components/parameters/LimitQueryParameter"},{"$ref":"#/components/parameters/CursorQueryParameter"},{"$ref":"#/components/parameters/AuthorUserIdHeaderParameter"}],"responses":{"200":{"description":"Author's comments with their most recent submitted revision.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"pending":{"summary":"Unpublished submission recovered after reload","value":{"data":[{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"PENDING","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"This explanation of **cursor pagination** was helpful.","status":"PENDING"}}],"nextCursor":null}},"pendingEdit":{"summary":"Published content retained during edit review","value":{"data":[{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"Updated explanation.","status":"PENDING"}}],"nextCursor":null}},"empty":{"summary":"No matching live submissions","value":{"data":[],"nextCursor":null}}},"schema":{"$ref":"#/components/schemas/AuthorCommentListResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequestResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/resources/counts":{"get":{"operationId":"getResourceCommentCounts","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"Get published comment counts for multiple resources","description":"Render comment-count badges for an article or resource list without paginating roots and replies. Supply 1–100 distinct resourceId query values in request order; opaque IDs may contain slashes and must not be encoded as path segments. Returns exactly one entry per requested resource, including zeros. commentCount counts live comments with an APPROVED published version at every depth; rootCommentCount counts the subset with no parent. Deleted and never-published submissions are excluded, while pending/failed/rejected edits preserve counts of the previous published version. Counts reflect current committed state, not a stored counter; replyCount uses the same visibility but counts only a comment's direct replies. Project API key only; no author header. Read-only, available in frozen projects, consumes no write permits; no separate read rate limiter is applied.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"schema":{"type":"array","items":{"type":"string","minLength":1,"maxLength":512,"description":"Opaque identifier assigned by your backend to the resource being discussed, such as an article or product. Use the same value for its entire discussion. Required, 1–512 Unicode code points; U+0000 and unpaired UTF-16 surrogates are forbidden. Slashes are accepted; URL-encode it in query parameters. This is an identifier, not a resource URL.","examples":["article:postgresql-cursors"]},"minItems":1,"maxItems":100,"description":"Required 1–100 distinct opaque resource IDs, in requested response order. Repeat resourceId in the query; do not comma-separate. IDs may contain slashes and must each contain 1–512 Unicode code points. Duplicates are invalid.","uniqueItems":true,"examples":[["article:postgresql-cursors","articles/nextjs"]]},"required":true,"description":"Required 1–100 distinct opaque resource IDs, in requested response order. Repeat resourceId in the query; do not comma-separate. IDs may contain slashes and must each contain 1–512 Unicode code points. Duplicates are invalid.","name":"resourceId","in":"query","style":"form","explode":true}],"responses":{"200":{"description":"Published counts in requested resource order.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"counts":{"summary":"A populated resource followed by an empty resource","value":{"data":[{"resourceId":"article:postgresql-cursors","commentCount":42,"rootCommentCount":12},{"resourceId":"articles/empty","commentCount":0,"rootCommentCount":0}]}}},"schema":{"$ref":"#/components/schemas/ResourceCommentCountsResponse"}}}},"400":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.","x-error-codes":["VALIDATION_ERROR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"query","path":["resourceId"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/comments":{"get":{"operationId":"listRootComments","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"List root comments for a resource","description":"Public reads return approved content and deleted structural nodes only; unapproved revisions are never published. Authenticate from your backend with the project's API key, never from an end-user browser. Returns root comments only, not their replies. The required resourceId is your opaque resource identifier. Default sort is NEWEST; limit defaults to 20 (1–100). To continue, copy nextCursor into cursor with the same resourceId and sort. A null nextCursor ends the list. Replies have a separate listDirectReplies operation. Reads do not consume write permits.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"$ref":"#/components/parameters/ResourceIdQueryParameter"},{"schema":{"type":"string","enum":["NEWEST","OLDEST","REACTIONS","REPLIES"],"default":"NEWEST","description":"Root comment order. NEWEST: createdAt descending, id descending. OLDEST: createdAt ascending, id ascending. REACTIONS: reactionCount descending, then createdAt/id descending. REPLIES: direct replyCount descending, then createdAt/id descending. The ID tie-breaker makes pagination deterministic. Defaults to NEWEST when omitted.","examples":["NEWEST"]},"required":false,"description":"Root comment order. NEWEST: createdAt descending, id descending. OLDEST: createdAt ascending, id ascending. REACTIONS: reactionCount descending, then createdAt/id descending. REPLIES: direct replyCount descending, then createdAt/id descending. The ID tie-breaker makes pagination deterministic. Defaults to NEWEST when omitted.","name":"sort","in":"query"},{"$ref":"#/components/parameters/LimitQueryParameter"},{"$ref":"#/components/parameters/CursorQueryParameter"}],"responses":{"200":{"description":"Current page of public root comments.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"page":{"summary":"One approved root comment and no further page","value":{"data":[{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z"}],"nextCursor":null}},"empty":{"summary":"No public root comments for this resource","value":{"data":[],"nextCursor":null}}},"schema":{"$ref":"#/components/schemas/CommentListResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequestResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}},"post":{"operationId":"createComment","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"Submit a root comment or reply","description":"Authenticate the end user in your backend and pass their stable external author ID. The project key selects the tenant; it is not proof of end-user identity. Send JSON with Content-Type: application/json. Bodies must not exceed 16 KiB. Use Idempotency-Key when retrying an action after network failure. Successful AI moderation failure is not an HTTP error: inspect latestRevision.status. Omit parentId for a root comment, or supply the immediate parent to create a reply. A parent must be in the same project and resource and either have approved content or be a deleted node. Moderation may approve, reject, await review, or fail technically. The 201 response confirms the submission, not guaranteed publication. Read its latestRevision.status; only APPROVED content is public. New activity is blocked in frozen projects and when submission quota is unavailable.\n\nRequired API-key permission: comments:write. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"write","confirmation":"required","retryPolicy":"idempotency_key","permissions":{"comments":["write"]}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeaderParameter"}],"requestBody":{"required":true,"description":"New comment source and author/resource identity.","content":{"application/json":{"examples":{"root":{"summary":"Create a root comment","value":{"resourceId":"article:postgresql-cursors","authorUserId":"usr_42","content":"This explanation of **cursor pagination** was helpful."}},"reply":{"summary":"Reply to the root comment in the same resource","value":{"resourceId":"article:postgresql-cursors","authorUserId":"usr_43","parentId":"0198a6ec-9e00-7000-8000-000000000001","content":"Thanks for the example."}}},"schema":{"$ref":"#/components/schemas/CreateCommentRequest"}}}},"responses":{"201":{"description":"Committed submission, including its latest revision. A replay returns the original 201 response.","links":{"authorState":{"operationId":"getAuthorComment","description":"Read the submission using its author's X-Author-User-Id header.","parameters":{"commentId":"$response.body#/data/id","X-Author-User-Id":"$request.body#/authorUserId"}}},"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"pending":{"summary":"Submitted for human review; not public yet","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"PENDING","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"This explanation of **cursor pagination** was helpful.","status":"PENDING"}}}},"approved":{"summary":"Approved and published","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED"}}}},"failed":{"summary":"Committed but required AI check failed; await moderator review","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"FAILED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"This explanation of **cursor pagination** was helpful.","status":"FAILED"}}}}},"schema":{"$ref":"#/components/schemas/AuthorCommentResponse"}}}},"400":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.","x-error-codes":["VALIDATION_ERROR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"body","path":["resourceId"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"402":{"$ref":"#/components/responses/SubmissionAccessDeniedResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"405":{"x-error-codes":["METHOD_NOT_ALLOWED"],"description":"METHOD_NOT_ALLOWED: The path exists but does not support the requested HTTP method. Choose an operation documented for this path; do not retry the same method.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"},"Allow":{"schema":{"type":"string","description":"Comma-separated methods currently supported by this path.","examples":["GET, HEAD, POST"]},"required":true,"description":"Comma-separated methods currently supported by this path."}},"content":{"application/json":{"example":{"error":{"code":"METHOD_NOT_ALLOWED","message":"This route does not support the requested HTTP method."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"PARENT_RESOURCE_MISMATCH: The parent belongs to another resource. Use the parent's resourceId or omit parentId to create a root comment.\n\nIDEMPOTENCY_KEY_REUSED: This Idempotency-Key was used with different input. Restore the original request or use a new key for a genuinely different action.","x-error-codes":["PARENT_RESOURCE_MISMATCH","IDEMPOTENCY_KEY_REUSED"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"PARENT_RESOURCE_MISMATCH":{"summary":"PARENT_RESOURCE_MISMATCH","value":{"error":{"code":"PARENT_RESOURCE_MISMATCH","message":"The parent belongs to another resource. Use the parent's resourceId or omit parentId to create a root comment."}}},"IDEMPOTENCY_KEY_REUSED":{"summary":"IDEMPOTENCY_KEY_REUSED","value":{"error":{"code":"IDEMPOTENCY_KEY_REUSED","message":"This Idempotency-Key was used with different input. Restore the original request or use a new key for a genuinely different action."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"$ref":"#/components/responses/CommentApiRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/CommentApiRateLimitedResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/comments/{commentId}":{"get":{"operationId":"getPublicComment","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"Get a publicly visible comment","description":"Public reads return approved content and deleted structural nodes only; unapproved revisions are never published. Authenticate from your backend with the project's API key, never from an end-user browser. Pending, rejected or failed submissions without a public version return 404. A pending edit does not hide the previous approved text. Deleted comments return DELETED with null content and retain their position in the thread. Use getAuthorComment to inspect an author's unpublished submission.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"}],"responses":{"200":{"description":"Published comment or deleted structural node.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"approved":{"summary":"Published Markdown","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z"}}},"deleted":{"summary":"Deleted node; text is not disclosed","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"DELETED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z"}}}},"schema":{"$ref":"#/components/schemas/CommentResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiPathCommentIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}},"patch":{"operationId":"editComment","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"Submit a replacement revision of an owned comment","description":"Authenticate the end user in your backend and pass their stable external author ID. The project key selects the tenant; it is not proof of end-user identity. Send JSON with Content-Type: application/json. Bodies must not exceed 16 KiB. Use Idempotency-Key when retrying an action after network failure. Successful AI moderation failure is not an HTTP error: inspect latestRevision.status. authorUserId must own the comment. The entire Markdown source is replaced by a new revision; resource and parent identity cannot be changed. Existing published content stays visible while an edit is PENDING, FAILED or REJECTED. An APPROVED edit replaces it. Frozen projects and unavailable submission quota block new edits. Deleted comments cannot be edited. Use getAuthorComment to follow moderation state.\n\nRequired API-key permission: comments:write. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"write","confirmation":"required","retryPolicy":"idempotency_key","permissions":{"comments":["write"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/IdempotencyKeyHeaderParameter"}],"requestBody":{"required":true,"description":"Owner identity and complete replacement Markdown source.","content":{"application/json":{"examples":{"pendingEdit":{"summary":"Previous public source remains visible during review","value":{"authorUserId":"usr_42","content":"Updated explanation of **cursor pagination**."}}},"schema":{"$ref":"#/components/schemas/EditCommentRequest"}}}},"responses":{"200":{"description":"Committed edit; inspect latestRevision.status for its moderation outcome.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"pendingEdit":{"summary":"Previous public source remains visible during review","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"Updated explanation of **cursor pagination**.","status":"PENDING"}}}}},"schema":{"$ref":"#/components/schemas/AuthorCommentResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiBodyAuthorUserIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"402":{"$ref":"#/components/responses/SubmissionAccessDeniedResponse"},"403":{"$ref":"#/components/responses/CommentAccessDeniedResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"405":{"$ref":"#/components/responses/CommentMethodNotAllowedResponse"},"409":{"$ref":"#/components/responses/CommentMutationConflictResponse"},"413":{"$ref":"#/components/responses/CommentApiRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/CommentApiRateLimitedResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}},"delete":{"operationId":"deleteComment","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"Soft delete an owned comment","description":"The externally authenticated author in X-Author-User-Id must own the comment. Soft deletion hides its source in public and author responses but preserves the structural node, reply relationships and stored revision history for authorized dashboard members. This is not author erasure. Deletion remains available in frozen projects and consumes write permits. A second deletion without replay returns 409 COMMENT_DELETED; use the original Idempotency-Key to retry the same action. No request body is required.\n\nRequired API-key permission: comments:write. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"destructive","confirmation":"required","retryPolicy":"idempotency_key","permissions":{"comments":["write"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/AuthorUserIdHeaderParameter"},{"$ref":"#/components/parameters/IdempotencyKeyHeaderParameter"}],"responses":{"200":{"description":"Soft deletion completed, or the original successful deletion was replayed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"deleted":{"summary":"Deletion confirmation","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","status":"DELETED"}}}},"schema":{"$ref":"#/components/schemas/DeleteCommentResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiPathCommentIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentAccessDeniedResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"405":{"$ref":"#/components/responses/CommentMethodNotAllowedResponse"},"409":{"$ref":"#/components/responses/CommentMutationConflictResponse"},"413":{"$ref":"#/components/responses/CommentApiRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/CommentApiRateLimitedResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/me/comments/{commentId}":{"get":{"operationId":"getAuthorComment","tags":["Author state"],"security":[{"ProjectApiKey":[]}],"summary":"Get the author's submission and published state","description":"Read a comment owned by the externally authenticated user identified by X-Author-User-Id. Your backend must establish this identity; never trust an arbitrary client-supplied user ID. The response contains published content and the latest submitted revision, including PENDING, FAILED or REJECTED. A published comment can remain APPROVED while its edit awaits review. Deleted source is hidden in both content fields. This read does not consume write permits.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/AuthorUserIdHeaderParameter"}],"responses":{"200":{"description":"Owned comment with latest submission state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"pending":{"summary":"Author's submission awaiting review","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"PENDING","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"This explanation of **cursor pagination** was helpful.","status":"PENDING"}}}},"pendingEdit":{"summary":"Published text retained while the new edit awaits review","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"This explanation of **cursor pagination** was helpful.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":"Updated explanation of **cursor pagination**.","status":"PENDING"}}}},"deleted":{"summary":"Deleted author state with both texts hidden","value":{"data":{"id":"0198a6ec-9e00-7000-8000-000000000001","resourceId":"article:postgresql-cursors","authorUserId":"usr_42","parentId":null,"rootId":"0198a6ec-9e00-7000-8000-000000000001","content":null,"status":"DELETED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z","latestRevision":{"content":null,"status":"APPROVED"}}}}},"schema":{"$ref":"#/components/schemas/AuthorCommentResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiPathCommentIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentAccessDeniedResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/comments/{commentId}/replies":{"get":{"operationId":"listDirectReplies","tags":["Comments"],"security":[{"ProjectApiKey":[]}],"summary":"List direct replies to a comment","description":"Public reads return approved content and deleted structural nodes only; unapproved revisions are never published. Authenticate from your backend with the project's API key, never from an end-user browser. Lists immediate children, not all descendants, ordered by createdAt ascending then id ascending. The parent must be publicly accessible, including deleted nodes. To traverse deeper, call this operation for each reply. Limit defaults to 20 (1–100). Copy nextCursor into cursor for this same parent and project; null ends the list. A cursor from root comments or another parent is invalid.\n\nRequired API-key permission: comments:read. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/LimitQueryParameter"},{"$ref":"#/components/parameters/CursorQueryParameter"}],"responses":{"200":{"description":"Current page of public direct replies.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"page":{"summary":"One direct reply to the example root","value":{"data":[{"id":"0198a6ec-9e00-7000-8000-000000000002","resourceId":"article:postgresql-cursors","authorUserId":"usr_43","parentId":"0198a6ec-9e00-7000-8000-000000000001","rootId":"0198a6ec-9e00-7000-8000-000000000001","content":"Thanks for the example.","status":"APPROVED","replyCount":0,"likeCount":0,"dislikeCount":0,"reactionCount":0,"createdAt":"2026-08-14T12:00:00.000Z","updatedAt":"2026-08-14T12:00:00.000Z"}],"nextCursor":null}},"empty":{"summary":"No public direct replies","value":{"data":[],"nextCursor":null}}},"schema":{"$ref":"#/components/schemas/CommentListResponse"}}}},"400":{"description":"VALIDATION_ERROR: The request is invalid. Correct the indicated body, query, identifier or header before retrying.\n\nINVALID_CURSOR: The cursor does not match this collection or query. Restart without cursor and keep the same filters while paginating.","x-error-codes":["VALIDATION_ERROR","INVALID_CURSOR"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"The request is invalid. Correct the indicated body, query, identifier or header before retrying.","details":[{"location":"path","path":["commentId"],"rule":"invalid_type"}]}}},"INVALID_CURSOR":{"summary":"INVALID_CURSOR","value":{"error":{"code":"INVALID_CURSOR","message":"The cursor does not match this collection or query. Restart without cursor and keep the same filters while paginating."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/comments/{commentId}/reaction":{"put":{"operationId":"setReaction","tags":["Reactions"],"security":[{"ProjectApiKey":[]}],"summary":"Set or replace the user's reaction","description":"Authenticate the end user in your backend and pass their external authorUserId. Only comments with approved public content accept reactions. Each user has at most one LIKE or DISLIKE per comment: setting the same type leaves it unchanged and setting the other replaces it. A pending edit does not disable reactions to existing public content. Frozen projects block new reaction activity. This mutation consumes write permits; optional Idempotency-Key provides replay for network retries. Send JSON; the maximum body is 16 KiB.\n\nRequired API-key permission: comments:write. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"write","confirmation":"required","retryPolicy":"idempotency_key","permissions":{"comments":["write"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/IdempotencyKeyHeaderParameter"}],"requestBody":{"required":true,"description":"Authenticated user's identity and desired reaction.","content":{"application/json":{"examples":{"liked":{"summary":"One LIKE on the example comment","value":{"authorUserId":"usr_42","type":"LIKE"}},"disliked":{"summary":"The same user replaced LIKE with DISLIKE","value":{"authorUserId":"usr_42","type":"DISLIKE"}}},"schema":{"$ref":"#/components/schemas/SetReactionRequest"}}}},"responses":{"200":{"description":"User's current reaction and updated aggregate counts.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"liked":{"summary":"One LIKE on the example comment","value":{"data":{"commentId":"0198a6ec-9e00-7000-8000-000000000001","type":"LIKE","likeCount":1,"dislikeCount":0,"reactionCount":1}}},"disliked":{"summary":"The same user replaced LIKE with DISLIKE","value":{"data":{"commentId":"0198a6ec-9e00-7000-8000-000000000001","type":"DISLIKE","likeCount":0,"dislikeCount":1,"reactionCount":1}}}},"schema":{"$ref":"#/components/schemas/ReactionResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiBodyAuthorUserIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"402":{"description":"PROJECT_FROZEN: The project is frozen. The owner must activate Free or upgrade to Pro before new comments, edits or reactions are accepted. Reads and removals remain available.","x-error-codes":["PROJECT_FROZEN"],"headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"PROJECT_FROZEN":{"summary":"PROJECT_FROZEN","value":{"error":{"code":"PROJECT_FROZEN","message":"The project is frozen. The owner must activate Free or upgrade to Pro before new comments, edits or reactions are accepted. Reads and removals remain available."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"405":{"$ref":"#/components/responses/ReactionMethodNotAllowedResponse"},"409":{"$ref":"#/components/responses/CommentMutationConflictResponse"},"413":{"$ref":"#/components/responses/CommentApiRequestBodyTooLargeResponse"},"422":{"$ref":"#/components/responses/CommentNotPublishedResponse"},"429":{"$ref":"#/components/responses/CommentApiRateLimitedResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}},"delete":{"operationId":"removeReaction","tags":["Reactions"],"security":[{"ProjectApiKey":[]}],"summary":"Remove the user's reaction","description":"Remove the reaction belonging to the external user in X-Author-User-Id. Your backend authenticates this identity. No request body is required. If no reaction exists, return type null and unchanged counters. Deleted comments and comments without approved public content cannot be targeted. Removal remains available in frozen projects and consumes write permits. Use the original Idempotency-Key to retry an uncertain action; other users' reactions are unchanged.\n\nRequired API-key permission: comments:write. A valid key without this permission is not sufficient; author identity and ownership requirements still apply where documented.","x-agent":{"effect":"destructive","confirmation":"required","retryPolicy":"idempotency_key","permissions":{"comments":["write"]}},"parameters":[{"$ref":"#/components/parameters/CommentIdPathParameter"},{"$ref":"#/components/parameters/AuthorUserIdHeaderParameter"},{"$ref":"#/components/parameters/IdempotencyKeyHeaderParameter"}],"responses":{"200":{"description":"User has no reaction; aggregate counts reflect removal.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"removed":{"summary":"Example user's reaction removed","value":{"data":{"commentId":"0198a6ec-9e00-7000-8000-000000000001","type":null,"likeCount":0,"dislikeCount":0,"reactionCount":0}}}},"schema":{"$ref":"#/components/schemas/ReactionResponse"}}}},"400":{"$ref":"#/components/responses/CommentApiPathCommentIdValidationErrorResponse"},"401":{"$ref":"#/components/responses/CommentApiAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/CommentApiForbiddenResponse"},"404":{"$ref":"#/components/responses/CommentNotFoundResponse"},"405":{"$ref":"#/components/responses/ReactionMethodNotAllowedResponse"},"409":{"$ref":"#/components/responses/CommentMutationConflictResponse"},"413":{"$ref":"#/components/responses/CommentApiRequestBodyTooLargeResponse"},"422":{"$ref":"#/components/responses/CommentNotPublishedResponse"},"429":{"$ref":"#/components/responses/CommentApiRateLimitedResponse"},"500":{"$ref":"#/components/responses/CommentApiInternalErrorResponse"}}}},"/v1/project/context":{"get":{"operationId":"getProjectContext","summary":"Read project settings and effective access","description":"Read project settings, billing state and non-secret provider configuration. Requires project:read. The key selects the project. access returns its keyId and effective permissions in resource:action form, allowing you to choose available operations before sending a request. Permissions can change; the API checks each request independently. Moderation rules and the recommended reset policy are included for management access. Available while frozen; no write permits are consumed.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"project":["read"]}},"responses":{"200":{"description":"Project settings and the authenticated key's effective permissions.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"project":{"id":"0198a6ec-9e00-7000-8000-000000000001","name":"Product discussions","slug":"product-discussions","createdAt":"2026-08-14T12:00:00.000Z","moderationMode":"MANUAL","moderationPolicy":"Reject unsolicited advertising; allow relevant discussion.","analyticsTimezone":"UTC","billingAccount":{"id":"0198a6ec-9e00-7000-8000-000000000001","planCode":"FREE","accessState":"ACTIVE","usageAnchorAt":"2026-08-14T12:00:00.000Z"},"aiConfiguration":null},"access":{"keyId":"0198a6ec-9e00-7000-8000-000000000001","permissions":["comments:read","project:read"]},"defaultModerationPolicy":"Reject unsolicited advertising; allow relevant discussion."}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectContext"}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/review-queues":{"get":{"operationId":"getProjectReviewQueues","summary":"Count revisions awaiting moderator attention","description":"Count the latest PENDING and FAILED revisions of live comments, including pending edits of already published comments. Requires comments:read. Deleted comments are excluded. FAILED is a technical check failure, not rejection. These counts and a separate list are not a shared snapshot. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"pending":3,"failed":1}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectReviewQueues"}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/comments":{"get":{"operationId":"listProjectComments","summary":"List comments for project moderation","description":"List roots and replies with their latest moderation revision. Requires comments:read. Filter status follows the latest revision, so an approved comment with a pending edit appears under PENDING. Creation time or current reaction/reply counts determine order; ID breaks ties. Continue with nextCursor and identical filters/sort; null ends pagination. Pages are not a snapshot. hasComments ignores filters; queues describes moderation backlog. Available while frozen; no write permits are consumed.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"comments":["read"]}},"parameters":[{"schema":{"type":"string","maxLength":512,"description":"Text search in comment content; at most 512 Unicode code points. Omit or use an empty string for no text filter."},"required":false,"description":"Text search in comment content; at most 512 Unicode code points. Omit or use an empty string for no text filter.","name":"search","in":"query"},{"schema":{"$ref":"#/components/schemas/ProjectCommentStatus"},"required":false,"description":"Filter by latest revision decision or deleted node. ALL and unpublished states require moderation access.","name":"status","in":"query"},{"schema":{"$ref":"#/components/schemas/ProjectCommentSort"},"required":false,"description":"Creation time descending or ascending, total reactions descending, or published direct reply count descending; identifier breaks ties.","name":"sort","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Maximum rows in this page, inclusive range 1–100."},"required":false,"description":"Maximum rows in this page, inclusive range 1–100.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque nextCursor from the preceding page. Keep filters and sort unchanged; incompatible tokens return INVALID_CURSOR. Omit to restart. Pagination is not a snapshot."},"required":false,"description":"Opaque nextCursor from the preceding page. Keep filters and sort unchanged; incompatible tokens return INVALID_CURSOR. Omit to restart. Pagination is not a snapshot.","name":"cursor","in":"query"}],"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":[],"nextCursor":null,"hasComments":false,"queues":{"pending":0,"failed":0}},"schema":{"$ref":"#/components/schemas/ProjectComments"}}}},"400":{"x-error-codes":["VALIDATION_ERROR","INVALID_CURSOR"],"description":"VALIDATION_ERROR: correct the indicated query fields. INVALID_CURSOR: discard the cursor and restart pagination.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated query fields. INVALID_CURSOR: discard the cursor and restart pagination.","details":[{"location":"query","path":["search"],"rule":"invalid_type"}]}}},"INVALID_CURSOR":{"summary":"INVALID_CURSOR","value":{"error":{"code":"INVALID_CURSOR","message":"VALIDATION_ERROR: correct the indicated query fields. INVALID_CURSOR: discard the cursor and restart pagination."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/comments/{commentId}/review":{"post":{"operationId":"reviewComment","summary":"Approve or reject a submitted revision","description":"Approve or reject the specified latest PENDING or FAILED revision. Requires comments:review. Approval publishes that revision; rejection retains an earlier approved version. A key decision has no verified moderator-account attribution. A stale or already decided revision returns REVISION_ALREADY_REVIEWED. Refresh before another action or after an uncertain network outcome; this operation does not replay Idempotency-Key. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"write","confirmation":"required","retryPolicy":"refresh_before_retry","permissions":{"comments":["review"]}},"parameters":[{"schema":{"type":"string","format":"uuid","description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","examples":["0198a6ec-9e00-7000-8000-000000000001"]},"required":true,"description":"RECOMS-generated UUID. Copy it from the corresponding project, member, comment or revision response; identifiers are not interchangeable.","name":"commentId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewComment"}}}},"responses":{"200":{"$ref":"#/components/responses/OperationCompletedResponse"},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"body","path":["revisionId"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"409":{"x-error-codes":["REVISION_ALREADY_REVIEWED"],"description":"REVISION_ALREADY_REVIEWED: another decision has already been recorded. Refresh the comment before continuing.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"REVISION_ALREADY_REVIEWED":{"summary":"REVISION_ALREADY_REVIEWED","value":{"error":{"code":"REVISION_ALREADY_REVIEWED","message":"REVISION_ALREADY_REVIEWED: another decision has already been recorded. Refresh the comment before continuing."}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"$ref":"#/components/responses/ManagementRequestBodyTooLargeResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/settings":{"patch":{"operationId":"updateProjectSettings","summary":"Change moderation rules, mode or analytics timezone","description":"Atomically replace the supplied moderation mode, policy or analytics timezone. Requires project:update. Omitted fields remain unchanged; supply at least one. OFF publishes immediately, MANUAL queues review, AI resolves automatically, HYBRID queues suspected violations for review. Rules are trimmed. A timezone changes analytics calendar boundaries, not stored UTC timestamps. Existing decisions are not rerun. Refresh settings before retrying an uncertain outcome; Idempotency-Key replay is not supported. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"write","confirmation":"required","retryPolicy":"refresh_before_retry","permissions":{"project":["update"]}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectSettings"}}}},"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"savedPolicy":"Reject unsolicited advertising; allow relevant discussion."}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectSettingsResult"}},"required":["data"]}}}},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"body","path":["moderationMode"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"413":{"$ref":"#/components/responses/ManagementRequestBodyTooLargeResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/analytics":{"get":{"operationId":"getProjectAnalytics","summary":"Read publication and moderation analytics","description":"Read discussion and moderation metrics in the project's timezone. Requires analytics:read. Discussion counts first publication rather than edits; moderation summary counts unique comments with approval/rejection decisions. CUSTOM requires inclusive YYYY-MM-DD dates; the returned endAt is exclusive and current-day ranges stop at request time. Free history is limited to 90 days; unavailable segmentation metrics are null. planUsage follows the current billing period independently of the selected range. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"analytics":["read"]}},"parameters":[{"schema":{"$ref":"#/components/schemas/AnalyticsRangePreset"},"required":false,"description":"Calendar preset in the project's analytics timezone; CUSTOM requires both custom dates. Available history follows project entitlements.","name":"preset","in":"query"},{"schema":{"type":"string","format":"date","description":"Inclusive calendar start date, YYYY-MM-DD, in the project's timezone; required for CUSTOM."},"required":false,"description":"Inclusive calendar start date, YYYY-MM-DD, in the project's timezone; required for CUSTOM.","name":"customStartDate","in":"query"},{"schema":{"type":"string","format":"date","description":"Inclusive calendar end date, YYYY-MM-DD, in the project's timezone; required for CUSTOM. The response endAt is the next day's exclusive boundary, or the request time when the current date is selected. Dates must be ordered and not in the future."},"required":false,"description":"Inclusive calendar end date, YYYY-MM-DD, in the project's timezone; required for CUSTOM. The response endAt is the next day's exclusive boundary, or the request time when the current date is selected. Dates must be ordered and not in the future.","name":"customEndDate","in":"query"}],"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"analytics":{"publishedComments":100,"contributors":24,"replyRate":20,"threadsWithReplies":null,"range":{"startAt":"2026-08-01T00:00:00.000Z","endAt":"2026-08-31T00:00:00.000Z","bucket":"DAY","preset":"LAST_30_DAYS"},"series":[{"bucketStartAt":"2026-08-14T00:00:00.000Z","rootComments":80,"replies":20,"contributors":24,"newContributors":null,"returningContributors":null}],"topResources":[{"resourceId":"article:postgresql-cursors","publishedComments":100,"contributors":24,"replyRate":20}],"topAuthors":[{"authorUserId":"usr_42","publishedComments":12}],"comparison":null,"contributorSegmentationAvailable":false},"moderation":{"aiModerationAttempts":0,"autoResolvedRate":null,"aiFailureRate":null,"medianManualReviewTimeMs":120000,"moderatorAgreementRate":null,"moderatorAgreementSampleSize":0,"outcomes":[]},"summary":{"approved":100,"blocked":25,"blockedRate":20,"comparison":{"approved":80,"blocked":20},"series":[{"approved":100,"blocked":25,"bucketStartAt":"2026-08-14T00:00:00.000Z"}]},"planUsage":{"consumed":42,"included":1000,"period":{"startAt":"2026-08-14T12:00:00.000Z","endAt":"2026-09-14T12:00:00.000Z"}}}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectAnalytics"}},"required":["data"]}}}},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"query","path":["preset"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/usage":{"get":{"operationId":"getProjectUsage","summary":"Read the current submission allowance and usage","description":"Read submission usage and allowance for the current project billing period. Requires usage:read. Edits count as submissions; freeze/activation does not reset usage. Returns null without billing state. With billing state but no current usage interval, consumed is zero, included follows the plan and period is null. This read never reserves quota and is available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"read","confirmation":"none","retryPolicy":"safe","permissions":{"usage":["read"]}},"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"consumed":42,"included":1000,"period":{"startAt":"2026-08-14T12:00:00.000Z","endAt":"2026-09-14T12:00:00.000Z"}}},"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/ProjectUsage"},{"description":"Operation result; inspect its fields as documented by this endpoint."}]}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"}}}},"/v1/project/moderation/test":{"post":{"operationId":"testModerationPolicy","summary":"Test draft rules against example text","description":"Test draft rules against example text using the current provider. Requires provider:test. No policy or comment is saved; no submission quota is consumed. The provider attempt is recorded and the separate provider-test rate limit applies. ALLOWED and DISALLOWED are successful test outcomes. Provider absence or technical failure returns PROVIDER_UNAVAILABLE rather than a content rejection. RATE_LIMITED includes Retry-After. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"diagnostic","confirmation":"none","retryPolicy":"refresh_before_retry","permissions":{"provider":["test"]}},"requestBody":{"required":true,"content":{"application/json":{"example":{"moderationPolicy":"Reject unsolicited advertising; allow relevant discussion.","exampleComment":"Thank you for this explanation."},"schema":{"$ref":"#/components/schemas/TestModerationPolicy"}}}},"responses":{"200":{"description":"Operation completed successfully.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"example":{"data":{"policyTest":{"outcome":"ALLOWED"}}},"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectPolicyTestResult"}},"required":["data"]}}}},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"body","path":["moderationPolicy"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"409":{"$ref":"#/components/responses/ProviderConfigurationChangedResponse"},"413":{"$ref":"#/components/responses/ManagementRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/ManagementRateLimitedResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"},"502":{"$ref":"#/components/responses/ProviderUnavailableResponse"}}}},"/v1/project/provider":{"post":{"operationId":"configureProjectProvider","summary":"Test and save a custom moderation provider","description":"Test and save an OpenAI-compatible HTTPS provider. Requires provider:configure. Unsafe outbound destinations are blocked. Settings are saved only after a successful connection test; a failed candidate does not replace the current configuration. preserve retains an existing credential or accepts an initial one; replace requires credential; remove clears authentication. Credentials are encrypted and never returned. Refresh after PROVIDER_CONFIGURATION_CHANGED; wait Retry-After on RATE_LIMITED. Provider tests do not consume submission quota. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"write","confirmation":"required","retryPolicy":"refresh_before_retry","permissions":{"provider":["configure"]}},"requestBody":{"required":true,"content":{"application/json":{"example":{"baseUrl":"https://api.openai.com/v1","model":"gpt-4.1-mini","credentialIntent":"replace","credential":"example-provider-key-not-a-secret"},"schema":{"$ref":"#/components/schemas/ConfigureProvider"}}}},"responses":{"200":{"$ref":"#/components/responses/OperationCompletedResponse"},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"body","path":["baseUrl"],"rule":"invalid_type"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"409":{"$ref":"#/components/responses/ProviderConfigurationChangedResponse"},"413":{"$ref":"#/components/responses/ManagementRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/ManagementRateLimitedResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"},"502":{"$ref":"#/components/responses/ProviderUnavailableResponse"}}}},"/v1/project/provider/test":{"post":{"operationId":"testProjectProvider","summary":"Check the saved provider connection","description":"Retry the saved provider connection. Requires provider:test. Availability and last test outcome may change and the attempt is recorded, but settings are not replaced. Missing/unreadable credentials or an unusable provider return PROVIDER_UNAVAILABLE; concurrent changes return PROVIDER_CONFIGURATION_CHANGED. Respect Retry-After on RATE_LIMITED. This diagnostic call changes test state, consumes provider-test capacity and does not consume comment submission quota. Available while frozen.","tags":["Management"],"security":[{"ProjectApiKey":[]}],"x-agent":{"effect":"diagnostic","confirmation":"none","retryPolicy":"refresh_before_retry","permissions":{"provider":["test"]}},"requestBody":{"required":true,"content":{"application/json":{"example":{},"schema":{"$ref":"#/components/schemas/EmptyProjectAction"}}}},"responses":{"200":{"$ref":"#/components/responses/OperationCompletedResponse"},"400":{"x-error-codes":["VALIDATION_ERROR"],"description":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestIdHeader"}},"content":{"application/json":{"examples":{"VALIDATION_ERROR":{"summary":"VALIDATION_ERROR","value":{"error":{"code":"VALIDATION_ERROR","message":"VALIDATION_ERROR: correct the indicated fields or JSON body before retrying.","details":[{"location":"body","path":["unexpectedField"],"rule":"unknown_field"}]}}}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/ManagementAuthenticationRequiredResponse"},"403":{"$ref":"#/components/responses/ManagementForbiddenResponse"},"404":{"$ref":"#/components/responses/NotFoundResponse"},"409":{"$ref":"#/components/responses/ProviderConfigurationChangedResponse"},"413":{"$ref":"#/components/responses/ManagementRequestBodyTooLargeResponse"},"429":{"$ref":"#/components/responses/ManagementRateLimitedResponse"},"500":{"$ref":"#/components/responses/ManagementInternalErrorResponse"},"502":{"$ref":"#/components/responses/ProviderUnavailableResponse"}}}}}}