How to Save the Paid Amount in the Deal Field
Scope:
crmWho can execute the methods: to complete the entire scenario, the strictest of the listed rights is required — administrative access to the CRM section
- crm.deal.userfield.add — a CRM administrator
- crm.item.payment.list — a user with permission to read the deal the payments are selected from
- crm.item.update — a user with permission to modify items of a CRM object
- crm.item.get and crm.item.fields — a user with permission to read items of a CRM object
- crm.currency.base.get — any user
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.
The deal amount shows the total cost rather than how much the customer has already transferred. A single deal can have several payments, some of them unpaid, and the amount field does not separate them. To let the manager see the amount already paid right in the deal card, create a separate Money field and store the total of the completed payments in it.
A Money field stores the amount and the currency in a single string: 1700|EUR. The separator is a vertical bar, and the currency is written in three uppercase letters. If the format is broken, the method returns success while the field ends up empty, so build the value carefully.
The scenario continues the How to Save the Payment Date in the Deal Field tutorial. The earlier tutorial copies the payment date to the deal, this one copies the paid amount and the currency.
The scenario consists of four steps.
- Create the Money field using the crm.deal.userfield.add method
- Retrieve the deal payments using the crm.item.payment.list method
- Calculate the paid amount and write it to the field using the crm.item.update method
- Compare the paid amount with the deal amount using the crm.item.get method
As a result, the deal card gets a field with the paid amount in the required currency, and the method response shows how much is still unpaid.
Before You Start
Prepare the scenario data:
- A deal with payments. You will need its
id. For deals,entityTypeIdequals2 - REST access. A webhook or an application with the
crmscope. Only a CRM administrator can create a Money field - Currency. If the currency is not specified in the value, Bitrix24 substitutes the base currency. It is returned by the crm.currency.base.get method
The examples below use deal 8423 with three payments: 1000 is paid, 500 is unpaid, and 700 is paid. In your Bitrix24, substitute your own deal and your own payments.
For server-side JS examples with B24Hook, Node.js 18, 20, 22 or newer is required. For new projects, use version 22 or later. B24JsSDK is an ES module: save the code in an .mjs file or add "type": "module" to package.json. For examples with b24pysdk, Python 3.9 or newer is required.
Store the webhook URL in an environment variable and do not publish it in open code.
How to Use Examples in Documentation
1. Create the Money Field
The crm.deal.userfield.add method creates a custom field for all deals. Pass the parameters:
FIELD_NAME— field code. The parameter is required. If the code does not start withUF_CRM_, the prefix is added automaticallyUSER_TYPE_ID— field type,moneyfor a Money fieldMULTIPLE—Nfor a single amount,Yif you need to store each payment as a separate stringEDIT_FORM_LABEL— field title in the deal card, specified by language
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
async function callMethod(method, params, requestId) {
const response = await $b24.actions.v2.call.make({
method,
params,
requestId
})
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
return response.getData().result
}
const paidFieldId = await callMethod(
'crm.deal.userfield.add',
{
fields: {
FIELD_NAME: 'UF_CRM_MONEY_PAID',
USER_TYPE_ID: 'money',
MULTIPLE: 'N',
EDIT_FORM_LABEL: { en: 'Paid amount' }
}
},
'userfield-add-money'
)
console.log(paidFieldId)
<?php
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
$serviceBuilder = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook(getenv('B24_HOOK'));
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
function callMethod($serviceBuilder, string $method, array $params = []): mixed
{
return $serviceBuilder
->core
->call($method, $params)
->getResponseData()
->getResult();
}
$paidFieldId = callMethod($serviceBuilder, 'crm.deal.userfield.add', [
'fields' => [
'FIELD_NAME' => 'UF_CRM_MONEY_PAID',
'USER_TYPE_ID' => 'money',
'MULTIPLE' => 'N',
'EDIT_FORM_LABEL' => ['en' => 'Paid amount'],
],
]);
print_r($paidFieldId);
import os
from b24pysdk import BitrixWebhook
bitrix_token = BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token=os.environ["B24_HOOK_TOKEN"],
)
# B24_HOOK_TOKEN = 'USER_ID/TOKEN'
def call_method(method, params=None):
# A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
return bitrix_token.call_method(
api_method=method,
params=params or {},
)["result"]
paid_field_id = call_method(
"crm.deal.userfield.add",
{
"fields": {
"FIELD_NAME": "UF_CRM_MONEY_PAID",
"USER_TYPE_ID": "money",
"MULTIPLE": "N",
"EDIT_FORM_LABEL": {"en": "Paid amount"},
},
},
)
print(paid_field_id)
The response contains the field identifier:
{
"result": 6007785
}
The rest of the scenario uses universal methods, where the field code is written in a different case: UF_CRM_MONEY_PAID becomes ufCrmMoneyPaid. Check the exact spelling for your Bitrix24 in the response of the crm.item.fields method with entityTypeId: 2.
2. Retrieve the Deal Payments
The crm.item.payment.list method returns the payments of a single CRM object. Pass the parameters:
entityTypeId— identifier of the CRM object type. For a deal, it is2entityId— deal identifier
Three fields of each payment are needed from the response:
paid— payment flag,YorN. Only payments with theYvalue are summed upsum— payment amount as a numbercurrency— payment currency, a three-letter code
const payments = await callMethod(
'crm.item.payment.list',
{
entityTypeId: 2,
entityId: 8423
},
'payment-list'
)
const paidPayments = payments.filter((payment) => payment.paid === 'Y')
console.table(paidPayments)
$payments = callMethod($serviceBuilder, 'crm.item.payment.list', [
'entityTypeId' => 2,
'entityId' => 8423,
]);
$paidPayments = array_values(
array_filter($payments, static fn(array $payment): bool => $payment['paid'] === 'Y')
);
print_r($paidPayments);
payments = call_method(
"crm.item.payment.list",
{
"entityTypeId": 2,
"entityId": 8423,
},
)
paid_payments = [payment for payment in payments if payment["paid"] == "Y"]
print(paid_payments)
Abbreviated response:
{
"result": [
{
"id": 515,
"accountNumber": "917/1",
"paid": "Y",
"datePaid": "2026-09-10T01:52:39+03:00",
"sum": 1000,
"currency": "EUR"
},
{
"id": 517,
"accountNumber": "917/2",
"paid": "N",
"datePaid": null,
"sum": 500,
"currency": "EUR"
},
{
"id": 519,
"accountNumber": "917/3",
"paid": "Y",
"datePaid": "2026-09-10T01:52:39+03:00",
"sum": 700,
"currency": "EUR"
}
]
}
Two of the three payments are paid: 1000 and 700. Their sum, 1700, is the value for the Money field. The 500 payment is not included in the calculation.
3. Calculate the Paid Amount and Write It to the Field
The value of a Money field is built from the amount and the currency: first the number with a dot as the decimal separator, then a vertical bar, then the currency code in three uppercase letters. For this example, it is 1700|EUR.
Only payments in the same currency can be summed up. If the currencies differ, calculate the amount for each of them separately and decide which one to write to the field or at which rate to convert.
The crm.item.update method writes the value to the deal. Pass the parameters:
entityTypeId—2for a dealid— deal identifierfields— object with the field code in the format of universal methods,ufCrmMoneyPaidin the example
Check the format before sending. For the value 1700,00|EUR with a comma, 1700.00|eur in lowercase, or any other string that does not match the format, the method returns true while the field stays empty. There is no error.
function buildMoneyValue(payments) {
const currencies = new Set(payments.map((payment) => payment.currency))
if (currencies.size > 1) {
throw new Error(`Payments in different currencies: ${[...currencies].join(', ')}`)
}
const currency = [...currencies][0]
const total = payments.reduce((sum, payment) => sum + Number(payment.sum), 0)
return `${total.toFixed(2)}|${currency}`
}
const moneyValue = buildMoneyValue(paidPayments)
await callMethod(
'crm.item.update',
{
entityTypeId: 2,
id: 8423,
fields: { ufCrmMoneyPaid: moneyValue }
},
'item-update-money'
)
console.log(moneyValue)
function buildMoneyValue(array $payments): string
{
$currencies = array_unique(array_column($payments, 'currency'));
if (count($currencies) > 1) {
throw new RuntimeException('Payments in different currencies: ' . implode(', ', $currencies));
}
$total = array_sum(array_map(
static fn(array $payment): float => (float)$payment['sum'],
$payments
));
return number_format($total, 2, '.', '') . '|' . reset($currencies);
}
$moneyValue = buildMoneyValue($paidPayments);
callMethod($serviceBuilder, 'crm.item.update', [
'entityTypeId' => 2,
'id' => 8423,
'fields' => ['ufCrmMoneyPaid' => $moneyValue],
]);
print_r($moneyValue);
def build_money_value(payments):
currencies = {payment["currency"] for payment in payments}
if len(currencies) > 1:
raise RuntimeError(f"Payments in different currencies: {', '.join(sorted(currencies))}")
total = sum(float(payment["sum"]) for payment in payments)
return f"{total:.2f}|{currencies.pop()}"
money_value = build_money_value(paid_payments)
call_method(
"crm.item.update",
{
"entityTypeId": 2,
"id": 8423,
"fields": {"ufCrmMoneyPaid": money_value},
},
)
print(money_value)
The method returns the updated item. Abbreviated response:
{
"result": {
"item": {
"id": 8423,
"ufCrmMoneyPaid": "1700|EUR"
}
}
}
The value comes back normalized: 1700.00 is stored as 1700. Bitrix24 trims trailing zeros while a significant fractional part is kept, so 1750.25|EUR stays unchanged.
4. Compare the Paid Amount with the Deal Amount
The crm.item.get method returns the entire deal. Three fields are of interest:
opportunity— deal amount. It is calculated from the product rows and is not split into paid and unpaid partscurrencyId— deal currencyufCrmMoneyPaid— the Money field with the paid amount
The difference between the deal amount and the paid amount is the outstanding balance.
const item = await callMethod(
'crm.item.get',
{ entityTypeId: 2, id: 8423 },
'item-get-money'
)
const [paidSum, paidCurrency] = item.item.ufCrmMoneyPaid.split('|')
const rest = Number(item.item.opportunity) - Number(paidSum)
console.log(`Deal: ${item.item.opportunity} ${item.item.currencyId}`)
console.log(`Paid: ${paidSum} ${paidCurrency}`)
console.log(`Remaining: ${rest.toFixed(2)} ${item.item.currencyId}`)
$item = callMethod($serviceBuilder, 'crm.item.get', [
'entityTypeId' => 2,
'id' => 8423,
]);
[$paidSum, $paidCurrency] = explode('|', $item['item']['ufCrmMoneyPaid']);
$rest = (float)$item['item']['opportunity'] - (float)$paidSum;
echo 'Deal: ' . $item['item']['opportunity'] . ' ' . $item['item']['currencyId'] . PHP_EOL;
echo 'Paid: ' . $paidSum . ' ' . $paidCurrency . PHP_EOL;
echo 'Remaining: ' . number_format($rest, 2, '.', '') . ' ' . $item['item']['currencyId'] . PHP_EOL;
item = call_method("crm.item.get", {"entityTypeId": 2, "id": 8423})["item"]
paid_sum, paid_currency = item["ufCrmMoneyPaid"].split("|")
rest = float(item["opportunity"]) - float(paid_sum)
print(f"Deal: {item['opportunity']} {item['currencyId']}")
print(f"Paid: {paid_sum} {paid_currency}")
print(f"Remaining: {rest:.2f} {item['currencyId']}")
Abbreviated response:
{
"result": {
"item": {
"id": 8423,
"opportunity": 2000,
"currencyId": "EUR",
"ufCrmMoneyPaid": "1700|EUR"
}
}
}
The deal amount is 2000, the paid amount is 1700, and 300 is still outstanding. The opportunity field itself does not show this difference, which is exactly why a separate Money field is needed.
Verify the Result
The scenario is complete if the Money field is filled in and its value matches the sum of the paid payments.
What to check in the responses:
ufCrmMoneyPaidcontains a string with the amount and the currency rather thannullor an empty string- the amount in the field equals the sum of the
sumvalues of the payments wherepaidequalsY - the currency code in the field matches the currency of the payments
- the difference between
opportunityand the amount from the field equals the outstanding balance
Open the deal card in the interface: the Paid Amount field shows the amount with the currency symbol.
Errors and Troubleshooting
If the method returns an error, check the request data.
|
Code or Error Text |
Reason and Action |
|
|
The field code is not passed to crm.deal.userfield.add. Pass |
|
|
The identifier of a nonexistent deal is passed to crm.item.get or crm.item.update. Check |
Format errors are silent: the method returns success while the value is lost. Check the field with the crm.item.get method.
- The field comes back as
nullor an empty string: the value format did not match the expected one. Common reasons are a comma instead of a dot, a lowercase currency, spaces around the separator, or a currency symbol instead of a three-letter code - The field contains the amount with the wrong currency: the currency was not passed, and the base currency of Bitrix24 was substituted. It can be retrieved with the crm.currency.base.get method
- The field contains an unfamiliar currency code: the code is not checked for existence. A string of three uppercase letters is stored even if there is no such currency in Bitrix24
- The amount in the field is larger than expected: unpaid payments got into the calculation. Filter for payments where
paidisY
To clear the field, pass an empty string to it.
Key Points
- The value of a Money field is a string in the
amount|CURRENCYformat. The number is written with a dot, and the currency in three uppercase letters - Trailing zeros are trimmed:
1700.00is stored as1700, while1750.25stays unchanged - Negative amounts are allowed, for example
-20|USDfor a refund - If the currency is not specified, the base currency of Bitrix24 is substituted. In an integration, specify the currency explicitly: the base currency in another Bitrix24 can be different
- A Money field keeps a single setting when created through REST,
DEFAULT_VALUE. Other keys inSETTINGSare not stored, so the value parsing rules cannot be changed through REST - Payments in different currencies cannot be summed up. Calculate the amounts for each currency separately or convert them at your own rate
- A multiple Money field stores an array of such strings:
["1000|EUR", "700|EUR"]. Each payment is stored as a separate string there, but the values have to be summed up on your side - The
opportunitydeal amount is calculated from the product rows and does not answer the question of how much has already been paid - The date of the last payment is transferred in the same way, as described in the How to Save the Payment Date in the Deal Field tutorial
Code Example
The complete scenario in a single script: it creates the Money field, reads the deal payments, sums up the paid ones, writes the value, and compares it with the deal amount.
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
const DEAL_ID = 8423
const DEAL_ENTITY_TYPE_ID = 2
// A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
async function callMethod(method, params, requestId) {
const response = await $b24.actions.v2.call.make({ method, params, requestId })
if (!response.isSuccess) {
throw new Error(response.getErrorMessages().join('; '))
}
return response.getData().result
}
function buildMoneyValue(payments) {
const currencies = new Set(payments.map((payment) => payment.currency))
if (currencies.size === 0) {
return ''
}
if (currencies.size > 1) {
throw new Error(`Payments in different currencies: ${[...currencies].join(', ')}`)
}
const total = payments.reduce((sum, payment) => sum + Number(payment.sum), 0)
return `${total.toFixed(2)}|${[...currencies][0]}`
}
async function main() {
await callMethod('crm.deal.userfield.add', {
fields: {
FIELD_NAME: 'UF_CRM_MONEY_PAID',
USER_TYPE_ID: 'money',
MULTIPLE: 'N',
EDIT_FORM_LABEL: { en: 'Paid amount' }
}
}, 'userfield-add-money')
const payments = await callMethod('crm.item.payment.list', {
entityTypeId: DEAL_ENTITY_TYPE_ID,
entityId: DEAL_ID
}, 'payment-list')
const paidPayments = payments.filter((payment) => payment.paid === 'Y')
const moneyValue = buildMoneyValue(paidPayments)
await callMethod('crm.item.update', {
entityTypeId: DEAL_ENTITY_TYPE_ID,
id: DEAL_ID,
fields: { ufCrmMoneyPaid: moneyValue }
}, 'item-update-money')
const item = await callMethod('crm.item.get', {
entityTypeId: DEAL_ENTITY_TYPE_ID,
id: DEAL_ID
}, 'item-get-money')
const [paidSum, paidCurrency] = (item.item.ufCrmMoneyPaid ?? '|').split('|')
const rest = Number(item.item.opportunity) - Number(paidSum || 0)
console.log(`Deal: ${item.item.opportunity} ${item.item.currencyId}`)
console.log(`Paid: ${paidSum} ${paidCurrency}`)
console.log(`Remaining: ${rest.toFixed(2)} ${item.item.currencyId}`)
}
main().catch((error) => console.error(error.message))
<?php
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
const DEAL_ID = 8423;
const DEAL_ENTITY_TYPE_ID = 2;
$serviceBuilder = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook(getenv('B24_HOOK'));
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
function callMethod($serviceBuilder, string $method, array $params = []): mixed
{
return $serviceBuilder
->core
->call($method, $params)
->getResponseData()
->getResult();
}
function buildMoneyValue(array $payments): string
{
$currencies = array_unique(array_column($payments, 'currency'));
if ($currencies === []) {
return '';
}
if (count($currencies) > 1) {
throw new RuntimeException('Payments in different currencies: ' . implode(', ', $currencies));
}
$total = array_sum(array_map(
static fn(array $payment): float => (float)$payment['sum'],
$payments
));
return number_format($total, 2, '.', '') . '|' . reset($currencies);
}
callMethod($serviceBuilder, 'crm.deal.userfield.add', [
'fields' => [
'FIELD_NAME' => 'UF_CRM_MONEY_PAID',
'USER_TYPE_ID' => 'money',
'MULTIPLE' => 'N',
'EDIT_FORM_LABEL' => ['en' => 'Paid amount'],
],
]);
$payments = callMethod($serviceBuilder, 'crm.item.payment.list', [
'entityTypeId' => DEAL_ENTITY_TYPE_ID,
'entityId' => DEAL_ID,
]);
$paidPayments = array_values(
array_filter($payments, static fn(array $payment): bool => $payment['paid'] === 'Y')
);
$moneyValue = buildMoneyValue($paidPayments);
callMethod($serviceBuilder, 'crm.item.update', [
'entityTypeId' => DEAL_ENTITY_TYPE_ID,
'id' => DEAL_ID,
'fields' => ['ufCrmMoneyPaid' => $moneyValue],
]);
$item = callMethod($serviceBuilder, 'crm.item.get', [
'entityTypeId' => DEAL_ENTITY_TYPE_ID,
'id' => DEAL_ID,
]);
[$paidSum, $paidCurrency] = explode('|', (string)$item['item']['ufCrmMoneyPaid'] . '|');
$rest = (float)$item['item']['opportunity'] - (float)$paidSum;
echo 'Deal: ' . $item['item']['opportunity'] . ' ' . $item['item']['currencyId'] . PHP_EOL;
echo 'Paid: ' . $paidSum . ' ' . $paidCurrency . PHP_EOL;
echo 'Remaining: ' . number_format($rest, 2, '.', '') . ' ' . $item['item']['currencyId'] . PHP_EOL;
import os
from b24pysdk import BitrixWebhook
DEAL_ID = 8423
DEAL_ENTITY_TYPE_ID = 2
bitrix_token = BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token=os.environ["B24_HOOK_TOKEN"],
)
# B24_HOOK_TOKEN = 'USER_ID/TOKEN'
def call_method(method, params=None):
# A single helper for all calls in the scenario: the method name and parameters are passed the same way as in REST
return bitrix_token.call_method(
api_method=method,
params=params or {},
)["result"]
def build_money_value(payments):
currencies = {payment["currency"] for payment in payments}
if not currencies:
return ""
if len(currencies) > 1:
raise RuntimeError(f"Payments in different currencies: {', '.join(sorted(currencies))}")
total = sum(float(payment["sum"]) for payment in payments)
return f"{total:.2f}|{currencies.pop()}"
call_method(
"crm.deal.userfield.add",
{
"fields": {
"FIELD_NAME": "UF_CRM_MONEY_PAID",
"USER_TYPE_ID": "money",
"MULTIPLE": "N",
"EDIT_FORM_LABEL": {"en": "Paid amount"},
},
},
)
payments = call_method(
"crm.item.payment.list",
{"entityTypeId": DEAL_ENTITY_TYPE_ID, "entityId": DEAL_ID},
)
paid_payments = [payment for payment in payments if payment["paid"] == "Y"]
money_value = build_money_value(paid_payments)
call_method(
"crm.item.update",
{
"entityTypeId": DEAL_ENTITY_TYPE_ID,
"id": DEAL_ID,
"fields": {"ufCrmMoneyPaid": money_value},
},
)
item = call_method(
"crm.item.get",
{"entityTypeId": DEAL_ENTITY_TYPE_ID, "id": DEAL_ID},
)["item"]
paid_sum, _, paid_currency = (item["ufCrmMoneyPaid"] or "").partition("|")
rest = float(item["opportunity"]) - float(paid_sum or 0)
print(f"Deal: {item['opportunity']} {item['currencyId']}")
print(f"Paid: {paid_sum} {paid_currency}")
print(f"Remaining: {rest:.2f} {item['currencyId']}")