> ## Documentation Index
> Fetch the complete documentation index at: https://developers.novala.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Leads API — Create and Manage Inbound Prospects

> List, create, and retrieve leads with cursor-based pagination. Capture inbound leads from web forms, external tools, or any channel with real field names.

Leads represent inbound prospects before they are converted to pipeline deals or contacts. The leads API uses cursor-based pagination for efficient traversal of large lead lists, and supports custom fields defined in your tenant configuration.

## Required scopes

| Operation             | Required scope     |
| --------------------- | ------------------ |
| List / retrieve leads | `leads.lead.read`  |
| Create a lead         | `leads.lead.write` |

## Lead statuses

A lead moves through the following statuses:

| Status         | Meaning                         |
| -------------- | ------------------------------- |
| `new`          | Just created, not yet actioned. |
| `contacted`    | Initial outreach sent.          |
| `qualifying`   | Qualification in progress.      |
| `qualified`    | Lead meets your criteria.       |
| `converted`    | Converted to a deal or contact. |
| `disqualified` | Does not meet your criteria.    |
| `lost`         | Previously engaged, now lost.   |

## Lead sources

When creating a lead, `source` tells Novala where it originated:

`web_form` · `referral` · `agent_chat` · `inbound_call` · `mcp` · `import`

***

## List leads

`GET /api/v1/leads`

Returns a cursor-paginated list of leads for the authenticated tenant.

### Query parameters

<ParamField query="limit" type="integer" default="25">
  Maximum number of results to return.
</ParamField>

<ParamField query="cursor" type="string">
  Opaque cursor from the previous response's `nextCursor` field. Omit for the first page.
</ParamField>

<ParamField query="status" type="string">
  Filter by lead status. One of: `new`, `contacted`, `qualifying`, `qualified`, `converted`, `disqualified`, `lost`.
</ParamField>

<ParamField query="assignedTo" type="string">
  Filter by the UUID of the assigned user.
</ParamField>

### Response

