# Leads

A lead is one contact taking part in one campaign. If the same person is in two campaigns, that is two leads with different `id` values.

Base URL: https://api.melioralab.io/v1. Pass the key in the `Authorization: Bearer mel_live_...` header.

Endpoints:

- `GET /v1/leads`: List leads
- `GET /v1/leads/{id}`: Retrieve a lead

## The Lead object

Lead: the participation of one contact in one campaign. The same person in two campaigns produces two different leads.

| Field | Type | Description |
|---|---|---|
| `object` | string | Object type, always `lead`. |
| `id` | string (uuid) | Lead ID (participation in a campaign). |
| `workspace_id` | string (uuid) | Workspace of the campaign. |
| `campaign_id` | string (uuid) | Campaign the contact participates in. |
| `username` | string or null | Telegram username without `@`. Can be `null`. Not unique: use `fields.record_id` to link to your CRM. |
| `status` | string | Lead status in the campaign. Values: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. |
| `external_id` | null | Reserved for future versions. Always `null` in v1. |
| `fields` | object | Allowed fields from the contact record. |
| `fields.record_id` | string or null | Record ID in an external CRM, passed when the contacts were uploaded. `null` if `record_id_status` is not `valid`. May be absent. |
| `fields.first_name` | string or null | First name. May be absent. |
| `fields.last_name` | string or null | Last name. May be absent. |
| `fields.company` | string or null | Company. May be absent. |
| `fields.position` | string or null | Job title. May be absent. |
| `fields.website` | string or null | Website. May be absent. |
| `fields.source` | string or null | Contact source. May be absent. |
| `fields.utm_source` | string or null | The `utm_source` tag. May be absent. |
| `fields.utm_medium` | string or null | The `utm_medium` tag. May be absent. |
| `fields.utm_campaign` | string or null | The `utm_campaign` tag. May be absent. |
| `fields_truncated` | boolean | `true` if some field values were shortened or hidden due to size limits. |
| `record_id_status` | string | How reliable `fields.record_id` is: `valid`, `missing`, `conflict` or `unknown`. Values: `valid`, `missing`, `conflict`, `unknown`. |
| `first_contact_account` | object or null | Account that sent the first message. `null` if unknown. |
| `first_contact_account.id` | string (uuid) | Account ID. |
| `first_contact_account.username` | string or null | Account username without `@`. Can be `null`. |
| `first_contact_account.name` | string or null | Account display name. Can be `null`. |
| `first_message_sent_at` | string (date-time) or null | When the first message was sent. `null` if sending is not confirmed. |
| `first_reply_at` | string (date-time) or null | When the first reply arrived. `null` if there was no reply. |
| `delivery_state` | string | Delivery state of the next or most recent message in the sequence. Values: `waiting_queue`, `queued`, `held`, `sent`, `failed`, `unknown`. |
| `automation_state` | string | State of the automatic sequence for this lead. Leads without a scheduled message, such as those who replied, currently often have `unknown`. Values: `running`, `paused`, `held`, `stopped`, `completed`, `unknown`. |
| `reason` | object | Why the sequence is waiting, if known. |
| `reason.reason_code` | string | Reason code. Values: `none`, `schedule_window`, `daily_limit`, `queue_pending`, `telegram_wait`, `account_unavailable`, `account_restricted`, `campaign_paused`, `subscription_inactive`, `manual_hold`, `automation_stopped`, `service_unavailable`, `unknown`. |
| `reason.reason_observed_at` | string (date-time) or null | When the reason was recorded. `null` if unknown. |
| `reason.next_attempt_at` | string (date-time) or null | When the next attempt is expected. `null` if the time is unknown. |
| `reason.requires_customer_action` | boolean or null | Whether someone needs to act in Meliora for sending to resume. `null` if unknown. |
| `created_at` | string (date-time) | When the contact was added to the campaign. |
| `observed_at` | string (date-time) | Time the server read the data. |

Example:

```json
{
  "object": "lead",
  "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31",
  "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43",
  "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14",
  "username": "example_user",
  "status": "replied",
  "external_id": null,
  "fields": {
    "record_id": "crm-10482",
    "first_name": "Anna",
    "company": "Example Ltd",
    "position": "Head of sales",
    "utm_source": "partner"
  },
  "fields_truncated": false,
  "record_id_status": "valid",
  "first_contact_account": {
    "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83",
    "username": "example_sender",
    "name": "Mary from Example"
  },
  "first_message_sent_at": "2026-10-26T07:30:00.000Z",
  "first_reply_at": "2026-10-26T09:12:00.000Z",
  "delivery_state": "sent",
  "automation_state": "unknown",
  "reason": {
    "reason_code": "none",
    "reason_observed_at": null,
    "next_attempt_at": null,
    "requires_customer_action": null
  },
  "created_at": "2026-10-25T14:00:00.000Z",
  "observed_at": "2026-10-27T09:15:42.000Z"
}
```

## List leads

`GET /v1/leads`

