Skip to content

Comments API ​

Public API

The comment read and submit endpoints are part of Studio's public, versioned API (/api/comments/v1/), designed to be called directly from the site that renders the entry. Moderation, thread locking and import are part of the internal application API.

Studio-managed comments attach to content entries. Comments live in the Studio database (never in Git) and are served to any site through the public API. Per-model configuration lives on the model definition next to form, so it travels with the repository.

Enabling Comments on a Model ​

Comments are enabled per model with a comments block, saved through the model update route (workspace owner or admin). Only collection and document models can carry comments.

PATCH /api/workspaces/:workspaceId/projects/:projectId/models/:modelId
json
{
  "comments": {
    "enabled": true,
    "requireApproval": true,
    "captcha": "turnstile"
  }
}
KeyDefaultDescription
enabledfalseAccept comments on this model's entries
requireApprovaltrueNew comments start pending; false publishes immediately (needs comments.auto_approve, otherwise it stays true)
maxDepth4Reply nesting cap for new public submissions (0 = flat, max 10). Imports are never clamped.
requireEmailtrueCommenters must supply an email; it is never shown publicly
honeypottrueHidden _hp field; a filled honeypot is silently accepted and dropped
notificationstrueEmail the workspace owners and admins when a comment arrives
captchanull'turnstile' to require a Cloudflare Turnstile token (needs comments.captcha, otherwise it is reset to null)
rateLimitPerIp5Submissions per IP per minute on one entry (1 to 60)
maxBodyLength5000Body length cap for public submissions (100 to 20000)

