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

# Deals

> Board, listing, creation, editing and moving of deals

A **deal** is an opportunity inside a stage. It always points to a real Lead — contact data
comes from the Lead, it is not copied.

## The Deal object

<ResponseField name="id" type="string">
  Deal UUID
</ResponseField>

<ResponseField name="number" type="integer">
  Human-readable sequential deal number (e.g. `1042`)
</ResponseField>

<ResponseField name="pipeline_id" type="string">
  Pipeline UUID
</ResponseField>

<ResponseField name="stage_id" type="string">
  UUID of the current stage
</ResponseField>

<ResponseField name="lead_id" type="string">
  UUID of the linked Lead
</ResponseField>

<ResponseField name="owner_id" type="string">
  UUID of the responsible user, or `null`
</ResponseField>

<ResponseField name="value" type="number">
  Manual deal value. Only used when the deal **has no products**.
</ResponseField>

<ResponseField name="total" type="number">
  Calculated total: the sum of `price × qty` of the products, or `value` when there are no products
</ResponseField>

<ResponseField name="origin" type="string">
  Deal origin (`manual` by default)
</ResponseField>

<ResponseField name="loss_reason" type="string">
  Loss reason. Present only on lost deals.
</ResponseField>

<ResponseField name="note" type="string">
  Free-form note
</ResponseField>

<ResponseField name="won_at" type="string">
  Date it entered the won stage (ISO 8601)
</ResponseField>

<ResponseField name="lost_at" type="string">
  Date it entered the lost stage (ISO 8601)
</ResponseField>

<ResponseField name="lead_name" type="string">
  Name of the linked Lead (resolved at read time)
</ResponseField>

<ResponseField name="lead_surname" type="string">
  Lead's surname
</ResponseField>

<ResponseField name="lead_company" type="string">
  Lead's company
</ResponseField>

<ResponseField name="lead_email" type="string">
  Lead's email
</ResponseField>

<ResponseField name="lead_phone" type="string">
  Lead's phone
</ResponseField>

<ResponseField name="lead_avatar" type="string">
  URL of the Lead's avatar
</ResponseField>

<ResponseField name="lead_status_id" type="string">
  UUID of the Lead's status
</ResponseField>

<ResponseField name="products" type="DealProduct[]">
  The deal's product lines
</ResponseField>

<ResponseField name="tags" type="Tag[]">
  Deal tags — they reuse the same tag catalog as Leads
</ResponseField>

<ResponseField name="pending_todos" type="integer">
  Number of pending activities
</ResponseField>

<ResponseField name="overdue_todos" type="integer">
  Number of activities that are pending and overdue
</ResponseField>

***

## Board

<Card>
  <strong>GET</strong> `/backend/pipelines/{id}/board`
</Card>

