# List a stage's leads

`GET /v1/campaigns/{campaign_id}/pipeline/stages/{stage}/leads`

Returns the campaign's leads in one stage, the most recent arrival first.

**Scope:** `leads:read`

`{stage}` is a stage `slug` from the board. Without `pipeline_id` it is a stage of the campaign's default pipeline. When the campaign has more than one pipeline, send `pipeline_id` for a stage of any pipeline but the default: slugs repeat across pipelines, so `new` without `pipeline_id` is always the default pipeline's `new`.

Rows are ordered by `stage_entered_at`, newest first, and rows already returned are never repeated. A lead that enters the stage while you page is at the top of the list, not on a later page: read the first page again for new arrivals.

The campaign must be one the key may reach, otherwise `404`. Only leads the key's member can see are listed, and only on cards they may open: a card outside the assigned records their role limits them to is left out even when they see its lead through another card. Contact fields follow the member's field visibility as in the conversion report; `form_name` is attribution, so it is `null` when the role hides custom fields and `Restricted` when it masks them. `assigned_member`, `stage_entered_at` and `deal_value` are always shown.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `campaign_id` | `UUID` | Yes | A campaign the key may reach. |
| `stage` | `string` | Yes | The stage slug, as the board lists it. |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `pipeline_id` | `UUID` | No | The stage's pipeline, a `pipelines[].id` from the board. It must be one of the campaign's pipelines the member may open. Default: the campaign's default pipeline. |
| `limit` | `integer` | No | Rows per page. 1 to 100. Default: `50`. |
| `cursor` | `string` | No | The `next_cursor` of the previous page, unchanged. Opaque: never build or edit one. Up to 4,096 characters. |

## Response

**Status:** `200 OK`

| Name | Type | Description |
| --- | --- | --- |
| `data` | `array of objects` | Leads in the stage, most recent arrival first. |
| `data.lead_id` | `UUID` | The lead. Read it with `GET /v1/leads/{lead_id}` and its outcome with `GET /v1/leads/{lead_id}/conversion`. |
| `data.first_name` | `string \| null` | First name. |
| `data.last_name` | `string \| null` | Last name. |
| `data.phone` | `string \| null` | Phone number in E.164; a stored number that is not a valid phone number is returned as stored. |
| `data.email` | `string \| null` | Email address. |
| `data.assigned_member` | `object \| null` | The team member the lead's card on this pipeline is assigned to, or `null`. A follow-through card has its own assignee. |
| `data.assigned_member.id` | `UUID` | The member. |
| `data.assigned_member.name` | `string \| null` | Their DialEcho profile name. |
| `data.stage_entered_at` | `timestamp \| null` | When the card entered its current stage. |
| `data.deal_value` | `number \| null` | The deal value. A follow-through card shows the value of the deal it came from, as in DialEcho. |
| `data.created_at` | `timestamp \| null` | When the lead was created in DialEcho. |
| `data.form_name` | `string \| null` | The `attribution.form_name` the lead was sent with. |
| `next_cursor` | `string \| null` | Pass it back as `cursor` for the next page; `null` when there is none. |
| `has_more` | `boolean` | `true` while more pages may follow. Keep paging until it is `false`. |

## Example request

**curl**

```bash
curl "https://development.agencydevworks.ai/v1/campaigns/cccccccc-0000-4000-8000-000000000001/pipeline/stages/appointment/leads?limit=2" \
  -H "Authorization: Bearer dei_your_key"
```

**JavaScript**

```js
const response = await fetch("https://development.agencydevworks.ai/v1/campaigns/cccccccc-0000-4000-8000-000000000001/pipeline/stages/appointment/leads?limit=2", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();
```

## Example response

**Status:** `200 OK`

```json
{
  "data": [
    {
      "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
      "first_name": "Pat",
      "last_name": "Lee",
      "phone": "+14155550132",
      "email": "pat@example.com",
      "assigned_member": { "id": "ffffffff-0000-4000-8000-000000000001", "name": "Jordan Rivera" },
      "stage_entered_at": "2026-11-02T16:04:12Z",
      "deal_value": null,
      "created_at": "2026-11-02T15:55:55Z",
      "form_name": "quote-step-1"
    },
    {
      "lead_id": "aaaaaaaa-0000-4000-8000-000000000002",
      "first_name": "Sam",
      "last_name": null,
      "phone": "+18035550142",
      "email": null,
      "assigned_member": null,
      "stage_entered_at": "2026-11-01T15:30:00Z",
      "deal_value": null,
      "created_at": "2026-11-01T15:21:07Z",
      "form_name": null
    }
  ],
  "next_cursor": "v2.exampleOpaqueCursor",
  "has_more": true
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `leads:read`. Checked before the query is validated. |
| 404 | `not_found` | The campaign does not exist or the key cannot reach it; or `pipeline_id` is not one of the campaign's pipelines the member may open (without `pipeline_id`: the campaign has no such default pipeline). |
| 409 | `resync_required` | The cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again from the first page. |
| 422 | `invalid_cursor` | The cursor was not issued to this key for this request, was changed, or was sent with different parameters than the page it came from. |
| 422 | `unknown_stage` | `{stage}` is not a stage of that pipeline. `error.valid_stages` lists its slugs in board order. |
| 422 | `invalid_request` | `campaign_id` or `pipeline_id` is not a UUID, `limit` is outside 1 to 100, or `cursor` is over 4,096 characters. `error.fields` names it (`campaign_id`, `query.pipeline_id`, `query.limit`). |
| 503 | `dependency_unavailable` | The report or the access check is temporarily unavailable. Retry the same request with backoff. |

Every endpoint can also answer `401 invalid_api_key`, `429 rate_limited` and `503 dependency_unavailable`. See [Errors](/api/#errors) in the overview.

## Related endpoints

- `GET /v1/campaigns/{campaign_id}/pipeline` · [Retrieve the pipeline](/api/reporting/pipeline-board)
- `GET /v1/leads/{lead_id}` · [Retrieve a lead](/api/leads/get)
- `GET /v1/leads/{lead_id}/conversion` · [Retrieve a conversion](/api/reporting/get-conversion)
