Как добавить позицию в заказ с произвольной ценой
Scope:
saleКто может выполнять методы: чтобы пройти сценарий целиком, нужно самое строгое из перечисленных прав — «администратор»
- sale.basketitem.add и sale.order.get — администратор
- sale.basketitem.list — менеджер магазина
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
Позиция корзины — это строка заказа: товар или услуга с количеством, ценой и валютой. Метод sale.basketitem.add добавляет такую строку в уже созданный заказ.
Разберем два случая, когда цену задает интеграция, а не каталог:
- товар есть в каталоге, но продать его нужно по другой цене — со скидкой или с наценкой
- позиции нет в каталоге: разовая услуга, работа по договору, доставка
Оба случая закрывает один и тот же метод, отличается только набор полей. Если цену менять не нужно и она должна прийти из каталога, используйте sale.basketitem.addCatalogProduct — он сам подставит цену и характеристики товара.
Сценарий состоит из двух шагов, и они независимы. Выполните тот, который подходит вашей задаче, или оба подряд — как в примерах ниже.
- Добавим товар из каталога и назначим свою цену методом sale.basketitem.add
- Добавим позицию, которой нет в каталоге, тем же методом с другим набором полей
Результат обоих шагов проверим методами sale.basketitem.list и sale.order.get.
Подготовим данные
До первого вызова соберите значения.
orderId— идентификатор заказа, в который добавляется позиция. Заказ должен существовать: метод только добавляет в него строку. Список заказов вернет sale.order.list, а создать новый заказ можно методом sale.order.add. В примерах используется заказ891currency— валюта позиции. Она должна совпадать с валютой заказа, иначе метод вернет ошибку. В примерах используется валютаRUBproductId— идентификатор товара из каталога. Найти его можно методом catalog.product.list. В примерах используется товар7075. Для позиции, которой нет в каталоге, передайте0
Идентификаторы 891 и 7075 — значения из примеров. Замените их на свои.
Полный список полей позиции вернет метод sale.basketitem.getFields.
Как назначить цену вручную
За ручную цену отвечают четыре поля.
customPrice— признак того, что цену задает интеграция. Со значениемYданные каталога игнорируютсяbasePrice— исходная цена без скидки и наценкиprice— итоговая цена, по которой продаетеdiscountPrice— величина скидки или наценки
Три числовых поля связаны условием basePrice = price + discountPrice. Скидка — это положительный discountPrice, наценка — отрицательный. Например, товар стоит в каталоге 1030, а продать его нужно за 1100: наценка равна 70, поэтому discountPrice = -70.
Метод не проверяет это условие и сохранит любые три числа, которые вы передали. Посчитайте их сами, иначе в заказе будет показана неверная скидка.
Как использовать примеры в документации
1. Добавим товар из каталога и назначим свою цену
Передадим идентификатор товара в productId и зададим цену вручную. Название, единицу измерения и внешние коды метод возьмет из карточки товара.
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
const response = await $b24.actions.v2.call.make({
method: 'sale.basketitem.add',
params: {
fields: {
orderId: 891,
productId: 7075,
quantity: 4,
currency: 'RUB',
customPrice: 'Y', // цену задаем сами, каталожная не применяется
basePrice: 1030, // цена товара в каталоге
price: 1100, // цена продажи
discountPrice: -70, // наценка, поэтому значение отрицательное
}
},
requestId: 'basketitem-add-catalog'
})
if (response.isSuccess) {
console.log(response.getData().result)
} else {
console.error(response.getErrorMessages().join('; '))
}
<?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.ru/rest/USER_ID/TOKEN/');
$result = $sb->getSaleScope()->basketItem()->add([
'orderId' => 891,
'productId' => 7075,
'quantity' => 4,
'currency' => 'RUB',
'customPrice' => 'Y', // цену задаем сами, каталожная не применяется
'basePrice' => 1030, // цена товара в каталоге
'price' => 1100, // цена продажи
'discountPrice' => -70, // наценка, поэтому значение отрицательное
]);
echo '<PRE>';
print_r($result->getId());
echo '</PRE>';
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:
result = client.sale.basketitem.add(
fields={
"orderId": 891,
"productId": 7075,
"quantity": 4,
"currency": "RUB",
"customPrice": "Y", # цену задаем сами, каталожная не применяется
"basePrice": 1030, # цена товара в каталоге
"price": 1100, # цена продажи
"discountPrice": -70, # наценка, поэтому значение отрицательное
},
).response.result
print(result)
except BitrixAPIError as error:
print(error)
// Подготовка в пустом каталоге — go get без go mod init не сработает:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Запуск:
//
// export B24_WEBHOOK_URL='https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/' && go run .
//
// Пример самодостаточный: он создает товар с ценой и заказ, добавляет позицию
// с ручной ценой, показывает ее и убирает за собой. Запускается на любом
// Битрикс24, ничего править не нужно.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
b24 "github.com/bitrix24/b24gosdk"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- подготовка: товар с ценой и пустой заказ
iblockID, err := firstCatalog(ctx, core)
if err != nil {
return err
}
productID, err := addProduct(ctx, core, iblockID, "Пример позиции (b24gosdk)")
if err != nil {
return err
}
defer del(ctx, core, "catalog.product.delete", productID)
if err := addPrice(ctx, core, productID, 1030); err != nil {
return err
}
orderID, err := addOrder(ctx, core)
if err != nil {
return err
}
defer del(ctx, core, "sale.order.delete", orderID)
// --- собственно сценарий шага
// Цена задается вручную, поэтому customPrice = "Y". Наценка выражается
// ОТРИЦАТЕЛЬНЫМ discountPrice: базовая цена 1030, продаем за 1100.
res, err := core.Call(ctx, "sale.basketitem.add", b24.Params{
"fields": b24.Params{
"orderId": orderID,
"productId": productID,
"quantity": 4,
"currency": "RUB",
"customPrice": "Y",
"basePrice": 1030,
"price": 1100,
"discountPrice": -70,
},
})
if err != nil {
// Код ошибки сравнивается через errors.Is, а не строкой: опечатка в
// литерале скомпилируется и молча уведет в другую ветку.
if errors.Is(err, b24.ErrAccessDenied) {
return fmt.Errorf("вебхуку не хватает прав на sale: %w", err)
}
return fmt.Errorf("sale.basketitem.add: %w", err)
}
// Метод заворачивает ответ в объект с ключом basketItem.
raw, ok := b24.Unwrap(res.Result, "basketItem")
if !ok {
return fmt.Errorf("в ответе нет basketItem: %s", res.Result)
}
var item struct {
ID b24.ID `json:"id"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
BasePrice float64 `json:"basePrice"`
}
if err := json.Unmarshal(raw, &item); err != nil {
return fmt.Errorf("разбор позиции: %w", err)
}
fmt.Printf("позиция %d: %.0f x %.2f при базовой цене %.2f\n",
item.ID, item.Quantity, item.Price, item.BasePrice)
return nil
}
// --- вспомогательное: подготовка данных и уборка
func firstCatalog(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "catalog.catalog.list", b24.Params{
"filter": b24.Params{"iblockTypeId": "CRM_PRODUCT_CATALOG"},
}, b24.WithIdempotent())
if err != nil {
return 0, fmt.Errorf("catalog.catalog.list: %w", err)
}
var out struct {
Catalogs []struct {
IblockID b24.ID `json:"iblockId"`
} `json:"catalogs"`
}
if err := json.Unmarshal(res.Result, &out); err != nil {
return 0, err
}
if len(out.Catalogs) == 0 {
return 0, fmt.Errorf("в Битрикс24 нет торгового каталога")
}
return out.Catalogs[0].IblockID, nil
}
func addProduct(ctx context.Context, core *b24.Core, iblockID b24.ID, name string) (b24.ID, error) {
res, err := core.Call(ctx, "catalog.product.add", b24.Params{
"fields": b24.Params{"iblockId": iblockID, "name": name, "active": "Y"},
})
if err != nil {
return 0, fmt.Errorf("catalog.product.add: %w", err)
}
// add отвечает ключом element, а get — ключом product для той же сущности.
raw, ok := b24.Unwrap(res.Result, "element", "id")
if !ok {
return 0, fmt.Errorf("нет element.id в %s", res.Result)
}
var id b24.ID
return id, json.Unmarshal(raw, &id)
}
func addPrice(ctx context.Context, core *b24.Core, productID b24.ID, price float64) error {
res, err := core.Call(ctx, "catalog.priceType.list", nil, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("catalog.priceType.list: %w", err)
}
var types struct {
PriceTypes []struct {
ID b24.ID `json:"id"`
} `json:"priceTypes"`
}
if err := json.Unmarshal(res.Result, &types); err != nil {
return err
}
if len(types.PriceTypes) == 0 {
return fmt.Errorf("в Битрикс24 нет типов цен")
}
_, err = core.Call(ctx, "catalog.price.add", b24.Params{
"fields": b24.Params{
"productId": productID, "catalogGroupId": types.PriceTypes[0].ID,
"price": price, "currency": "RUB",
},
})
return err
}
func addOrder(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "sale.persontype.list", nil, b24.WithIdempotent())
if err != nil {
return 0, fmt.Errorf("sale.persontype.list: %w", err)
}
var types struct {
PersonTypes []struct {
ID b24.ID `json:"id"`
} `json:"personTypes"`
}
if err := json.Unmarshal(res.Result, &types); err != nil {
return 0, err
}
if len(types.PersonTypes) == 0 {
return 0, fmt.Errorf("в Битрикс24 нет типов плательщика")
}
res, err = core.Call(ctx, "sale.order.add", b24.Params{
"fields": b24.Params{
"lid": "s1", "personTypeId": types.PersonTypes[0].ID, "currency": "RUB",
},
})
if err != nil {
return 0, fmt.Errorf("sale.order.add: %w", err)
}
raw, ok := b24.Unwrap(res.Result, "order", "id")
if !ok {
return 0, fmt.Errorf("нет order.id в %s", res.Result)
}
var id b24.ID
return id, json.Unmarshal(raw, &id)
}
// del убирает созданное. Ошибку уборки печатаем, но не возвращаем: она не
// должна маскировать настоящую ошибку сценария.
func del(ctx context.Context, core *b24.Core, method string, id b24.ID) {
if _, err := core.Call(ctx, method, b24.Params{"id": id}); err != nil {
fmt.Fprintf(os.Stderr, "%s(%d): %v\n", method, id, err)
}
}
Ответ:
{
"result": {
"basketItem": {
"barcodeMulti": "N",
"basePrice": 1030,
"canBuy": "Y",
"catalogXmlId": "FUTURE-ERP-CATALOG",
"currency": "RUB",
"customPrice": "Y",
"dateInsert": "2026-08-18T09:12:34+03:00",
"dateUpdate": "2026-08-18T09:12:34+03:00",
"dimensions": "a:3:{s:5:\"WIDTH\";N;s:6:\"HEIGHT\";N;s:6:\"LENGTH\";N;}",
"discountPrice": -70,
"id": 1173,
"measureCode": "796",
"measureName": "шт",
"name": "Кофемашина",
"orderId": 891,
"price": 1100,
"productId": 7075,
"productXmlId": "7075",
"properties": [],
"quantity": 4,
"reservations": [],
"sort": 100,
"vatIncluded": "N",
"vatRate": null,
"weight": 0,
"xmlId": "bx_6a8405e2d0998"
}
},
"total": 1,
"time": {
"start": 1787037154,
"finish": 1787037155.489497,
"duration": 1.4894969463348389,
"processing": 1,
"date_start": "2026-08-18T10:12:34+03:00",
"date_finish": "2026-08-18T10:12:35+03:00",
"operating_reset_at": 1787037225,
"operating": 4.078084945678711
}
}
Название name, единица измерения measureCode и measureName, внешние коды catalogXmlId и productXmlId пришли из карточки товара. Сохраните id позиции — он лежит в result.basketItem.id. Этот идентификатор передается в параметр id методов sale.basketitem.update и sale.basketitem.delete, если строку нужно изменить или удалить.
2. Добавим позицию, которой нет в каталоге
Для разовой услуги или работы по договору товара в каталоге нет, поэтому передадим productId: 0 и заполним характеристики сами.
name— название позиции. Метод не потребует это поле, но без него строка заказа останется без названияmeasureCodeиmeasureName— код и обозначение единицы измерения. Код796— это штука по классификатору ОКЕИ. Единицы измерения, настроенные в Битрикс24, вернет метод catalog.measure.listweight— вес позицииdimensions— габариты позиции сериализованным массивомvatRate— ставка налога долей от единицы:0.1— это 10 %. Чтобы указать ставку «Без НДС», передайте пустую строкуvatIncluded— включен ли налог в ценуcanBuy— доступна ли позиция к покупкеsort— положение строки в списке позиций заказаxmlIdиproductXmlId— внешние коды позиции и товара. По ним удобно связывать строку заказа с записью в вашей системе
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
const response = await $b24.actions.v2.call.make({
method: 'sale.basketitem.add',
params: {
fields: {
orderId: 891,
productId: 0, // позиции нет в каталоге
name: 'Настройка оборудования',
quantity: 2,
currency: 'RUB',
customPrice: 'Y',
basePrice: 1000, // цена без скидки
price: 900, // цена продажи
discountPrice: 100, // скидка, поэтому значение положительное
canBuy: 'Y',
weight: 40,
measureCode: '796',
measureName: 'шт',
sort: 400,
xmlId: 'service-setup-1',
dimensions: 'a:3:{s:5:"WIDTH";i:244;s:6:"HEIGHT";i:100;s:6:"LENGTH";i:31;}', // сериализованный массив
vatRate: 0.1, // ставка 10 %
vatIncluded: 'Y',
productXmlId: 'service-setup',
}
},
requestId: 'basketitem-add-custom'
})
if (response.isSuccess) {
console.log(response.getData().result)
} else {
console.error(response.getErrorMessages().join('; '))
}
<?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.ru/rest/USER_ID/TOKEN/');
$result = $sb->getSaleScope()->basketItem()->add([
'orderId' => 891,
'productId' => 0, // позиции нет в каталоге
'name' => 'Настройка оборудования',
'quantity' => 2,
'currency' => 'RUB',
'customPrice' => 'Y',
'basePrice' => 1000, // цена без скидки
'price' => 900, // цена продажи
'discountPrice' => 100, // скидка, поэтому значение положительное
'canBuy' => 'Y',
'weight' => 40,
'measureCode' => '796',
'measureName' => 'шт',
'sort' => 400,
'xmlId' => 'service-setup-1',
'dimensions' => 'a:3:{s:5:"WIDTH";i:244;s:6:"HEIGHT";i:100;s:6:"LENGTH";i:31;}', // сериализованный массив
'vatRate' => 0.1, // ставка 10 %
'vatIncluded' => 'Y',
'productXmlId' => 'service-setup',
]);
echo '<PRE>';
print_r($result->getId());
echo '</PRE>';
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:
result = client.sale.basketitem.add(
fields={
"orderId": 891,
"productId": 0, # позиции нет в каталоге
"name": "Настройка оборудования",
"quantity": 2,
"currency": "RUB",
"customPrice": "Y",
"basePrice": 1000, # цена без скидки
"price": 900, # цена продажи
"discountPrice": 100, # скидка, поэтому значение положительное
"canBuy": "Y",
"weight": 40,
"measureCode": "796",
"measureName": "шт",
"sort": 400,
"xmlId": "service-setup-1",
"dimensions": 'a:3:{s:5:"WIDTH";i:244;s:6:"HEIGHT";i:100;s:6:"LENGTH";i:31;}', # сериализованный массив
"vatRate": 0.1, # ставка 10 %
"vatIncluded": "Y",
"productXmlId": "service-setup",
},
).response.result
print(result)
except BitrixAPIError as error:
print(error)
Ответ:
{
"result": {
"basketItem": {
"basePrice": 1000,
"canBuy": "Y",
"currency": "RUB",
"customPrice": "Y",
"dateInsert": "2026-08-18T09:13:24+03:00",
"dateUpdate": "2026-08-18T09:13:24+03:00",
"dimensions": "a:3:{s:5:\"WIDTH\";i:244;s:6:\"HEIGHT\";i:100;s:6:\"LENGTH\";i:31;}",
"discountPrice": 100,
"id": 1175,
"measureCode": "796",
"measureName": "шт",
"name": "Настройка оборудования",
"orderId": 891,
"price": 900,
"productId": 0,
"productXmlId": "service-setup",
"properties": [],
"quantity": 2,
"reservations": [],
"sort": 400,
"vatIncluded": "Y",
"vatRate": 0.1,
"weight": 40,
"xmlId": "service-setup-1"
}
},
"total": 1,
"time": {
"start": 1787037204,
"finish": 1787037204.794355,
"duration": 0.7943549156188965,
"processing": 0,
"date_start": "2026-08-18T10:13:24+03:00",
"date_finish": "2026-08-18T10:13:24+03:00",
"operating_reset_at": 1787037427,
"operating": 3.351419687271118
}
}
В ответе нет полей catalogXmlId и barcodeMulti: у позиции без товара нет карточки в каталоге, откуда их можно взять. Значение productId: 0 подтверждает, что строка не связана с каталогом.
Проверим результат
Позиции заказа вернет метод sale.basketitem.list. Отфильтруем их по идентификатору заказа. В примерах используется тот же клиент SDK, что и на предыдущих шагах, — инициализация не повторяется.
const response = await $b24.actions.v2.call.make({
method: 'sale.basketitem.list',
params: {
select: ['id', 'productId', 'name', 'quantity', 'basePrice', 'price', 'discountPrice', 'customPrice'],
filter: { '=orderId': 891 }
},
requestId: 'basketitem-list'
})
if (response.isSuccess) {
console.log(response.getData().result.basketItems)
} else {
console.error(response.getErrorMessages().join('; '))
}
$result = $sb->getSaleScope()->basketItem()->list(
['id', 'productId', 'name', 'quantity', 'basePrice', 'price', 'discountPrice', 'customPrice'],
['=orderId' => 891]
);
echo '<PRE>';
print_r($result->getBasketItems());
echo '</PRE>';
try:
result = client.sale.basketitem.list(
select=["id", "productId", "name", "quantity", "basePrice", "price", "discountPrice", "customPrice"],
filter={"=orderId": 891},
).response.result
print(result)
except BitrixAPIError as error:
print(error)
Ответ:
{
"result": {
"basketItems": [
{
"basePrice": 1030,
"customPrice": "Y",
"discountPrice": -70,
"id": 1173,
"name": "Кофемашина",
"price": 1100,
"productId": 7075,
"quantity": 4
},
{
"basePrice": 1000,
"customPrice": "Y",
"discountPrice": 100,
"id": 1175,
"name": "Настройка оборудования",
"price": 900,
"productId": 0,
"quantity": 2
}
]
},
"total": 2
}
Обе строки на месте, цены и количество совпадают с тем, что вы передали, а customPrice: "Y" подтверждает, что каталожная цена не применялась.
Сумму заказа и налог покажет метод sale.order.get. Для двух позиций из примеров в ответе будут значения:
{
"price": 6200,
"taxValue": 163.63636364
}
Сумма 6200 — это 4 × 1100 плюс 2 × 900. Налог 163.64 — это 10 % от 1800: столько стоит вторая позиция, для которой мы передали vatRate: 0.1. Если бы значение поля было 10, Битрикс24 посчитал бы ставку 1000 %.
В интерфейсе результат виден в карточке заказа: новые строки появятся в списке товаров.
Если метод вернул ошибку
Проверьте данные запроса.
0—Required fields: orderId— не передан обязательныйorderId200140400009—Order not found— заказа с таким идентификатором нет. ПроверьтеorderIdметодом sale.order.list200140400011—Currency must be the currency of the order— валюта позиции не совпадает с валютой заказа. Возьмите валюту из поляcurrencyзаказа200140400007—basket item is not saved - bad data— позицию не удалось сохранить. Проверьте набор полей методом sale.basketitem.getFields
Первые три ошибки метод возвращает до записи в заказ, поэтому исправьте данные и повторите вызов целиком. После ошибки 200140400007 сначала посмотрите позиции заказа методом sale.basketitem.list: проверка выполняется уже после сохранения заказа.
Что важно учитывать
- метод не ищет дубликаты: повторный вызов с теми же полями добавит в заказ еще одну строку, а не изменит существующую. Чтобы изменить строку, используйте sale.basketitem.update
- достаточно передать
price, чтобы позиция перешла на ручную цену: Битрикс24 сам выставитcustomPrice: "Y". Если цена должна остаться каталожной, не передавайтеprice vatRateзадается долей, а не процентами: значение0.1— это ставка 10 %- позиция с
productId: 0не связана с каталогом. Название, единицу измерения, вес, габариты и ставку налога Битрикс24 неоткуда взять, поэтому передавайте их в каждом вызове
Продолжите изучение
- Добавить элемент (позицию) в корзину существующего заказа sale.basketitem.add
- Добавить позицию с товаром или услугой из модуля catalog в корзину существующего заказа sale.basketitem.addCatalogProduct
- Изменить позицию корзины существующего заказа sale.basketitem.update
- Получить доступные поля элемента (позиции) корзины sale.basketitem.getFields
- Типы данных и структура объектов в REST API Интернет-магазина