
## Convert lead

`POST /v1/leads/:id/convert`

Creates a deal, a contact and a company out of a lead, links the created records to each other and closes the lead as converted. The request body controls which records are created.

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|----------|
| `id` (path) | number | yes | Lead ID. List: `GET /v1/leads` |

## Request fields (body)

The request body is optional. With no body, or with an empty object `{}`, the defaults apply.

| Field | Type | Default | Description |
|------|-----|-----------|----------|
| `createDeal` | boolean | `true` | Create a deal |
| `createContact` | boolean | `true` | Create a contact |
| `createCompany` | boolean | `false` | Create a company from the company name stored on the lead |

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/leads/1001207/convert \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "createDeal": true,
    "createContact": true,
    "createCompany": false
  }'
```

### curl — OAuth app

```bash
curl -X POST https://vibecode.bitrix24.com/v1/leads/1001207/convert \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "createDeal": true,
    "createContact": true,
    "createCompany": false
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch(`https://vibecode.bitrix24.com/v1/leads/${leadId}/convert`, {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    createDeal: true,
    createContact: true,
    createCompany: false,
  }),
})

const body = await res.json()

if (!res.ok) {
  // 422 CONVERSION_FAILED: some records are already created and will not be
  // rolled back — error.details says which ones
  throw new Error(`${body.error.code}: ${JSON.stringify(body.error.details ?? {})}`)
}

// The created records are read through the same method as an ordinary read, so
// their keys come in the platform's usual camelCase
console.log('Deal:', body.data.deal?.id, 'Contact:', body.data.contact?.id)
```

### JavaScript — OAuth app

```javascript
const res = await fetch(`https://vibecode.bitrix24.com/v1/leads/${leadId}/convert`, {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    createDeal: true,
    createContact: true,
    createCompany: false,
  }),
})

const body = await res.json()

if (!res.ok) {
  throw new Error(`${body.error.code}: ${JSON.stringify(body.error.details ?? {})}`)
}

