Skip to content
Social SDK Cloud, the hosted SDK, is taking early-access signups.Join the list
Social SDK megaphone markSocial SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Handle comments and messages

List, reply to, and moderate comments, and read and send direct messages, with per-platform examples for X, Bluesky, Threads, YouTube, and more.

The normalized facades use platform references and opaque cursors. The examples assume one social client built with the platform’s adapter, an account from connectedAccountRef, and a post from platformPostRef.

Comments

Comment and message items expose id, authorId, authorHandle, authorName, text, createdAt, and parentId when the provider supplies them. Provider fields remain available, and native retains the provider record used for the projection. Missing fields stay absent rather than becoming invented values.

Use id as the comment identity across platforms. Bluesky exposes the reply’s AT-URI there; LinkedIn exposes the full comment URN needed for replies. Its raw numeric ID remains in native.id.

const comments = await social.comments.list(post, { limit: 20 });
const commentId = comments.items[0]?.id;

if (commentId !== undefined) {
  await social.comments.reply({ ...post, kind: "comment", commentId }, { text: "Thanks." });
}

comments.list(post, { cursor, limit }) returns { items, nextCursor? }. Pass nextCursor back unchanged. comments.reply takes a comment reference: the post reference fields plus kind: "comment" and the provider commentId.

Pagination is route-specific:

  • X, Instagram, YouTube, and LinkedIn accept limit and return a cursor when more pages exist. X takes 10 through 100, Instagram 1 through 50, and YouTube and LinkedIn 1 through 100. Instagram, YouTube, and LinkedIn default to 25.
  • Threads ignores limit and returns Meta’s default page size with an after cursor.
  • Bluesky returns only the direct replies in the post’s thread view, never returns a cursor, and rejects a cursor with unsupported_capability.

comments.iterate follows only the nextCursor values the route returns. It does not walk nested replies, so on Bluesky it yields one page of direct replies.

Messages

const conversations = await social.messages.listConversations(account, { limit: 20 });

messages.listConversations(account, { cursor, limit }) returns { items, nextCursor? }. listMessages takes a conversation reference, and messages.send(ref, { text }) writes to it. messages.iterateConversations and messages.iterateMessages follow pages.

Normalized messages run on two kinds of route:

  • The direct X adapter.
  • The direct Instagram adapter with Instagram Login and messaging permission.
  • The managed Zernio backend for X, Bluesky, Instagram, and Facebook accounts connected through Zernio. Recipient permissions and reply windows still apply upstream.

The direct Bluesky adapter exposes chat only through its native module, shown below. Calling social.messages on a direct Bluesky account raises unsupported_capability.

Read an inbox across accounts

inbox.list reads a conversation page for each supplied account and a comment page for each supplied post. Supply up to 50 sources across backends. Enable it with inboxAggregation() from @opencoredev/social-sdk/inbox, so clients that never aggregate an inbox do not bundle it:

import { createSocial } from "@opencoredev/social-sdk";
import { inboxAggregation } from "@opencoredev/social-sdk/inbox";

const social = createSocial({ backends, inbox: inboxAggregation() });
const inbox = await social.inbox.list({ accounts: [account], posts: [post], limit: 20 });
const records = inbox.entries.map((entry) => entry.record);

Each entry carries its source account or post and a normalized record. sources reports each source’s read, exhausted, or failed state. A failed source does not discard successful reads. Every source is authorized on every call, including sources that finished on an earlier page.

limit applies to each source, so the combined page can contain more items. Entries stay grouped by source and are never truncated or sorted by timestamp. Pass the returned nextCursor with the same sources, order, limit, and tenant to continue; exhausted sources stay exhausted, while failed sources retry from their previous position. Comment aggregation requires post references because providers do not expose a common account-wide comments endpoint.

The aggregate cursor retains bounded provider cursors for up to 50 sources. Store it as an opaque value; it can be larger than a single-account cursor.

Platform support

Native examples in the tabs assume a client built with that platform’s adapter under the "default" key, a guarded native module, and an operation context:

import type { AdapterOperationContext } from "@opencoredev/social-sdk";

