---
title: "Get Outreach Review"
method: GET
path: "/api/outreach/review"
tags: ["outreach"]
---

# Get Outreach Review

`GET /api/outreach/review`

## Request

- Base URL: `https://api.vruum.ai`
- URL: `https://api.vruum.ai/api/outreach/review`
- Auth: `HTTPBearer` (a scheme the document does not define)

Default tool for outreach queue triage — pending messages with full context for review.

Start here when reviewing or managing the outreach queue. Returns pending
messages — both 'needs_draft' (unauthored, awaiting harness authoring: write
via manage_messages edit) and 'draft' (authored, awaiting approve/reject),
disambiguated by each item's `status` — enriched with: person context,
objective instructions (tone rules, selling strategy, touch sequence), match
analysis (summary, alignment points, recommended approach), company research
summary, recent LinkedIn posts, conversation thread, and outreach plan state.
One call gives you everything needed to author, approve, or reject.

`channel_holds` reports LinkedIn sender blocks such as daily quota,
provider throttling, or reconnect-required state. If
`channel_holds_degraded` is true, an empty hold list is unknown rather than
proof that the channel is healthy.

Use content_length='full' for detailed review (capped at 5 items to prevent
connection drops from oversized payloads — use offset to paginate),
or 'preview' (default) for quick scanning with truncated content.

Filter by objective name or ID to review a specific queue (e.g. objective='DFW CFOs').

Use message_ids to fetch specific messages by ID (comma-separated UUIDs, max 20).
Bypasses sorting/pagination — returns full context for exactly those messages.
Useful for subagent dispatch: pull the lightweight queue first, split into batches,
then each subagent calls with its assigned message_ids.

Operators: pass for_company parameter to review a specific client company.

Returns (units in parentheses; scope = messages.status IN ('draft','needs_draft')
for this tenant unless message_ids is set):
    items (list of ReviewItem, unit=messages): one entry per pending message
        (not per person — a person with two pending messages shows up twice).
        Each item:
        - message_id, status ('needs_draft' = author it, 'draft' = approve it),
          channel, subject, content, category (initial / followup /
          reply_response), priority_score, created_at
        - expires_at (needs_draft only): when the nightly sweep auto-rejects
          this row if still unauthored (the action's expires_at —
          regenerate grants a fresh window). Author nearest-deadline-first.
          An expired touch shows up later as status='rejected'
          with blank content and ai_decision_context.expired_signal — that is
          GC, NOT an operator rejection; revive via manage_messages
          action=regenerate, then author and edit as normal.
        - person_id, person_name, person_title, person_company, match_score,
          stage (the company_people stage)
        - campaign_name, campaign_tone_instructions, campaign_selling_strategy,
          campaign_touch_sequence (per-touch instructions): VRU-878 — sourced
          from the message's PLAN OBJECTIVE (name / policy_envelope.tone_rules
          / sequence pin), not a campaign row. Field names are fixed by the
          shared ReviewItem schema (app/shared/schemas/mcp_compound.py, out
          of scope here).
        - match_summary, alignment_points, recommended_approach
        - company_summary (recent company research),
          recent_linkedin_posts (last 3 posts; unit=posts)
        - manual_notes (full content only): up to 10 newest-first person
          timeline notes. Each note is capped at 4,000 characters, each
          person at 8,000, and all repeated item payloads at 30,000 total.
          A cut note ends with `...[truncated]`. When objective instructions
          name a saved per-person draft, use it instead of the 200-character
          activity summary.
        - conversation_thread (list of outbound+inbound rows, unit=messages,
          ordered by created_at; truncated to 200 chars when content_length='preview')
        - prior_reply (cold touches only: category initial/followup): the
          person's newest human reply {message_id, received_at, channel,
          intent, excerpt}. The prospect already wrote back, so the draft
          is a re-pitch: read the reply, and hold the draft for the
          operator unless they chose to continue the sequence. None when
          the person never replied.
        - outreach_plan_id (the linked plan's id — pass straight to
          manage_outreach pause/resume/stop; no get_person_360 needed),
          plan_status, plan_strategy, touches_completed, max_touches,
          plan_next_action_at (when the plan's next touch fires — an
          approved-but-deferred send happens at this time),
          connection_status (the plan's LinkedIn connection state) (from
          the linked outreach_plan; unit=plans)
        - PRIORITIZE WARM ITEMS: a linkedin_message item with
          connection_status='connected' is a follow-up to someone who
          ACCEPTED the connection request — author/approve these before
          any cold lane; they are the highest-EV rows in the queue.
    total_pending (int, unit=messages): EXACT count of actionable pending messages
        matching the objective / message_ids filter for the tenant,
        BEFORE pagination. Filters: status IN ('draft','needs_draft'),
        excludes externally scheduled/reservation-protected rows,
        user_company_id, and the objective filter when set. Without
        message_ids, a cold touch (initial/followup) is listed only while
        approving it can send it: its plan is active and still runs the
        touch's objective, that objective is active, and its person is
        not archived. A hidden draft keeps its state and returns when its
        plan or objective resumes; replies and retained emails are not
        narrowed. Does NOT date-filter — the queue is "everything
        currently pending."
    offset (int): echoes the request offset.
    limit (int, unit=messages): echoes the page size. When content_length='full'
        this is capped at 5 (regardless of the requested limit).
    approved_pending_send_count (int, unit=messages): approved rows that
        have NOT sent yet (they no longer show as items). Non-zero after a
        bulk approve means those sends are SCHEDULED (per-person cooldown /
        designed sequence timing) — they are not lost, not sent, and must
        not be re-drafted or manually re-sent.
    externally_scheduled_count (int, unit=messages): Gmail/provider-scheduled
        rows protected by an external-send reservation. They are excluded
        from actionable items and must not be approved or re-drafted.

