How to Embed a Widget into a CRM Item Tab
Scope:
placement,crmWho can execute the methods:
placement.bind— administratorcrm.item.get— any user with permission to read the deal
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
A tab in a CRM item card allows you to display an application interface alongside the item's primary data. In this scenario, we will add a tab to a deal card, retrieve the identifier of the open deal, and request its data.
To complete this scenario, we will sequentially execute the following methods:
- placement.bind — register a handler for the
CRM_DEAL_DETAIL_TABtab - crm.item.get — retrieve deal data using the identifier from
PLACEMENT_OPTIONS
The original example and additional materials are available in the Embedding into a CRM Item Card lesson.
How the Scenario Works
The application registers a handler URL using the placement.bind method and specifies the CRM_DEAL_DETAIL_TAB code. Once the application installation is complete, a new tab appears in the deal card.
When a user opens the tab, Bitrix24 loads the handler in an iframe and passes the invocation context to it. The current deal identifier is sent to PLACEMENT_OPTIONS.ID. The handler passes this identifier to crm.item.get and displays the retrieved data.
1. Prepare the Application
Create an application with an interface and add the following permissions:
placement— to register the widget handlercrm— to retrieve deal data
The registration code and the tab handler can be placed in separate files or combined into a single file using different execution branches.
Host the handler page at a public HTTPS address. The examples use the following address:
https://your-domain.example/deal-tab.php
The server must allow the page to be opened in an iframe. Check the X-Frame-Options header and the frame-ancestors directive of the Content-Security-Policy header: they must not prohibit embedding the page into Bitrix24.
The handler URL must be accessible from an external network. Do not use localhost, local network addresses, or self-signed SSL certificates.
The placement.bind method works only within the application context. An incoming webhook is not suitable for registering a tab.
2. Register the Tab
Register the handler using the placement.bind method. Pass the following parameters:
PLACEMENT— the placement code forCRM_DEAL_DETAIL_TABHANDLER— the public URL of the page that will open in the tabTITLE— the tab nameLANG_ALL— localized tab names
How to Use Examples in Documentation
// npm install @bitrix24/b24jssdk
// App settings page opened in a Bitrix24 iframe
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
const response = await $b24.actions.v2.call.make({
method: 'placement.bind',
params: {
PLACEMENT: 'CRM_DEAL_DETAIL_TAB',
HANDLER: 'https://your-domain.example/deal-tab.php',
TITLE: 'Deal data',
LANG_ALL: {
ru: {
TITLE: 'Deal data',
},
en: {
TITLE: 'Deal data',
},
},
},
requestId: 'placement-bind',
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
console.info('Registered tab')
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Core\Exceptions\BaseException;
// $b24 is built on the app token — see scenario
// "How to embed a widget into a lead as a custom field"
try
{
$b24->getPlacementScope()->placement()->bind(
'CRM_DEAL_DETAIL_TAB',
'https://your-domain.example/deal-tab.php',
[
'de' => ['TITLE' => 'Deal data'],
'en' => ['TITLE' => 'Deal data'],
]
);
echo 'Registered tab';
}
catch (BaseException $exception)
{
echo $exception->getMessage();
}
# pip install b24pysdk
# client is built on the app token — see scenario
# "How to embed a widget into a lead as a custom field"
from b24pysdk.errors import BitrixAPIError
try:
bitrix_response = client.placement.bind(
placement="CRM_DEAL_DETAIL_TAB",
handler="https://your-domain.example/deal-tab.php",
title="Deal data",
lang_all={
"de": {"TITLE": "Deal data"},
"en": {"TITLE": "Deal data"},
},
).response
print("Registered tab:", bitrix_response.result)
except BitrixAPIError as error:
print(error)
If the handler is successfully registered, the method will return true.
{
"result": true
}
After registration, complete the application installation. Until the installation is complete, the tab is unavailable to regular users.
3. Handle Tab Opening
When a tab is opened, Bitrix24 sends data to the handler via a POST request. For a deal card, the main parameters look like this:
PLACEMENT=CRM_DEAL_DETAIL_TAB
PLACEMENT_OPTIONS={"ID":"3473"}
PLACEMENT_OPTIONS is passed as a JSON string. In PHP and Python, convert it into an array or dictionary—for example, using the json_decode or json.loads function. In B24JsSDK, the property $b24.placement.options returns a ready-to-use object, while $b24.placement.placement returns the placement code.
|
Parameter |
Description |
|
PLACEMENT |
Embedding location code. For the deal tab, |
|
PLACEMENT_OPTIONS |
JSON string with the context of the open card |
|
ID |
Deal identifier within |
|
DOMAIN |
Bitrix24 address where the user opened the tab |
|
PROTOCOL |
Protocol for accessing Bitrix24: |
|
AUTH_ID |
OAuth token of the user who opened the tab. The PHP handler uses the token to call |
The full set of request service parameters is described on the Tabs in CRM Cards page.
4. Retrieve Deal Data
Call crm.item.get from the handler. For a deal, pass:
entityTypeId: 2— the identifier for the "Deal" CRM object typeid— the identifier fromPLACEMENT_OPTIONS.ID
The method is executed with the authorization of the user who opened the tab:
- JS runs inside an iframe —
initializeB24Frameretrieves authorization from the tab context - PHP and Python build a client using the request data that Bitrix24 passes to the handler, including the
AUTH_IDtoken
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Deal data</title>
</head>
<body>
<h2 id="deal-title">Loading deal data</h2>
<div id="deal-stage"></div>
<script type="module">
// npm install @bitrix24/b24jssdk
import { initializeB24Frame } from '@bitrix24/b24jssdk'
const $b24 = await initializeB24Frame()
const dealId = Number($b24.placement.options.ID)
if (
$b24.placement.placement !== 'CRM_DEAL_DETAIL_TAB'
|| !Number.isInteger(dealId)
|| dealId <= 0
) {
document.getElementById('deal-title').textContent =
'Failed to identify deal'
} else {
const response = await $b24.actions.v2.call.make({
method: 'crm.item.get',
params: {
entityTypeId: 2,
id: dealId,
},
requestId: 'deal-get',
})
if (!response.isSuccess) {
document.getElementById('deal-title').textContent =
response.getErrorMessages().join('; ')
} else {
const deal = response.getData().result.item
document.getElementById('deal-title').textContent =
deal.title || 'Deal without title'
document.getElementById('deal-stage').textContent =
'Stage: ' + deal.stageId
}
}
</script>
</body>
</html>
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Core\Credentials\ApplicationProfile;
use Bitrix24\SDK\Core\Exceptions\BaseException;
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$placement = (string)$request->request->get('PLACEMENT', '');
$placementOptions = json_decode(
(string)$request->request->get('PLACEMENT_OPTIONS', '[]'),
true
) ?: [];
$dealId = (int)($placementOptions['ID'] ?? 0);
$error = '';
$deal = null;
if ($placement !== 'CRM_DEAL_DETAIL_TAB' || $dealId <= 0)
{
$error = 'Failed to get call context';
}
else
{
$appProfile = ApplicationProfile::initFromArray([
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => 'local.xxxxxxxx.xxxxxxxx',
'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => 'yyyyyyyy',
'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'crm,placement',
]);
try
{
// The SDK will automatically take DOMAIN and AUTH_ID from the embedding request
$b24 = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest(
$request,
$appProfile
);
$deal = $b24->getCRMScope()->item()->get(2, $dealId)->item();
}
catch (BaseException $exception)
{
$error = $exception->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Deal data</title>
</head>
<body>
<?php if ($error !== ''): ?>
<p><?=htmlspecialchars($error)?></p>
<?php else: ?>
<h2><?=htmlspecialchars($deal->title ?? 'Deal without title')?></h2>
<p>Stage: <?=htmlspecialchars($deal->stageId ?? '')?></p>
<?php endif; ?>
</body>
</html>
# pip install b24pysdk flask
from flask import Flask, request
from b24pysdk import BitrixApp, BitrixToken, Client
from b24pysdk.errors import BitrixAPIError
import json
app = Flask(__name__)
bitrix_app = BitrixApp(
client_id="local.xxxxxxxx.xxxxxxxx",
client_secret="yyyyyyyy",
)
@app.post("/deal-tab")
def deal_tab():
placement = request.form.get("PLACEMENT", "")
options = json.loads(request.form.get("PLACEMENT_OPTIONS", "{}") or "{}")
deal_id = int(options.get("ID", 0))
if placement != "CRM_DEAL_DETAIL_TAB" or deal_id <= 0:
return "Failed to get call context"
# Bitrix24 passes the domain and user token to the handler
client = Client(
BitrixToken(
domain=request.args.get("DOMAIN", ""),
auth_token=request.form.get("AUTH_ID", ""),
bitrix_app=bitrix_app,
)
)
try:
deal = client.crm.item.get(
entity_type_id=2,
bitrix_id=deal_id,
).response.result["item"]
except BitrixAPIError as error:
return str(error)
return f"{deal.get('title', 'Deal without title')} — stage: {deal.get('stageId', '')}"
The method returns a item object containing the deal data available to the user whose authorization is used in the request.
{
"result": {
"item": {
"id": 3473,
"title": "Preparing offer",
"stageId": "NEW"
}
}
}
The identifier from PLACEMENT_OPTIONS can also be used for other actions: retrieve related contacts and the company, request data from an external system, or display a custom interface for working with the deal.
5. Verify the Widget
- Install the application in a test Bitrix24
- Ensure that the application installation is complete
- Open the CRM section
- Open any deal
- Find the Deal Data tab
- Verify that the handler displays the name and stage of the opened deal
If the tab does not appear, check the handler registration using the placement.get method. The response must contain the CRM_DEAL_DETAIL_TAB code and the handler page URL.
Other CRM Cards
You can follow the same scenario to add a tab to cards of other objects. Replace the code in PLACEMENT and specify the corresponding entityTypeId in crm.item.get.
|
CRM Object |
PLACEMENT |
entityTypeId |
|
Lead |
|
|
|
Deal |
|
|
|
Contact |
|
|
|
Company |
|
|
|
Invoice |
|
|
For codes for commercial proposals and SPAs, see the description of the CRM_XXX_DETAIL_TAB endpoint.