> ## Documentation Index
> Fetch the complete documentation index at: https://docs.madiad.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Moderate a comment

> Hide, unhide, like, unlike, pin or unpin one comment. `hide`, `unhide`, `pin` and `unpin` also need `post_id`. Requires a plan that includes comment writes. Only TikTok answers today; other channels return `platform_not_supported`.



## OpenAPI

````yaml /openapi.yaml post /v1/engagement/comments/action
openapi: 3.1.0
info:
  title: MADIAD Hub API
  version: 1.0.0
  description: >-
    One API for publishing to 22 channels, reading back what happened, and

    managing the profiles and connections behind it.


    ## Rules that bind


    - **Only `success: false` is a failure.** A missing `success` field does not
    mean it broke. Many
      channels accept a post and process it afterwards, so the first response is rarely the final
      outcome — poll `GET /v1/posts/status`.
    - **Read results per channel.** One call can be accepted by some channels
    and refused by others;
      `failed_platforms` and `results` say which, and there is no single overall code that means it.
    - **`Idempotency-Key` is per post.** Generate a NEW key for each post and
    write it down BEFORE
      calling. Replaying a completed key returns the earlier result instead of publishing again, which
      from outside looks exactly like being ignored.
    - **A pinned Page beats the field you send.** When the profile has a Page
    pinned,
      `facebook_page_id` in the request does not change the destination, and scheduled jobs drop the
      field entirely.
    - **`caption` is the body, `title` is the title.** They are different
    fields. Putting the body in
      `title` publishes, so nobody notices, but it skips the caption-length check and then fails at the
      channel instead of returning a clean 400.

    ## Hub refusals versus channel refusals


    An error in the table below is the Hub refusing the call before anything was
    spent — the request

    never left. A channel refusing a post it accepted arrives differently: HTTP
    200 with

    `success: false` for that channel inside `results`. The two need different
    handling, so they are

    never merged.


    ## Error codes


    Every error answers with `{ "error": { "code", "message" } }`.


    | Code | HTTP | What it means |

    | --- | --- | --- |

    | `unauthorized` | 401 | The API key is missing, malformed, or no longer
    valid. |

    | `forbidden` | 403 | The key is valid but this account is not allowed to do
    this. |

    | `not_found` | 404 | No such resource on this account. |

    | `invalid_request` | 400 | The call was refused before anything was spent —
    a field is missing or malformed. The message names it. |

    | `quota_exceeded` | 429 | The plan's allowance for this action is used up.
    Check `GET /v1/usage`. |

    | `no_active_plan` | 403 | The account has no active plan, or the plan has
    expired. |

    | `profile_limit_reached` | 403 | The plan's profile limit is reached.
    Nothing was created. |

    | `rate_limited` | 429 | Too many calls in a short window. Wait for
    `Retry-After` seconds. |

    | `idempotency_conflict` | 409 | This `Idempotency-Key` was already used for
    a DIFFERENT request. Use a new key for each post. |

    | `idempotency_in_progress` | 409 | A call with this `Idempotency-Key` is
    still running. Poll rather than retry. |

    | `page_pin_conflict` | 409 | The call named a Page but the profile has a
    different one pinned. The pin wins, so publishing would put the post
    somewhere the call did not address. Drop the field, or clear the pin. |

    | `page_pin_unverified` | 503 | The profile's pinned Page could not be read,
    so it is unknown whether the post would be redirected. Nothing was published
    — safe to retry. |

    | `platform_not_startable` | 400 | That channel does not use a sign-in
    redirect; it is linked with `POST /v1/connections/{channel}` instead. |

    | `platform_unavailable` | 503 | That channel's linking flow is being
    upgraded and cannot complete right now. Try again later. |

    | `link_not_found` | 404 | The connect link does not exist or has expired.
    Issue a new one. |

    | `platform_not_connected` | 400, 409 | The profile has no account linked
    for a channel the call targeted, so the post could never go out. Link the
    account or drop the channel — retrying will not help. |

    | `location_required` | 400 | This profile's Google account owns more than
    one Business location and none was named. Pass `location_id`, or pin one. |

    | `upstream_error` | 502 | Publishing failed. Nothing is confirmed
    published. |

    | `upstream_timeout` | 502 | The publish timed out and the post MAY have
    gone out. Do NOT retry blind — read `GET /v1/posts/status` first. |

    | `internal_error` | 500, 502 | Unexpected error on our side. |


    ## How this document is produced


    Generated by `api/_build.py` from the gateway source. Paths come from
    `extract_endpoints()`,

    required field names from `extract_required()` (which reads the gateway's
    own error messages),

    error codes from the `ErrorCode` union, and the channel and credential lists
    from

    `gateway/src/platforms.ts`. Data types, prose and response shapes are
    hand-written and are the part

    that can still drift; `dashboard/test/openapi-drift.test.ts` is what stops
    the rest from drifting.


    Full docs: https://docs.madiad.com
  contact:
    name: MADIAD
    url: https://docs.madiad.com
    email: info@madiad.com
  termsOfService: https://madiad.com/en/terms
  license:
    name: Proprietary — MADIAD Terms of Service
    url: https://madiad.com/en/terms
