---
title: "The inbox: first-level comments across the whole organization"
method: GET
path: "/organizations/{id_organization}/comments"
tags: ["comments"]
---

# The inbox: first-level comments across the whole organization

`GET /organizations/{id_organization}/comments`

## Request

- Base URL: `https://api.planvortex.com/v1.0.0`
- URL: `https://api.planvortex.com/v1.0.0/organizations/{id_organization}/comments`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (client credentials, token URL `https://api.planvortex.com/v1.0.0/oauth/token`)

Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.

Ordered by `creation_date` descending — the date on the network, not the date we collected it — so rows from ten networks interleave correctly.

**On `telegram` the inbox starts the day the channel was connected.** The Bot API has no way to read the past: a bot only learns what happens while it is inside, so nothing written before the connection exists here and never will. Say so in your UI — an inbox that opens empty on a busy channel reads like a failure.

**Your own replies are not in here.** Anything with `author.is_own: true` is filtered out: what you wrote is not incoming mail. They are still stored, and they do show up in the thread.

Requires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.

## Path parameters

- `id_organization` string, required

## Query parameters

- `social_network` CommentsCommentNetworkName[]
- `id_account` string
- `id_publication` string
- `unread` boolean
- `search` string
- `rating` integer[]
- `offset` integer
- `limit` integer

## Response `200`

Successful operation