```json theme={null}
{
  "data": [
    {
      "id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
      "firstName": "Alex",
      "lastName": "Rivera",
      "email": "alex@example.com",
      "phone": "+15550009876",
      "companyName": "Acme Corp",
      "status": "new",
      "source": "web_form",
      "intent": "Interested in enterprise plan",
      "notes": null,
      "assignedTo": null,
      "customFields": {},
      "createdAt": "2024-06-01T14:22:00Z"
    }
  ],
  "nextCursor": "eyJpZCI6Ii4uLi4iLCJjcmVhdGVkQXQiOiIuLi4ifQ==",
  "hasMore": true
}
```

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://app.novala.ai/api/v1/leads?status=new&limit=50" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    'https://app.novala.ai/api/v1/leads?status=new&limit=50',
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );
  const { data, nextCursor, hasMore } = await response.json();
  ```
</CodeGroup>

### Paginating through all leads

```typescript TypeScript theme={null}
async function* allLeads(apiKey: string, status?: string) {
  let cursor: string | undefined;
  do {
    const params = new URLSearchParams({ limit: '100' });
    if (status) params.set('status', status);
    if (cursor) params.set('cursor', cursor);

    const res = await fetch(`https://app.novala.ai/api/v1/leads?${params}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const { data, nextCursor, hasMore } = await res.json();
    yield* data;
    cursor = hasMore ? nextCursor : undefined;
  } while (cursor);
}
```

***

## Create a lead

`POST /api/v1/leads`

Creates a new lead. Use this to capture leads from external web forms, CRM integrations, or any channel that produces inbound interest.

### Request body

<ParamField body="firstName" type="string" required>
  Lead's first name. Maximum 255 characters.
</ParamField>

<ParamField body="lastName" type="string" required>
  Lead's last name. Maximum 255 characters.
</ParamField>

<ParamField body="source" type="string" default="web_form">
  Origin of the lead. One of: `web_form`, `referral`, `agent_chat`, `inbound_call`, `mcp`, `import`.
</ParamField>

<ParamField body="channelType" type="string">
  Free-text channel descriptor (for example, `"instagram"` or `"trade_show"`).
</ParamField>

<ParamField body="email" type="string">
  Lead's email address.
</ParamField>

<ParamField body="phone" type="string">
  Lead's phone number. Maximum 50 characters.
</ParamField>

<ParamField body="companyName" type="string">
  Name of the lead's company. Maximum 255 characters.
</ParamField>

<ParamField body="intent" type="string">
  Short description of what the lead is interested in.
</ParamField>

<ParamField body="notes" type="string">
  Internal notes about the lead.
</ParamField>

<ParamField body="assignedTo" type="string">
  UUID of the user to assign this lead to.
</ParamField>

<ParamField body="sourceAttribution" type="object">
  Free-form attribution metadata, for example UTM parameters or ad campaign IDs.
</ParamField>

<ParamField body="customFields" type="object">
  Custom field key-value map. Any required custom fields your tenant has configured must be included.
</ParamField>

### Response

Returns `201 Created` with `{ "data": { lead object } }`.

### Example: capturing a web form submission

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://app.novala.ai/api/v1/leads \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "firstName": "Alex",
      "lastName": "Rivera",
      "email": "alex@example.com",
      "phone": "+15550009876",
      "companyName": "Acme Corp",
      "source": "web_form",
      "intent": "Interested in enterprise plan",
      "sourceAttribution": {
        "utm_source": "google",
        "utm_campaign": "spring-launch"
      }
    }'
  ```

  ```typescript TypeScript theme={null}
  // Typical Next.js API route handling a form POST
  export async function POST(request: Request) {
    const body = await request.json();

    const response = await fetch('https://app.novala.ai/api/v1/leads', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.NOVALA_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        firstName: body.firstName,
        lastName: body.lastName,
        email: body.email,
        phone: body.phone,
        source: 'web_form',
        sourceAttribution: { page: body.page, referrer: body.referrer },
      }),
    });

    const { data } = await response.json();
    return Response.json({ leadId: data.id });
  }
  ```
</CodeGroup>

***

## Get a lead

`GET /api/v1/leads/{id}`

Retrieves a single lead by ID.

### Path parameters

<ParamField path="id" type="string" required>
  UUID of the lead.
</ParamField>

### Response fields

<ResponseField name="data" type="object">
  <Expandable title="Lead object">
    <ResponseField name="id" type="string">UUID of the lead.</ResponseField>
    <ResponseField name="firstName" type="string">First name.</ResponseField>
    <ResponseField name="lastName" type="string">Last name.</ResponseField>
    <ResponseField name="email" type="string | null">Email address.</ResponseField>
    <ResponseField name="phone" type="string | null">Phone number.</ResponseField>
    <ResponseField name="companyName" type="string | null">Company name as entered.</ResponseField>
    <ResponseField name="status" type="string">Current lead status.</ResponseField>
    <ResponseField name="source" type="string">Lead source channel.</ResponseField>
    <ResponseField name="channelType" type="string | null">Additional channel descriptor.</ResponseField>
    <ResponseField name="intent" type="string | null">Stated interest or intent.</ResponseField>
    <ResponseField name="notes" type="string | null">Internal notes.</ResponseField>
    <ResponseField name="assignedTo" type="string | null">UUID of the assigned user, or `null`.</ResponseField>
    <ResponseField name="sourceAttribution" type="object | null">Attribution metadata.</ResponseField>
    <ResponseField name="customFields" type="object">Custom field key-value pairs.</ResponseField>
    <ResponseField name="createdAt" type="string">ISO 8601 creation timestamp.</ResponseField>
  </Expandable>
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET https://app.novala.ai/api/v1/leads/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    'https://app.novala.ai/api/v1/leads/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );
  const { data } = await response.json();
  ```
</CodeGroup>