Returns the **board metadata**: the pipeline with its stages and, per column, the deal count
and the money total. It does not return the deals themselves — use
[List deals](#list-deals) to load each column.

### Query Parameters

<ParamField query="search" type="string">
  Search by **deal number** or by the linked Lead's **name/company**
</ParamField>

<ParamField query="stage_id" type="string">
  Restrict to a specific stage
</ParamField>

<ParamField query="sort" type="string" default="recent">
  `recent` (newest first) or `oldest`
</ParamField>

<ParamField query="mode" type="string" default="created">
  Which date the period filter considers: `created`, `won` or `lost`
</ParamField>

<ParamField query="period" type="string">
  `7d`, `30d`, `1y` or `custom`. With `custom`, also provide `from` and `to`.
</ParamField>

<ParamField query="from" type="string">
  Start of the period (ISO 8601). Used only with `period=custom`.
</ParamField>

<ParamField query="to" type="string">
  End of the period (ISO 8601). Used only with `period=custom`.
</ParamField>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "pipeline": {
      "id": "bb0e8400-e29b-41d4-a716-446655440000",
      "group_id": "aa0e8400-e29b-41d4-a716-446655440000",
      "name": "Inbound Sales",
      "description": "Leads coming from the website",
      "position": 0,
      "stages": [
        { "id": "cc0e8400-e29b-41d4-a716-446655440000", "name": "Entrada", "color": "217 91% 60%", "position": 0 }
      ],
      "loss_reasons": ["Sem orçamento"],
      "created_at": "2026-07-16T09:00:00Z"
    },
    "stages": [
      {
        "stage": { "id": "cc0e8400-e29b-41d4-a716-446655440000", "name": "Entrada", "color": "217 91% 60%", "position": 0 },
        "deal_count": 12,
        "total": 48500.00
      }
    ],
    "total_deals": 12,
    "total_amount": 48500.00
  }
  ```
</ResponseExample>

***

## List deals

<Card>
  <strong>GET</strong> `/backend/pipelines/{id}/deals`
</Card>

Returns a page of deals. The board paginates **per column** — pass `stage_id` to load one
stage at a time instead of the whole pipeline.

### Query Parameters

Accepts every filter of the [board](#board), plus:

<ParamField query="page" type="integer" default="1">
  Desired page
</ParamField>

<ParamField query="limit" type="integer" default="20">
  Items per page. Values outside 1–100 fall back to the default of 20.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X GET "https://api.leavo.ai/backend/pipelines/bb0e8400-e29b-41d4-a716-446655440000/deals?stage_id=cc0e8400-e29b-41d4-a716-446655440000&limit=20" \
    -H "Authorization: Bearer your_api_key_here"
  ```

  ```javascript JavaScript theme={null}
  const params = new URLSearchParams({
    stage_id: 'cc0e8400-e29b-41d4-a716-446655440000',
    limit: '20'
  });

  const response = await fetch(
    `https://api.leavo.ai/backend/pipelines/bb0e8400-e29b-41d4-a716-446655440000/deals?${params}`,
    { headers: { 'Authorization': 'Bearer your_api_key_here' } }
  );
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "data": [
      {
        "id": "dd0e8400-e29b-41d4-a716-446655440000",
        "number": 1042,
        "pipeline_id": "bb0e8400-e29b-41d4-a716-446655440000",
        "stage_id": "cc0e8400-e29b-41d4-a716-446655440000",
        "lead_id": "550e8400-e29b-41d4-a716-446655440000",
        "owner_id": null,
        "value": 0,
        "total": 4500.00,
        "origin": "manual",
        "note": "",
        "created_at": "2026-07-16T09:30:00Z",
        "updated_at": "2026-07-16T09:30:00Z",
        "lead_name": "John Smith",
        "lead_company": "XYZ Company",
        "lead_email": "john@example.com",
        "lead_phone": "+15551234567",
        "products": [
          {
            "id": "ee0e8400-e29b-41d4-a716-446655440000",
            "product_id": "ff0e8400-e29b-41d4-a716-446655440000",
            "name": "Premium Plan",
            "price": 1500.00,
            "qty": 3,
            "custom": false,
            "total": 4500.00
          }
        ],
        "tags": [],
        "pending_todos": 1,
        "overdue_todos": 0
      }
    ],
    "pagination": {
      "current_page": 1,
      "total_pages": 1,
      "total_count": 12,
      "has_next": false,
      "has_prev": false
    }
  }
  ```
</ResponseExample>

***

## Get a deal

<Card>
  <strong>GET</strong> `/backend/pipeline/deals/{id}`
</Card>

Returns the full deal, with products, tags and Lead data.

***

## Create a deal

<Card>
  <strong>POST</strong> `/backend/pipeline/deals`
</Card>

Requires the `pipeline.deals.create` permission.

### Request Body

<ParamField body="pipeline_id" type="string" required>
  Pipeline UUID
</ParamField>

<ParamField body="stage_id" type="string" required>
  UUID of the initial stage. It must belong to the given pipeline, otherwise the response is **400**.
</ParamField>

<ParamField body="lead_id" type="string">
  UUID of an existing Lead
</ParamField>

<ParamField body="new_lead" type="object">
  Data to create a new Lead along with the deal
</ParamField>

<Expandable title="new_lead properties">
  <ParamField body="name" type="string" required>
    Lead name
  </ParamField>

  <ParamField body="phone" type="string" required>
    Lead phone
  </ParamField>

  <ParamField body="email" type="string">
    Lead email
  </ParamField>

  <ParamField body="company" type="string">
    Lead company
  </ParamField>
</Expandable>

<Warning>
  Provide **`lead_id` or `new_lead`**. Without either one, the response is **400** — a deal
  never exists without a Lead.
</Warning>

<ParamField body="owner_id" type="string">
  UUID of the responsible user
</ParamField>

<ParamField body="loss_reason" type="string">
  Loss reason. **Required** when `stage_id` points to the lost stage.
</ParamField>

<ParamField body="lead_status_id" type="string">
  Status to apply to the linked Lead
</ParamField>

<ParamField body="products" type="DealProduct[]">
  The deal's product lines
</ParamField>

<Expandable title="Product line properties">
  <ParamField body="product_id" type="string">
    UUID of a catalog product. Name and price are copied at the moment it is added.
  </ParamField>

  <ParamField body="name" type="string">
    Name of the one-off line. Required when `custom` is `true`.
  </ParamField>

  <ParamField body="price" type="number">
    Unit price. Cannot be negative.
  </ParamField>

  <ParamField body="qty" type="integer">
    Quantity. Values lower than 1 are treated as 1.
  </ParamField>

  <ParamField body="custom" type="boolean">
    `true` for a one-off line, which exists only in this deal and does not enter the global catalog
  </ParamField>
</Expandable>

<ParamField body="tag_ids" type="string[]">
  UUIDs of tags to associate
</ParamField>

<ParamField body="origin" type="string">
  Deal origin. Default: `manual`.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.leavo.ai/backend/pipeline/deals" \
    -H "Authorization: Bearer your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "pipeline_id": "bb0e8400-e29b-41d4-a716-446655440000",
      "stage_id": "cc0e8400-e29b-41d4-a716-446655440000",
      "lead_id": "550e8400-e29b-41d4-a716-446655440000",
      "products": [
        { "product_id": "ff0e8400-e29b-41d4-a716-446655440000", "qty": 3 },
        { "custom": true, "name": "Onboarding", "price": 800.00, "qty": 1 }
      ],
      "tag_ids": ["880e8400-e29b-41d4-a716-446655440000"]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.leavo.ai/backend/pipeline/deals', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer your_api_key_here',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      pipeline_id: 'bb0e8400-e29b-41d4-a716-446655440000',
      stage_id: 'cc0e8400-e29b-41d4-a716-446655440000',
      new_lead: {
        name: 'Mary Jones',
        phone: '+15557778888',
        company: 'ACME'
      }
    })
  });
  ```
