# List leads

> Leads newest first with status, score, primary contact and your own columns, paged by an opaque cursor. Draft bodies are only on GET /leads/{id}.

Canonical: https://revnu.com/docs/leads
Last updated: 2026-10-21

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

## Query

| Param | Meaning |
|---|---|
| `status` | Filter by lead status: `qualified`, `drafted`, `sent`, `partially_sent`, `replied`, `declined` or `bounced`. Anything else is a `400`. |
| `limit` | 1 to 100. Default 50. |
| `cursor` | The `nextCursor` from the previous page. |
| `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 }`, plus `runContactIds`, `runPeople` and `runCreatedLead` when you pass `workflowRunId`. `workflowRunId` is the workflow run that created the lead, or `null`. `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": "account_case", "value": "…" }, { "key": "slack_thread", "value": "1790064680.268899" }]`. The list never carries the draft or the research behind a value; read `GET /leads/{id}` for those.

| Code | Meaning |
|---|---|
| 404 | `workflowRunId` is not one of your workflow runs. |

## 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).

## There is no route that creates leads

The API reads leads and sets their labels. It can't create or import them. To hand Revnu a list from your own system:

1. **Block first.** Put your customers and anyone you are already talking to on the [block list](https://revnu.com/docs/blocklist-add.md), up to 500 entries a call. Revnu checks it before researching or contacting anyone, so they can't end up in 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 only. It can't attach a CSV or spreadsheet file. For a big file, uploading it in the dashboard chat is easier: 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 a message that waits more than six hours behind earlier work fails. 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

Columns from your source system, such as an owner or a CRM id, can't be written through the API. `PATCH /leads/{id}` sets only `vertical`, `archetype` and `tags`. To fill one of your account's lead columns, ask Revnu in chat, for example: "Put the CRM id from my list in the CRM ID column."

## Frequently asked questions

### Can I create leads through the API?

Through Revnu. Block your existing customers first, 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?

Ask Revnu in chat to fill a custom column. From code, the API sets `vertical`, `archetype` and `tags`.

### 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?

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