- object
  - `comments` CommentsComment[], required
    - `_id` string, required
    - `id_account` union, required: The connected account it arrived on. **It is not always the same shape.** The inbox listing (`GET /organizations/{id}/comments`) returns the whole account resolved; every other operation — the live threads, the reply, the update — returns its identifier as a string. Check before using it.
      - string
      - Account: A social account connected to an organization. On `discord`, on `telegram` and on `slack` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand, or to `#anuncios` and `#general` of the same Slack workspace — costs two accounts of the plan. On `linkedin` one authorization brings two kinds of account: the **personal profile** of whoever authorizes and each **page** they manage. Both publish and both have statistics, but only pages have a comment inbox: LinkedIn does not let any app read the comments on a member's posts, so on a profile every comment endpoint returns error `2600`. `extra_data.is_personal_profile` tells them apart. `error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968). On `slack` the bot token does not expire either: what breaks it is the app being removed from the channel (error 980) or the channel being archived or deleted (error 985).
        - `_id` string, required
        - `id_organization` string, required
        - `id_client` string, required: The client the organization hangs from. Denormalized here for the plan checks.
        - `name` string, required: Display name of the profile, page or channel.
        - `username` string: The handle, when the network has one. Absent on the networks that do not (a Discord channel, a WhatsApp number, a Google Business listing) and on a **private** Telegram channel, which has no `@name` at all — only public ones do. That is also why a private channel's publications come back with no `url`.
        - `social_network` 'facebook' | 'instagram' | 'linkedin' | 'tiktok' | 'twitter' | 'whatsapp' | 'youtube' | 'google_business' | 'bluesky' | 'discord' | 'telegram' | 'threads' | 'slack' | 'pinterest', required: A social network supported by PlanVortex. **This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`.
        - `creation_date` string, date-time, required
        - `error_code` integer, required: `0` is a healthy account. Anything else is a PlanVortex error code explaining why the connection stopped working; the account keeps its data but cannot be used until it is reconnected.
        - `image` string: Avatar URL as the network publishes it. Empty string when there is none.
        - `followers_count` integer: Followers the network reports. Absent on an account that has never been measured. On `telegram` and on `slack` it is the channel's member count, and it is the **only** audience figure either network publishes: there are no views, no impressions and no reach anywhere in the Bot API nor in the Slack Web API.
        - `next_stats_update` string, date-time: When the collector will ask the network for this account's stats again.
        - `next_comments_update` string, date-time: When the collector will read this account's comments again. Only on the networks whose comments hang off the account and not off a publication — today, `google_business`, whose reviews belong to the listing.
        - `private_message_link` string: A link that opens a private chat with this account (`m.me`, `ig.me`, `wa.me`). Absent on every other network.
        - `extra_data` object: What only makes sense on this account's network. Only the keys documented here are part of the contract: anything else in the object can change without notice.
          - `is_personal_profile` boolean: `linkedin` only. `true` on the personal profile of whoever authorized, `false` on a page. A profile publishes and has statistics but no comment inbox (error `2600`). Pages connected before personal profiles existed do not carry it, so treat absent as a page.
    - `id_organization` string, required
    - `id_publication` union: **Your** publication, when there is one — and there often is not: a video uploaded to the channel by hand, a post that predates PlanVortex, and every Google Business review have comments with no publication of ours behind them. What always identifies the target is `publication_external_id`. Same asymmetry as `id_account`: the inbox listing resolves it into the whole publication, every other operation returns the identifier as a string.
      - string
      - Publication
        - `_id` string, required
        - `id_organization` string, required
        - `id_account` union, required: The account the publication goes out through. **It is not always the same shape.** The single-publication operations (create, read, retry) return the account already resolved; the listing and the update return its identifier as a string. Check before using it.
          - string
          - Account: A social account connected to an organization. On `discord`, on `telegram` and on `slack` an account is a **channel**, not a profile: publishing to two Discord channels of the same server — or to two Telegram channels of the same brand, or to `#anuncios` and `#general` of the same Slack workspace — costs two accounts of the plan. On `linkedin` one authorization brings two kinds of account: the **personal profile** of whoever authorizes and each **page** they manage. Both publish and both have statistics, but only pages have a comment inbox: LinkedIn does not let any app read the comments on a member's posts, so on a profile every comment endpoint returns error `2600`. `extra_data.is_personal_profile` tells them apart. `error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again. On `telegram` nothing expires, because there is no account token: what breaks the connection is the bot being removed from the channel or losing its permission to post there (error 968). On `slack` the bot token does not expire either: what breaks it is the app being removed from the channel (error 980) or the channel being archived or deleted (error 985).
            - `_id` string, required
            - `id_organization` string, required
            - `id_client` string, required: The client the organization hangs from. Denormalized here for the plan checks.
            - `name` string, required: Display name of the profile, page or channel.
            - `username` string: The handle, when the network has one. Absent on the networks that do not (a Discord channel, a WhatsApp number, a Google Business listing) and on a **private** Telegram channel, which has no `@name` at all — only public ones do. That is also why a private channel's publications come back with no `url`.
            - `social_network` 'facebook' | 'instagram' | 'linkedin' | 'tiktok' | 'twitter' | 'whatsapp' | 'youtube' | 'google_business' | 'bluesky' | 'discord' | 'telegram' | 'threads' | 'slack' | 'pinterest', required: A social network supported by PlanVortex. **This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`.
            - `creation_date` string, date-time, required
            - `error_code` integer, required: `0` is a healthy account. Anything else is a PlanVortex error code explaining why the connection stopped working; the account keeps its data but cannot be used until it is reconnected.
            - `image` string: Avatar URL as the network publishes it. Empty string when there is none.
            - `followers_count` integer: Followers the network reports. Absent on an account that has never been measured. On `telegram` and on `slack` it is the channel's member count, and it is the **only** audience figure either network publishes: there are no views, no impressions and no reach anywhere in the Bot API nor in the Slack Web API.
            - `next_stats_update` string, date-time: When the collector will ask the network for this account's stats again.
            - `next_comments_update` string, date-time: When the collector will read this account's comments again. Only on the networks whose comments hang off the account and not off a publication — today, `google_business`, whose reviews belong to the listing.
            - `private_message_link` string: A link that opens a private chat with this account (`m.me`, `ig.me`, `wa.me`). Absent on every other network.
            - `extra_data` object: What only makes sense on this account's network. Only the keys documented here are part of the contract: anything else in the object can change without notice.
              - …
        - `social_network` 'facebook' | 'instagram' | 'linkedin' | 'tiktok' | 'twitter' | 'whatsapp' | 'youtube' | 'google_business' | 'bluesky' | 'discord' | 'telegram' | 'threads' | 'slack' | 'pinterest', required: A social network supported by PlanVortex. **This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`.
        - `name` string: Internal name. Never shown on the social network.
        - `text` string
        - `title` string: Only the networks that have a title field use it.
        - `destination` PublicationDestination: **Where inside the account** a publication goes, on the networks where choosing the account is not yet choosing the destination. Today that is `pinterest` alone, where it is the board the pin is saved to. Ask `destinations` in `GET /social_capabilities` instead of keeping a list of your own, and read the account's boards with `GET /organizations/{id_organization}/accounts/{id_account}/destinations`. **On Pinterest it is required.** A publication created without it is created in state `withErrors` with `publication_errors[].code = 987` and the background job will not attempt it — the same treatment as a YouTube video with no title, and for the same reason: a scheduled publication that dies at 3 a.m. over a board that was missing from the start is a failure that could have been told to the person while they were still there. **On every other network the field is deleted, not rejected.** A `destination` on a Facebook publication would be a field that does nothing, and giving it back in the response would be worse than dropping it. `name` and `section_name` are labels for display ("Recipes > Desserts"). The server never checks them and nothing is decided by them: what publishes is `id`.
          - `id` string: The board id. **Always a string**, never a number: Pinterest's ids are long integers, so a client that sends one as a JSON number has already lost digits to rounding before the request arrives. That is refused — `publication_errors[].code = 987` with `data.reason = "invalid_id"` — rather than published to some other board. Sending the board's *name* instead of its id fails the same way, which is the likeliest first mistake of an integration.
          - `name` string: The board's label, for display. Decides nothing.
          - `section_id` string: Pinterest only: the section of the board, optional. Absent or empty means the board itself. Same rule as `id` — anything that is not a string of digits is `data.reason = "invalid_section_id"`. A board's sections arrive in the detail of one destination, not in the list of them.
          - `section_name` string: The section's label, for display. Decides nothing.
        - `link` string: **The destination link**: where the publication takes whoever clicks it. Not to be confused with `url`, which is the link to the publication *on the network* and only exists once it is published. Only the networks that answer `link: true` in `GET /social_capabilities` carry it — today `pinterest` alone, where a pin has a `link` field of its own and sending traffic somewhere is the whole point of publishing there. Putting the URL inside `text` instead leaves it visible and unclickable. **On every other network the field is deleted on save**, like `destination`.
        - `reply_control` 'everyone' | 'accounts_you_follow' | 'mentioned_only' | 'followers_only': **Who can reply** to the publication, on the networks that answer `reply_control: true` in `GET /social_capabilities` (today `threads`). Absent means the network's default, which is anyone. It is set when the publication goes out and cannot be changed afterwards. **On every other network the field is deleted on save**, like `link`.
        - `visibility` 'public' | 'unlisted' | 'friends' | 'followers' | 'private': **Who can see the publication** when it goes out, on the networks that answer `visibility: true` in `GET /social_capabilities` (today `youtube` and `tiktok`). Absent means `public`. It is set when the publication goes out. `unlisted` exists only on YouTube; `friends` and `followers` only on TikTok. **On every other network the field is deleted on save**, like `reply_control`.
        - `made_for_kids` boolean: Whether the video is **made for kids** (COPPA), on the networks that answer `made_for_kids: true` in `GET /social_capabilities` (today `youtube`). Absent means `false`. **On every other network the field is deleted on save**, like `reply_control`.
        - `interactions` PublicationInteractions: What the audience can do with a publication. `true` allows it; a missing key is the network's default.
          - `comments` boolean: The audience can comment.
          - `duet` boolean: The audience can make a Duet with the video. Ignored on a photo post.
          - `stitch` boolean: The audience can make a Stitch with the video. Ignored on a photo post.
        - `commercial_content` PublicationCommercialContent: Whether a publication is commercial content. Both `false` is the same as omitting it.
          - `your_brand` boolean: It promotes the creator or their own business. TikTok labels it "Promotional content".
          - `branded_content` boolean: It is a paid partnership promoting a third party. TikTok labels it "Paid partnership", and it cannot be private.
        - `publication_type` 'profile' | 'page' | 'group' | 'reels' | 'stories' | 'message', required
        - `state` 'ready' | 'withErrors' | 'sended' | 'draft' | 'publishing', required: `draft` is never sent; `ready` is scheduled; `publishing` is in the network's hands right now; `sended` went out; `withErrors` failed and carries the reason in `publication_errors`. `publishing` usually lasts a few seconds, but it can last **up to 10 minutes** when the network is still processing a video (today, Instagram). Then `pending_publish` is present: wait and read the publication again, do not retry it.
        - `pending_publish` PublicationPending: Present only while the network is still **processing** what was sent, and PlanVortex is waiting to publish it. Today that happens on Instagram alone, when Meta takes longer than the ~30 seconds the request waits to process the media. In practice, that means videos. The publication stays in state `publishing`, and the background job asks Instagram again about once a minute until it goes out (`sended`) or fails (`withErrors`). It gives up 10 minutes after the media was sent to Instagram, and then the publication ends in `withErrors` with code 999. The object disappears as soon as the publication is resolved. **Wait for it. Do not retry it and do not create it again**: it has not failed, and a second publication would put the same video out twice. There is no webhook for the outcome: read the publication again after `next_check`. While this object is present the publication **cannot be updated** (error 921), for the same reason.
          - `next_check` string, date-time: When PlanVortex will ask the network again. A good moment to read the publication again: asking before it changes nothing.
          - `deadline` string, date-time: When PlanVortex stops waiting. If the network has not finished by then, the publication ends in `withErrors` (999 on Instagram).
          - `temp_keys` string[]: PlanVortex's own bookkeeping: the temporary cropped files the network may still be downloading. Do not rely on it.
          - `data` object: PlanVortex's own bookkeeping to resume the publication (on Instagram, the ids of Meta's containers). Its content may change without notice.
        - `files` Upload[], required: The attached files, **already resolved**: every read and write path returns full uploads, not identifiers. Identifiers are what you SEND (see `PublicationInput.files`).
          - `_id` string, required
          - `id_organization` string, required
          - `name` string, required
          - `file_type` 'video' | 'image', required
          - `file_format` 'mp4' | 'jpeg' | 'gif' | 'png' | 'jpg', required: The format of the stored bytes. `heic`/`heif` are accepted at the door — that is what an iPhone produces — but never stored: they are converted to JPEG while being ingested, so an upload never comes back with one.
          - `file_properties` FileProperties, required: Measurements taken when the file was ingested. Always present on an upload.
            - `width` integer, required: `0` on a file whose dimensions could not be read.
            - `height` integer, required
            - `duration` integer, required: Seconds. `0` on an image.
            - `size_in_bytes` integer, required
            - `aspect_ratio` object, required: Width divided by height, as a number and as a person writes it.
              - …
            - `allowed_social_networks` string[], required: The networks whose accepted crops this file's ratio matches, with a 0.1 margin. It is about the RATIO only: a network listed here can still reject the file for its duration or its weight (see `GET /social_limits`).
          - `file_externals` object[], required: Where this file ended up on each network it has been published to. Some networks keep their own copy and give it an identifier that is reused instead of uploading the bytes again.
            - `social_network` 'facebook' | 'instagram' | 'linkedin' | 'tiktok' | 'twitter' | 'whatsapp' | 'youtube' | 'google_business' | 'bluesky' | 'discord' | 'telegram' | 'threads' | 'slack' | 'pinterest', required: A social network supported by PlanVortex. **This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`.
            - `external_identifier` string, required
            - `external_url` string, required
          - `cover_image` Upload (recursive)
          - `cover_offset` integer: Point of the video, in milliseconds, used as the cover frame.
          - `ai_generated` AiImageOrigin: Where an AI-generated image came from. **Only images a model generated carry it**: a photo the organization uploaded, a product photo pulled from a connected shop or an image imported from an integration never does. It is set when the image is generated and cannot be edited. Use it to show your own AI label: under article 50(4) of the EU AI Act, telling the audience that an image is AI-generated is the job of whoever publishes it. Images made with the default model also carry a signed C2PA manifest, IPTC metadata and a SynthID watermark in the file itself. PlanVortex keeps the IPTC marker when it crops or recompresses an image for a network; the C2PA signature does not survive a change of pixels.
            - `provider` string, required: The AI provider that answered: `openrouter` unless the client brought its own key.
            - `model` string, required: The model, as it was requested, for instance `google/gemini-3.1-flash-image`.
            - `generated_at` string, date-time, required
            - `id_ai_plan` string: The AI plan that generated it.
          - `is_temporal` boolean, required: `true` on a file the platform created for itself — a crop made to fit a network's aspect ratio. Temporary files are not part of the library listing.
          - `public_path` string, uri, required: Download URL. IMPORTANT: it is a TEMPORARY, signed URL, not a permanent link. It expires (24 hours by default) and must not be stored or shared: ask for the upload again to get a valid one. The URL stays byte-identical within the same hour, so it can be cached for that long.
          - `creation_date` string, date-time, required
        - `publish_date` string, date-time
        - `creation_date` string, date-time, required
        - `publication_errors` object[], required: Why the publication failed, one entry per problem. **It is an array**, and it is empty on a publication that has not failed. For a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors`. No webhook announces it: read the publication to find out. On Instagram, two codes tell you whether a retry makes sense. **998**: Meta rejected the media while processing it (a codec, a duration, a URL it could not download). `data` carries `container_id`, `status_code` and, when Meta gives one, its reason in `status`. Retrying the same file fails the same way: change it first. **999**: Meta had not finished processing the media 10 minutes after it was sent. `data` carries `container_ids` and `minutes`. The file is not the problem, and a retry usually works.
          - `code` integer, required
          - `message` string, required
          - `data` object
        - `publication_warnings` object[]: What still has to be done **by hand on the network** for a publication that DID go out. Same shape as `publication_errors`, and an empty array when there is nothing to do. It is a separate field because these are not failures: the publication stays in state `sended` and must **not** be retried, because retrying would publish it twice. Like `publication_errors`, it belongs to the last attempt. Today there is one code. **2400** (YouTube): YouTube accepted the upload but did not make the video public, so YouTube Studio shows it as a draft or private until someone sets its audience and visibility there. The upload does not fail when this happens; PlanVortex reads the video back right after uploading it to find out. `data` carries `requested_privacy_status`, `privacy_status` (what YouTube actually left), `upload_status`, `made_for_kids` and `studio_url`, the YouTube Studio page where it is finished.
          - `code` integer, required
          - `message` string, required
          - `data` object
        - `retries` integer, required: Manual retries already spent on a failed publication, against the `max_retries` published by `GET /publication_limits`. Only `POST .../retry` increases it; updating the publication resets it to 0.
        - `extra_data` object: What one network needs to remember about **this** publication and that has no common field. Absent on a publication whose network needs nothing, which is almost all of them. Today only `telegram` writes here, and only `telegram_message_ids`: the ids of every message an album turned into, because deleting the album means deleting all of them.
        - `external_identifier` string: The network's own identifier, once published. On `telegram` an album is one publication that is **several messages**, and this holds the first one; the rest travel in `extra_data.telegram_message_ids`.
        - `url` string: Link to the publication on the network, when there is one. A **private** Telegram channel has no public URL, so it comes back empty even though the post went out.
        - `statistics` PublicationStats: Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned. **An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them. On `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count. On `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers. On `threads` there are six, and the one that matters is `views`: it is the only one of the four newest networks with something like impressions, so its engagement rate is computed over a real base and not over followers. Sharing arrives split in three — `reposts`, `quotes` and `shares` (outside Threads) — and the three are added into one figure, the same criterion as on X; the breakdown stays in `raw`. A reply is what every other network calls a comment. On `telegram` there are two as well, and **neither of them is asked for**: the Bot API has no method that returns a message's metrics, so `reactions` arrives on its own through the bot and `comments` is counted in PlanVortex's own inbox. There are no impressions, no reach, no views and no forwards to be had anywhere in it, so engagement is computed over followers. On `slack` there is exactly **one**, `reactions`, and the other absences are the informative part: the Web API publishes no impressions, no reach, no views and no clicks for a message, so those keys are missing rather than zero. There is no `comments` either — Slack threads are not read at all (`comments` is `false` in `GET /social_capabilities`). Engagement is computed over the channel's members. On `pinterest` there are seven, and three of them exist nowhere else: `saves` — the gesture the whole network is built on —, `outbound_clicks` (clicks that left the pin towards its `link`, which is the one normalised as the common `clicks`) and `pin_clicks` (clicks that opened the pin inside Pinterest, with no common equivalent). `impressions` is real here, so the engagement rate is computed over it and not over followers. `video_views` only exists on a video pin. And `comments` is the odd one: Pinterest **counts** them and offers no way to read them, so the number is reported while `comments` is `false` in `GET /social_capabilities` — this network has no comment inbox, and it is not a gap waiting to be filled.
          - `likes` integer
          - `loves` integer
          - `wows` integer
          - `hahas` integer
          - `sorrys` integer
          - `angers` integer
          - `impressions` integer
          - `negative_feedback` integer
          - `clicks` integer
          - `page_likes` integer
          - `video_views` integer
          - `comments` integer
          - `follows` integer
          - `profile_activity` integer
          - `profile_visits` integer
          - `shares` integer
          - `reach` integer
          - `saved` integer
          - `saves` integer: Pinterest. Times the pin was saved to a board — the gesture the network is built on. Normalised as `saves` and counted towards engagement. Not the same key as `saved`, which is Instagram's.
          - `outbound_clicks` integer: Pinterest. Clicks that left the pin towards its `link`. This is the one normalised as the common `clicks`.
          - `pin_clicks` integer: Pinterest. Clicks that opened the pin inside Pinterest without leaving it. No common equivalent, so it lives only here.
          - `retwets` integer
          - `replys` integer
          - `quotes` integer
          - `url_link_clicks` integer: X (Twitter). Clicks on links in the post. Comes from X's non-public metrics: omitted when unavailable.
          - `user_profile_clicks` integer: X (Twitter). Clicks on the author's profile from the post. Comes from X's non-public metrics: omitted when unavailable.
          - `bookmarks` integer: X (Twitter). Times the post was saved to bookmarks. Always available (`public_metrics`). Normalised as `saves` and counted towards engagement.
          - `engagement` integer: Total interactions. LinkedIn reports it directly. On X it comes from the non-public metrics and, when present, takes precedence over the sum of the individual interactions — it includes interactions the API does not break down.
          - `playback_0_count` integer: X (Twitter). Video playbacks that reached 0% — i.e. started. Comes from X's non-public metrics: omitted when unavailable.
          - `playback_25_count` integer: X (Twitter). Video playbacks that reached 25%. Comes from X's non-public metrics: omitted when unavailable.
          - `playback_50_count` integer: X (Twitter). Video playbacks that reached 50%. Comes from X's non-public metrics: omitted when unavailable.
          - `playback_75_count` integer: X (Twitter). Video playbacks that reached 75%. Comes from X's non-public metrics: omitted when unavailable.
          - `playback_100_count` integer: X (Twitter). Video playbacks that reached 100%. Comes from X's non-public metrics: omitted when unavailable.
          - `share` integer
          - `shareMentions` integer
          - `views` integer
          - `reactions` integer: Telegram, Slack and Pinterest. Every reaction on the post, all emoji together, normalised as `likes`. On `telegram` and `slack` it is the **complete state and not an increment**: it goes down when somebody takes theirs back. On `slack` it is the only metric the network gives.
          - `reactions_by_emoji` object: Telegram. The same total broken down by emoji. Reactions with a custom emoji are grouped under a single key: their identifier means nothing outside the server that created it.
        - `metrics` NormalizedMetrics: Metrics translated to a **common vocabulary** shared by every network, which is what makes two networks comparable and summable (each network names them differently: `page_post_engagements`, `total_interactions`, `views`…). **A missing key means the network does not publish that metric** — it is never an implicit zero. A key present with value `0` means it was measured and came out zero. Never default a missing key to 0 when displaying it. `engagement` is the network's own total when it provides one, and otherwise the sum of likes, comments, shares, saves and clicks. Video views are deliberately excluded from it: a view is not an interaction.
          - `impressions` integer: Times the content was shown (not unique)
          - `reach` integer: Unique users reached
          - `engagement` integer: Total interactions
          - `likes` integer
          - `comments` integer
          - `shares` integer
          - `saves` integer
          - `clicks` integer
          - `video_views` integer
          - `profile_views` integer
          - `followers` integer: Followers accumulated at that date, not the day's gain.
          - `followers_gained` integer: Followers gained that day.
        - `engagement_base` 'reach' | 'impressions' | 'followers': What this publication's engagement rate is divided by.
        - `stats_updated_date` string, date-time
        - `next_stats_update` string, date-time
        - `id_integration` string: The integration this publication came from — an RSS feed, for instance. Set when it is created and never changed afterwards.
        - `ai_generated` PublicationAiGenerated: Which part of a publication was generated by AI. **Absent means nothing was.** Use it to show your own AI label: under article 50(4) of the EU AI Act, telling the audience is the job of whoever publishes.
          - `text` boolean, required: The text was written by the AI planner. Set when the draft is created and never changed afterwards, not even if the text is rewritten: it says where the text came from, not who reviewed it.
          - `image` boolean, required: At least one of the files carries `ai_generated`. Recomputed every time the publication is saved, so replacing an AI image with a photo of your own turns it off.
    - `publication_external_id` string, required: What the comment hangs off, on the network: the post/video id in most networks, the **listing** (`locations/{id}`) for a Google Business review, and the published message's id on Telegram — where the thread that holds the comments *is* the forwarded post.
    - `external_id` string, required: The comment's id on the network. Unique per account, and what makes repeated webhook deliveries idempotent. Two exceptions worth knowing, and both are composite ids you should treat as opaque: • A Google Business reply has no id of its own — it is a *field* of the review — so PlanVortex fabricates a stable one, `{reviewId}/reply`. • A Telegram comment lives in a **different chat** from the post it answers (the channel's linked discussion group), so it needs two ids at once and travels as `{thread}/{message}`: replying wants the first, deleting wants the second.
    - `parent_external_id` string: Present only when this is a reply to another comment
    - `social_network` 'facebook' | 'instagram' | 'twitter' | 'linkedin' | 'youtube' | 'google_business' | 'bluesky' | 'discord' | 'telegram' | 'threads', required: A network that has comments. Treat it as an open list: a new one is added before your integration hears about it.
    - `author` CommentsCommentAuthor, required: Who wrote it. Embedded in the comment and **not** a contact: a YouTube commenter has no private inbox you could ever write to, so they get no contact record.
      - `external_id` string, required: The author's id on the network. When the network gives none — Google Business publishes no identifier for a reviewer — this falls back to the review's own id, which means two reviews by the same person look like two different authors. There is no way around it from the API.
      - `name` string: Display name. Always present for a review, including anonymous ones, which get a placeholder rather than an empty string.
      - `profile_pic` string
      - `is_own` boolean, required: Whether the connected account wrote it. This is what separates "delete mine" from "delete theirs", and what keeps your own replies out of the inbox.
    - `text` string, required: **May legitimately be empty.** A stars-only review carries no text at all, so render `rating` alongside it and never assume a blank comment is a loading failure.
    - `rating` integer: Star rating of a **review**. Present only on review networks — today Google Business. Its absence means "this network has no such thing", never zero. It is not decorative: without it a one-star review and a five-star one are the same row, and a stars-only review is a blank line.
    - `creation_date` string, date-time, required: When it was written **on the network**. This is what orders the inbox.
    - `collected_date` string, date-time, required: When PlanVortex last read it. The inbox is a photograph and this says how old it is.
    - `read` boolean, required: Yours, not the network's. A live read never overwrites it.
    - `replied` boolean, required: Yours, not the network's. Set when you reply through the API.
    - `hidden` boolean, required: Hidden on the network. A hidden comment is never swept as deleted: on YouTube, hiding one makes the API stop returning it forever, and without that exception hiding would be indistinguishable from deleting.
    - `deleted` boolean, required: Gone from the network. The row is kept so it is not created again by the next read or by a repeated webhook; it stops appearing in the inbox.
    - `like_count` integer: Absent when the network does not publish it — not zero
    - `reply_count` integer: Absent when the network does not publish it — not zero
    - `our_reply_external_id` string: The network id of your reply, so you can find it or delete it later
  - `total` integer, required: Rows matching the filters, not rows returned