</RequestExample>

Returns **201 Created** with the deal.

***

## Update a deal

<Card>
  <strong>PUT</strong> `/backend/pipeline/deals/{id}`
</Card>

Requires `pipeline.deals.edit`. Omitted fields stay unchanged.

### Request Body

<ParamField body="owner_id" type="string">
  New owner
</ParamField>

<ParamField body="value" type="number">
  Manual value. Cannot be negative. Only affects `total` when the deal has no products.
</ParamField>

<ParamField body="note" type="string">
  Note
</ParamField>

<ParamField body="origin" type="string">
  Origin
</ParamField>

<ParamField body="products" type="DealProduct[]">
  When sent, it **replaces the entire list** of products. Send `[]` to remove them all.
</ParamField>

<Note>
  The stage is **not** changed by this endpoint. Use [Move between stages](#move-between-stages).
</Note>

***

## Move between stages

<Card>
  <strong>PUT</strong> `/backend/pipeline/deals/{id}/stage`
</Card>

Moves the deal between board columns. Requires `pipeline.deals.edit`.

### Request Body

<ParamField body="stage_id" type="string" required>
  UUID of the destination stage. It must belong to the same pipeline as the deal, otherwise the
  response is **400**.
</ParamField>

<ParamField body="loss_reason" type="string">
  Loss reason. **Required** when the destination stage has `kind: "lost"`.
</ParamField>

<Warning>
  Moving to the lost stage **without** `loss_reason` returns **400**. There is no path that
  loses a deal silently.
</Warning>

Side effects of moving. Every move **always clears `won_at` and `lost_at` first** before
applying the destination, so pulling a deal out of Won/Lost discards the marking with it:

| Destination    | Effect                                                          |
| -------------- | --------------------------------------------------------------- |
| `kind: "won"`  | Sets `won_at`; `lost_at` and `loss_reason` become null          |
| `kind: "lost"` | Sets `lost_at` and records `loss_reason`; `won_at` becomes null |
| Regular stage  | `won_at`, `lost_at` and `loss_reason` become null               |

The reason can be one of the `loss_reasons` configured on the pipeline or free text — the
"Other" option is not stored in the list of reasons.

<RequestExample>
  ```bash cURL theme={null}
  curl -X PUT "https://api.leavo.ai/backend/pipeline/deals/dd0e8400-e29b-41d4-a716-446655440000/stage" \
    -H "Authorization: Bearer your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "stage_id": "cc3e8400-e29b-41d4-a716-446655440000",
      "loss_reason": "Fechou com concorrente"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 400 Bad Request theme={null}
  {
    "code": "ERR_INVALID_INPUT",
    "message": "Informe o motivo da perda para marcar o negócio como perdido"
  }
  ```
</ResponseExample>

<Note>
  Error `message` strings are returned by the API in Portuguese. Match on `code`
  (`ERR_INVALID_INPUT`), never on the message text.
</Note>

***

## Deal tags

Deal tags reuse the same tag catalog as Leads — there is no separate catalog.

### Add a tag

<Card>
  <strong>POST</strong> `/backend/pipeline/deals/{id}/tags/{tagID}`
</Card>

### Remove a tag

<Card>
  <strong>DELETE</strong> `/backend/pipeline/deals/{id}/tags/{tagID}`
</Card>

Both require `pipeline.deals.edit` and return `{ "success": true }`.

***

## Delete a deal

<Card>
  <strong>DELETE</strong> `/backend/pipeline/deals/{id}`
</Card>

Requires `pipeline.deals.delete`.

<Warning>
  Deletes the deal and everything hanging off it: products, tags, activities, files and history.
  The **linked Lead is not deleted**.
</Warning>

<ResponseExample>
  ```json 200 OK theme={null}
  { "success": true }
  ```
</ResponseExample>
