---
title: "List Flow Run Messages"
method: GET
path: "/api/v1/flows/{flow_id}/runs/{run_id}/messages"
tags: ["Flows"]
---

# List Flow Run Messages

`GET /api/v1/flows/{flow_id}/runs/{run_id}/messages`

## Request

- Base URL: `https://api.sendseven.com`
- URL: `https://api.sendseven.com/api/v1/flows/{flow_id}/runs/{run_id}/messages`
- Auth: HTTP bearer

List every message attributed to a single flow run, oldest-first.

Returns the chronological transcript of outbound (Send-node) and
inbound (Wait-for-reply matched) messages stamped with this
``flow_run_id``. Reuses the existing
``idx_msg_flow_run_id`` / ``idx_messages_tenant_flow_run`` index.

404 when the run does not exist on this tenant OR when the run
exists but does not belong to ``flow_id`` — never leak cross-tenant
runs.

**Required Scopes:** `flows:read` AND `conversations:read`

## Path parameters

- `flow_id` string, required
- `run_id` string, required

## Query parameters

- `page` integer
- `page_size` integer

## Response `200`

Successful Response

- FlowRunMessageListResponse: Paginated flow-run transcript — same envelope as ``MessageListResponse``.
  - `items` FlowRunMessageResponse[], required: Messages attributed to the flow run, oldest-first
    - `id` string, required
    - `conversation_id` string, nullable: Conversation the message belongs to. Null for flow messages that were sent before any conversation existed (e.g. test runs, or runs the contact never replied to).
    - `tenant_id` string, nullable
    - `platform` string, nullable
    - `channel_id` string, nullable
    - `contact_id` string, nullable
    - `contact_method_id` string, nullable
    - `contact_method` ContactMethodInfo: Lightweight contact method for embedding in responses
      - `id` string, required
      - `method_type` string, required
      - `value` string, required
      - `display_name` string, nullable
      - `is_primary` boolean
    - `direction` 'inbound' | 'outbound', required: Message direction (lowercase to match MySQL ENUM values)
    - `message_type` 'text' | 'image' | 'video' | 'audio' | 'document' | 'location' | 'contact' | 'sticker' | 'system' | 'quick_reply' | 'interactive' | 'carousel' | 'button' | 'file' | 'reaction' | 'referral' | 'whatsapp_template' | 'kb_response' | 'notification' | 'call' | 'order', required: Message content type (lowercase to match MySQL ENUM values). MUST stay in lockstep with ``app.models.message.MessageType`` AND the MySQL ENUM column ``messages.message_type``. Validation here is what gates the API response — any value the DB column accepts MUST be a member of this enum, or ``MessageResponse.model_validate`` raises ``ValidationError`` and the whole list endpoint 500s. Bug found 2026-05-30: prod conversations carrying ``button`` rows from a template-quick-reply click crashed ``GET /api/v1/messages``. Interactive message types: - quick_reply: Cross-platform quick reply buttons (Messenger, Instagram, Telegram) - interactive: WhatsApp interactive messages (buttons/lists) - carousel: Multi-card carousel messages - button: WhatsApp legacy template-button reply (callback button payload) Adapter-level types: - file: Generic file attachment fallback (Messenger/Instagram unknown content_type) - reaction: Emoji reaction (merged into target message metadata) - referral: Messenger m.me deep link / ad referral event Worker-emitted types: - whatsapp_template: Campaign-dispatched WhatsApp template message - kb_response: Team-chat AI knowledge-base response - notification: System notification message
    - `from` string, nullable
    - `to` string, nullable
    - `contact_info` object, nullable: Provider-echoed sender profile captured from the inbound channel webhook at receipt time (inbound message.received only; not a DB lookup). Fields vary by channel: WhatsApp {name, phone}, Telegram {name, username, profile_picture}, Messenger {name, profile_picture}, Instagram {name?, username, profile_picture}. NOTE: `phone` (WhatsApp-only) is deprecated — use `data.contact_method.value` for the authoritative DB-sourced identifier of the method actually used.
    - `text` string, nullable
    - `status` 'pending' | 'sent' | 'delivered' | 'read' | 'failed', required: Message status (lowercase to match MySQL ENUM values)
    - `attachments` AppSchemasMessageAttachmentResponse[]
      - `id` string, required
      - `filename` string, required
      - `content_type` string, required
      - `file_size` integer, required
      - `storage_path` string, required
      - `created_at` string, required
      - `url` string, nullable
      - `thumbnail_url` string, nullable: Short-lived signed thumbnail URL (60 min); null when there is no thumbnail.
      - `width` integer, nullable: Pixel width (image/video), when known
      - `height` integer, nullable: Pixel height (image/video), when known
      - `ai_summary` AttachmentAISummaryOutput: AI-generated summary of an inbound image/PDF attachment. The text is derived from a CUSTOMER-SUPPLIED file: treat it as untrusted plain text (never render as HTML/markdown, never execute links).
        - `status` 'ok' | 'failed' | 'pending', required: `ok` summary ready, `pending` being generated, `failed` generation failed (retry allowed).
        - `kind` 'image' | 'pdf', required: File kind that was summarized.
        - `text` string, nullable: Full plain-text summary (untrusted customer-file content).
        - `sections` AttachmentAISummarySection[]: Parsed sections; empty when the summary could not be split (show `text` instead).
          - `key` 'description' | 'text' | 'summary' | 'key_facts', required: Section label: `description`/`text` (images), `summary`/`key_facts` (PDFs).
          - `text` string, required: Plain-text section body (untrusted customer-file content).
        - `pages_read` integer, nullable: PDF only: pages read (max 10).
        - `pages_total` integer, nullable: PDF only: total pages in the document.
        - `truncated` boolean: PDF only: true when only the first pages were read.
        - `model` string, nullable: Model that produced the summary.
        - `source` 'agent' | 'on_demand', required: `agent` = produced while a Conversational Agent read the file; `on_demand` = requested by a team member/API.
        - `created_at` string, required: When the summary was created (UTC).
        - `updated_at` string, nullable: When the summary was last updated (UTC).
      - `ai_summary_available` boolean: True when an on-demand summary can be requested via `POST /attachments/{id}/summary` (AI enabled, supported inbound image/PDF, no ok/pending summary).
      - `ai_summary_credits` integer, nullable: AI credits an on-demand summary costs (image 2, PDF 3); null when not available. From 1 November 2026 always null: summaries are charged by the AI work used. `ai_summary_available` tells whether the summary can be requested.
    - `related_message_ids` string[]: IDs of the additional messages created by the same send request, in delivery order. Non-empty only when a send carried several attachments on a channel that delivers one attachment per message — the response body itself is the FIRST message (it holds the caption and `attachments[0]`), and each id here is a further message carrying the next attachment. Each has its own status and its own delivery webhooks, so poll them individually via `GET /api/v1/messages/{id}`. Empty array for every other message.
    - `meta` object, nullable
    - `content_format` string: How this message's body is formatted for the send pipeline: `markdown` (canonical markdown, rendered per-channel) or `plain` (human text, passed through untouched). Derived from `meta.content_format`; defaults to `plain` for every message that did not explicitly opt into markdown.
    - `interactive` InteractiveContent: Outbound interactive message content (WhatsApp-style Meta format). Mirrors the object an adapter sends and is persisted verbatim under ``messages.meta.interactive`` by the flow Send-node handler. The conversation UI + flow test panel render an interactive bubble from this shape (``MessageBubble.getInteractiveContent``). All fields optional / extra allowed so older rows and channel-specific variants pass through untouched. Header media (image/video headers) is surfaced as the sibling ``MessageResponse.header_media_url`` / ``header_media_attachment_id`` fields, NOT inside this object — the FE streams header media via the signed-GCS-URL path keyed on the attachment id.
      - `type` string, nullable: Interactive type: 'button' or 'list'
      - `header` object, nullable: Header object (text or media), Meta format
      - `body` object, nullable: Body object: {text: ...}
      - `footer` object, nullable: Footer object: {text: ...}
      - `action` object, nullable: Action object: {buttons:[...]} or {button, sections:[...]}
    - `header_media_url` string, nullable: Resolved media URL for an interactive image/video header
    - `header_media_attachment_id` string, nullable: Durable attachment id for an interactive image/video header
    - `external_id` string, nullable
    - `reply_to_external_id` string, nullable: Platform message ID of quoted message
    - `quoted_message` QuotedMessage: Summary of a quoted (replied-to) message shown as reply context. Displayed as a preview bubble above the message content, similar to how WhatsApp/Telegram show quoted messages.
      - `id` string, required: Message UUID of the quoted message
      - `text` string, nullable: Text preview (truncated to 150 chars)
      - `message_type` string, nullable: Type of the quoted message (text, image, etc.)
      - `direction` string, nullable: Direction of the quoted message
      - `sender_name` string, nullable: Name of the person who sent the quoted message
      - `created_at` string, nullable: When the quoted message was sent
    - `created_at` string, required
    - `sent_at` string, nullable
    - `delivered_at` string, nullable
    - `read_at` string, nullable
    - `is_internal` boolean, nullable
    - `error_message` string, nullable
    - `error_code` string, nullable: Machine-readable failure code for failed messages, e.g. 'contact_blocked' (the contact is blocked) or a provider error code.
    - `campaign` CampaignInfo: Campaign metadata for messages sent via campaigns. When a message was sent as part of a campaign (newsletter/broadcast), this object is populated so the frontend can display a campaign badge in the conversation timeline.
      - `campaign_id` string, required: Campaign UUID
      - `campaign_name` string, required: Campaign display name
      - `campaign_message_id` string, nullable: CampaignMessage UUID for tracking
    - `flow_run_id` string, nullable: FlowRun UUID — set when this message was sent by an automation flow. Frontends use this (together with `flow_name`) to render a 'Sent by flow' badge in the conversation timeline.
    - `flow_step_id` string, nullable: FlowRunStep UUID — identifies the specific Send-node step inside the flow that produced this message. Optional companion to `flow_run_id`.
    - `flow_name` string, nullable: Display name of the flow that sent this message. NOT stored on the message row — populated by the API layer via a single batch lookup against `flow_runs JOIN flows` and only set when `flow_run_id` is non-null.
    - `edited_at` string, nullable: When this message was last edited by the sender (real column on `messages`, stamped when an edit event is applied). Non-null is the ONLY signal that a message was edited — `text` is silently replaced in place, so without this a client cannot tell an edited message from an original one. Null for every message that was never edited (the overwhelming majority).
    - `previous_text` string, nullable: Body this message had immediately BEFORE its most recent edit. Not stored on the message row — resolved by the API layer from the newest APPLIED `message_revisions` row in one batched lookup per page. Null means 'edited, but the superseded body is unknown': most edits arrive before/without the message they target, so the original body was often never captured. Clients must therefore treat null as valid and fall back to a plain non-interactive 'edited' marker instead of an edit-history UI.
    - `revision_count` integer: Number of edits that have actually been APPLIED to this message. Pending revisions (an edit whose target message we never resolved) are deliberately excluded — they describe no visible change to this row. 0 for un-edited messages; a value >1 tells the client the body was edited repeatedly, so `previous_text` is only the last step of a longer history.
  - `pagination` MessagePaginationMeta, required: Extended pagination metadata for messages with cursor support. Messages tables can have billions of rows, so cursor-based pagination is recommended for efficient keyset pagination.
    - `total` integer, required: Total number of items matching the query
    - `page` integer, required: Current page number (1-indexed)
    - `page_size` integer, required: Number of items per page
    - `total_pages` integer, required: Total number of pages
    - `has_next` boolean, required: Whether there are more pages
    - `has_prev` boolean, required: Whether there are previous pages
    - `next_cursor` string, nullable: Cursor for next page (use instead of page for large datasets)
    - `prev_cursor` string, nullable: Cursor for previous page
    - `has_more` boolean: Whether there are more messages after this page

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/sendseven/apis/sendseven.md) · [All operations](https://skmtc.dev/sendseven/apis/sendseven/llms.txt) · [OpenAPI document](https://skmtc.dev/sendseven/apis/sendseven/revisions/ea628483e29a?raw)
