# List leads

> Leads with status, deal, last interaction, score, primary contact and your own columns. Search by company or contact, filter by deal stage, sort by last interaction, paged by an opaque cursor.

Canonical: https://revnu.com/docs/leads
Last updated: 2026-11-07

```bash
curl -H "Authorization: Bearer $REVNU_API_TOKEN" "https://revnu.com/api/v1/leads?status=replied&limit=50"
```

## Query

| Param | Meaning |
|---|---|
| `status` | Filter by outreach status: `qualified`, `drafted`, `sent`, `partially_sent`, `replied`, `declined` or `bounced`. Anything else is a `400`. |
| `stage` | Only leads whose deal is at this stage: `interested`, `meeting`, `proposal`, `won` or `lost`, most recently updated deal first. Can't be combined with a `sort` other than `newest`. |
| `q` | Words to find in the company name, domain, or the primary contact's name, title or email, up to 200 characters. Every word must match the start of a word, so `acm` finds Acme and `dana acme` needs both. Pages through one window of up to 100 matches with `nextCursor`, and `capped: true` when the window was full so some may be missing: add a word to narrow. |
| `sort` | `newest` (the default, by when the lead was saved), `last_interaction` (most recent email or LinkedIn send or reply first, never-contacted leads last) or `recently_updated` (most recently changed first). |
| `limit` | 1 to 100. Default 50. |
| `cursor` | The `nextCursor` from the previous page, with the same filters and `sort`. A cursor from another order is a `400`. |
| `workflowRunId` | Only the leads this workflow run created or returned people on (a run `id` from [`GET /workflows/{id}/runs`](https://revnu.com/docs/workflow-runs.md)). Each lead then also has `runContactIds`, the contacts this run created, `runPeople`, the people this run returned on the company's research list without saving them as contacts (`{ name, title, email, linkedinUrl, profileUrl, photoUrl, role, alreadyKnown }`; `role` is `economic` for the buyer, `champion`, `technical`, `user` or `null`, and `alreadyKnown` is `true` when the company had the person before this run), and `runCreatedLead`, `true` when the run created the lead itself. |

## Response

`{ "leads": [lead, …], "nextCursor": "…" | null }`. A page is bounded in rows and in bytes, and holds at most 30 leads that are in a LinkedIn sequence, so pass `nextCursor` back until it is null.

A lead is `{ id, domain, companyName, status, replyKind, declineReason, score, scoreReasons, vertical, archetype, tags, fields, contact, linkedin, createdAt, workflowRunId, deal, lastInteractionAt, lastReplyAt, waitingOn }`, plus `runContactIds`, `runPeople` and `runCreatedLead` when you pass `workflowRunId`. `workflowRunId` is the workflow run that created the lead, or `null`. `deal` is `{ stage, amount, currency, closeDate }` or `null`. `lastInteractionAt` and `lastReplyAt` are the newest send or reply and the newest reply from anyone at the company, in epoch milliseconds or `null`; `waitingOn` is `us` when they replied last, `them` when you wrote last, and `null` before any reply (see [Get a lead](https://revnu.com/docs/lead.md) for the full interaction detail). `fields` is your account's configured lead columns, exactly the dashboard table, as `[{ key, value }]` entries with each value cut at 1,000 characters, for example `[{ "key": "crm_id", "value": "…" }, { "key": "segment", "value": "mid-market" }]`. The draft and the research behind each value are on `GET /leads/{id}`, which keeps the list light.

| Code | Meaning |
|---|---|
| 400 | `workflowRunId` combined with `q`, `stage` or a `sort` other than `newest`, or `stage` combined with a `sort` other than `newest`. |
| 404 | `workflowRunId` is not one of your workflow runs. |
| 503 | `q` on an account where search isn't switched on yet. |

## LinkedIn sequence progress

`status` follows the email pipeline and the first LinkedIn message: a connection request on its own leaves a lead `qualified`, a LinkedIn message moves it to `sent`, and a reply to one moves it to `replied`. Where a lead stands in a sequence with LinkedIn steps is in `linkedin`, the same state as the Status column in the dashboard's Leads table. It is `null` for a lead outside such a sequence.

`linkedin` is `{ sequence, status, reason, invitedAt, acceptedAt, lastMessageAt, seenAt, repliedAt, nextStep }`, for the lead's primary contact, with times in epoch milliseconds or `null`:

- `status` is `not_started`, `cannot_start` (`reason` says why, such as no LinkedIn profile), `checking` (a send is being confirmed with LinkedIn), `invite_sent`, `connected`, `messaged`, `seen`, `replied`, `finished` or `stopped`.
- `acceptedAt` is when Revnu first saw the request accepted. LinkedIn does not give the exact time.
- `seenAt` is when they read a message, from accounts that report reads, so `null` does not mean unread.
- `repliedAt` is when they first replied on LinkedIn.
- `nextStep` is `{ kind, dueAt }` for the step still to come, where `kind` is `linkedin_invite`, `linkedin_message` or `email`, and `dueAt` is `null` while it waits for an accept. It is `null` when nothing is left.

This comes from Revnu's own send records, so reading it spends none of your LinkedIn account's daily profile lookups. To keep a CRM in step with a LinkedIn sequence, poll `GET /leads` and compare `linkedin` with what you stored, rather than calling [`GET /linkedin/person`](https://revnu.com/docs/linkedin-person.md) for each person.

## The Why Now field

The Why Now is the recent reason a company is worth contacting, such as a funding round or a job post. When your account's lead table has a Why Now column, it comes back in `fields` with the key `why_now`. The reasoning and source links behind it are on [`GET /leads/{id}`](https://revnu.com/docs/lead.md), in the `research` entry with the same key. What it means and how to correct a wrong one is in [What's on a Lead](https://revnu.com/docs/lead-details.md).

## Hand Revnu a list of prospects

Revnu researches every prospect itself, so a list from your own system goes to Revnu rather than straight into the table. The one route that writes lead rows directly is [Sync Existing Customers](https://revnu.com/docs/customer-sync.md), and those rows are your customers, kept out of cold outreach. To hand Revnu a list:

1. **Block first.** Sync your customers with [`POST /customers`](https://revnu.com/docs/customer-sync.md), and put anyone else you are already talking to on the [block list](https://revnu.com/docs/blocklist-add.md), up to 500 entries a call. Revnu checks both before researching or contacting anyone, so they stay out of outreach.
2. **Send the list.** Send it as a [message](https://revnu.com/docs/agent-messages.md). One message holds up to 4,000 characters, so split a long CSV into chunks and send each with `queue: true` and its own `clientRequestId`. Or POST it as the JSON body of an [inbound webhook](https://revnu.com/docs/webhooks.md), up to 64,000 bytes.
3. **Read the results.** Revnu researches each company and saves the ones that fit as leads. Read them back here, or filter by `workflowRunId` when a workflow did the import.

The API takes text. For a big CSV or spreadsheet, upload the file in the dashboard chat: Revnu reads the whole file in one go.

### Send a big list in chunks

How many rows fit depends on how wide they are. A row like `acme.com,Acme,Dana Lee,dana@acme.io` is about 40 characters, so one 4,000-character message holds roughly 80 to 90 of those once you allow for a short instruction at the top. A 2,000-row export is about 25 messages. Wider rows mean fewer per message.

```ts
const rows: string[] = csv.trim().split("\n").slice(1); // drop the header row
const intro = "Research these companies and save the ones that fit as leads. Columns: domain,company,contact,email.\n";
const chunks: string[][] = [[]];
for (const row of rows) {
  const current = chunks[chunks.length - 1];
  if ((intro + [...current, row].join("\n")).length > 3900) chunks.push([row]);
  else current.push(row);
}
for (const [i, chunk] of chunks.entries()) {
  if (chunk.length === 0) continue;
  await fetch("https://revnu.com/api/v1/agent/messages", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.REVNU_API_TOKEN}`, "content-type": "application/json" },
    body: JSON.stringify({ text: intro + chunk.join("\n"), queue: true, clientRequestId: `import-2026-10-15-${i}` }),
  });
}
```

Each queued message runs after the one before and has six hours to start. Send a long list across the day, or as fewer, larger [webhook](https://revnu.com/docs/webhooks.md) bodies of up to 64,000 bytes. The agent-message limit is 30 calls a minute per token; see [Rate Limits](https://revnu.com/docs/rate-limits.md).

### Your own columns

Once a lead exists, [`PATCH /leads/{id}`](https://revnu.com/docs/lead-update.md) fills your account's lead columns with `customFields`, keyed by the column's `key`. To add a column, ask Revnu in chat, for example: "Add a CRM ID column."

## Frequently asked questions

### Can I create leads through the API?

Through Revnu. [Sync your existing customers](https://revnu.com/docs/customer-sync.md) first, and add anyone you're already talking to to the [block list](https://revnu.com/docs/blocklist-add.md). Then send the list to Revnu as [messages](https://revnu.com/docs/agent-messages.md) of up to 4,000 characters each, or as the JSON body of an [inbound webhook](https://revnu.com/docs/webhooks.md) of up to 64,000 bytes. Revnu researches each company and saves the ones that fit as leads. In the dashboard, upload a CSV or spreadsheet in chat instead. The API itself reads leads and sets their labels.

### Can I upload a CSV file through the API?

Paste the rows into messages in chunks (see [Send a big list in chunks](#send-a-big-list-in-chunks)), or upload the file in the dashboard chat, which is easier for a big list. The API takes text rather than files.

### Can I set my own lead columns from code?

Yes, on a lead that exists: `PATCH /leads/{id}` with `customFields`. Columns themselves are added by asking Revnu.

### How do I find who is waiting on a reply from us?

`GET /leads?sort=last_interaction` and read `waitingOn`. Rows with `us` replied after your last message.

### How do I see new replies?

Poll `GET /leads?status=replied`. Leads come newest first by when they were created, not by when they replied, so a reply on an old lead can be several pages down. Page through to the end and keep the ids you have already seen. Polling is how replies reach your code. A worked recipe is in [Inbound Webhooks](https://revnu.com/docs/webhooks.md#push-replies-into-your-crm).

### How do I see who accepted a LinkedIn invite?

Read `linkedin.acceptedAt` on each lead. It is set once Revnu sees the connection request accepted, and `linkedin.status` moves to `connected`. See [LinkedIn sequence progress](#linkedin-sequence-progress).

### Which scope does this need?

`leads:read`. A token without it gets `403` with `requiredScope` in the body. `GET /me` shows a token's scopes.

### Are responses cached?

Every response is `Cache-Control: no-store`, so what you read is always current.
