# What agents cannot do
> The human gates in front of sending, and the actions no credential can take. Plan around them rather than hitting them.
Source: https://rivomi.com/docs/concepts/guardrails
rivomi sends from a real person's LinkedIn account, so a few decisions belong to a human in the
browser. No tool, key or OAuth grant can make them. When a plan reaches one of these, **stop and
tell the user what to do in the app**. The error message names the place.
## The sending gate [#the-sending-gate]
Nothing reaches LinkedIn until a workspace owner or admin has accepted the LinkedIn sending terms
under **Settings › Senders**. Until then:
* `rivomi_get_workspace.sending_consent.accepted` is `false`, and `how_to_accept` says where.
* `rivomi_review_queue` approvals, `rivomi_enroll_prospects`, and `set_agent_state` with
`start_outreach: true` fail with `consent_required` (HTTP `412`).
* A `send`-scoped key cannot be created.
Rejections, scoring, drafting and every read still work. Check `sending_consent` before planning
any outreach, and plan reads and drafts only when it is `false`.
## The approval gate [#the-approval-gate]
Every person queued for outreach needs an **approved drafted message**. `rivomi_enroll_prospects`
skips anyone without one and lists them in `skipped`. With an agent's review mode on, approval
happens through `rivomi_review_queue` or the Queue tab in the app.
## Not available to any credential [#not-available-to-any-credential]
| Action | Where it happens instead |
| -------------------------------------------------------------------------- | -------------------------------------------- |
| Accept the LinkedIn sending terms | Settings › Senders, by an owner or admin |
| Connect, reconnect or repair a LinkedIn sender (involves a 2FA checkpoint) | Settings › Senders |
| Invite or remove members, change roles | Settings › Members |
| Change billing, delete or export the workspace | The app, by an owner |
| Read another workspace | A separate credential made in that workspace |
| Read LinkedIn credentials, cookies or provider account ids | Nowhere. No tool returns them. |
## Irreversible tools [#irreversible-tools]
Three calls act outside rivomi and cannot be undone. Confirm with the user before each one:
* `rivomi_reply` delivers the text to a real LinkedIn inbox immediately, exactly as written.
* `rivomi_set_enrollment_state` with `stopped` withdraws a pending invitation on LinkedIn.
* `rivomi_review_queue` approvals and `rivomi_enroll_prospects` put real invitations on a sender's
queue.
Everything that spends credits is reversible in effect but not in cost. See [Credits](/reference/credits).
---
# How rivomi works
> The nouns an agent works with (workspace, agent, signal, prospect, list, run, sequence, conversation) and how one leads to the next.
Source: https://rivomi.com/docs/concepts/model
One workspace sells one thing to one kind of buyer. Everything below lives inside it, and every
score and draft is written against the workspace's company profile.
```
agent ──watches──▶ signals ──find──▶ prospects ──land in──▶ list
│
run: enrich → score → draft
│
human approves ──▶ sequence (enrollment) on a LinkedIn sender
│
conversation (inbox)
```
## The nouns [#the-nouns]
| Noun | What it is | Id comes from |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| **Workspace** | The tenant. Plan, credits, company profile, senders, sending consent. A credential reaches exactly one. | The credential itself |
| **Agent** | One goal: where to look (sources), who counts (targeting), what to say (sequence), and which LinkedIn account says it. States: `draft`, `live`, `paused`, `archived`. | `rivomi_list_agents` |
| **Signal** | Why a person showed up: they commented on a post, liked one, wrote a post matching a keyword, changed jobs, their company is hiring or raised money. | Read on the prospect |
| **Prospect** | One person, with their signals, score, reasoning, drafts and messages. | `rivomi_search_prospects` |
| **List** | A set of people with a reason attached (`context`). Every agent owns one; you can make more. | `rivomi_list_lists`, or an agent's `list_id` |
| **Run** | Any long job: an agent's search, an import, a scoring pass. Poll it until `finished`. | Returned by whatever started it; `rivomi_list_runs` |
| **Sequence** (enrollment) | One person moving through an agent's steps on a sender: invitation, then messages at a delay. | `rivomi_list_enrollments` |
| **Sender** | A connected LinkedIn account with daily invitation and message caps. | `rivomi_get_workspace.senders` |
| **Conversation** | The thread with one person, opened by the invitation note. Keyed by the enrollment id. | `rivomi_list_conversations` |
Ids are opaque strings. Take every id from a list or search tool's response.
## Signals: a comment beats a like [#signals-a-comment-beats-a-like]
Somebody who wrote a sentence under a post has told you what they think. Somebody who tapped
like has told you almost nothing. Filters that take a signal kind let you prefer
`post_comment`, and `rivomi_import_prospects` pulls comments by default.
An agent's sources come in three kinds:
| Source | Watches |
| ----------- | --------------------------------------------------------------------------------------------------------------- |
| `signals` | Keyword posts, named creators' posts, competitor company pages, and buying events (job change, hiring, funding) |
| `lookalike` | One ideal buyer's profile: people engaging with them, and their colleagues |
| `list` | People already in the workspace |
## Scoring [#scoring]
Each prospect is enriched, then scored 0–100 against the targeting (titles, industries, sizes,
locations, exclusions) with a one-sentence reason. The agent's `matching` sets the bar:
| `matching` | A fit scores |
| ------------------- | ---------------------------------------------------- |
| `precise` (default) | 75 or more |
| `broad` | 60 or more |
| `skip` | Everyone found is kept, and no scoring spend happens |
A human label teaches the scorer. `fit: "not"` on a prospect, or rejecting them from an agent's
queue, records them as a **counter-example** the scorer sees on every later run. See
[Tune the scorer](/use-cases/tune-the-scorer).
## Sending [#sending]
A qualified person gets a drafted invitation note (300 characters at most, a LinkedIn limit) and
follow-ups. With **review mode** on, each one waits in the agent's queue for approval. Approved
people are queued onto a sender, which paces them inside its working hours and daily caps. That's
why every sending tool answers "queued", not "sent".
When a person replies, their sequence stops and the thread appears in the inbox. Once anyone replies
by hand, the scheduler leaves that thread alone.
Two gates sit in front of all of this, and both are human. See [What agents cannot do](/concepts/guardrails).
---
# API keys and OAuth
> The two kinds of credential, the three scopes, and how each is created, bound to a workspace, and revoked.
Source: https://rivomi.com/docs/connect/authentication
Every credential acts as **one person in one workspace**. It reaches no other workspace, and
membership is re-read on every request: remove the person and their credentials stop working on
the next call.
## Scopes [#scopes]
Three words that nest: `send` includes `write`, which includes `read`.
| Scope | Allows | Who can grant it |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `read` | Prospects, lists, agents, runs, conversations, the dashboard. Also pricing a post with `rivomi_import_prospects` and `estimate_only: true`. | Any member |
| `write` | Change lists, agents, targeting and sequences; start imports and scoring runs, which spend credits. | Owner or admin |
| `send` | Approve people for outreach, queue sequences, pause or stop them, reply on LinkedIn. | Owner or admin, once someone has accepted the LinkedIn sending terms |
Grant the narrowest scope that covers the job. A reporting agent needs `read`; an agent that
builds and tunes agents needs `write`; only an agent that approves or replies needs `send`.
A call outside the credential's scope fails with `insufficient_scope`, and the message names the
scope it needed. Nothing is partially done.
## API keys [#api-keys]
For anything you configure yourself: Claude Code, Cursor, scripts.
1. In rivomi, open **Settings › API** and choose **Create key**.
2. Name it after where it will live ("Cursor on my laptop"), tick its scopes, and pick an expiry
(or none). The `send` box stays disabled until the sending terms are accepted.
3. Copy it. **It is shown once.** It is stored hashed, so a lost key is revoked and replaced.
Production keys start with `pk_live_`; keys from any other environment start with `pk_test_`.
Send the key as a bearer token on MCP and REST alike:
```bash
curl "https://rivomi.com/api/v1/workspace" \
-H "Authorization: Bearer $RIVOMI_KEY"
```
Revoking a key under **Settings › API** takes effect on its next request.
## OAuth [#oauth]
For hosted connectors (claude.ai, Claude Desktop, ChatGPT) and any client that runs the flow
itself. Add the MCP URL with no key; the client sends you to rivomi to sign in, choose a
workspace, and approve the requested scopes.
The grant then appears under **Settings › API › Connected apps**. Disconnecting it there takes
effect on the app's next request.
For client authors: OAuth 2.1 with PKCE (S256), dynamic client registration, and Client ID
Metadata Documents, discoverable from `/.well-known/oauth-authorization-server` and the RFC 9728
protected-resource metadata. Tokens are bound to the `https://rivomi.com/api/mcp`
resource.
---
# ChatGPT
> Add rivomi to ChatGPT as a connector, signed in with OAuth.
Source: https://rivomi.com/docs/connect/clients/chatgpt
### Add the connector [#add-the-connector]
In ChatGPT, open **Settings › Connectors**, create a connector, and give it:
```
https://rivomi.com/api/mcp
```
Choose OAuth as the authentication.
### Sign in [#sign-in]
ChatGPT opens rivomi. Sign in, pick the workspace, and approve the scopes.
### Verify [#verify]
Enable the connector in a chat and ask: "Use rivomi to show my workspace and credit balance."
Setup is complete when it returns the real workspace name and plan.
Custom connectors may require developer mode on your ChatGPT plan. If the tools do not appear
after signing in, start a new chat.
---
# Claude Code
> Add rivomi to Claude Code from the terminal, with OAuth or an API key.
Source: https://rivomi.com/docs/connect/clients/claude-code
Sign in with OAuth:
```bash
claude mcp add --transport http rivomi https://rivomi.com/api/mcp
```
Then run `/mcp` inside Claude Code and authenticate `rivomi` in the browser.
Or use an [API key](/connect/authentication#api-keys) with exactly the scopes you want:
```bash
claude mcp add --transport http rivomi https://rivomi.com/api/mcp \
--header "Authorization: Bearer $RIVOMI_KEY"
```
## Verify the connection [#verify-the-connection]
Ask: "Use rivomi to show my workspace, my credits and whether sending is enabled." Setup is
complete when it returns the real workspace name without an authentication error.
Claude Code is the strongest host for the [use cases](/use-cases/launch-an-agent): it can create an
agent, poll its run, and walk the review queue in one session. If a call returns `401`, run `/mcp`
and reconnect, or replace the key.
---
# Claude
> Add rivomi to claude.ai or Claude Desktop as a custom connector, signed in with OAuth.
Source: https://rivomi.com/docs/connect/clients/claude
### Add the connector [#add-the-connector]
In Claude, open **Settings › Connectors › Add custom connector** and give it:
```
https://rivomi.com/api/mcp
```
Leave the OAuth fields empty. There is no key to paste.
### Sign in and choose the workspace [#sign-in-and-choose-the-workspace]
Claude opens rivomi. Sign in, pick the workspace this connector should act in, and approve the
scopes. The grant appears under **Settings › API › Connected apps** in rivomi.
### Verify [#verify]
In a new conversation with the connector enabled, ask: "Use rivomi to show my workspace and
credit balance." Setup is complete when Claude returns the workspace's real name and plan.
## First message [#first-message]
> Use rivomi to tell me who replied this week and which of those threads still need an answer.
Claude should call `rivomi_list_conversations` with `needs_reply: true` and summarise the
threads. Nothing is sent: `rivomi_reply` requires you to ask for a specific reply.
If the connector returns `401` or disappears, remove it, add it again, and repeat sign-in.
---
# Codex
> Add rivomi to the Codex CLI.
Source: https://rivomi.com/docs/connect/clients/codex
Add the server to `~/.codex/config.toml`, reading the key from the environment:
```toml
[mcp_servers.rivomi]
url = "https://rivomi.com/api/mcp"
bearer_token_env_var = "RIVOMI_KEY"
```
Export `RIVOMI_KEY` with an [API key](/connect/authentication#api-keys), then start a new Codex
session so it discovers the tools.
## Verify the connection [#verify-the-connection]
Ask: "Use rivomi to show my workspace and credit balance." Setup is complete when it returns the
real workspace name. If a call returns `401`, check that `RIVOMI_KEY` is exported in the shell
that started Codex.
---
# Cursor
> Add rivomi to Cursor's MCP configuration.
Source: https://rivomi.com/docs/connect/clients/cursor
Add rivomi to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):
```json
{
"mcpServers": {
"rivomi": {
"url": "https://rivomi.com/api/mcp",
"headers": { "Authorization": "Bearer pk_live_…" }
}
}
}
```
Drop the `headers` block to sign in with OAuth instead; Cursor prompts on first use.
## Verify the connection [#verify-the-connection]
In a new agent chat, ask: "Use rivomi to show my workspace and credit balance." Setup is complete
when it returns the real workspace name.
The same JSON works in Windsurf and any client that reads an `mcpServers` block. If the tools do
not appear, reload the window. Keep the key out of a committed project file.
---
# Other clients
> Connect any MCP client, or write your own against the rivomi endpoint.
Source: https://rivomi.com/docs/connect/clients/other
rivomi is a standard Streamable HTTP MCP server. Every client needs two things:
| | |
| -------- | -------------------------------------------------------------------------------- |
| **URL** | `https://rivomi.com/api/mcp` |
| **Auth** | OAuth 2.1 with dynamic client registration, or `Authorization: Bearer ` |
A client that only speaks stdio can bridge through `mcp-remote`:
```bash
npx mcp-remote https://rivomi.com/api/mcp --transport http-only
```
The connection is complete when `tools/list` includes `rivomi_get_workspace` and calling it returns
your workspace.
## Writing your own client [#writing-your-own-client]
* The server is stateless: `POST` JSON-RPC to `/api/mcp`. There is no session id and no SSE stream.
* `initialize` returns server instructions: a short brief on the product, what costs money, and
what needs a human. Show them to the model.
* Read tools take `response_format`: `markdown` (default) or `json`. Results over 25,000 characters
are cut and say so; narrow the query rather than paging blindly.
* Errors arrive as tool results with `isError: true`, written for a model to act on. They are not
JSON-RPC protocol errors. See [Errors and limits](/reference/errors).
* A retried call with the same JSON-RPC id and arguments is deduplicated, so retrying a timed-out
`$` tool does not spend twice.
For plain HTTP without MCP, the [REST API](/tools/rest) calls the same functions.
---
# VS Code
> Add rivomi to VS Code's MCP configuration.
Source: https://rivomi.com/docs/connect/clients/vscode
VS Code uses a `servers` block and names the transport explicitly. In `.vscode/mcp.json`:
```json
{
"servers": {
"rivomi": {
"type": "http",
"url": "https://rivomi.com/api/mcp",
"headers": { "Authorization": "Bearer ${input:rivomi-key}" }
}
},
"inputs": [
{ "id": "rivomi-key", "type": "promptString", "description": "rivomi API key", "password": true }
]
}
```
Drop `headers` and `inputs` to sign in with OAuth instead.
## Verify the connection [#verify-the-connection]
In agent mode, ask: "Use rivomi to show my workspace and credit balance." Setup is complete when it
returns the real workspace name. Keep the transport `http`: rivomi has no stdio or SSE transport.
---
# Connect over MCP
> Add the rivomi endpoint to an MCP client, authenticate, and confirm the connection with one free call.
Source: https://rivomi.com/docs/connect
| | |
| ------------- | ------------------------------------------------------------------------------------- |
| **Endpoint** | `https://rivomi.com/api/mcp` |
| **Transport** | Streamable HTTP, stateless. No stdio, no SSE stream. |
| **Auth** | OAuth 2.1 (hosted connectors) or an API key sent as `Authorization: Bearer pk_live_…` |
| **Tools** | Thirty, all named `rivomi__` |
## Pick the credential [#pick-the-credential]
The choice turns on one question: can the client hold a header?
| Client | Credential | Why |
| ----------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| claude.ai, Claude Desktop, ChatGPT | **OAuth**. Add the URL, no key. | Hosted connectors cannot store a custom header, and signing in lets you pick the workspace. |
| Claude Code, Cursor, VS Code, Codex | Either. OAuth by default; an **API key** for a fixed, scoped credential. | A key is tied to exactly the scopes you ticked, which suits unattended use. |
| Scripts, CRMs, cron jobs | **API key** | Nothing to sign in with. |
Both are described in [API keys and OAuth](/connect/authentication), including the three scopes
(`read`, `write`, `send`) and what each unlocks.
## Connect [#connect]
Use the page for your client:
[Claude](/connect/clients/claude) · [Claude Code](/connect/clients/claude-code) ·
[Cursor](/connect/clients/cursor) · [ChatGPT](/connect/clients/chatgpt) ·
[VS Code](/connect/clients/vscode) · [Codex](/connect/clients/codex) ·
[Other clients](/connect/clients/other)
## Verify [#verify]
Ask the agent:
> Use rivomi to show my workspace, my credit balance, and whether sending is enabled.
That calls `rivomi_get_workspace`, which is free and changes nothing. The connection is verified
when the reply names your real workspace and plan. Check three fields before planning any work:
| Field | Meaning |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `plan.credits_remaining`, `plan.spend_blocked` | What `$` tools can still spend this month |
| `senders[]` | LinkedIn accounts outreach can go out from, with `invites_left_today` and `messages_left_today` |
| `sending_consent.accepted` | Whether anything can be sent at all. Only a human in the browser can set it. |
## Connection failures [#connection-failures]
| Symptom | Cause | Fix |
| ------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------- |
| `401` on every call | Token expired, key revoked, or the app was disconnected | Reconnect and sign in again, or create a new key |
| `401` with `grant_revoked` | Someone disconnected the app under Settings › API › Connected apps | Reconnect; the grant is gone for good |
| A tool result naming a missing scope | The credential lacks `write` or `send` | Create a key with that scope, or reconnect and approve it |
| Tools missing after sign-in | The client cached the tool list before auth | Start a new conversation or reload the client |
| `405` on `GET /api/mcp` | Expected: the server is stateless and has no SSE stream | Send JSON-RPC as `POST` |
The server speaks the current MCP specification. Discovery starts at
`/.well-known/oauth-authorization-server` on the same origin; an unauthenticated request to
`/api/mcp` answers with the RFC 9728 challenge that points there.
---
# rivomi API and MCP documentation
> Connect an AI agent to rivomi, find people showing buying intent on LinkedIn, and run outreach from your own account over MCP or REST.
Source: https://rivomi.com/docs
rivomi finds people who are showing buying intent on LinkedIn, scores each one against what your
workspace sells, drafts a message that cites what they said, and sends it from your own LinkedIn
account once a human approves. Every action in the app is available to an AI agent over MCP and to
scripts over REST.
```
https://rivomi.com/api/mcp
```
The MCP server uses **Streamable HTTP**. Hosted connectors sign in with **OAuth 2.1**. Anything you
configure yourself can send an **API key** as a bearer token instead.
## Choose the shortest path [#choose-the-shortest-path]
| Goal | Start here |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Connect an agent for the first time | [Connect over MCP](/connect) |
| Configure a specific client | [Claude](/connect/clients/claude), [Claude Code](/connect/clients/claude-code), [Cursor](/connect/clients/cursor), [ChatGPT](/connect/clients/chatgpt), [VS Code](/connect/clients/vscode), [Codex](/connect/clients/codex), or [another client](/connect/clients/other) |
| Create a key or understand scopes | [API keys and OAuth](/connect/authentication) |
| Learn the nouns: agent, list, run, sequence | [How rivomi works](/concepts/model) |
| Choose or inspect a tool | [Tool reference](/tools), then the live `tools/list` schema |
| Call rivomi from a script | [REST API](/tools/rest) |
| Complete a job end to end | [Use cases](/use-cases/launch-an-agent) |
| Handle an error or a cost question | [Errors and limits](/reference/errors), [Credits](/reference/credits) |
## The agent workflow [#the-agent-workflow]
### Connect and authorise [#connect-and-authorise]
Point a Streamable HTTP client at `/api/mcp`, then sign in through OAuth or send
`Authorization: Bearer `. A credential belongs to exactly one workspace.
### Read the workspace [#read-the-workspace]
Call `rivomi_get_workspace` first. It returns the three facts every later decision depends on: the
**credit balance** (and whether spending is blocked), the **LinkedIn senders** and their headroom
today, and whether a human has accepted the **sending terms**.
### Discover the live tools [#discover-the-live-tools]
Use MCP `tools/list`. Tool schemas are the source of truth for arguments, defaults, and limits.
A description that starts with `$` spends credits.
### Act, then poll [#act-then-poll]
Reads return at once. Anything long (a search, an import, a scoring pass) returns a `run_id`; poll
`rivomi_get_run` until `finished` is true. Sending always returns "queued": a sender paces the
work inside its working hours.
The connection is ready when `rivomi_get_workspace` returns your workspace's real name and plan.
A job is complete when every run it started reports `finished: true`, and the result
(people found, drafts approved, replies sent) is confirmed by a read tool rather than assumed from a
write's response.
## What the server exposes [#what-the-server-exposes]
| Capability | Tools cover |
| ----------------- | ---------------------------------------------------------------------------- |
| Workspace | Plan, credits, senders, sending consent, the funnel dashboard |
| Agents | Create, target, write the sequence, launch, pause, run now, review the queue |
| Prospects | Search, read in full, label fit, write a research brief |
| Lists and imports | Make lists, move people, import a post's engagers, enrich, score and draft |
| Runs | List, poll, abort long jobs |
| Outreach | Queue people into sequences, pause or stop one |
| Inbox | Find threads that need a reply, read them, reply |
## Machine-readable documentation [#machine-readable-documentation]
Every page has a markdown twin. Append `.md` to a docs URL, or request it with
`Accept: text/markdown`.
* [`/docs/llms.txt`](/llms.txt) indexes every page.
* [`/docs/llms-full.txt`](/llms-full.txt) contains the whole manual in one file.
* [`/api/v1/openapi.json`](https://rivomi.com/api/v1/openapi.json) is the REST surface as OpenAPI 3.1.
---
# Credits
> What spends credits, what is free, how to price work before paying for it, and how retries are protected.
Source: https://rivomi.com/docs/reference/credits
A credit is one person enriched or one AI judgement (a score, a draft, a brief). Reading data is
free. Sending on LinkedIn uses the sender's daily allowance, not credits.
## The four tools that spend [#the-four-tools-that-spend]
Each description starts with `$` and states its unit cost and the case where nothing is charged.
| Tool | Charged for | Free case |
| ------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `rivomi_run_agent_now` (and `set_agent_state` → `live`) | Each profile actually looked at | A run that finds nobody new costs only its searches. A second call while a run is going returns the run already in flight. |
| `rivomi_process_list` | Each profile fetched, each person scored or drafted | People already enriched are skipped unless `rescore` is set, so a second pass costs a fraction of the first |
| `rivomi_import_prospects` | Each engager actually pulled | `estimate_only: true` prices the post and imports nothing |
| `rivomi_research_prospect` | A fraction of a credit per person | None. Charged whether or not the brief says anything new. |
## Before spending [#before-spending]
1. Call `rivomi_get_workspace`. If `plan.spend_blocked` is `true`, `spend_blocked_reason` says which
limit is hit, and nothing more will be spent until it resets.
2. For an import, call it with `estimate_only: true` and show the user the price.
3. State the planned `$` calls to the user before making them.
After spending, `rivomi_get_run` on the run returns what it actually cost, line by line.
## Retries [#retries]
A retried call spends nothing twice:
* **MCP:** the server derives an idempotency key from the JSON-RPC request id and the arguments.
A host that retries a timed-out call with the same id gets the first result back.
* **REST:** send an `Idempotency-Key` header. See [REST API](/tools/rest#idempotency).
Keys live 24 hours.
## Limits [#limits]
Plans cap credits and prospects per month, a daily provider spend, the number of agents, senders
and signals watched, and the largest import. `rivomi_get_workspace.plan.limits` and `plan.used`
give the current values. Going over one returns `402` naming the limit. See
[Errors and limits](/reference/errors).
---
# Errors and limits
> How errors arrive on MCP and REST, every error code with the fix, and the rate limits.
Source: https://rivomi.com/docs/reference/errors
Every error is a sentence the caller can act on, plus a stable code. Read the sentence: it usually
names the tool to call or the setting to change.
* **MCP:** a tool result with `isError: true`, not a JSON-RPC protocol error, so the model reads it
and can correct its next call. The code follows the message as `(code: …)`.
* **REST:** `{ "error": "…", "code": "…" }` with the HTTP status below.
A wrong id is always an error that names the tool listing the right ids. It is never an empty
result.
## Codes [#codes]
| Code | Status | Meaning | Fix |
| -------------------- | ------ | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `unauthorized` | 401 | No credential, or an expired or revoked one | Reconnect, or use a valid key |
| `grant_revoked` | 401 | The OAuth grant was disconnected in Settings › API | Reconnect and sign in |
| `insufficient_scope` | 403 | The credential lacks `write` or `send` | A key or grant with that scope ([scopes](/connect/authentication#scopes)) |
| `not_found` | 404 | No such id in this workspace | Take the id from the list tool the message names |
| `not_configured` | 409 | The workspace has no company profile yet | The user fills in Settings › Workspace |
| `no_source` | 409 | Launching an agent with nothing to watch | Add a source |
| `conflict` | 409 | The action does not apply in the current state (for example, pausing a draft) | Read the state first |
| `consent_required` | 412 | Sending terms not accepted | A human accepts them under Settings › Senders ([guardrails](/concepts/guardrails)) |
| `no_seat` | 409 | No connected LinkedIn sender to send from | The user connects one under Settings › Senders |
| (plan limit) | 402 | A plan limit or the credit balance is exhausted; the message names which | Wait for the reset or change plan. Nothing was half-done. |
| `rate_limited` | 429 | Over the request budget | Wait `Retry-After` seconds |
| `org_deleted` | 403 | The workspace no longer exists | Nothing to retry |
A `400` without a code is a bad argument. The message says which argument and why.
## Rate limits [#rate-limits]
600 requests a minute per workspace, plus a per-key limit. Over either, `429` with a
`Retry-After` header. Poll runs every minute or two, not in a tight loop.
---
# Statuses
> Every state an agent, prospect, run and sequence can be in, and what moves it.
Source: https://rivomi.com/docs/reference/statuses
## Agent [#agent]
| State | Meaning | Moves by |
| ---------- | ---------------------------------------------------------------------- | --------------------------------- |
| `draft` | Created, never launched. Finds nobody, sends nothing. | `rivomi_set_agent_state` → `live` |
| `live` | Searching its sources every six hours. Sending too, if outreach is on. | → `paused` |
| `paused` | Search stopped, every sequence held, nothing cancelled | → `live` resumes where it stopped |
| `archived` | Retired in the app | Not reachable from a tool |
## Prospect [#prospect]
The `status` filter on `rivomi_search_prospects`:
| Status | Meaning |
| ------------ | --------------------------------------------------------------------- |
| `new` | Found, not judged yet |
| `qualified` | Scored as a fit, waiting to be contacted |
| `contacted` | Invitation sent |
| `accepted` | Connected |
| `replied` | Wrote back |
| `not_fit` | Ruled out by the targeting |
| `suppressed` | Do not contact (labelled `fit: "not"`, or on the do-not-contact list) |
`enriched`, `scored`, `drafted`, `disqualified` and `screened_out` are raw pipeline states and
match exactly.
## Run [#run]
`queued` → `running` → `done` or `failed`. Poll `rivomi_get_run` on `finished`, not `status`.
One failing signal does not fail an agent's run: it is recorded against that signal and the run
carries on.
Kinds: `sweep` (an agent or workspace search), `import` (a post's engagers), `discovery`,
`score` (enrich and rate), `draft`.
## Sequence (enrollment) [#sequence-enrollment]
| State | Meaning |
| ----------- | --------------------------------------------------------------------------------------------- |
| `queued` | Assigned to a sender, waiting for its slot inside working hours and the daily cap |
| `invited` | Connection request sent |
| `accepted` | They connected; follow-up messages run on their delays |
| `replied` | They wrote back. The sequence stops and the thread is in the inbox. |
| `done` | Every step sent, no reply |
| `paused` | Held. `rivomi_set_enrollment_state` → `active` resumes it exactly where it stopped. |
| `withdrawn` | Stopped with the invitation withdrawn on LinkedIn. Final. |
| `failed` | Could not proceed (for example, already connected when first-degree connections are excluded) |
---
# Tool reference
> All thirty rivomi MCP tools grouped by job, with the scope each needs, whether it spends credits, and its REST twin.
Source: https://rivomi.com/docs/tools
Use this page to choose a tool. Before calling it, read its live schema from MCP `tools/list`;
the schema is authoritative for arguments, defaults and limits. Each tool's description also says
what it does **not** do and names the tool that does.
`$` marks the four tools that spend credits. Scope is the minimum the credential needs; see
[API keys and OAuth](/connect/authentication#scopes).
## Choose the tool [#choose-the-tool]
| Need | Use |
| -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Know the balance, senders and whether sending is allowed | `rivomi_get_workspace`. **Call it first.** |
| Answer "how are we doing" | `rivomi_get_dashboard` |
| Find people | `rivomi_search_prospects`, then `rivomi_get_prospect` |
| Build an agent | `rivomi_create_agent` → `rivomi_update_agent_sequence` → `rivomi_set_agent_state` |
| See who is waiting for approval | `rivomi_list_agent_queue` |
| Approve or reject them | `rivomi_review_queue` |
| Pull a LinkedIn post's commenters | `rivomi_import_prospects` with `estimate_only: true` first |
| Enrich, score and draft a list | `rivomi_process_list` |
| Follow a long job | `rivomi_get_run` |
| Find threads needing an answer | `rivomi_list_conversations` with `needs_reply: true` |
| Answer one | `rivomi_get_conversation`, then `rivomi_reply` |
## Workspace [#workspace]
| Tool | Scope | What it does | REST |
| ---------------------- | ----- | --------------------------------------------------------------------------------------------------------------- | ------------------- |
| `rivomi_get_workspace` | read | Plan, credits left, spend block, senders and their headroom today, sending consent | `GET /v1/workspace` |
| `rivomi_get_dashboard` | read | The funnel over `7d`, `30d`, `3m` or `month`: found, qualified, invited, messaged, replied, spend, best sources | `GET /v1/dashboard` |
## Agents [#agents]
| Tool | Scope | What it does | REST |
| ------------------------------- | ----- | ---------------------------------------------------------------------------------------------- | ----------------------------------- |
| `rivomi_list_agents` | read | Every agent with state, totals and `list_id` | `GET /v1/agents` |
| `rivomi_get_agent` | read | One agent in full: targeting, sequence, signals, sources, stats, recent runs | `GET /v1/agents/{id}` |
| `rivomi_list_agent_queue` | read | `awaiting_review` (drafted, nothing sent) and `enrollments` (mid-sequence) | `GET /v1/agents/{id}/queue` |
| `rivomi_create_agent` | write | A **draft** agent from a name, one source and optional targeting. Finds nobody until launched. | `POST /v1/agents` |
| `rivomi_update_agent_targeting` | write | Who the agent looks for. Each array sent replaces the whole array. | `PATCH /v1/agents/{id}/targeting` |
| `rivomi_update_agent_sequence` | write | Replace the steps, goal, tone, review mode, daily cap | `PATCH /v1/agents/{id}/sequence` |
| `rivomi_set_agent_state` | write | `live` (launches and runs now, spends) or `paused` (holds everything) | `POST /v1/agents/{id}/state` |
| `rivomi_run_agent_now` $ | write | Run a live agent's sources now instead of on the six-hourly schedule | `POST /v1/agents/{id}/run` |
| `rivomi_review_queue` | send | Approve (queues real outreach) or reject (counter-example) waiting people | `POST /v1/agents/{id}/queue/review` |
## Prospects [#prospects]
| Tool | Scope | What it does | REST |
| ---------------------------- | ----- | ---------------------------------------------------------------------------------------------- | ---------------------------------- |
| `rivomi_search_prospects` | read | Filter by text, score band, signal, list, fit label, sequence stage, replied, date | `GET /v1/prospects` |
| `rivomi_get_prospect` | read | One person: signals with what they wrote, score and reason, lists, drafts, sequences, messages | `GET /v1/prospects/{id}` |
| `rivomi_update_prospect` | write | Set `fit` (`good`, `not`, `none`) and notes. `not` also suppresses them. | `PATCH /v1/prospects/{id}` |
| `rivomi_research_prospect` $ | write | An AI research brief against what the workspace sells; replaces any earlier brief | `POST /v1/prospects/{id}/research` |
## Lists and imports [#lists-and-imports]
| Tool | Scope | What it does | REST |
| ---------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `rivomi_list_lists` | read | Every list with member count and owning agent | `GET /v1/lists` |
| `rivomi_get_list` | read | One list's pipeline breakdown and approved-but-unqueued count | `GET /v1/lists/{id}` |
| `rivomi_create_list` | write | An empty list. Its `context` sentence is read by the scorer and the drafter. | `POST /v1/lists` |
| `rivomi_update_list_members` | write | Add and remove people. Membership only: nothing is queued or cancelled. | `POST /v1/lists/{id}/members` |
| `rivomi_process_list` $ | write | Run `enrich`, `score`, `draft` over a list. Sends nothing. | `POST /v1/lists/{id}/process` |
| `rivomi_import_prospects` $ | write | Pull a LinkedIn post's commenters (default) or likers into a list. `estimate_only` is free and needs only `read`. | `POST /v1/imports` |
## Runs [#runs]
| Tool | Scope | What it does | REST |
| ------------------ | ----- | ------------------------------------------------------------- | -------------------------- |
| `rivomi_list_runs` | read | Long jobs, newest first, by kind, status, agent, date | `GET /v1/runs` |
| `rivomi_get_run` | read | One run: `finished`, what it found, errors, itemised cost | `GET /v1/runs/{id}` |
| `rivomi_abort_run` | write | Stop the rest of a run. Spent stays spent; found stays found. | `POST /v1/runs/{id}/abort` |
## Outreach [#outreach]
| Tool | Scope | What it does | REST |
| ----------------------------- | ----- | -------------------------------------------------------------------------------- | --------------------------------- |
| `rivomi_list_enrollments` | read | Everyone in a sequence, their state and next step due | `GET /v1/enrollments` |
| `rivomi_enroll_prospects` | send | Queue named people or a whole list. Anyone without an approved draft is skipped. | `POST /v1/enrollments` |
| `rivomi_set_enrollment_state` | send | `paused`, `active`, or `stopped` (irreversible, withdraws a pending invitation) | `POST /v1/enrollments/{id}/state` |
## Inbox [#inbox]
| Tool | Scope | What it does | REST |
| --------------------------- | ----- | ------------------------------------------------------------------------------------------------------- | --------------------------- |
| `rivomi_list_conversations` | read | Threads newest first. `needs_reply` returns unanswered replies plus accepted-but-never-messaged people. | `GET /v1/inbox` |
| `rivomi_get_conversation` | read | The invitation note, then every message in order | `GET /v1/inbox/{id}` |
| `rivomi_reply` | send | Send a LinkedIn message now. No draft step, no undo, no merge fields. | `POST /v1/inbox/{id}/reply` |
## REST only [#rest-only]
Two reads exist only on REST, where there is no tool budget: `GET /v1/templates` (saved outbound
copy) and `GET /v1/senders` (the long form of `get_workspace.senders`).
## Call conventions [#call-conventions]
* **Reads** take `response_format`: `markdown` (default, compact) or `json` (exact). Writes answer in JSON.
* **Lists** return `{ total, count, offset, has_more, next_offset, items }`. `limit` defaults to 25, caps at 200.
* **Long jobs** return a `run_id` immediately. Poll `rivomi_get_run` until `finished` is `true`; runs usually take minutes.
* **Results** over 25,000 characters are cut and say so. Narrow the filter rather than paging everything.
---
# REST API
> The same actions as the MCP tools over plain HTTP, for scripts, CRMs and scheduled jobs.
Source: https://rivomi.com/docs/tools/rest
Every MCP tool and every REST endpoint calls the same function, so they can't give different answers.
The [tool reference](/tools) maps each tool to its endpoint.
| | |
| ----------------------- | ------------------------------------------------------------------------------------ |
| **Base URL** | `https://rivomi.com/api/v1` |
| **Auth** | `Authorization: Bearer ` (see [API keys](/connect/authentication#api-keys)) |
| **Schema** | [`/api/v1/openapi.json`](https://rivomi.com/api/v1/openapi.json), OpenAPI 3.1 |
| **Browsable reference** | [`/api/v1/docs`](https://rivomi.com/api/v1/docs) |
The OpenAPI document is authoritative for paths, parameters and response shapes. Generate a
client from it rather than copying shapes from this page.
## First call [#first-call]
```bash
export RIVOMI=https://rivomi.com
curl "$RIVOMI/api/v1/workspace" -H "Authorization: Bearer $RIVOMI_KEY"
```
## Paging [#paging]
Every list endpoint takes `limit` (default 25, maximum 200) and `offset`, and returns:
```json
{ "total": 412, "count": 25, "offset": 0, "has_more": true, "next_offset": 25, "items": [] }
```
Loop until `has_more` is `false`, passing `next_offset` as the next `offset`.
## Idempotency [#idempotency]
`POST /v1/agents/{id}/run`, `POST /v1/lists/{id}/process` and `POST /v1/imports` accept an
`Idempotency-Key` header. A repeat with the same key within 24 hours replays the first response
instead of spending again. Send one on every call you might retry:
```bash
curl -X POST "$RIVOMI/api/v1/agents/$AGENT_ID/run" \
-H "Authorization: Bearer $RIVOMI_KEY" \
-H "Idempotency-Key: $(uuidgen)"
```
## Errors [#errors]
Errors are JSON with a sentence written for the caller and a stable code:
```json
{ "error": "This key needs the write scope. Create one under Settings › API.", "code": "insufficient_scope" }
```
Status codes and codes are listed in [Errors and limits](/reference/errors).
---
# Import a post's engagers
> Price a LinkedIn post for free, pull its commenters into a list, then enrich, score and draft them.
Source: https://rivomi.com/docs/use-cases/import-post-engagers
**Done when:** both runs (import, then processing) report `finished: true`, and the list holds
scored people with drafts.
The fastest warm list there is: the people who commented on a post about your buyer's problem.
Needs `write`; the estimate alone needs only `read`.
## 1. Estimate first [#1-estimate-first]
```
rivomi_import_prospects {
post_url: "https://www.linkedin.com/posts/…",
list_id: "",
estimate_only: true
}
→ comment and like counts, estimated cost
```
Free, imports nothing. Show the user the cost and wait for a yes. Every import is charged per
engager actually pulled, and re-importing the same post charges again.
**Step complete when** the user has approved the estimate.
## 2. Make the list [#2-make-the-list]
```
rivomi_create_list {
name: "Commenters: 'our CRM migration' post",
context: "People who commented on a post about painful CRM migrations. They are living the problem we solve."
}
→ id
```
Write `context` as one real sentence. The scorer and the drafter both read it, so it changes the
output.
## 3. Import [#3-import]
```
rivomi_import_prospects { post_url, list_id, mode: "comments", context }
→ run_id
```
`comments` is the default and the stronger signal. `both` adds likers at the same unit cost. Poll
`rivomi_get_run` until `finished`.
## 4. Enrich, score and draft [#4-enrich-score-and-draft]
```
rivomi_process_list { list_id, stages: ["enrich", "score", "draft"] }
→ run_id
```
Charged per profile fetched and per person scored. Poll `rivomi_get_run` until `finished`, then
report the result:
```
rivomi_get_list { list_id } → breakdown by status
rivomi_search_prospects { list_id, sort: "score" } → the best fits and why
```
Nothing has been sent. To start outreach, see [Review and send](/use-cases/review-and-send).
---
# Launch an outreach agent
> Create an agent from a goal, give it targeting and a sequence, launch it, and confirm its first run found people.
Source: https://rivomi.com/docs/use-cases/launch-an-agent
**Done when:** the agent is `live`, its first run reports `finished: true`, and
`rivomi_search_prospects` on its `list_id` returns people with scores.
Needs `write`. Launching spends credits, so confirm the plan with the user before step 4.
## 1. Read the workspace [#1-read-the-workspace]
```
rivomi_get_workspace {}
→ plan.credits_remaining, plan.spend_blocked, senders[], sending_consent.accepted
```
Stop here if `spend_blocked` is `true`: the run would find nobody. Note whether sending consent is
accepted; it decides step 4.
## 2. Create the draft [#2-create-the-draft]
Pick one source from the user's goal. Keywords are the usual start: phrases their buyers write
when they have the problem.
```
rivomi_create_agent {
name: "RevOps leaders complaining about CRM data",
source: {
kind: "signals",
keywords: ["CRM data is a mess", "cleaning up Salesforce"],
competitors: ["https://www.linkedin.com/company/example-competitor/"],
events: { hiring: true }
},
targeting: { titles: ["Head of RevOps", "VP Sales Operations"], company_sizes: ["51-200", "201-500"] },
matching: "precise"
}
→ id, list_id, status: "draft", source.signals_watched
```
The agent is a **draft**: it finds nobody and sends nothing yet. Omit `targeting` to inherit the
workspace's buyer segment. The call fails with `not_configured` when the workspace has no company
profile. The user fills that in under Settings › Workspace.
**Step complete when** the response has an `id` and `signals_watched` is above zero.
## 3. Write the sequence [#3-write-the-sequence]
```
rivomi_update_agent_sequence {
agent_id,
steps: [
{ kind: "invite", note: "ai" },
{ kind: "message", delay_days: 2, text: "ai" },
{ kind: "message", delay_days: 4, text: "ai" }
],
tone: "conversational",
review_mode: true,
daily_cap: 20
}
```
The rules: at most one `invite`, and only as the first step. Each message waits at least one day.
Hand-written text may use `{{firstName}}`, `{{company}}` and `{{title}}`, and an invitation note
must be 300 characters or less. Keep `review_mode: true` unless the user asks for full automation.
**Step complete when** the response echoes the steps you sent.
## 4. Launch [#4-launch]
```
rivomi_set_agent_state { agent_id, state: "live", start_outreach: false }
→ status: "live", run_id
```
`start_outreach: true` also begins sending. It requires accepted sending consent, and with review
mode on it still sends nobody until someone is approved. Launch with `false`, then turn outreach
on once the user has seen the first finds.
## 5. Wait for the first run [#5-wait-for-the-first-run]
```
rivomi_get_run { run_id } → finished, stats, cost
```
Poll every minute or two. A run usually takes several minutes.
**Step complete when** `finished` is `true`. If the run reports errors on a signal, the rest of
the run still counts. Report the failed signal to the user.
## 6. Confirm the finds [#6-confirm-the-finds]
```
rivomi_search_prospects { list_id, sort: "score", limit: 10 }
```
Report to the user how many were found and how many qualified, and give the top few with their
one-line reasons. Next: [Review and send](/use-cases/review-and-send).
---
# Review and send
> Walk an agent's review queue with the user, approve the right people, and confirm their sequences are queued.
Source: https://rivomi.com/docs/use-cases/review-and-send
**Done when:** every person in `awaiting_review` is approved or rejected, and each approved person
appears in `rivomi_list_enrollments` as `queued` or later.
Needs `send`, and a human must already have accepted the LinkedIn sending terms. Approving puts
real invitations on a real LinkedIn account. **The user decides each approval.** Present the people
and let them choose.
## 1. Check the gate [#1-check-the-gate]
```
rivomi_get_workspace {} → sending_consent.accepted, senders[].invites_left_today
```
If `accepted` is `false`, stop and tell the user: an owner or admin accepts the terms under
Settings › Senders. Rejections still work without it.
## 2. Read the queue [#2-read-the-queue]
```
rivomi_list_agent_queue { agent_id } → awaiting_review[], enrollments[]
```
For each waiting person, show the user the name, the title and company, the signal (what they
said), the score reason and the drafted note. Pull any detail you are missing with
`rivomi_get_prospect`.
## 3. Record the decisions [#3-record-the-decisions]
```
rivomi_review_queue {
agent_id,
approve: ["", …],
reject: ["", …]
}
```
Rejected people leave the agent and become counter-examples for the scorer, so the next run finds
fewer like them. Approved people are queued, not sent: the sender paces them inside its working
hours and the agent's daily cap.
**Step complete when** `rivomi_list_agent_queue` shows an empty `awaiting_review`.
## 4. Confirm [#4-confirm]
```
rivomi_list_enrollments { agent_id, state: "queued" }
```
Report how many were queued, on which sender, and roughly when the first invitations go out given
`invites_left_today`.
## Queueing a list directly [#queueing-a-list-directly]
With no agent involved, queue everyone in a list who has an approved draft:
```
rivomi_enroll_prospects { list_id } → queued[], skipped[]
```
Anyone without an approved draft lands in `skipped` with the reason. Report them rather than
retrying.
---
# Tune the scorer
> Find the borderline people, label them with the user, tighten targeting, and rescore.
Source: https://rivomi.com/docs/use-cases/tune-the-scorer
**Done when:** the borderline band has been labelled, any targeting change is saved, and the
rescoring run reports `finished: true`.
Scores are only as good as the targeting and the counter-examples behind them. This loop is how a
workspace teaches the scorer. Needs `write`; the rescore spends credits.
## 1. Pull the borderline band [#1-pull-the-borderline-band]
```
rivomi_search_prospects { list_id, min_score: 55, max_score: 75, limit: 25 }
```
These are the people the scorer was least sure about, so a label here teaches it the most.
## 2. Label them with the user [#2-label-them-with-the-user]
Show each person's title, company, signal and score reason. For each verdict:
```
rivomi_update_prospect { prospect_id, fit: "good" }
rivomi_update_prospect { prospect_id, fit: "not", notes: "Agency, resells rather than buys" }
```
`fit: "not"` suppresses the person and records them as a **counter-example** that every later scoring
run sees. It is the main teaching signal, and it keeps the person in the workspace. `fit: "none"`
undoes a label.
## 3. Tighten the targeting [#3-tighten-the-targeting]
When the labels show a pattern (the wrong seniority, an industry to exclude), change the agent:
```
rivomi_get_agent { agent_id } → targeting
rivomi_update_agent_targeting { agent_id, exclude_keywords: [ …current…, "agency" ] }
```
Each array you send **replaces** the whole array. Read the current values first and send the full
list you want to end up with.
## 4. Rescore [#4-rescore]
```
rivomi_process_list { list_id, stages: ["score"], rescore: true } → run_id
```
Poll `rivomi_get_run` until `finished`, then rerun step 1 and report how the band moved.
---
# Weekly report
> Answer "how is outreach going" in five free reads, covering the funnel, each agent, failures and open replies.
Source: https://rivomi.com/docs/use-cases/weekly-report
**Done when:** the report covers the funnel, each live agent, every failed run and every
conversation needing a reply, and each number is taken from a tool response.
Everything here is free and needs only `read`, so it's safe to run on a schedule.
## 1. The funnel [#1-the-funnel]
```
rivomi_get_dashboard { range: "7d" }
```
Found → qualified → invited → messaged → replied, the daily series, which signal sources produced
fits, and credits spent. `range` also takes `30d`, `3m` and `month`.
## 2. Each agent [#2-each-agent]
```
rivomi_list_agents {} → state and totals per agent
```
Flag any `live` agent that found nobody new this week. Its signals may be too narrow.
## 3. Failures [#3-failures]
```
rivomi_list_runs { status: "failed", started_from: "<7 days ago>" }
```
For each one, `rivomi_get_run` says what went wrong.
## 4. Open replies [#4-open-replies]
```
rivomi_list_conversations { needs_reply: true }
```
Count them and name the oldest. These are the most valuable line in the report.
## 5. Headroom [#5-headroom]
```
rivomi_get_workspace {} → plan.credits_remaining, senders[]
```
Close with the credits left for the month and whether any sender is disconnected or paused.
## Report shape [#report-shape]
Lead with replies waiting. Then give the funnel with week-over-week change, then agents that need
attention, then failures, then headroom. Keep it to one screen.
---
# Work the inbox
> Find every conversation that needs an answer, read each in full, and reply with the user's approval.
Source: https://rivomi.com/docs/use-cases/work-the-inbox
**Done when:** `rivomi_list_conversations { needs_reply: true }` returns only threads the user
chose to leave.
Reading needs `read`; replying needs `send`. A reply goes to a real person's LinkedIn inbox
immediately and cannot be taken back, so **show the user each reply and send only what they approve**.
## 1. Find the open doors [#1-find-the-open-doors]
```
rivomi_list_conversations { needs_reply: true }
```
This returns two kinds of thread:
* They replied and nobody has answered.
* They accepted the invitation and nobody has written yet.
Both are warm, and the second kind is the easiest to lose.
## 2. Read each thread in full [#2-read-each-thread-in-full]
```
rivomi_get_conversation { enrollment_id } → invitation note, messages[]
```
Read the invitation note. It isn't in the message list, and it is usually what the person is
answering. Add `rivomi_get_prospect` for their signal and score reason when drafting.
## 3. Draft, confirm, send [#3-draft-confirm-send]
Draft a reply that answers what they actually said. Show it to the user. When they approve:
```
rivomi_reply { enrollment_id, text: "…" }
```
The text is sent exactly as written. Merge fields are not filled here. Replying by hand also takes
the thread out of the automated sequence for good.
**Step complete when** each reply the user approved has been sent and the thread has left the
`needs_reply` results.