const native = social.native("default", { acknowledgeUnsafe: true });
if (native === undefined) throw new Error("This adapter has no native module");

const context: AdapterOperationContext = {
  backendInstance: "default",
  correlationId: crypto.randomUUID(),
  retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
};

X

Comments list and reply. Messages: conversations, messages, and send. Access: user token. Listing replies needs tweet.read and users.read. DM reads need dm.read, users.read, and tweet.read; writes also need dm.write.

comments.list runs X recent search for conversation_id:<postId>, so it only returns replies from the last 7 days, includes nested replies, and accepts a limit from 10 through 100. Pass the conversation’s root post.

Configure x({ auth: { userId, accessToken } }). Normalized DMs need a user token; an app bearer token is rejected with missing_permission. listConversations derives one latest event per conversation from /2/dm_events, never repeats a conversation across pages, and stops after 1,200 conversations. X keeps only 30 days of DM events, so it is not a complete historical inbox.

Native sendDirectMessage, sendConversationMessage, and createGroupConversation are available from the acknowledged X native adapter.

A reply on X is a post, so delete one with native deletePost and the reply’s post ID. It calls DELETE /2/tweets/{id}. X only lets the authenticated user delete their own posts, so you cannot remove someone else’s reply.

const conversations = await social.messages.listConversations(account, { limit: 20 });
await native.sendDirectMessage({ account, participantId, text: "Hello.", context });
await native.deletePost({ account, postId: replyPostId, context });

Native hideReply hides or unhides a reply in a conversation the authenticated user started. It needs a user token with tweet.moderate.write, tweet.read, and users.read, and returns the hidden state X reports.

await native.hideReply({ account, replyId, hidden: true, context });

Bluesky

Comments list and reply. Messages: native chat methods only. Access: an authenticated session. Chat needs a session that includes transition:chat.bsky; see connect accounts.

Configure bluesky({ auth: { service, did, accessJwt } }). Comment items are thread views with the reply’s AT-URI in normalized item.id and the original view in item.native. Use item.id as the commentId when replying.

Chat runs through the native module, not social.messages. Each method takes the acting account and returns the provider-shaped chat.bsky.convo response:

const convos = await native.listConversations({ account, limit: 20, context });
const messages = await native.listMessages({ account, conversationId, limit: 50, context });
await native.sendMessage({ account, conversationId, text: "Thanks for reaching out.", context });

A reply on Bluesky is a post. Delete one with native deletePost and its uri and cid. It only deletes posts in the authenticated account’s repository; a URI from another DID raises unauthorized. To hide another account’s reply in your own thread, use hideReply.

await native.deletePost({ account, post: { uri: replyUri, cid: replyCid }, context });

Native hideReply hides or unhides a reply by adding its AT-URI to, or removing it from, the hiddenReplies list on the root post’s threadgate. Only the root post’s author can do this, so the call raises invalid_input for replies in other accounts’ threads. The adapter creates a threadgate without reply rules when none exists, and updates an existing one with swapRecord so a concurrent change fails instead of being overwritten. Hidden replies stay in the thread; Bluesky clients collapse them.

await native.hideReply({ account, replyUri, hidden: true, context });

Threads

Comments list and reply. Messages: not supported. Listing needs threads_basic and threads_read_replies. Replying needs threads_basic and threads_content_publish. Hiding or managing replies needs threads_basic and threads_manage_replies.

Configure threads({ auth: { userId, accessToken } }). Messaging is unsupported-by-platform, so message calls raise unsupported_capability.

Moderation is native. hideReply hides or unhides a top-level reply to your post; Meta hides its nested replies with it. listConversation, listPendingReplies, and managePendingReply cover the rest.

Native deleteComment deletes a reply the authenticated user published, using DELETE /{threads-media-id}. It needs threads_delete and counts toward Meta’s limit of 100 deletions per 24 hours. Use hideReply for other users’ replies.

await native.hideReply({ account, replyId, hide: true, context });
await native.deleteComment({ account, commentId: replyMediaId, context });

YouTube