servers:
  - url: https://api.madiad.com
security:
  - bearerAuth: []
tags:
  - name: posts
    description: Publishing a post and reading what became of it
  - name: schedule
    description: Scheduled jobs
  - name: queue
    description: The posting queue
  - name: profiles
    description: Brand profiles
  - name: connections
    description: Linking channels to a profile
  - name: tiktok_music
    description: TikTok music for photo posts
  - name: analytics
    description: Metrics
  - name: engagement
    description: Comments and direct messages
  - name: autodm
    description: Monitors that answer comments on their own
  - name: gbp
    description: Google Business locations and reviews
  - name: ffmpeg
    description: Video processing
  - name: usage
    description: Plan limits
  - name: status
    description: Service status
  - name: health
    description: Liveness
paths:
  /v1/engagement/comments/action:
    post:
      tags:
        - engagement
      summary: Moderate a comment
      description: >-
        Hide, unhide, like, unlike, pin or unpin one comment. `hide`, `unhide`,
        `pin` and `unpin` also need `post_id`. Requires a plan that includes
        comment writes. Only TikTok answers today; other channels return
        `platform_not_supported`.
      operationId: actOnComment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                profile_id:
                  type: string
                  description: >-
                    Id of the brand profile to act on. List them with `GET
                    /v1/profiles`.
                platform:
                  type: string
                  enum:
                    - tiktok
                    - instagram
                    - linkedin
                    - youtube
                    - facebook
                    - x
                    - threads
                    - pinterest
                    - reddit
                    - bluesky
                    - google_business
                    - telegram
                    - discord
                    - slack
                    - mastodon
                    - nostr
                    - lemmy
                    - devto
                    - hashnode
                    - wordpress
                    - whop
                    - listmonk
                  description: One channel key.
                comment_id:
                  type: string
                  description: Id of the comment to act on.
                action:
                  type: string
                  description: >-
                    Moderation verb, e.g. `hide`, `like`, `pin`. Availability is
                    per channel.
                post_id:
                  type: string
                  description: >-
                    REQUIRED for comment actions that act on a post, not on a
                    comment. Id of the post on the channel.
                  x-required-when: for comment actions that act on a post, not on a comment
              required:
                - profile_id
                - platform
                - comment_id
      responses:
        '200':
          description: Success.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Passthrough'
        '400':
          description: The request was refused before anything was spent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The plan does not include this.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such resource for this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit or plan quota reached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Unexpected error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: Publishing failed, or timed out with the outcome unknown.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Passthrough:
      type: object
      description: >-
        The response envelope is stable — a `profile_id` plus the payload — but
        its inner fields are not yet pinned down in this document.
      additionalProperties: true
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
    ErrorCode:
      type: string
      description: >-
        Every code the API can answer with. See the error table in the
        introduction for what each one means.
      enum:
        - unauthorized
        - forbidden
        - not_found
        - invalid_request
        - quota_exceeded
        - no_active_plan
        - profile_limit_reached
        - rate_limited
        - idempotency_conflict
        - idempotency_in_progress
        - page_pin_conflict
        - page_pin_unverified
        - platform_not_startable
        - platform_unavailable
        - link_not_found
        - platform_not_connected
        - location_required
        - upstream_error
        - upstream_timeout
        - internal_error
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Your API key, sent as `Authorization: Bearer <key>`. One key manages
        every profile on the account.

````