const { data } = body
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` when every requested record was created |
| `data.lead` | object | The lead after conversion |
| `data.deal` | object \| null | The created deal. `null` when no deal was requested |
| `data.contact` | object \| null | The created contact. `null` when no contact was requested |
| `data.company` | object \| null | The created company. `null` when no company was requested |

Keys inside `data.lead`, `data.deal`, `data.contact` and `data.company` come in the same shape as an ordinary read of the record — `id`, `title`, `stageId`, `createdTime`. This route used to answer in raw Bitrix24 shape (upper case with underscores); the field names now match every other page of this section.

Each created record carries `leadId`, a reference to the lead it came from, and the lead itself receives references to the contact and company that were created.

## Response example

Representative fields are shown. Each record comes with its full set of fields, user fields included.

```json
{
  "success": true,
  "data": {
    "lead": {
      "id": 1001207,
      "title": "Website request",
      "name": "Maria",
      "sourceId": "CALL",
      "statusId": "CONVERTED",
      "statusSemanticId": "S",
      "contactId": 2733,
      "currencyId": "USD",
      "amount": "0.00",
      "assignedById": "1",
      "createdTime": "2026-08-25T13:47:11+00:00",
      "updatedTime": "2026-08-25T13:47:25+00:00"
    },
    "deal": {
      "id": 8345,
      "title": "Website request",
      "stageId": "NEW",
      "categoryId": "0",
      "currencyId": "USD",
      "amount": "0.00",
      "contactId": "2733",
      "companyId": null,
      "leadId": 1001207,
      "sourceId": "CALL",
      "assignedById": "1",
      "createdTime": "2026-08-25T13:47:24+00:00"
    },
    "contact": {
      "id": 2733,
      "name": "Maria",
      "lastName": null,
      "typeId": "CLIENT",
      "sourceId": "CALL",
      "leadId": 1001207,
      "companyId": null,
      "assignedById": "1",
      "createdTime": "2026-08-25T13:47:23+00:00"
    },
    "company": null
  }
}
```

## Error response example

404 — no lead with the given ID exists:

```json
{
  "success": false,
  "error": {
    "code": "ENTITY_NOT_FOUND",
    "message": "Not found"
  }
}
```

422 — the contact was created, the deal was not. The response shape differs here: an `error.details` field is added, carrying the outcome of every requested operation.

```json
{
  "success": false,
  "error": {
    "code": "CONVERSION_FAILED",
    "message": "Lead conversion partially failed",
    "details": {
      "contact": { "id": 2733, "success": true },
      "deal": { "success": false, "error": "Required field is missing" }
    }
  }
}
```

Contact `2733` stays in CRM: there is no rollback. Before repeating the call, check what already landed in CRM — a repeat creates a second set of records.

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_PARAMS` | The lead id in the path is not an integer. The check runs before any call reaches Bitrix24: a non-numeric value used to be cast on the Bitrix24 side, and the conversion then ran against a DIFFERENT record |
| 422 | `CONVERSION_FAILED` | Not every requested record was created. The outcome of each individual operation is in `error.details`: one key per requested record. Records already created are not rolled back, and the lead keeps its status |
| 404 | `ENTITY_NOT_FOUND` | No lead with the given `id` exists |
| 422 | `BITRIX_ERROR` | Bitrix24 rejected reading the lead or closing it. A refusal while creating the deal, contact or company does not land here — it arrives as `CONVERSION_FAILED`. The text in `message` comes from Bitrix24 verbatim, and this response carries no `error.details` field |
| 403 | `BITRIX_ACCESS_DENIED` | Bitrix24 denied access to the lead |
| 403 | `SCOPE_DENIED` | The API key lacks the `crm` scope |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is in read-only mode, and conversion creates records |
| 401 | `TOKEN_MISSING` | The API key has no valid Bitrix24 tokens |
| 429 | `RATE_LIMITED` | The request limit to the portal was exceeded |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **A repeat call creates a second set of records.** The endpoint does not check whether the lead was converted earlier, so a retry after a dropped connection doubles the data in CRM. The marker of an already converted lead is the `CONVERTED` status. Before retrying, read the lead through [`GET /v1/leads/:id`](/docs/entities/leads/get) — the status arrives there in the `stageId` field.
- **There is no rollback on a partial failure.** The deal, the contact and the company are created as separate records one after another. If one of them fails, the ones created earlier stay in CRM and the lead status does not change. Before calling again, check what already appeared in CRM, otherwise the retry adds duplicates on top of what already exists.
- **Reading the record back returns camelCase.** Take the `ID` of the created record and request it through [`GET /v1/deals/:id`](/docs/entities/deals/get) or [`GET /v1/contacts/:id`](/docs/entities/contacts/get) — the fields come in camelCase there.
- **A limited set of fields is carried into the created records.** The contact gets the first name, last name, middle name, source and assignee. The company gets the company name from the lead and the assignee. The deal gets the title, amount, currency, source and assignee. The remaining lead fields, user fields included, are not copied, so fill the missing values in with a separate [`PATCH /v1/deals/:id`](/docs/entities/deals/update) call.
- **Product rows of the lead are copied into the created deal.** When copying the rows fails, the deal stays created and the conversion stays successful. Check the product set with a [`GET /v1/deals/:id/products`](/docs/entities/deals/products-get) call.
- **The created records carry a back reference to the source lead.** The deal, the contact and the company each carry `leadId`, and the lead carries `contactId` / `companyId` back — so a deal can be traced to the lead it came from by filtering deals on the lead id, no mapping of your own required.

## See also

- [Leads](/docs/entities/leads)
- [Get lead](/docs/entities/leads/get)
- [Update lead](/docs/entities/leads/update)
- [Deals](/docs/entities/deals)
- [Contacts](/docs/entities/contacts)
- [Companies](/docs/entities/companies)