## Other responses

- `400`: The request failed. The body carries the PlanVortex error code in `code`: | Code | Meaning | | --- | --- | | `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. | | `700` | The account is not connected — no valid token, or nothing to ask the network about. | | `936` | The publication was never sent, so it has no thread to read. | | `940` | The client's monthly X credits are exhausted. Only X. | | `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. | | `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. | | `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. | | `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. | | `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. | | `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. | | `965` | This Telegram channel has no linked discussion group, so it has no comments. The network gate says `true`; what is missing is a setting of **that channel**, which its owner fixes in two taps. Carries `chat_id` in `data`. | | `969` | The PlanVortex bot is not allowed to delete messages in this Telegram discussion group. The network allows it; this installation does not. Carries `chat_id` in `data`. | | `970` | Telegram is rate limiting the bot and the wait was longer than PlanVortex is willing to hold the request for. Carries `retry_after_seconds` in `data`: retry after it. | | `1101` | Invalid organization. | | `1501` | The comment's account could not be resolved. | | `2600` | The network has comments but **this account** has none: a LinkedIn personal profile. LinkedIn does not let any app read a member's comments, so only pages have an inbox. Carries `social_network` and `id_account` in `data`. |

---

[API](https://skmtc.dev/planvortex/apis/planvortex-api.md) · [All operations](https://skmtc.dev/planvortex/apis/planvortex-api/llms.txt) · [OpenAPI document](https://skmtc.dev/planvortex/apis/planvortex-api/revisions/abedbbc1cd52?raw)