Comments list and reply. Messages: not supported. Access: an OAuth token for the channel. Replies, moderation, and deletion all need https://www.googleapis.com/auth/youtube.force-ssl, which the default connection scopes do not include.

Configure youtube({ auth: { accessToken, channelId } }) for the authorized channel. Native heldComments lists comment threads held for review. Each held thread’s comment ID is snippet.topLevelComment.id, not the thread id.

Native commentsModeration supports setModerationStatus, update, and delete. setModerationStatus must be called by the owner of the channel or video and requires a moderationStatus of published, heldForReview, or rejected; omitting it raises invalid_input. banAuthor: true is valid only with rejected.

Native deleteComment calls comments.delete. Google’s reference does not say which comments a channel may delete; it documents a 403 forbidden for insufficient permissions, which raises missing_permission. To remove another user’s comment from your video, set its moderation status to rejected.

const held = await native.heldComments({ maxResults: 20, context });
await native.commentsModeration({
  action: "setModerationStatus",
  commentId, // snippet.topLevelComment.id from a held thread
  moderationStatus: "rejected",
  banAuthor: true,
  context,
});

Source: comments.setModerationStatus, accessed 2026-10-05.

Instagram

Comments list and reply. Messages: conversation and message reads, plus text replies with Instagram Login. Access: a professional account with the matching comment-management or messaging permission.

Configure instagram({ auth: { accessToken, accountId, flavor } }). flavor defaults to instagram-login; use facebook-login where the adapter requires Facebook Login. Direct messaging requires Instagram Login. Conversation reads include participants, and message reads include bodies, senders, recipients, timestamps, and attachments. messages.send looks up the conversation participants and sends only when it finds one other recipient. Facebook Login messaging requires the separate Messenger Platform and a Page token. Zernio also exposes Instagram messages as a managed backend.

Native moderation covers hiding and deleting comments, listing replies to one comment, and turning comments on or off for a media object:

await native.moderateComment({ account, commentId, hidden: true, context });
await native.deleteComment({ account, commentId, context });
const replies = await native.listCommentReplies({ account, commentId, limit: 25, context });
await native.setCommentsEnabled({ account, mediaId, enabled: false, context });

LinkedIn

Comments list and reply. Messages: not supported. Member comments need r_member_social_feed for reads and w_member_social_feed for replies. Organization comments need r_organization_social_feed and w_organization_social_feed, respectively. These permissions require Community Management approval. The adapter also reads the parent post before comment operations: grant r_member_social for member authors or r_organization_social for organization authors for that separate preflight.

Configure linkedin({ auth: { accessToken, author }, apiVersion }). author is a urn:li:person:<id> or urn:li:organization:<id>; organization actions need administrator access.

LinkedIn comments use Social Actions. Reply with the normalized id, which contains the full comment URN (urn:li:comment:(urn:li:activity:…,…)); any other value raises invalid_input. If a write is accepted without a returned URN, the SDK can raise ambiguous_outcome. LinkedIn messages raise unsupported_capability. Hiding comments is unsupported-by-platform: the Comments API has no hide or moderation-status operation.

const comments = await social.comments.list(post, { limit: 20 });
const commentUrn = comments.items[0]?.id;

if (typeof commentUrn === "string") {
  await social.comments.reply(
    { ...post, kind: "comment", commentId: commentUrn },
    { text: "Thanks." },
  );
}

Native deleteComment takes the share or UGC post URN and the full comment URN, and calls DELETE /rest/socialActions/{postUrn}/comments/{commentId}. The SDK adds the actor query when the author is an organization. LinkedIn does not document which comments an actor may delete.

await native.deleteComment({ account, postId: "urn:li:share:123", commentId, context });

Errors and native access

An unavailable operation raises unsupported_capability, not an empty page. Provider permission or token failures use missing_permission; malformed references and missing YouTube moderation status use invalid_input. ambiguous_outcome means a write may have succeeded and must be reconciled.

social.native(backend, { acknowledgeUnsafe: true }) returns the native handle used above. It bypasses tenant authorization and concurrency middleware, so wrap native calls in your own policy.

Last updated on November 5, 2026