Fetch Many Comments
curl --request GET \
--url https://api.sublay.io/v8/:projectId/comments \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.sublay.io/v8/:projectId/comments"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.sublay.io/v8/:projectId/comments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sublay.io/v8/:projectId/comments",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.sublay.io/v8/:projectId/comments"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.sublay.io/v8/:projectId/comments")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sublay.io/v8/:projectId/comments")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"data": [
{}
],
"pagination.nextCursor": {},
"pagination.hasMore": true
}Comment Endpoints
Fetch Many Comments
Get a comment thread: an entity’s top-level comments, or a comment’s replies, with optional nested replies
Fetch Many Comments
curl --request GET \
--url https://api.sublay.io/v8/:projectId/comments \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.sublay.io/v8/:projectId/comments"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.sublay.io/v8/:projectId/comments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sublay.io/v8/:projectId/comments",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.sublay.io/v8/:projectId/comments"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.sublay.io/v8/:projectId/comments")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sublay.io/v8/:projectId/comments")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"data": [
{}
],
"pagination.nextCursor": {},
"pagination.hasMore": true
}Returns one level of a comment thread, cursor-paginated:
The params come in pairs:
using the values after
Other unknown query parameters are ignored.
entityId→ the entity’s top-level comments.parentId→ that comment’s direct replies.
parentId decides the level, and entityId must match the parent’s entity (otherwise the page is empty).
Each request can also bring a few levels of replies nested inside every returned comment (replyDepth, replyLimit), each with its own “load more” cursor — so a threaded view can render its first screen with a single request.
To list a user’s comments across entities, use Fetch Comments by User instead.
Authentication is optional. When the caller is signed in, comments by users they are block-edged with are marked or shown as placeholders (see Blocked authors).
For previews (for example, the best comment under an entity in a feed), use
include=topComments on the entity fetch instead — it returns real content only. Thread fetches include placeholders.Query Parameters
Thread
string
Return this entity’s top-level comments. Required unless
parentId is sent.string
Return the direct replies of this comment. Required unless
entityId is sent.Pagination
number
default:"10"
How many comments to return at the level being fetched (top-level comments for
entityId, direct replies for parentId). A whole number of at least 1 — 0, a negative number or a non-integer value returns 400 comment/invalid-query. Values above 100 are treated as 100.string
Continue after the last comment of a previous response. Leave it out on the first request (an empty
cursor is treated the same way), then pass pagination.nextCursor from the previous response. The cursor must be used with the same sortBy, sortDir and sortByReaction it was created under. See Cursors.Sorting
string
default:"createdAt"
Sort for the level being fetched. One of:
createdAt— by creation time.top— by the raw count of one reaction (sortByReaction, defaultupvote). Nothing is subtracted — a comment with 10 upvotes and 8 downvotes ranks above one with 5 upvotes.controversial— many up/down votes with a close split. Comments with no up/down votes sort last (underdesc).
desc and oldest-first under asc.string
default:"desc"
asc or desc. Applies to every sort, including top and controversial.string
default:"upvote"
With
sortBy=top only: the reaction to rank by. One of upvote, downvote, like, love, wow, sad, angry, funny. Sending it with any other sortBy returns 400.new and old are deprecated sortBy aliases: new is createdAt with the requested sortDir, old is createdAt ascending. They still work, but responses carry a non-blocking Deprecation header — use createdAt with sortDir instead. They aren’t accepted for replySortBy.Nested replies
number
default:"0"
How many levels of replies to nest under each returned comment, from
0 (none) to 5.number
default:"3"
How many replies per comment in the nested levels only. Must be at least
1 — to nest nothing, use replyDepth=0. Values above 100 are treated as 100.string
Sort for the nested levels:
createdAt, top or controversial. Defaults to the level’s sortBy.string
asc or desc for the nested levels. Defaults to the level’s sortDir.string
The reaction the nested levels rank by. Only accepted when the nested sort is
top (replySortBy=top, or sortBy=top without replySortBy) — otherwise 400. Defaults to sortByReaction when the level is also sorted by top, else upvote. When replySortBy is omitted, all three reply-sort params follow the level’s sort.limit and sortBy/sortDir/sortByReaction describe the level you’re asking for; replyLimit and replySortBy/replySortDir/replySortByReaction describe what’s nested underneath. A parentId request is a request for replies, so to get replies oldest-first there, send sortBy=createdAt&sortDir=asc.
Request size limit
Before running any query, the server computes the most comments a request could return:limit × (1 + replyLimit + replyLimit² + … + replyLimit^replyDepth)
limit and replyLimit are capped at 100. Requests above 500 are rejected with 400 comment/reply-budget-exceeded — they are never silently trimmed. For example, limit=10&replyDepth=2&replyLimit=3 can return up to 10 × 13 = 130 comments, while limit=10&replyDepth=5&replyLimit=2 (10 × 63 = 630) is rejected.
Optional associations
string
Comma-separated list of associations to include, applied at every level (nested replies included):
user— the comment author, with the avatar file URLsgrants— the reputation-grant summary
entity, space and parent aren’t available here (see Unsupported parameters).Space-scoped reputation
This endpoint has a space in context, so it accepts the opt-in reputation params. They add aspaceReputation field to each populated author user (at every level), alongside the always-present reputation total. Requires the reputation bundle. See the Reputation data model for the full contract.
Response
{
"data": [
{
"id": "c1",
"parentId": null,
"repliesCount": 7,
"isAuthorBlocked": false,
"isUnavailable": false,
"replies": {
"data": [
{
"id": "r1",
"parentId": "c1",
"repliesCount": 2,
"isAuthorBlocked": false,
"isUnavailable": false,
"replies": {
"data": [{ "id": "r1a", "parentId": "r1", "repliesCount": 0, "…": "…" }],
"pagination": { "nextCursor": null, "hasMore": false }
}
}
],
"pagination": { "nextCursor": "eyJ2Ijox…", "hasMore": true }
}
}
],
"pagination": { "nextCursor": "eyJ2Ijox…", "hasMore": true }
}
string | null
Pass this as
cursor to get the next batch. null when there’s nothing more.boolean
Whether more comments exist after this batch. Exact — use it (not
repliesCount) to decide whether to show a “load more” button.Nested replies
WithreplyDepth above 0, each comment carries a replies field with the same envelope as the top level: { data, pagination }. A nested replies.pagination.nextCursor continues that comment’s replies — see Cursors.
- Comments at the last nested level have no
repliesfield — their replies weren’t fetched.replies: { data: [] }means they were fetched and there are none. - With
replyDepth=0(the default), no comment carriesreplies. - Placeholders and flagged comments get nested replies like any other comment, so other people’s replies under them stay in place.
Counts
repliesCount is the number of direct replies a comment has, for display. It counts every reply the thread contains — including deleted, removed and unavailable ones, which appear as placeholders — so it’s the same number for every viewer and always matches what paging through the replies returns. Drive buttons from hasMore.
Deleted and removed comments
Deleted and moderator-removed comments stay in the thread as placeholders, so the replies under them aren’t lost. A placeholder keeps its position,id, parentId, timestamps, reactionCounts and repliesCount, with the author and content blanked. A moderator-removed comment is shown in full to its own author. See Placeholders.
Blocked authors
Every comment carries two flags,isAuthorBlocked and isUnavailable. Both are always present, and always false when the caller isn’t signed in or the moderation bundle isn’t installed. Otherwise:
| The caller and the comment’s author | The comment is returned | Flags |
|---|---|---|
| The caller blocked the author | in full | isAuthorBlocked: true — your app decides whether to collapse it, offer “show anyway”, or hide it |
| The author blocked the caller (or both blocked each other) | as a placeholder | isUnavailable: true |
| No block | as usual | both false |
isUnavailable is deliberately neutral: it means the comment exists but can’t be shown to this caller, without saying why. Replying to or reacting to an isUnavailable comment is refused, so hide those controls.
When the entity’s author has blocked the caller, the whole thread is unavailable to them: a request with that entityId, or with the parentId of any comment on that entity, returns an empty page — exactly what an entity that doesn’t exist returns.
Cursors
A cursor is an opaque string. It records the sort it was created under and the last comment you received, and the next request continues right after that comment. Comments posted in the meantime don’t shift what you get.-
Send the same
sortBy,sortDirandsortByReactionwith a cursor as the request that returned it. A different sort returns400 comment/cursor-sort-mismatch; to change the sort, start over without a cursor. -
A cursor doesn’t record which entity or comment it came from. Sending it with a different
entityIdorparentIdjust continues from the same sort position there. -
Continuing nested replies: a
replies.pagination.nextCursorwas created under the reply sort. Continue it with aparentIdrequest for that comment, sending the reply sort assortBy/sortDir/sortByReaction:The SDK’sGET /v8/:projectId/comments?parentId=c1&cursor=<replies.pagination.nextCursor>&sortBy=<replySortBy>&sortDir=<replySortDir>useRepliesdoes this for you. -
Vote-ranked sorts can shift: under
topandcontroversial, a comment whose votes change between two requests can move past the cursor — it may be skipped (it shows up on refresh) or appear twice (de-duplicate byid).createdAtis exact.
Unsupported parameters
These parameters return400 with code comment/unsupported-param, a param field naming the parameter, and an error message pointing to the replacement:
| Sent | Use instead |
|---|---|
page | Cursor pagination: leave cursor out on the first request, then pass pagination.nextCursor. |
userId | GET /users/:userId/comments. |
blockedFilter | Threads always include comments by users the caller blocked, marked isAuthorBlocked: true. blockedFilter is available on GET /users/:userId/comments. |
sourceId | A thread already belongs to one entity. To filter a user’s comments by source, use GET /users/:userId/comments?sourceId=…. |
include=entity, include=space | Every comment in a thread belongs to the entity you requested. Supported values: user, grants. |
include=parent | Top-level comments have no parent, and replies all share the one you requested. Available on GET /users/:userId/comments and the single-comment fetches. |
Error Responses
Checks run in this order; the first failing check is returned.Comments bundle not installed — 403
Comments bundle not installed — 403
Unsupported parameter — 400
Unsupported parameter — 400
{
"error": "`page` isn't supported on this endpoint. Comments use cursor pagination: leave `cursor` out on the first request, then pass `pagination.nextCursor` from the previous response to get the next batch.",
"code": "comment/unsupported-param",
"param": "page"
}
Invalid query — 400
Invalid query — 400
Returned with code
comment/invalid-query for invalid values — for example an unknown include value, sortByReaction without sortBy=top, replySortByReaction when the nested sort isn’t top, limit below 1 or not a whole number, replyDepth above 5, or replyLimit below 1.Missing filter — 400
Missing filter — 400
{
"error": "`entityId` or `parentId` is required. To list a user's comments, use `GET /users/:userId/comments`.",
"code": "comment/missing-filter"
}
Request too large — 400
Request too large — 400
{
"error": "This request could return up to 630 comments (max 500). Lower `limit`, `replyLimit` or `replyDepth`.",
"code": "comment/reply-budget-exceeded",
"maxRows": 500,
"requestedRows": 630
}
Invalid cursor — 400
Invalid cursor — 400
{
"error": "Invalid `cursor`. Pass `pagination.nextCursor` from a previous response unchanged, or leave `cursor` out to start from the beginning.",
"code": "comment/invalid-cursor"
}
Cursor from a different sort — 400
Cursor from a different sort — 400
{
"error": "This `cursor` was created under a different sort. Send the same `sortBy`, `sortDir` and `sortByReaction` as the request that returned it, or leave `cursor` out to start from the beginning.",
"code": "comment/cursor-sort-mismatch"
}


commentsbundle, ahead of the checks below. The body has noerrorfield: it carries thecode, the missing tables, and a link to the project’s Database page in the dashboard, where the bundle is installed. See Bundles.