---
title: "Get topic messages"
method: GET
path: "/v2/topics/{topicId}/messages"
tags: ["Topics (group chats/channels/conversations)"]
---

# Get topic messages

`GET /v2/topics/{topicId}/messages`

## Request

- Base URL: `https://api.zenzap.co`
- URL: `https://api.zenzap.co/v2/topics/{topicId}/messages`
- Auth: one of:
  - HTTP bearer and API key in header `X-Signature`
  - OAuth 2
    - Client credentials
    - Token URL: `https://api.zenzap.co/oauth/token` scopes: `message:read`
- TypeScript: <https://skmtc.dev/zenzap/apis/zenzap-external-integration-api/docs/v2/topics/:topicId/messages/get.ts> (a function with typed inputs; its response is checked against the schema)

Get messages from a topic with cursor-based pagination. Messages are returned in the specified order.

**Authorization:** Your API key bot must be a member of the topic to access its messages.

**Pagination:** Use the `cursor` parameter with the value from `nextCursor` in the response to get the next page.
The `before` and `after` parameters are mutually exclusive - use one or the other, not both.

**Note:** For security reasons, a 404 response is returned both when the topic does not exist
and when your bot is not a member of the topic.

## Path parameters

- `topicId` string, uuid, required

## Query parameters

- `limit` integer
- `cursor` string
- `before` integer
- `after` integer
- `senderId` string
- `order` 'asc' | 'desc'
- `includeSystem` boolean
- `threadId` string, uuid
- `includeReactionUsers` boolean

## Headers

- `X-Signature` string
- `X-Timestamp` integer

## Response `200`

Messages retrieved successfully