## Query parameters

- `limit` integer: Max messages to review (up to 200)
- `offset` integer: Pagination offset
- `content_length` string: 'full' for untruncated conversation content, 'preview' (default) for 200-char truncation. When 'full', limit is auto-capped at 5 to prevent oversized responses — use offset to paginate.
- `objective` string, nullable: Filter by objective name or ID. Example: 'DFW CFOs' or a UUID. Returns only drafts for people on that objective.
- `message_ids` string, nullable: Comma-separated message UUIDs to fetch specific messages. Bypasses sorting/pagination — returns full context for exactly these messages. Example: 'uuid1,uuid2,uuid3'. Max 20.

## Response `200`

Successful Response

- OutreachReviewResponse
  - `items` ReviewItem[]
    - `review_fingerprint` string, nullable
    - `recipient_address` string, nullable
    - `message_id` string, required
    - `status` string, nullable
    - `channel` string, nullable
    - `subject` string, nullable
    - `content` string, nullable
    - `category` string, nullable
    - `priority_score` number, nullable
    - `created_at` string, nullable
    - `expires_at` string, nullable
    - `person_id` string, nullable
    - `person_name` string, nullable
    - `person_title` string, nullable
    - `person_company` string, nullable
    - `person_current_roles` object[]: Every current role (people.current_positions) with is_primary (an explicit workspace pin, else the first role) and is_objective_account (the role at one of the objective's target accounts). person_title and person_company name the objective-account role, else the primary.
    - `match_score` number, nullable
    - `stage` string, nullable
    - `objective_name` string, nullable
    - `company_writing_guidance` string, nullable: Shared company writing context. Objective instructions take priority; execution policies are separate.
    - `objective_tone_instructions` string, nullable
    - `objective_selling_strategy` string, nullable
    - `objective_forbidden_phrases` string[], nullable: Phrases the objective forbids; the prose gate blocks a draft containing one.
    - `objective_touch_sequence` object[], nullable
    - `sequence_step` object, nullable: The sequence step THIS touch executes: {step, total_steps, channels: [{channel, requires}], instructions, char_limit (the step's own character limit, parsed from its instructions), delay_days, has_template}. The step's instructions are the most specific authoring instructions. None for replies and for rows minted outside a sequence.
    - `match_summary` string, nullable
    - `alignment_points` unknown[], nullable
      - unknown
    - `recommended_approach` string, nullable
    - `company_summary` string, nullable
    - `research_facts` object[]: Full reads only. The verified facts an author may acknowledge: source-attributed observations about the person's own activity (opening hooks, why now, their own posts) and company events (funding, priorities, growth), each {id, kind, about, basis, text, source, observed_at}. basis 'inferred' (opening hooks, why now) is a researcher's reading of the source: use it only for the concrete event it names. Empty means research holds no such fact: write a fact-free draft from role and company and never imply one. Identity (title, employer) and company_summary are context, not facts to acknowledge.
    - `authoring_notes` object[]: Notes the in-app author left for the reviewer, each {code, message}: no_verified_fact (research held no verified fact, so the draft relies on role and company only), step_char_limit (the draft is over the step's own character limit), author_note (the author's own note to the reviewer). Empty for harness and operator prose.
    - `recent_linkedin_posts` object[], nullable
    - `linkedin_activity` LinkedInActivity: VRU-893: recent LinkedIn activity, the receptivity signal, derived at read time from the person's live ``recent_posts`` observations. ``active`` means a dated post falls inside ``window_days``; ``quiet`` means nothing dated falls inside it as of the check at ``checked_at``; ``unknown`` means no live observation exists (never read, or read too long ago), or the read saw an undated post or an unattributed newer item that may be recent. Unknown is not the same as quiet.
      - `status` 'active' | 'quiet' | 'unknown', required
      - `last_posted_at` string, nullable
      - `checked_at` string, nullable
      - `window_days` integer
    - `manual_notes` string[]
    - `research_status` string, nullable
    - `person_researched_at` string, nullable
    - `company_researched_at` string, nullable
    - `conversation_thread` object[]
    - `from_email` string, nullable
    - `sending_mailbox` SendingMailbox: VRU-653: the email mailbox a message sends / sent from. ``sender_config_id`` is pinned at SEND time by the rotator, so the ``resolution`` flag tells the operator how literally to read this: * ``actual`` — the message already has a ``sender_config_id`` (sent, or otherwise pinned); ``email_address``/``is_default`` are the real sender. * ``thread_predicted`` — unsent draft, but the person has a prior sent mailbox, so the rotator's thread-consistency rule will most likely reuse it. Best-effort hint, not a guarantee (the mailbox can be rested/at-limit at send time, in which case rotation picks another). * ``rotation_pending`` — unsent draft with no thread history; the sender is chosen by weighted rotation at send time, so it is not knowable yet.
      - `sender_config_id` string, nullable
      - `email_address` string, nullable
      - `is_default` boolean, nullable
      - `resolution` string
    - `outreach_plan_id` string, nullable
    - `plan_status` string, nullable
    - `plan_strategy` string, nullable
    - `touches_completed` integer, nullable
    - `max_touches` integer, nullable
    - `plan_next_action_at` string, nullable
    - `action_id` string, nullable
    - `action_state` string, nullable
    - `action_version` integer, nullable
    - `retained_email` RetainedEmailReview
      - `task_action_id` string, uuid, required
      - `proposal_revision` integer, required
      - `fingerprint` string, required
      - `sender_config_id` string, uuid, required
      - `from_email` string, required
      - `to_email` string, required
      - `sender_display_name` string, nullable
      - `to_name` string, nullable
      - `expires_at` string, nullable
    - `connection_status` string, nullable
    - `attachments` object[]
    - `asset` BoundAsset
      - `asset_id` string, required
      - `version` integer, required: The version bound to this draft; approving the draft approves it.
      - `kind` string, nullable
      - `state` string, nullable: active or revoked.
      - `latest_version` integer, nullable: The asset's newest version; newer than `version` means the draft carries an older one.
      - `approved_version` integer, nullable: The version recipients' links show today.
      - `title` string, required
      - `body` string, nullable: The work itself, in Markdown.
      - `sources` BoundAssetSource[]
        - `url` string, required
        - `title` string, required
        - `accessed_at` string, nullable
        - `check` object, nullable: Result of the source check: result reachable|unverified|failed, with a reason and the HTTP status when there is one.
      - `check_state` string, required: passed, or failed: a failed version cannot be approved.
      - `check_failures` object[]
      - `approved_at` string, nullable
    - `conversation_state` object, nullable
    - `objective_id` string, nullable
    - `activation` ActivationReport: VRU-873 PR C: the objective's activation manifest for this person. ``missing`` is the acquisition plan: research exactly these facts before drafting, nothing else. ``unavailable`` facts were looked for and do not exist (a Hunter miss, a suppressed conversation) — do not retry them. Present on ``content_length='full'`` reads of needs_draft items whose plan belongs to an objective; None otherwise.
      - `objective_id` string, required
      - `stage` string
      - `mode` string, nullable
      - `sufficient` boolean, required
      - `missing` string[]
      - `unavailable` string[]
      - `error` string, nullable: Set when the activation check could not run (activation_check_failed); sufficient is False and the fact lists are empty. Distinct from a missing report, which means no requirement.
    - `prior_reply` object, nullable
  - `total_pending` integer
  - `offset` integer
  - `limit` integer
  - `approved_pending_send_count` integer
  - `externally_scheduled_count` integer
  - `channel_holds` SenderChannelHold[]
    - `channel` 'linkedin', required
    - `state` 'quota_exhausted' | 'provider_rate_limited' | 'reconnect_needed' | 'degraded', required
    - `reason_code` 'linkedin_daily_quota_exhausted' | 'linkedin_provider_rate_limited' | 'linkedin_reconnect_needed' | 'linkedin_channel_state_unknown', required
    - `blocking` boolean
    - `retryable` boolean
    - `sender_user_id` string, nullable
    - `account_id` string, nullable
    - `account_name` string, nullable
    - `used_today` integer, nullable
    - `effective_limit` integer, nullable
    - `remaining` integer, nullable
    - `retry_at` string, nullable
    - `affected_approved_count` integer
    - `affected_draft_count` integer, nullable
    - `affected_plan_count` integer, nullable
    - `next_action` string, required
  - `channel_holds_degraded` boolean

## Other responses

- `422`: Validation Error

---

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