How to Get Deal Pipelines with Stages and Semantics
Scope:
crmWho can execute the method: any user with access to CRM
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect the MCP server so that the assistant can utilize the official REST documentation.
Deal pipelines help separate different sales processes: new sales, contract renewals, partner management, or individual business lines. Each pipeline has its own set of stages. Each stage has semantics—the state of the deal: in progress, won, or lost.
Semantics are required for reports, automation, and deal filtering. For example, they allow you to distinguish active deals from won and lost deals, even if the stage names differ across various pipelines.
As a result, you will obtain a table for each deal pipeline. The rows of the table will contain the stages, their names, and their semantics.
To retrieve deal pipelines with stages and semantics, call two methods sequentially:
- crm.category.list — retrieve an array of
categoriescontaining deal pipelines and extractidandnamefrom it - crm.status.list — for each pipeline, pass the stage codes into
filter.ENTITY_IDto retrieve the stages
Data is passed between methods as follows:
- The
idof the pipeline fromcategoriesdetermines the stage code - For the main pipeline with
id = 0, use codeDEAL_STAGE - For the additional pipeline with
id > 0, use codeDEAL_STAGE_{id} - Pass the generated code into the
filter.ENTITY_IDof the crm.status.list method
1. Retrieve a List of Deal Pipelines
Call the crm.category.list method with the entityTypeId: 2 parameter, where 2 is the identifier of the deal object type. CRM object type identifiers can be retrieved using the crm.enum.ownertype method.
The list of pipelines is filtered by user permissions. If a user does not have permission to read a specific pipeline, the method will not return it in the response.
How to Use Examples in Documentation
const categoryResponse = await $b24.actions.v2.call.make({
method: 'crm.category.list',
params: {
entityTypeId: 2,
},
requestId: 'category-list',
})
if (!categoryResponse.isSuccess) {
throw new Error(categoryResponse.getErrorMessages().join('; '))
}
const arCategory = categoryResponse.getData().result.categories.reduce((acc, item) => {
acc[item.id] = item.name
return acc
}, {})
// crm.category.list does not have a typed wrapper — calling via core
$result = $sb->core->call('crm.category.list', ['entityTypeId' => 2])
->getResponseData()
->getResult();
$arCategory = array_column($result['categories'] ?? [], 'name', 'id');
categories = client.crm.category.list(entity_type_id=2).response.result.get("categories", [])
category_map = {item["id"]: item["name"] for item in categories}
The method returns an array of categories in the response containing the deal pipelines available to the user, including the main pipeline. Each pipeline has an id (the pipeline identifier), a name (the name), and an isDefault (the flag indicating the main pipeline).
{
"result": {
"categories": [
{
"id": 0,
"name": "General",
"sort": 100,
"entityTypeId": 2,
"isDefault": "Y"
},
{
"id": 7,
"name": "Contract renewal",
"sort": 200,
"entityTypeId": 2,
"isDefault": "N"
}
]
},
"total": 2
}
The total field shows the total number of pipelines found. The method returns a single page of results—up to 50 records. The examples above process the items received in the response.
2. Retrieve Stages and Semantics for Each Pipeline
The crm.status.list method retrieves stages using the ENTITY_ID filter. For deals, the stage code depends on the pipeline:
DEAL_STAGE— stages of the main pipelineDEAL_STAGE_{id}— stages of an additional pipeline, where{id}is the pipeline identifier
Retrieve the id field from the crm.category.list response, form ENTITY_ID, and call crm.status.list with sorting by SORT. For example, for a pipeline with ID 7, you need to pass DEAL_STAGE_7.
Use the following fields in the response:
STATUS_ID— stage identifierNAME— stage nameEXTRA.SEMANTICS— stage semanticsEXTRA.COLOR— stage color
The EXTRA.SEMANTICS value indicates the stage group:
process— deal in progresssuccess— deal wonfailure— deal lostapology— a separate group of lost stages
The examples below use data obtained in the previous step.
for (const [id, name] of Object.entries(arCategory)) {
const entityId = Number(id) > 0 ? `DEAL_STAGE_${id}` : 'DEAL_STAGE'
const stageResponse = await $b24.actions.v2.call.make({
method: 'crm.status.list',
params: {
order: { SORT: 'ASC' },
filter: { ENTITY_ID: entityId },
},
requestId: `status-list-${id}`,
})
if (!stageResponse.isSuccess) {
console.error(stageResponse.getErrorMessages().join('; '))
continue
}
for (const item of stageResponse.getData().result) {
console.log(name, item.STATUS_ID, item.NAME, item.EXTRA?.SEMANTICS)
}
}
foreach ($arCategory as $id => $name)
{
$entityId = $id > 0 ? 'DEAL_STAGE_' . $id : 'DEAL_STAGE';
$stages = $sb->getCRMScope()->status()->list(
['SORT' => 'ASC'],
['ENTITY_ID' => $entityId]
)->getStatuses();
foreach ($stages as $item)
{
echo $name . ': ' . $item->STATUS_ID . ': ' . $item->NAME
. ' - ' . ($item->EXTRA['SEMANTICS'] ?? '') . PHP_EOL;
}
}
for category_id, category_name in category_map.items():
entity_id = f"DEAL_STAGE_{category_id}" if int(category_id) > 0 else "DEAL_STAGE"
result_deal = client.crm.status.list(
order={"SORT": "ASC"},
filter={"ENTITY_ID": entity_id},
).response.result
for item in result_deal:
print(
category_name,
item.get("STATUS_ID", ""),
item.get("NAME", ""),
(item.get("EXTRA") or {}).get("SEMANTICS", ""),
)
The method returns an array of stages for the specified ENTITY_ID in the response.
{
"result": [
{
"ENTITY_ID": "DEAL_STAGE_7",
"STATUS_ID": "NEW",
"NAME": "New",
"EXTRA": {
"SEMANTICS": "process",
"COLOR": "#39A8EF"
}
},
{
"ENTITY_ID": "DEAL_STAGE_7",
"STATUS_ID": "WON",
"NAME": "Deal successful",
"EXTRA": {
"SEMANTICS": "success",
"COLOR": "#7BD500"
}
},
{
"ENTITY_ID": "DEAL_STAGE_7",
"STATUS_ID": "LOSE",
"NAME": "Deal failed",
"EXTRA": {
"SEMANTICS": "failure",
"COLOR": "#FF5752"
}
}
],
"total": 3
}
The total field shows the total number of stages found for the specified ENTITY_ID. The method returns a single response page — up to 50 records. The examples above process the items received in the response.
Full Code Example
The example outputs a table for each deal pipeline. The table shows the stage identifier, stage name, and semantics.
// npm install @bitrix24/b24jssdk
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/')
const categoryResponse = await $b24.actions.v2.call.make({
method: 'crm.category.list',
params: {
entityTypeId: 2,
},
requestId: 'category-list',
})
if (!categoryResponse.isSuccess) {
throw new Error(categoryResponse.getErrorMessages().join('; '))
}
const arCategory = categoryResponse.getData().result.categories.reduce((acc, item) => {
acc[item.id] = item.name
return acc
}, {})
for (const [id, name] of Object.entries(arCategory)) {
const entityId = Number(id) > 0 ? `DEAL_STAGE_${id}` : 'DEAL_STAGE'
const stageResponse = await $b24.actions.v2.call.make({
method: 'crm.status.list',
params: {
order: { SORT: 'ASC' },
filter: { ENTITY_ID: entityId },
},
requestId: `status-list-${id}`,
})
if (!stageResponse.isSuccess) {
console.error(stageResponse.getErrorMessages().join('; '))
continue
}
const table = document.createElement('table')
const caption = document.createElement('caption')
caption.textContent = name
table.appendChild(caption)
const thead = document.createElement('thead')
const trHead = document.createElement('tr')
for (const text of ['STATUS ID', 'NAME', 'SEMANTICS']) {
const th = document.createElement('th')
th.textContent = text
trHead.appendChild(th)
}
thead.appendChild(trHead)
table.appendChild(thead)
const tbody = document.createElement('tbody')
for (const item of stageResponse.getData().result) {
const tr = document.createElement('tr')
if (item.EXTRA?.COLOR) {
tr.style.color = item.EXTRA.COLOR
}
for (const value of [item.STATUS_ID, item.NAME, item.EXTRA?.SEMANTICS]) {
const td = document.createElement('td')
td.textContent = value ?? ''
tr.appendChild(td)
}
tbody.appendChild(tr)
}
table.appendChild(tbody)
document.body.appendChild(table)
}
$b24.destroy()
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
// crm.category.list does not have a typed wrapper — calling via core
$result = $sb->core->call('crm.category.list', ['entityTypeId' => 2])
->getResponseData()
->getResult();
$arCategory = array_column($result['categories'] ?? [], 'name', 'id');
foreach ($arCategory as $id => $name):
$entityId = $id > 0 ? 'DEAL_STAGE_' . $id : 'DEAL_STAGE';
$stages = $sb->getCRMScope()->status()->list(
['SORT' => 'ASC'],
['ENTITY_ID' => $entityId]
)->getStatuses();
if (!empty($stages)):
?>
<table>
<caption><?=htmlspecialchars((string)$name, ENT_QUOTES, 'UTF-8')?></caption>
<thead>
<tr>
<th>STATUS ID</th>
<th>NAME</th>
<th>SEMANTICS</th>
</tr>
</thead>
<tbody>
<?php foreach ($stages as $item): ?>
<?php
$statusId = htmlspecialchars((string)($item->STATUS_ID ?? ''), ENT_QUOTES, 'UTF-8');
$stageName = htmlspecialchars((string)($item->NAME ?? ''), ENT_QUOTES, 'UTF-8');
$semantics = htmlspecialchars((string)($item->EXTRA['SEMANTICS'] ?? ''), ENT_QUOTES, 'UTF-8');
$color = (string)($item->EXTRA['COLOR'] ?? '');
$colorStyle = preg_match('/^#[0-9A-Fa-f]{6}$/', $color) ? ' style="color:' . $color . '"' : '';
?>
<tr<?=$colorStyle?>>
<td><?=$statusId?></td>
<td><?=$stageName?></td>
<td><?=$semantics?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php endforeach; ?>
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
try:
categories = client.crm.category.list(entity_type_id=2).response.result.get("categories", [])
category_map = {item["id"]: item["name"] for item in categories}
for category_id, category_name in category_map.items():
entity_id = f"DEAL_STAGE_{category_id}" if int(category_id) > 0 else "DEAL_STAGE"
result_deal = client.crm.status.list(
order={"SORT": "ASC"},
filter={"ENTITY_ID": entity_id},
).response.result
print(category_name)
print("STATUS ID\tNAME\tSEMANTICS")
for item in result_deal:
print(
"\t".join(
[
str(item.get("STATUS_ID", "")),
str(item.get("NAME", "")),
str((item.get("EXTRA") or {}).get("SEMANTICS", "")),
]
)
)
except BitrixAPIError as error:
print(error)
If the Result Is Empty or an Error Occurs
If crm.category.list or crm.status.list returns an error, check the authorization and user permissions. To run the scenario, access to the CRM and the crm scope is required.
If crm.category.list returns an empty array categories, the user cannot see the available deal pipelines. Check the user's CRM read permissions and repeat the scenario from the first step.
If crm.status.list returns an empty array of stages, check the ENTITY_ID value:
- for the main pipeline with
id = 0, passDEAL_STAGE - for an additional pipeline with
id > 0, passDEAL_STAGE_{id} - do not pass
DEAL_STAGE_0
After fixing ENTITY_ID, repeat the second step for this pipeline.
If the response contains the total field but not all items are processed, take the single response page limit into account. The examples in the tutorial only process the items received in the current response.
Verify the Result
After running the example, tables containing the retrieved deal pipelines will appear on the page. The table heading is the pipeline name. The rows of the table display the stages of that pipeline:
STATUS ID— the stage code, which can be used in deal fields and filtersNAME— the stage name in the CRM interfaceSEMANTICS— the stage group:process,success,failure, orapology
If Bitrix24 contains only the main pipeline, the example will output one table. If additional pipelines have been created, there will be a separate table for each one.
The main pipeline has id = 0. Its stage code is DEAL_STAGE, without suffix _0.