Errors: 403 comments.upgrade (plan lacks comments.enabled, or the plan's model limit is reached), 400 comments.not_collection (model kind cannot carry comments).

Public Endpoints ​

These endpoints require no authentication. Both return Access-Control-Allow-Origin: *, and the OPTIONS preflight is answered with 204 (the preflight allows only the Content-Type header, so do not send Authorization).

Read Comments ​

Get the approved comment threads of one entry, plus the configuration your embed needs.

GET /api/comments/v1/:projectId/:modelId/:entryId
ParameterTypeDefaultDescription
localestringproject defaultEntry locale. An invalid value falls back to the default.
pagenumber1Page of root comments
limitnumber20Roots per page (1 to 100)
sortstringoldestoldest or newest (any other value means oldest)

Response ​

json
{
  "entry": { "modelId": "posts", "entryId": "a1b2c3d4e5f6", "locale": "en" },
  "config": {
    "closed": false,
    "requireApproval": true,
    "requireEmail": true,
    "maxDepth": 4,
    "maxBodyLength": 5000,
    "captcha": "turnstile",
    "captchaSiteKey": "0x4AAAA...",
    "honeypotField": "_hp"
  },
  "comments": [
    {
      "id": "uuid",
      "parentId": null,
      "depth": 0,
      "author": { "name": "Ada", "url": "https://ada.dev/", "isModerator": false },
      "body": "Plain text",
      "type": "comment",
      "createdAt": "2026-05-01T10:00:00.000Z",
      "replies": []
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20
}
  • Pagination is over root comments; every approved reply under the page's roots is nested inside them.
  • Only approved comments are returned. A reply whose parent is not approved is not shown.
  • Email, IP address, user agent and referrer are never returned.
  • body is plain text; render it escaped. author.url is an http(s) address or null.
  • config.closed is true when the thread is locked: render the comments but hide the form.
  • config.captcha is "turnstile" only when the model asks for it and the plan includes comments.captcha; captchaSiteKey is null until the operator sets NUXT_PUBLIC_TURNSTILE_SITE_KEY.

The read endpoint is limited per IP (429 comments.rate_limited).


Submit a Comment ​

Post a comment, or a reply to an approved comment.

POST /api/comments/v1/:projectId/:modelId/:entryId?locale=en

Request Body ​

FieldTypeRequiredDescription
author.namestringYesDisplay name (max 120 characters)
author.emailstringConditionalRequired when requireEmail is on; never shown publicly
author.urlstringNoAuthor website (http or https)
bodystringYesComment text (up to maxBodyLength); HTML is stripped
parentIdstringNoID of the comment being replied to
captchaTokenstringConditionalTurnstile token (when captcha is configured)
_hpstringNoHoneypot field (must be empty -- bots fill it)

Response ​

Success:

json
{
  "success": true,
  "status": "pending",
  "comment": { "id": "uuid", "parentId": null, "depth": 0, "author": { "name": "Ada", "url": null, "isModerator": false }, "body": "Hello", "type": "comment", "createdAt": "2026-05-01T10:00:00.000Z", "replies": [] }
}

status is approved when the model does not require approval (and the plan allows it), otherwise pending. Pending comments are only echoed back to their author.

Validation error (HTTP 200):

json
{
  "success": false,
  "errors": [
    { "field": "author.email", "message": "..." }
  ]
}

Error field values: author.name, author.email, author.url, body, parentId (missing, not found, or nested too deep) and captcha.

Other Errors ​

StatusCause
403The thread is closed for this entry, or the plan lacks comments.enabled (also when the model is beyond the plan's model limit)
404Unknown project or model, or comments are disabled on the model
429Per-IP rate limit (rateLimitPerIp, per IP and entry), or the workspace's monthly comment quota
402The workspace's billing is locked (also returned by the read endpoint). Do not retry; hide the widget.

A filled honeypot returns { "success": true, "status": "pending" } without storing anything.

Notifications and Webhooks ​

Unless the model sets notifications: false, Studio emails the workspace owners and admins when a comment arrives. Plans with comments.webhook_notification also dispatch comment.submitted and comment.approved webhook events.

Plan Features ​

Feature keyPurpose
comments.enabledComments at all
comments.modelsNumber of models with comments enabled
comments.per_monthPublic comments per month (imports and moderator replies do not count)
comments.captchaTurnstile captcha
comments.auto_approvePublish without approval
comments.importWordPress comment import
comments.webhook_notificationComment webhook events

See Billing & Plans for the values per plan.


Moderation Endpoints ​

These endpoints require a session and a plan with comments.enabled (403 comments.upgrade). Listing is open to workspace owners and admins and to members assigned to the project; every other route is limited to workspace owners and admins. A comment outside the project returns 404 comments.comment_not_found.

List Comments ​

GET /api/workspaces/:workspaceId/projects/:projectId/comments
ParameterTypeDefaultDescription
statusstring(all)pending, approved, rejected, spam
modelIdstring--Filter by model
entryIdstring--Filter by entry
localestring--Filter by locale
pagenumber1Page number
limitnumber50Items per page (max 100)
sortstringnewestnewest or oldest

Returns { comments, total, counts, page, limit }, where counts holds the number of comments per status.


Update Comment Status ​

PATCH /api/workspaces/:workspaceId/projects/:projectId/comments/:commentId
FieldTypeRequiredDescription
status'approved' | 'rejected' | 'spam' | 'pending'YesNew status

Returns the updated comment. 400 comments.invalid_status for any other value.


Delete Comment ​

DELETE /api/workspaces/:workspaceId/projects/:projectId/comments/:commentId

Replies are deleted with the comment. Returns { "deleted": true }.


Reply as Moderator ​

POST /api/workspaces/:workspaceId/projects/:projectId/comments/:commentId/reply
FieldTypeRequiredDescription
bodystringYesReply text (max 20000 characters)

The reply is published immediately and marked as a moderator reply (isModerator). Replying to a comment that is not yet approved approves it first. Returns the created reply.


Bulk Update ​

POST /api/workspaces/:workspaceId/projects/:projectId/comments/bulk
FieldTypeRequiredDescription
action'approve' | 'reject' | 'spam' | 'pending' | 'delete'YesAction applied to every comment
commentIdsstring[]YesComment IDs (max 50, else 400 comments.bulk_limit)

delete returns { "action", "results": [{ "id", "success", "error?" }], "succeeded": 3 } and writes an audit log entry per comment; the other actions return { "action", "updated": 3 }.


Open or Close a Thread ​

PATCH /api/workspaces/:workspaceId/projects/:projectId/comments/threads/:modelId/:entryId
FieldTypeRequiredDescription
closedbooleanYestrue to close, false to reopen
localestringNoEntry locale (project default when omitted)

A closed thread still shows its comments; the public submit endpoint refuses new ones with 403. Returns { modelId, entryId, locale, closed, closedAt }.


Import WordPress Comments ​

Import a contentrain-comments@1 export (the comments-export.json produced by the WordPress importer).

POST /api/workspaces/:workspaceId/projects/:projectId/comments/import

The body is the export document. The optional locale query parameter sets the default locale for entry references that carry none. Requires the comments.import plan feature (403 comments.upgrade). Up to 5000 comments per request; send larger exports in chunks.

Every comment whose post maps to an entry is inserted with its source date, status, type and parent link, and reply depth is never clamped. Rows are keyed on their source ID, so re-sending an export is safe. A comment whose post has no entry mapping is reported in unmapped, never dropped silently.

json
{
  "received": 3,
  "mapped": 2,
  "inserted": 2,
  "skippedExisting": 0,
  "unmapped": [{ "comment_id": 9, "post": 77 }],
  "orphanCount": 0,
  "orphanParents": [],
  "maxDepth": 3,
  "threadsClosed": 1,
  "datesDefaulted": 0
}
  • Forms API -- the same embedding model for form submissions
  • Webhooks API -- comment.submitted and comment.approved events

Released under the AGPL-3.0 License.