Leads (contacts' participation in campaigns) in the key's scope, oldest first by `created_at`.

Permission: `leads:read`. Request weight: 2.

### Parameters

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer | no | Number of objects to return per page. From 1 to 100. Defaults to `50`. |
| `cursor` | query | string | no | The `next_cursor` value from the previous response. Other filters must match the first request. |
| `workspace_id` | query | string (uuid) | no | Only objects in this workspace. |
| `campaign_id` | query | string (uuid) | no | Only objects in this campaign. |
| `username` | query | string | no | Exact Telegram username. A leading `@` and letter case are ignored. |
| `status` | query | string | no | Only leads with this status. Values: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. |
| `delivery_state` | query | string | no | Only leads with this delivery state. The values `waiting_queue` and `failed` are not returned at the moment, so filtering by them returns an empty list. Values: `waiting_queue`, `queued`, `held`, `sent`, `failed`, `unknown`. |

### Response

A list: `object`, `data`, `has_more`, `next_cursor`. Pagination: https://docs.melioralab.io/en/md/pagination.md

### Endpoint-specific errors

- `cursor_mismatch` (400): The cursor is corrupted, was issued for a different route or filters, or the key's permissions and scope have changed.
- `scope_required` (403): The key does not have permission for this read. Texts also require "Message text" to be enabled.
- `not_found` (404): The object was not found or is outside the key's scope. An unknown path also returns 404.
- `cursor_expired` (410): The cursor expired: 15 minutes for lists, 30 days for the event feed.

Common errors: https://docs.melioralab.io/en/md/errors.md

### Example request

```bash
curl -G https://api.melioralab.io/v1/leads \
  -H "Authorization: Bearer $MELIORA_API_KEY" \
  -d campaign_id=4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14 \
  -d limit=50
```

### Example response

```json
{
  "object": "list",
  "data": [
    {
      "object": "lead",
      "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31",
      "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43",
      "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14",
      "username": "example_user",
      "status": "replied",
      "external_id": null,
      "fields": {
        "record_id": "crm-10482",
        "first_name": "Anna",
        "company": "Example Ltd",
        "position": "Head of sales",
        "utm_source": "partner"
      },
      "fields_truncated": false,
      "record_id_status": "valid",
      "first_contact_account": {
        "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83",
        "username": "example_sender",
        "name": "Mary from Example"
      },
      "first_message_sent_at": "2026-10-26T07:30:00.000Z",
      "first_reply_at": "2026-10-26T09:12:00.000Z",
      "delivery_state": "sent",
      "automation_state": "unknown",
      "reason": {
        "reason_code": "none",
        "reason_observed_at": null,
        "next_attempt_at": null,
        "requires_customer_action": null
      },
      "created_at": "2026-10-25T14:00:00.000Z",
      "observed_at": "2026-10-27T09:15:42.000Z"
    },
    {
      "object": "lead",
      "id": "8c1e5a37-9d2f-4b74-a6e8-2f9b3c7d5a40",
      "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43",
      "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14",
      "username": "sample_contact",
      "status": "contacted",
      "external_id": null,
      "fields": {
        "record_id": null,
        "first_name": "Igor"
      },
      "fields_truncated": false,
      "record_id_status": "missing",
      "first_contact_account": {
        "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83",
        "username": "example_sender",
        "name": "Mary from Example"
      },
      "first_message_sent_at": "2026-10-26T07:42:00.000Z",
      "first_reply_at": null,
      "delivery_state": "queued",
      "automation_state": "running",
      "reason": {
        "reason_code": "queue_pending",
        "reason_observed_at": "2026-10-27T08:58:10.000Z",
        "next_attempt_at": "2026-10-27T10:00:00.000Z",
        "requires_customer_action": false
      },
      "created_at": "2026-10-25T14:00:01.000Z",
      "observed_at": "2026-10-27T09:15:42.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU"
}
```

## Retrieve a lead

`GET /v1/leads/{id}`

A single lead by ID. A lead outside the key's scope returns 404.

Permission: `leads:read`. Request weight: 1.

### Parameters

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string (uuid) | yes | Lead ID. |

### Response

Returns a `Lead` object.

### Endpoint-specific errors

- `scope_required` (403): The key does not have permission for this read. Texts also require "Message text" to be enabled.
- `not_found` (404): The object was not found or is outside the key's scope. An unknown path also returns 404.

Common errors: https://docs.melioralab.io/en/md/errors.md

### Example request

```bash
curl https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31 \
  -H "Authorization: Bearer $MELIORA_API_KEY"
```

### Example response

```json
{
  "object": "lead",
  "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31",
  "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43",
  "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14",
  "username": "example_user",
  "status": "replied",
  "external_id": null,
  "fields": {
    "record_id": "crm-10482",
    "first_name": "Anna",
    "company": "Example Ltd",
    "position": "Head of sales",
    "utm_source": "partner"
  },
  "fields_truncated": false,
  "record_id_status": "valid",
  "first_contact_account": {
    "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83",
    "username": "example_sender",
    "name": "Mary from Example"
  },
  "first_message_sent_at": "2026-10-26T07:30:00.000Z",
  "first_reply_at": "2026-10-26T09:12:00.000Z",
  "delivery_state": "sent",
  "automation_state": "unknown",
  "reason": {
    "reason_code": "none",
    "reason_observed_at": null,
    "next_attempt_at": null,
    "requires_customer_action": null
  },
  "created_at": "2026-10-25T14:00:00.000Z",
  "observed_at": "2026-10-27T09:15:42.000Z"
}
```