- TopicMessagesResponse
  - `messages` MessageItem[]: Array of message objects
    - `id` string, uuid: The message ID
    - `topicId` string, uuid: The topic ID where the message was sent
    - `senderId` string: The sender's member ID
    - `senderName` string: The sender's display name
    - `senderType` 'user' | 'bot' | 'system': The type of sender
    - `type` 'text' | 'image' | 'file' | 'video' | 'audio' | 'location' | 'task' | 'contact' | 'poll' | 'call' | 'admin': The message type. - `text`: Regular text message - `image`, `file`, `video`, `audio`: Message with a file attachment - `location`: Shared location - `task`: Task snapshot - `contact`: Shared contact - `poll`: Poll - `call`: Audio/video call snapshot - `admin`: System/administrative message (also carries `isSystem: true`)
    - `text` string: The raw message text content (may be empty for non-text types). Mention tokens are preserved as `<@profileId>` (including bot profile IDs like `<@b@...>`).
    - `truncated` boolean: Always false; history returns the full stored message text.
    - `createdAt` integer: Unix timestamp in milliseconds when the message was created
    - `updatedAt` integer: Unix timestamp in milliseconds when the message was last updated
    - `isEdited` boolean: Whether the message has been edited
    - `isSystem` boolean: Whether this is a system message
    - `replyCount` integer: Number of replies to this message (if it's a thread parent)
    - `parentId` string, uuid: ID of the message being replied to (for threaded replies)
    - `externalId` string: Your own correlation ID, echoed back only to the bot that set it (scoped per bot, so one bot never sees another's IDs).
    - `attachments` string[]: IDs of all attachments associated with the message, including file-backed and structured attachments.
    - `attachmentDetails` WebhookAttachment[]: Details for file-backed attachments (image/file/video/audio). Each `url` is signed and expires after 60 minutes. Structured attachment details are returned in the top-level `poll`, `task`, `contact`, `location`, and `call` fields.
      - `id` string: The attachment ID
      - `type` 'image' | 'file' | 'video' | 'audio': The attachment type
      - `name` string: The original file name
      - `mimeType` string: Stored MIME type when known. Giphy GIFs are looping MP4s and use `video/mp4` with `type: video`.
      - `searchQuery` string: Giphy picker search text or GIF title, when the client stored one. Omitted when empty. Present on GIF attachments so consumers can treat it as the sender's intended reaction even if the clip is hard to interpret.
      - `url` string, uri: Signed URL to download the attachment. **Important:** URLs expire after 60 minutes.
      - `transcription` WebhookTranscription: Transcription data for audio attachments (voice messages). **Transcription flow:** 1. `message.created` arrives with `transcription.status: "Pending"` 2. `message.updated` arrives when transcription completes with `status: "Done"` and `text` populated
        - `status` 'Pending' | 'Started' | 'Done' | 'Failed': Transcription status: - `Pending`: Queued for transcription - `Started`: Transcription in progress - `Done`: Transcription complete, `text` field populated - `Failed`: Transcription failed
        - `text` string: The transcribed text (only present when status is "Done")
    - `mentions` string[]: Mentioned profile IDs.
    - `mentionDetails` MessageMention[]: Mentioned profiles as `{id, name}` objects, in the same order as `mentions`.
      - `id` string: The mentioned profile ID
      - `name` string: Display name used for the mention text
    - `reactions` MessageReaction[]: Array of reactions to the message
      - `emoji` string: The emoji used for the reaction
      - `count` integer: Number of users who reacted with this emoji
      - `userIds` string[]: IDs of users who reacted (if includeReactionUsers is true)
    - `mentionedProfiles` string[]: Mentioned profile IDs.
    - `links` object[]: Links parsed from message text widgets.
      - `url` string, uri
      - `text` string
    - `poll` WebhookPoll: Poll data
      - `id` string: The poll (attachment) ID
      - `title` string: Poll question
      - `subtitle` string: Optional secondary line
      - `selectionType` 'single' | 'multiple': Whether voters may pick one option or several
      - `anonymous` boolean: Whether votes are anonymous
      - `status` 'active' | 'closed': Current poll status
      - `expiresAt` integer: Expiry as Unix timestamp in milliseconds (optional)
      - `options` object[]: The poll options
        - `id` string: Option ID (use when casting a vote)
        - `text` string: Option label
    - `task` WebhookTask: Task snapshot sent when a task is created, updated, or its status changes. The `action` field indicates what triggered this snapshot.
      - `id` string: The task ID
      - `action` 'Added' | 'Updated' | 'Deleted' | 'MarkedAsDone' | 'MarkedAsOpened' | 'Replied': What triggered this task snapshot: - `Added`: Task was created - `Updated`: Task details were changed - `Deleted`: Task was removed - `MarkedAsDone`: Task was completed - `MarkedAsOpened`: Task was reopened - `Replied`: Comment was added to task
      - `title` string: Task title
      - `text` string: Task description
      - `status` 'Open' | 'Done': Current task status
      - `assignee` string: User ID of the assignee
      - `dueDate` integer: Due date as Unix timestamp in milliseconds
      - `isDueDateTimeSelected` boolean: Whether a specific time was selected for the due date
      - `parentId` string: Parent task ID (for subtasks)
      - `subItemsCount` integer: Number of subtasks
    - `contact` WebhookContact: Shared contact information
      - `name` string: Contact name
      - `phoneNumbers` string[]: Phone numbers
      - `emails` string[]: Email addresses
      - `role` string: Job title or role
      - `location` string: Location or office
      - `linkedIn` string: LinkedIn profile URL
      - `profileId` string: Zenzap profile ID if the contact is a Zenzap user
    - `location` WebhookLocation: Shared location data
      - `latitude` string: Latitude coordinate
      - `longitude` string: Longitude coordinate
      - `name` string: Location name (optional)
      - `address` string: Street address (optional)
    - `call` WebhookCall: Audio/video call snapshot. The inline fields (`id`, `initiatorId`, `callType`) come from the message; live-state fields (`status`, `startedAt`, `endedAt`, `endReason`, `duration`, `participantCount`) are present only when the call record resolves.
      - `id` string: The call ID
      - `initiatorId` string: Profile ID of whoever started the call
      - `callType` 'audio' | 'video': Whether the call is audio- or video-first
      - `status` 'initiating' | 'calling' | 'active' | 'ended' | 'cancelled': Current call status
      - `startedAt` integer: When the call started, as Unix timestamp in milliseconds
      - `endedAt` integer: When the call ended, as Unix timestamp in milliseconds
      - `endReason` 'caller_not_joined' | 'caller_cancelled' | 'no_one_answered' | 'finished' | 'max_duration' | 'last_participant': Why the call ended
      - `duration` integer: Call duration in milliseconds (`endedAt - startedAt`), when both are known
      - `participantCount` integer: Number of profiles that actually joined the call
  - `nextCursor` string: Cursor for the next page. Pass this value to the `cursor` parameter to get the next page.
  - `hasMore` boolean: Whether there are more messages to fetch

## Other responses

- `400`: Bad request - invalid input
- `401`: Unauthorized - invalid or missing API token
- `404`: Resource not found
- `500`: Internal server error

---

[API](https://skmtc.dev/zenzap/apis/zenzap-external-integration-api.md) - [All operations](https://skmtc.dev/zenzap/apis/zenzap-external-integration-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zenzap/apis/zenzap-external-integration-api/revisions/37faae2c2036?raw)
