# Social Inbox API: DMs, Comments, Mentions

import { TechArticle } from "../components/TechArticle";

<TechArticle slug="inbox" fm={frontmatter} />

Read and reply to your Instagram and Facebook DMs, comments, and mentions, LinkedIn company-page comments and mentions, TikTok and YouTube video comments, and Threads replies and mentions, all from the API. Conversations are served from OmniSocials, so you read at your own pace without hitting each platform's rate limits, and replies go out through your connected accounts.

There is also a work queue, `GET /inbox/next`, that hands you the next thing that needs an answer together with its conversation and post, and comment moderation (hide, delete) on the platforms that allow it. See [Working through the inbox](#working-through-the-inbox) and [Hiding and deleting comments](#hiding-and-deleting-comments).

X (Twitter) DMs are also available as a per-workspace opt-in, see [X direct messages](#x-direct-messages). Threads replies and mentions are covered too, see [Threads replies and mentions](#threads-replies-and-mentions).

## Enabling inbox access

The inbox scopes are opt-in. When you create an API key, turn on **Social Inbox access** to grant both inbox scopes. Existing keys are not given these scopes automatically.

| Scope | Grants |
|-------|--------|
| `inbox:read` | List conversations, read messages, and pull the next unanswered item |
| `inbox:write` | Mark conversations read, send replies, hide and delete comments |

When you connect through OAuth or the MCP server, inbox access is governed by the `access_social_inbox` workspace permission instead of scopes. Company owners and admins have it by default.

## Listing conversations

Returns one entry per conversation, newest activity first.

```bash
curl "https://api.omnisocials.com/v1/inbox/conversations?platform=instagram&unread=true&limit=25" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{
  "data": [
    {
      "conversation_id": "instagram_17841400000_17841464825884163",
      "platform": "instagram",
      "type": "dm",
      "participant": {
        "id": "17841400000",
        "name": "Amelia Rivera",
        "username": "amelia.rivera",
        "profile_picture": "https://..."
      },
      "unread_count": 2,
      "last_message": {
        "id": "78027900",
        "direction": "incoming",
        "text": "Do you ship internationally?",
        "timestamp": "2026-07-19T17:05:51Z",
        "is_read": false
      },
      "post": null
    }
  ],
  "pagination": { "next_cursor": "MjAyNi0wNy0xOF...", "has_more": true, "limit": 25 }
}
```

Query parameters (all optional):

| Parameter | Description |
|-----------|-------------|
| `platform` | `instagram`, `facebook`, `linkedin`, `tiktok`, `youtube`, `x`, or `threads` |
| `type` | `dm`, `comment`, or `mention` |
| `unread` | `true` returns only conversations with unread incoming messages |
| `unanswered` | `true` returns only conversations that still need an answer: the person's latest DM has no reply after it (Instagram and Facebook DMs past the 24-hour messaging window included: they cannot be answered through the API, but the person is still waiting), or a comment or mention has not been replied to and is not hidden. Replies sent from the native apps count as answers. Read state is ignored here; use `GET /inbox/next` for a work queue. See [What counts as unanswered](#what-counts-as-unanswered) |
| `limit` | Conversations per page, 1 to 100 (default 25) |
| `cursor` | The `next_cursor` from a previous response |

`post` is present for comment and mention conversations (the post the interaction is on) and `null` for DMs. See [Post context on comments](#post-context-on-comments).

## Cursor pagination

The inbox list endpoints use cursor pagination, not offset. Every response carries a `pagination` object:

```json
"pagination": { "next_cursor": "MjAyNi0w...", "has_more": true, "limit": 25 }
```

To fetch the next page, pass `next_cursor` back as the `cursor` query parameter. When `has_more` is `false`, `next_cursor` is `null` and you have reached the end.

## Reading a conversation

Returns the full message history, oldest first. Also cursor-paginated (default `limit` 50).

```bash
curl "https://api.omnisocials.com/v1/inbox/conversations/CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{
  "data": [
    {
      "id": "20457105",
      "conversation_id": "instagram_17841400000_17841464825884163",
      "platform": "instagram",
      "type": "dm",
      "direction": "incoming",
      "text": "Do you ship internationally?",
      "timestamp": "2026-07-19T17:05:51Z",
      "is_read": false,
      "is_replied": false,
      "reaction": null,
      "parent_comment_id": null,
      "hidden": null,
      "permalink": null,
      "attachment": null,
      "sender": {
        "id": "17841400000",
        "name": "Amelia Rivera",
        "username": "amelia.rivera",
        "profile_picture": "https://..."
      },
      "post": null
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false, "limit": 50 }
}
```

`direction` is `incoming` for messages from the other person and `outgoing` for your replies. Replies sent from the platform's own app show up as `outgoing` too, see [Replies sent from the native apps](#replies-sent-from-the-native-apps).

`permalink` links to the reply or mentioning post on the platform when one is known, `null` otherwise. `hidden` is set on comments and mentions: `true` when the comment is hidden on the platform (see [Hiding and deleting comments](#hiding-and-deleting-comments)), `false` when it is visible. It is `null` on DMs.

### Media on a message

`attachment` is set on messages that carry media, and `null` otherwise. It carries `url` and `type` (`image`, `video`, `audio`, or `file`):

```json
"attachment": {
  "url": "https://cdn.omnisocials.com/file/library-omnisocials/social-inbox/985468/inbound/1756540800000-a1b2c3.jpg",
  "type": "image"
}
```

This applies to inbound Instagram and Facebook DM photos, videos, voice messages, and Instagram story mentions, and to any reply you sent with `attachment_url`. Instagram/Facebook's own attachment URL is short-lived (it typically stops resolving within hours); we download it once when the message arrives and re-host it on our own CDN, so `attachment.url` keeps working indefinitely.

`type` reflects the fetched bytes, not the platform's label: a photo the platform sent under an unlabelled attachment type is `image`, and a story mention that is really an MP4 is `video`. `type: "file"` means we fetched the media but could not classify it (treat it as an opaque download). `type: "link"` means the platform's URL did not point at media, for example a shared post that resolves to an instagram.com page; `url` is then the platform's own link, not re-hosted, and may require login or expire.

### A note on conversation IDs

Conversation IDs are opaque strings. Some, LinkedIn especially, contain characters like `:` and `()` (for example `linkedin_comment_urn:li:activity:7484628878586646528`). Always URL-encode the ID when you place it in a request path. Our SDKs handle this for you.

## Post context on comments

Comment and mention conversations carry the post they belong to, on the conversation and on every message in it, so a reply can be drafted with the post in view: "what size is this?" needs the caption and the picture. DMs have `post: null`.

```json
"post": {
  "id": "17895695668004550",
  "caption": "New autumn colours are in. Which one is yours?",
  "thumbnail": "https://...",
  "url": "https://www.instagram.com/p/C9xYz1AbCdE/",
  "media_type": "CAROUSEL_ALBUM"
}
```

| Field | Description |
|-------|-------------|
| `id` | The platform's id of the post |
| `caption` | The post text, when known |
| `thumbnail` | Image URL of the post, or the video's cover |
| `url` | Public link to the post on Instagram, Facebook, YouTube, TikTok, LinkedIn, or Threads, when the platform provides one. `null` otherwise |
| `media_type` | The platform's own label for the post, for example `IMAGE`, `VIDEO`, or `CAROUSEL_ALBUM` on Instagram, when known. `null` otherwise |

Instagram stores the link and media type when the comment arrives. Instagram conversations that reached the inbox before September 6, 2026 get `url` and `media_type` filled in the first time you read them through the API.

## Marking a conversation read

Marks every unread incoming message in the conversation as read.

```bash
curl -X POST "https://api.omnisocials.com/v1/inbox/conversations/CONVERSATION_ID/read" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{ "conversation_id": "instagram_17841400000_17841464825884163", "marked_read": 2 }
```

Marking read also takes the conversation out of the [work queue](#working-through-the-inbox) for good, so this is how you skip an item you do not want to answer.

## Replying

Sends a reply in an existing conversation. Works for DMs, comments, and mentions. The reply `text` caps at 2,000 characters (150 on TikTok comments); longer text returns `400 validation_error`.

```bash
curl -X POST "https://api.omnisocials.com/v1/inbox/conversations/CONVERSATION_ID/reply" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Yes, we ship worldwide!" }'
```

Response:

```json
{
  "data": {
    "id": "22380029",
    "conversation_id": "instagram_17841400000_17841464825884163",
    "platform": "instagram",
    "type": "dm",
    "direction": "outgoing",
    "text": "Yes, we ship worldwide!",
    "timestamp": "2026-07-19T15:25:03Z",
    "is_read": true,
    "is_replied": false,
    "sender": { "id": "you", "name": "You", "username": "you", "profile_picture": null },
    "post": null
  }
}
```

On Facebook and Instagram DMs you can attach media by passing `attachment_url` together with `attachment_type` (`image`, `video`, `audio`, or `file`). `text` is optional when `attachment_url` is set: an attachment-only reply is allowed. Other platforms are text-only; `attachment_url` is ignored there.

### Replying to a specific comment

Every comment on a post shares one conversation. Pass `message_id`, the inbox id of the specific incoming comment or mention you are answering (`message.id` from `GET /inbox/next`, or a message `id` from [Reading a conversation](#reading-a-conversation)), so the reply lands under that comment. Without it the reply is posted under the newest incoming comment on the post, which may be a different person than the one you drafted for. Always pass it when replying to an item served by the queue. It is ignored for DMs: a DM reply goes to the conversation. The outgoing message's `parent_comment_id` is the platform id of the comment you answered.

```bash
curl -X POST "https://api.omnisocials.com/v1/inbox/conversations/CONVERSATION_ID/reply" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Back in stock next week!", "message_id": "101581150" }'
```

A `message_id` that is not an incoming message of this conversation returns `404 not_found`; one that is not numeric returns `400 validation_error`.

### Chaining to the next item

Pass `include_next: true` in the body and the response also carries `next`, the next conversation that needs an answer (same shape as `GET /inbox/next`, with its default order and filters), and `remaining`, the number of unanswered items still waiting after it. This saves the extra call when you are working through the inbox.

```bash
curl -X POST "https://api.omnisocials.com/v1/inbox/conversations/CONVERSATION_ID/reply" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Yes, we ship worldwide!", "message_id": "101581150", "include_next": true }'
```

```json
{
  "data": { "id": "22380029", "direction": "outgoing", "text": "Yes, we ship worldwide!", "...": "..." },
  "next": {
    "conversation": { "conversation_id": "instagram_comment_17895695668004550", "type": "comment", "...": "..." },
    "message": { "id": "20457211", "text": "Is the green one back in stock?", "...": "..." },
    "messages": [ "..." ],
    "reply_window": { "open": true, "closes_at": null }
  },
  "remaining": 11
}
```

`next` is `null` and `remaining` is `0` when nothing is waiting. The reply itself is never affected: if the queue lookup fails, the reply still goes out and `next` comes back `null`.

### Reply rules

Platform and policy limits apply. Handle these responses:

| Situation | Response |
|-----------|----------|
| The conversation does not exist in this workspace | `404 not_found` |
| `message_id` is not an incoming message of this conversation | `404 not_found` |
| `message_id` is not numeric | `400 validation_error` |
| The comment or message you reply to was deleted, hidden, or restricted on the platform | `404 not_found` |
| `text` is longer than the platform allows (2,000 overall, 1,000 on Instagram DMs, 150 on TikTok comments) | `400 validation_error` |
| Replying to an Instagram or Facebook DM more than 24 hours after the person's last message (Meta policy). Checked before the send, so nothing goes out; the message names when the window closed, and `GET /inbox/next` reports the same in `reply_window` | `422 outside_messaging_window` (answer that DM from the Instagram or Facebook app, which is mirrored into the inbox, or mark the conversation read to skip it; do not retry) |
| The same reply (same text and attachment) already went out to this comment or DM conversation in the last 10 minutes, or is being sent right now | `409 duplicate_reply` (nothing was sent again; `data` carries the earlier message) |
| The platform refuses replies to this item for good: Facebook `#1705` (the comment sits on a share or boosted copy of the post, or the commenter restricts replies), or a YouTube thread whose `canReply` is false (comments are off on the video, or the comment was removed or held for review) | `422 reply_not_allowed` (the item is dropped from `GET /inbox/next`; do not retry) |
| The workspace has sent 1,000 replies in the last 24 hours | `429 reply_limit_reached` |
| The key or role lacks inbox access | `403 insufficient_scope` |
| The platform rejected the reply | `502 platform_error` |
| The connection lost a needed permission (TikTok comments, LinkedIn, or the Threads reply permission) | `401 reauth_required` |
| An X DM reply, but the credit balance cannot cover it | `402 insufficient_credits` |
| The workspace's X inbox is paused because credits ran out | `402 x_inbox_suspended` |

You can only reply to conversations that already exist in your inbox. The API cannot start a new conversation: Instagram requires the person to message you first, and cold outreach is against platform policy. Direct-message replies must be sent within the platform's 24-hour messaging window. Each workspace can send up to 1,000 replies per rolling 24 hours; contact robert@omnisocials.com if you need a higher limit.

## Working through the inbox

`GET /inbox/next` is a work queue. It returns the one item that should be answered next, together with the conversation so far and the post it belongs to, so you can draft a reply from a single call.

Items are served in this order:

1. DMs that can still be answered: Instagram and Facebook DMs inside Meta's 24-hour messaging window, the one whose window closes soonest first, and X DMs, which have no window.
2. Instagram and Facebook DMs whose 24-hour window has closed. They are served with `reply_window.open = false`: the API cannot reply to them, but the person is still waiting. Answer them from the Instagram or Facebook app (that reply is mirrored into the inbox and clears the item), or mark the conversation read to skip it.
3. Comments and mentions, oldest first.

`order=newest` reverses the order within each group.

```bash
curl "https://api.omnisocials.com/v1/inbox/next" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{
  "data": {
    "conversation": {
      "conversation_id": "instagram_comment_17895695668004550",
      "platform": "instagram",
      "type": "comment",
      "participant": { "id": "17841400000", "name": "Amelia Rivera", "username": "amelia.rivera", "profile_picture": "https://..." },
      "unread_count": 1,
      "last_message": { "id": "20457211", "direction": "incoming", "text": "Is the green one back in stock?", "timestamp": "2026-09-05T09:12:40Z", "is_read": false },
      "post": {
        "id": "17895695668004550",
        "caption": "New autumn colours are in. Which one is yours?",
        "thumbnail": "https://...",
        "url": "https://www.instagram.com/p/C9xYz1AbCdE/",
        "media_type": "CAROUSEL_ALBUM"
      }
    },
    "message": {
      "id": "20457211",
      "conversation_id": "instagram_comment_17895695668004550",
      "direction": "incoming",
      "text": "Is the green one back in stock?",
      "is_read": false,
      "is_replied": false,
      "hidden": false,
      "...": "..."
    },
    "messages": [ "..." ],
    "reply_window": { "open": true, "closes_at": null }
  },
  "remaining": 11
}
```

- `message` is the unanswered incoming message itself: the person's latest DM, or the specific comment. Its `conversation_id` is what the reply endpoint takes, and its `id` is the `message_id` to pass along with the reply for comment threads, so the reply lands under this comment and not under the newest one on the post. The same `id` is what the hide and delete endpoints take.
- `messages` is the conversation so far, oldest first (the most recent 50 messages for long DM threads).
- `reply_window` says whether a reply can still be sent through the API. Only Instagram and Facebook DMs have a window (`closes_at` is the person's last message plus 24 hours); every other item has `open: true` and `closes_at: null`. When `open` is `false` the item is still served, because the person is still waiting, but the reply endpoint answers `422 outside_messaging_window`: answer it from the native app or mark the conversation read.
- `remaining` is how many unanswered items are still waiting after this one, capped at 500.
- `data` is `null` and `remaining` is `0` when the queue is empty.

### What counts as unanswered

- A DM needs an answer when the person's latest message has no reply after it. Instagram and Facebook DMs past Meta's 24-hour messaging window are included too, flagged with `reply_window.open = false`: the API cannot reply to them, but the person is still waiting.
- A comment or mention needs an answer when nobody replied to it and it is not hidden.
- Replies sent from the native apps count as answers (see [Replies sent from the native apps](#replies-sent-from-the-native-apps)), so a thread a colleague answered on their phone is not served again.
- Instagram mentions are skipped, there is no reply path for them.
- The queue looks at the last 30 days of activity.

The same rules power the `unanswered=true` filter on `GET /inbox/conversations`, which lists everything waiting at once rather than serving one item at a time. The list filter ignores read state; the queue serves unread items only by default.

### Skipping an item

Only unread items are served by default, so marking a conversation read (`POST /inbox/conversations/{id}/read`) is the durable skip: the item drops out of the queue and stays out. Pass `include_read=true` to also serve items that were marked read but never answered, for example for an end-of-day sweep. For a temporary skip within one session, pass `exclude` with a comma-separated list of conversation ids (up to 100); those are left out of that call only.

### Query parameters

All optional.

| Parameter | Description |
|-----------|-------------|
| `platform` | Only items from one platform: `instagram`, `facebook`, `linkedin`, `tiktok`, `youtube`, `x`, or `threads` |
| `type` | Only items of one type: `dm`, `comment`, or `mention` |
| `order` | `oldest` (default) serves the item that has waited longest first; `newest` serves the most recent first. Both apply within each group: answerable DMs still come before expired Instagram and Facebook DMs, which come before comments and mentions |
| `include_read` | `true` also serves items that were marked read but never answered |
| `exclude` | Comma-separated conversation ids to leave out of this call, up to 100 |

An unknown `platform`, `type`, or `order` returns `400 validation_error`.

### The loop

1. Call `GET /inbox/next`.
2. Draft a reply from `data.message`, `data.messages`, and `data.conversation.post`. If `data.reply_window.open` is `false`, skip the draft: answer that DM from the Instagram or Facebook app, or mark the conversation read, then call `GET /inbox/next` again.
3. Send it with `POST /inbox/conversations/{conversation_id}/reply`, passing `message.id` as `message_id` (so a comment reply lands under the served comment, not under the newest one on the post) and `include_next: true`. The response carries the sent reply plus `next` and `remaining`.
4. Repeat from step 2 with `next` until it is `null`.

To skip an item, mark it read (for good) or add it to `exclude` (this session only). To deal with spam, hide or delete it; a hidden comment no longer counts as unanswered.

## Hiding and deleting comments

Comments people leave on your posts can be moderated from the API, as the post owner. Both endpoints take the message `id` from the conversation messages list or from `GET /inbox/next`, not the conversation id, and need the `inbox:write` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).

| Platform | Hide | Delete |
|----------|------|--------|
| Instagram | Yes | Yes |
| Facebook | Yes | Yes |
| TikTok | Yes | Yes |
| YouTube | Yes, through YouTube's moderation status | No. YouTube's API does not let a channel delete other people's comments; hide instead |
| Threads | Yes, top-level replies on your posts only | No |
| LinkedIn | No | No |

### Hiding a comment

```bash
curl -X POST "https://api.omnisocials.com/v1/inbox/messages/MESSAGE_ID/hide" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hide": true }'
```

The body is optional; `hide` defaults to `true`, and `{ "hide": false }` unhides. The response returns the updated message under `data` with `hidden` flipped. The comment keeps its place in the conversation, disappears from public view on the platform until you unhide it, and no longer counts as unanswered.

On YouTube, hiding sets the comment's moderation status to rejected, which removes the comment and its replies from public view; unhiding publishes it again. On Threads, only incoming top-level replies can be hidden; Threads does not allow hiding nested replies. On Instagram, the API reads the comment back after the call and reports Instagram's own hidden state, so `hidden` on the returned message is what Instagram actually shows. When Instagram accepted the call but still reports the old state, the response is `502 hide_not_applied` and the inbox is left unchanged.

| Situation | Response |
|-----------|----------|
| The message is not an incoming comment on a platform that supports hiding | `400 unsupported_platform` |
| The Threads reply is nested, or Threads refused | `400 not_hideable` |
| The Threads reply permission or the TikTok comments authorization is missing or expired | `401 reauth_required` |
| The account was connected without the comment-moderation permission (Facebook `pages_manage_engagement`, Instagram `instagram_business_manage_comments`); reconnect it under Settings -> Organisation -> Workspaces | `403 reconnect_required` |
| The message does not exist in this workspace | `404 not_found` |
| No matching account connected to the workspace | `404 account_not_connected` |
| Facebook does not let the Page hide (or unhide) this particular comment: the commenter blocked the Page, their account is deactivated or restricted, or the comment sits on a shared copy of the post | `422 cannot_hide` (retrying does not change it; handle it in Facebook, or mark the conversation read to skip it) |
| Instagram accepted the hide (or unhide) but still reports the comment in the old state when read back. This happens with comments Instagram shows under "Comments from Facebook" on a reel that is also shared to Facebook: they live on Facebook, and Instagram's hide does not reach them | `502 hide_not_applied` (the inbox is left unchanged; hide it in the Instagram or Facebook app, do not retry) |
| YouTube's daily API quota is used up; retry after midnight Pacific | `429 quota_exceeded` |
| The platform rejected the call | `502 platform_error` |

### Deleting a comment

Deletes the comment on the platform and from the inbox. Facebook, Instagram, and TikTok comments only. This cannot be undone.

```bash
curl -X DELETE "https://api.omnisocials.com/v1/inbox/messages/MESSAGE_ID" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{
  "data": {
    "id": "20457211",
    "conversation_id": "instagram_comment_17895695668004550",
    "removed_reply_ids": ["20457218", "20457230"]
  }
}
```

Replies under the deleted comment go with it: the platforms cascade the delete and the inbox mirrors that, and their inbox ids come back in `removed_reply_ids`. A comment that is already gone on the platform is still removed from the inbox.

| Situation | Response |
|-----------|----------|
| The message is not an incoming Facebook, Instagram, or TikTok comment | `400 unsupported_platform` |
| The TikTok comments authorization expired | `401 reauth_required` |
| The account was connected without the comment-moderation permission; reconnect it under Settings -> Organisation -> Workspaces | `403 reconnect_required` |
| The message does not exist in this workspace | `404 not_found` |
| No matching account connected to the workspace | `404 account_not_connected` |
| The platform rejected the call | `502 platform_error` |

## Replies sent from the native apps

Replies you or a colleague send from the platform's own app are mirrored into the inbox as `outgoing` messages, so the thread in OmniSocials is complete and the person's message counts as answered. This covers Instagram DMs sent from the Instagram app, Messenger replies sent from Messenger, Meta Business Suite, or another tool, and comment replies posted natively on Instagram and Facebook. The mirrored reply marks the person's message as replied, which also takes it out of the [work queue](#working-through-the-inbox) and the `unanswered` filter.

Facebook Pages connected before September 6, 2026 need a one-time reconnect under Settings -> Organisation -> Workspaces to start receiving Messenger replies sent from the native apps, because the Page's webhook subscription gained a new field (or contact support to have it switched on without a reconnect). Instagram works without a reconnect.

## Platform notes

| Platform | What is supported |
|----------|-------------------|
| Instagram | DMs, comments, mentions, and story mentions. The person must have messaged you first to open a DM thread. DM replies can include a media attachment. Comments can be hidden and deleted. Replies sent from the Instagram app are mirrored into the inbox. |
| Facebook | Page DMs (Messenger), comments, and mentions. DM replies can include a media attachment. Comments can be hidden and deleted. Replies sent from Messenger or Business Suite are mirrored into the inbox (Pages connected before September 6, 2026 need a reconnect). |
| LinkedIn | Company page comments and mentions only. LinkedIn has no DM API, and personal profiles are not supported. Replies post as the organization. No hide or delete. |
| TikTok | Video comments only, after comments are enabled on the TikTok channel. Replies are text only, 150 characters max, and TikTok holds fresh comments and replies in spam review for roughly 15 to 20 minutes. Comments can be hidden and deleted. See [TikTok comments](#tiktok-comments). |
| YouTube | Video comments only, YouTube has no DMs. Conversations group per video, and replies are supported. Comments can be hidden through YouTube's moderation status, but not deleted. YouTube has no comment webhooks, so OmniSocials checks each channel for new comments once per day; a new comment can take up to a day to reach the inbox. The standard YouTube connection already covers comments, no extra authorization needed. |
| X (Twitter) | DMs only, after the workspace opts in. Uses credits. See [X direct messages](#x-direct-messages). |
| Threads | Replies people leave on your posts, and mentions. Replies publish as native Threads replies, and top-level replies on your posts can be hidden. See [Threads replies and mentions](#threads-replies-and-mentions). |

## X direct messages

X DMs work differently from the other platforms because X charges for its DM API. They are off by default, and each workspace opts in separately.

### Turning it on

In the app, open the Social Inbox and use the **X DMs** card. Enabling requires:

- An X account connected to the workspace, with DM permissions on the connection. Accounts connected before DM support shipped need a quick reconnect first; the card tells you when that is the case.
- The billing permission, since X DMs spend your organisation's credits.
- A credit balance of at least 25 credits.

### What it costs

X bills its DM API per use. We pass those rates through as [credits](/billing#credits-and-x-link-posts) (purchased at €0.01 per credit), rounded up to whole credits:

| Action | Cost |
|--------|------|
| DM received | 1 credit ($0.010, X's delivery rate) |
| DM reply sent | 2 credits (covers X's $0.015 send fee) |

A reply that fails to send is refunded automatically.

### Running out of credits

If the balance hits zero, the X inbox pauses itself: the message that used the last credit still lands in your inbox, but new DMs stop arriving and the person who enabled X DMs gets an email. Top up and re-enable X DMs to resume. DMs sent to you while paused are not recovered.

### No history, DMs only

The inbox receives DMs from the moment you enable it. Conversations from before you enabled are not imported, and neither are messages that arrived while the inbox was paused. X conversations are always type `dm`: comments and mentions on X are not part of the inbox, and replies are DM replies only.

## TikTok comments

TikTok comments need their own authorization on top of the regular TikTok connection, so they are off until you enable them per channel.

### Turning it on

In the app, go to **Settings -> Organisation -> Workspaces** and press **Enable comments** on the TikTok channel card. It runs a second TikTok authorization for the comment permissions. From then on, new comments on your videos land in the inbox in real time.

### Comments only

TikTok conversations are always type `comment`: DMs and mentions on TikTok are not part of the inbox. Commenter names and avatars can briefly show as "TikTok User" until TikTok releases the commenter's identity. You can also like and unlike comments from the inbox in the app; liking is not available through the API yet. Comments can be hidden and deleted through the API, see [Hiding and deleting comments](#hiding-and-deleting-comments).

### Reply rules

Replies post from the connected TikTok account and are text only, capped at 150 characters. A longer reply, or one with an attachment, returns `400 validation_error`. If the comments authorization expires, replies return `401 reauth_required`; press **Enable comments** on the channel card again to fix it.

### Spam review delay

TikTok holds fresh comments and replies in spam review for roughly 15 to 20 minutes before they are publicly visible on TikTok. A reply that is accepted by the API will appear under the video, just not immediately.

## Threads replies and mentions

:::caution
**Reconnect once.** The Threads inbox needs the Threads reply permissions. Threads accounts connected before these permissions were added need a one-time reconnect under Settings -> Organisation -> Workspaces; until then Threads conversations do not appear and Threads replies and hides answer `401 reauth_required`.
:::

Threads brings two conversation types into the inbox:

- **Replies on your posts** (`type: "comment"`): every reply someone leaves under one of your Threads posts. Conversations group per root post (`conversation_id` = `threads_comment_<postId>`).
- **Mentions** (`type: "mention"`): posts that mention your Threads profile (`conversation_id` = `threads_mention_<postId>`).

Threads has no DM API, so there are no Threads conversations of type `dm`.

### Reply rules

Replying to a Threads conversation publishes a native Threads reply from your connected account: under the person's reply on your post, or under the post that mentioned you. Replies are text only. If the Threads connection is missing the reply permission (connections made before the permission was added), replies return `401 reauth_required`; reconnect Threads under Settings -> Organisation -> Workspaces to fix it.

### Hiding a reply

As the post owner, you can hide (or unhide) a reply someone left on one of your Threads posts with the same `POST /inbox/messages/{id}/hide` endpoint described under [Hiding and deleting comments](#hiding-and-deleting-comments). Threads specifics: only incoming top-level replies can be hidden (a nested reply returns `400 not_hideable`), a missing reply permission returns `401 reauth_required`, and there is no delete for Threads.

## Using it from an AI assistant

The inbox is available through the OmniSocials MCP server, so you can connect Claude, ChatGPT, Cursor, or any MCP client and manage your inbox in natural language. The tools are:

| Tool | What it does |
|------|--------------|
| `list_inbox_conversations` | List conversations; accepts `platform`, `type`, `unread`, and `unanswered` |
| `get_inbox_conversation` | Read a thread with the post it belongs to (caption, link, image); every message shows its `#id`, which the hide and delete tools take |
| `get_next_unanswered` | The work queue: the next item that needs an answer, with the thread and the post, in one call. Accepts `platform`, `type`, `order`, `include_read`, and `exclude` |
| `mark_inbox_read` | Mark a conversation read, which also skips it in the queue for good |
| `reply_to_inbox` | Send a reply; pass `include_next: true` to get the next unanswered item back in the same call |
| `hide_inbox_comment` | Hide or unhide a comment on Instagram, Facebook, TikTok, YouTube, or Threads |
| `delete_inbox_comment` | Delete an Instagram, Facebook, or TikTok comment, with its replies. Irreversible, so the assistant confirms first |

Ask for "work through my unanswered comments" and the assistant runs the loop from [Working through the inbox](#working-through-the-inbox): `get_next_unanswered`, draft a reply with the post in view, `reply_to_inbox` with `include_next: true`, repeat until nothing is waiting. See [MCP Server](/mcp-server), [Claude](/integrations/claude), and [ChatGPT](/integrations/chatgpt).

Every SDK also ships an `inbox` resource. See [SDKs](/sdks).
