Для AI-агентов: markdown этой страницы — /docs-content/recipes/crm-files.md индекс документации — /llms.txt
Работа с файлами в полях CRM
Сложность: средний | Скоупы: crm | Стек: cURL / JavaScript / PHP
Читаем, скачиваем и записываем файлы в пользовательских полях типа «Файл» у сделок, лидов, контактов, компаний и смарт-процессов.
Что понадобится
- API-ключ Вайбкод со скоупом
crm - Node.js 18 или новее для примеров на JavaScript
Во всех примерах $VIBE_URL — базовый адрес https://vibecode.bitrix24.tech, $VIBE_API_KEY — ваш API-ключ.
Как устроено решение
- Скачиваем файл по адресу, который приходит в поле записи.
- Записываем файл парой из имени и содержимого в кодировке Base64.
- Удаляем лишнее, оставляя в поле только те файлы, что должны там остаться.
Перед этим нужно знать две вещи о поле — его тип и точное имя. Обе разобраны в разделах ниже, до шагов.
Два типа файловых полей
В Битрикс24 есть два разных типа файловых полей, и работают они по-разному.
- «Файл» — поле не связано с Диском. Содержимое хранится в самом поле. Эта страница про него.
- «Файл (диск)» — поле ссылается на объект Диска. С такими файлами работают через эндпоинты
/v1/files/*, см. Работа с файлами Диска.
Тип поля виден в ответе GET /v1/{entity}/fields.
Где применимо
Механика одинакова для всех перечисленных сущностей CRM.
- Сделки —
GET / PATCH /v1/deals/:id - Лиды —
GET / PATCH /v1/leads/:id - Контакты —
GET / PATCH /v1/contacts/:id - Компании —
GET / PATCH /v1/companies/:id - Предложения —
GET / PATCH /v1/quotes/:id - Счета —
GET / PATCH /v1/invoices/:id - Смарт-процессы —
GET / PATCH /v1/items/:entityTypeId/:id
Имя поля
Точное имя файлового поля возвращает GET /v1/{entity}/fields — написание различается по сущностям, см. Пользовательские поля (UF). Признак множественности приходит в списке определений полей, в поле multiple со значением Y или N: GET /v1/userfields/:entity для сделок, лидов, контактов, компаний и предложений, GET /v1/items/:entityTypeId/userfields для смарт-процессов и счетов.
Как поле выглядит в ответе
GET /v1/deals/:id возвращает файловое поле внутри конверта { success, data } — само поле лежит в data, а не в корне ответа.
{
"ufCrm_1a2b3c": [
{
"id": 35845,
"url": "https://portal.bitrix24.ru/bitrix/services/main/ajax.php?action=crm.controller.item.getFile&entityTypeId=2&id=100&fieldName=UF_CRM_1A2B3C&fileId=35845",
"urlMachine": "https://portal.bitrix24.ru/rest/1/WEBHOOK/crm.controller.item.getFile/?token=..."
}
]
}
В каждом объекте два адреса для разных задач.
url— для открытия в браузере. Требует активной сессии пользователя.urlMachine— для программного скачивания. Авторизация включена в сам адрес, поэтому обращайтесь с ним как с ключом: не пишите в журналы, не передавайте третьим лицам и не сохраняйте в задачах и тикетах.
Примеры шагов показывают отдельные вызовы и опираются на переменные, объявленные выше по сценарию.
Шаг 1. Скачать файл
Возьмите urlMachine из ответа и выполните по нему GET. В ответе придёт бинарное содержимое файла.
cURL
curl -s -o result.pdf "URL_MACHINE_FROM_RESPONSE"
JavaScript
import { writeFile } from 'node:fs/promises'
const res = await fetch(urlMachine)
await writeFile('result.pdf', Buffer.from(await res.arrayBuffer()))
PHP
<?php
file_put_contents('result.pdf', file_get_contents($urlMachine));
Для ключа-вебхука авторизация вшита в адрес и работает сразу. Для ключа OAuth-приложения адрес действует ограниченное время. Скачивайте файл сразу после чтения записи, а если прошло время, перечитайте запись и возьмите свежий urlMachine.
Шаг 2. Записать файл
Запись заменяет содержимое поля целиком. Если в поле уже лежат документы, а вы передали одну новую пару, прежние файлы будут удалены. Чтобы добавить файл к существующим, перечислите их идентификаторы вместе с новой парой — как показано в конце этого шага.
Файл передаётся парой из имени и содержимого в формате base64.
Поле с одним значением принимает одну пару.
{ "ufCrm_1a2b3c": ["contract.pdf", "BASE64_CONTENT"] }
Множественное поле принимает массив пар.
{ "ufCrm_1a2b3c": [["contract.pdf", "BASE64_CONTENT"], ["act.pdf", "BASE64_CONTENT"]] }
Примеры ниже показывают множественное поле. Для поля с одним значением передайте одну пару без внешнего массива.
cURL
curl -s -X PATCH -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
"$VIBE_URL/v1/deals/100" \
-d '{"ufCrm_1a2b3c": [["contract.pdf", "BASE64_CONTENT"]]}'
JavaScript
import { readFile } from 'node:fs/promises'
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
const base64Content = Buffer.from(await readFile('contract.pdf')).toString('base64')
await fetch(`${VIBE_URL}/v1/deals/100`, {
method: 'PATCH',
headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ ufCrm_1a2b3c: [['contract.pdf', base64Content]] }),
})
PHP
<?php
$vibeUrl = getenv('VIBE_URL');
$vibeApiKey = getenv('VIBE_API_KEY');
$ch = curl_init("$vibeUrl/v1/deals/100");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ["X-Api-Key: $vibeApiKey", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'ufCrm_1a2b3c' => [['contract.pdf', $base64Content]],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
Ответ возвращает запись целиком, и файловое поле в нём — уже новый набор. По нему и сверяйте результат записи.
{
"success": true,
"data": {
"id": 100,
"ufCrm_1a2b3c": [
{
"id": 35849,
"url": "https://portal.bitrix24.ru/bitrix/services/main/ajax.php?action=crm.controller.item.getFile&entityTypeId=2&id=100&fieldName=UF_CRM_1A2B3C&fileId=35849",
"urlMachine": "https://portal.bitrix24.ru/rest/1/WEBHOOK/crm.controller.item.getFile/?token=..."
}
]
}
}
Файл больше допустимого портал отклоняет:
{
"success": false,
"error": {
"code": "BITRIX_ERROR",
"message": "Максимальный размер файла превышен"
}
}
Чтобы сохранить уже прикреплённые файлы и добавить новый, передайте идентификаторы существующих файлов вместе с новой парой.
{ "ufCrm_1a2b3c": [{ "id": 35845 }, ["new.pdf", "BASE64_CONTENT"]] }
Шаг 3. Удалить файл
Передайте в поле только те файлы, которые нужно оставить. Пустой массив очищает поле с одним значением. У множественного поля пустой массив прежний набор не меняет — перечислите идентификаторы остающихся файлов, как показано в конце шага 2.
{ "ufCrm_1a2b3c": [] }
cURL
curl -s -X PATCH -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
"$VIBE_URL/v1/deals/100" \
-d '{"ufCrm_1a2b3c": []}'
JavaScript
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
await fetch(`${VIBE_URL}/v1/deals/100`, {
method: 'PATCH',
headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ ufCrm_1a2b3c: [] }),
})
Ограничения
Размер одного файла. Тело запроса на создание и обновление записи ограничено 40 МиБ, а base64 увеличивает размер примерно на треть, поэтому практический предел одного файла — чуть меньше 30 МиБ. Тело сверх лимита отклоняется с кодом PAYLOAD_TOO_LARGE.
Битрикс24 на своей стороне принимает и больше, так что этот потолок — наш. Отдельно учтите время: вызов в Битрикс24 ограничен 15 секундами без повторной попытки, поэтому файл у самой границы на медленном портале может отказать с BITRIX_TIMEOUT — надёжнее оставлять запас. Для файлов в десятки мегабайт используйте поле «Файл (диск)» и загрузку на Диск.
Отказы этого сценария. ENTITY_NOT_FOUND — записи с таким идентификатором нет, проверьте идентификатор и тип сущности. READONLY_FIELD — поле недоступно на запись, проверьте его в ответе GET /v1/{entity}/fields. BITRIX_ERROR — Битрикс24 отклонил значение, чаще всего файл больше допустимого: уменьшите файл или перенесите его на Диск.
Полный перечень кодов — Ошибки.
Полный код
Скрипт добавляет файл в поле, не потеряв то, что там уже лежит: читает запись, забирает идентификаторы прикреплённых файлов и отправляет их обратно вместе с новой парой. Это единственный запускаемый артефакт страницы — примеры шагов выше показывают отдельные вызовы.
// attach.mjs — добавить файл в поле «Файл» карточки CRM, сохранив прежние
// Расширение .mjs обязательно: скрипт использует await на верхнем уровне,
// а файл .js без "type": "module" Node читает как CommonJS и падает на разборе.
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('Переменная окружения VIBE_API_KEY не задана')
const ENTITY = 'deals' // deals | leads | contacts | companies | quotes | invoices
const ENTITY_ID = 100
const FIELD = 'ufCrm_1a2b3c' // имя поля из GET /v1/{entity}/fields
const FILE_PATH = './contract.pdf'
const headers = { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' }
async function apiCall(url, init = {}) {
const res = await fetch(url, init)
const body = await res.json().catch(() => null)
if (!body?.success) throw new Error(body?.error?.message ?? `запрос отклонён (${res.status})`)
return body.data
}
// Запись заменяет содержимое поля целиком, поэтому прежние файлы надо
// перечислить идентификаторами. Без этого шага они молча исчезнут.
const record = await apiCall(`${VIBE_URL}/v1/${ENTITY}/${ENTITY_ID}`, { headers })
const existing = Array.isArray(record[FIELD]) ? record[FIELD] : []
const keep = existing.map(file => ({ id: file.id }))
const bytes = await readFile(FILE_PATH)
if (bytes.length > 25 * 1024 * 1024) {
throw new Error(`${basename(FILE_PATH)} больше 25 МиБ — используйте поле «Файл (диск)»`)
}
const updated = await apiCall(`${VIBE_URL}/v1/${ENTITY}/${ENTITY_ID}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
[FIELD]: [...keep, [basename(FILE_PATH), bytes.toString('base64')]],
}),
})
// Портал отвечает успехом и на запись, которая ничего не изменила, поэтому
// число файлов сверяется по ответу, а не предполагается.
const after = Array.isArray(updated[FIELD]) ? updated[FIELD].length : 0
console.log(`было ${existing.length}, стало ${after}`)
Скрипт идемпотентен по количеству, но не по содержимому: повторный запуск добавит второй экземпляр того же файла. Если запускаете по расписанию, сверяйте имя файла с уже прикреплёнными и пропускайте совпадения.