---
title: "Webhook for reported events"
method: POST
path: "/content-actions"
tags: ["WebhookEvents"]
---

# Webhook for reported events

`POST /content-actions`

## Request

- Base URL: `https://client-provided.endpoint`
- URL: `https://client-provided.endpoint/content-actions`
- Auth: API key in header `Authorization`

Webhook endpoint called by Viafoura when there are events to report (Path is not required).

## Headers

- `X-Request-ID` string, uuid, required

## Request body

- Report
  - `request_uuid` string, uuid
  - `time` integer
  - `version` integer
  - `events` ReportedEvent[]
    - `event_uuid` string, uuid
    - `time` integer
    - `section_uuid` string, uuid
    - `content_container_uuid` string, uuid
    - `content_uuid` string, uuid: The content the event is about. On container-level events (livequestion) this repeats content_container_uuid, because the event concerns the container itself rather than a post inside it.
    - `actor_uuid` string, uuid
    - `author_uuid` string, uuid
    - `message_type` 'chat_message' | 'reply_to_chat_message' | 'liveblog_post' | 'reply_to_liveblog_post' | 'livecomment_post' | 'reply_to_livecomment_post' | 'livereview_post' | 'reply_to_livereview_post' | 'livequestion' | 'livequestion_post' | 'reply_to_livequestion_post' | 'follow_author' | 'follow_topic' | 'poll_vote' | 'user_ban'
    - `action` 'created' | 'updated' | 'disabled' | 'visible' | 'deleted' | 'liked' | 'unliked' | 'disliked' | 'undisliked' | 'flagged' | 'flags_cleared' | 'spammed' | 'picked' | 'unpicked' | 'pinned' | 'unpinned' | 'followed' | 'unfollowed' | 'voted' | 'started' | 'concluded' | 'reopened' | 'banned' | 'unbanned'
    - `page_url` string
    - `user_agent` string: HTTP User-Agent header captured when the event was generated; may be absent for internally-generated events (e.g. moderation actions).
    - `topic_id` string: Topic identifier for follow_topic events. Mirrors the topic_id field in the notification webhook payload (NotificationPayload.topic_id). Absent for other event types.
    - `topic_name` string: Optional human-readable topic name for follow_topic events. On unfollowed events this may be re-derived rather than the originally stored name. Absent for other event types.
    - `detail` union: Event-type-specific detail. Exactly one of the listed shapes, selected by the event's message_type. New event types add their shape here.
      - PollVoteDetail: The poll a poll_vote event refers to, as it stood when the vote was cast. Renaming a question or an option afterwards does not change events already delivered.
        - `poll_uuid` string, uuid, required: The poll that was voted in. Always equal to the event's content_uuid; repeated here so this object is self-contained.
        - `poll_question` string: The question the poll asked. Absent when the upstream record carried none.
        - `is_quiz` boolean: True when the poll is a quiz, meaning its options carry an answer key in is_correct. False on an ordinary poll, where no option is marked correct. No separate "answered correctly" field is emitted: read it as the option that carries both is_selected and is_correct. Do not rely on how many options are correct - a quiz is currently created with a single correct answer, but that is a rule of the product that creates the poll, not a guarantee of this contract.
        - `explanation` string: The quiz author's explanation of why the correct answer is correct, as written when the quiz was created. Present only when is_quiz is true and the quiz has an explanation; omitted entirely otherwise, never sent as null or an empty string. Plain text of at most 300 UTF-16 code units; render it as text, not markup. Unlike the audience-facing poll API, it is delivered on every quiz vote whether or not the voter had answered before, because the recipient is the publisher who wrote it.
        - `options` PollVoteOption[]: Every option on the poll, in presentation order. At most one has is_selected true; a vote whose option cannot be resolved upstream has none, and the array may be empty if the upstream record carried no options at all. On a quiz the correct answer carries is_correct, which need not be the selected option.
          - `option_uuid` string, uuid: The option's identifier.
          - `option_description` string: The option's label as it stood when the vote was cast.
          - `is_selected` boolean: True on the option this vote selected, false on the others. At most one option in a delivery has this set; none does when the upstream record carried no resolvable option.
          - `is_correct` boolean: The quiz answer key: true on a correct answer, false on the others. Present only when is_quiz is true. On an ordinary poll the field is omitted entirely rather than sent as false, so treat it as absent there instead of reading it as a boolean. Independent of is_selected: an option can be correct without having been chosen, which is exactly how a wrong answer is expressed, so the two flags differing on one option is normal and not a contradiction.
      - UserBanDetail: The ban a user_ban event is about. An early lift is delivered as unbanned with the same ban_uuid. A changed ban is delivered again as banned with the same ban_uuid, except when the change comes from a bulk user import: that arrives as unbanned for the old ban_uuid followed by banned with a new ban_uuid.
        - `author_uuid` string, uuid, required: Viafoura UUID of the banned reader. Always present; `user` carries the reader's details when they could be looked up.
        - `ban_uuid` string, uuid, required: Identifies the ban; stable across an early lift and, except for changes made by a bulk user import, across changes to the same ban.
        - `ban_type` 'full' | 'posting' | 'full_hide_posts' | 'ghost', required: full = Full Ban, posting = Posting Ban, full_hide_posts = Full Ban + Hide Posts, ghost = Ghost Ban (the reader is not told in-product).
        - `started_at` integer, required: Ban start, epoch milliseconds.
        - `ends_at` integer: Ban end, epoch milliseconds. Absent when permanent. On an unbanned event, the time the ban was lifted (equal to the event time).
        - `is_permanent` boolean, required: True when the ban has no end. Always false on an unbanned event.
        - `message_to_user` string: The message the moderator wrote for the reader, verbatim. Absent when none was written.
        - `user` BannedUser: Every property of [User](User.md) (name, viafoura_id, external_id), exactly as the Notification webhook sends it, plus email below. Only the banned reader is ever sent; never the moderator.
          - `name` string: Optional
          - `viafoura_id` object, required
            - `uuid` string, uuid, required: Viafoura UUID of the user
            - `id` integer, required: Viafoura integer ID of the user
          - `external_id` object
            - `provider` string: Optional user external login provider
            - `id` string: Optional user external login id
          - `email` string: The reader's email address. Present only when the site lets its administrators see personal data and the reader has an email.

## Response `202`

Accepted

## Other responses

- `400`: Failure that should NOT be retried
- `401`: Auth header missing or invalid
- `500`: Failure that should be retried

---

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