Для 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-ключ.

Как устроено решение

  1. Скачиваем файл по адресу, который приходит в поле записи.
  2. Записываем файл парой из имени и содержимого в кодировке Base64.
  3. Удаляем лишнее, оставляя в поле только те файлы, что должны там остаться.

Перед этим нужно знать две вещи о поле — его тип и точное имя. Обе разобраны в разделах ниже, до шагов.

Два типа файловых полей

В Битрикс24 есть два разных типа файловых полей, и работают они по-разному.

  • «Файл» — поле не связано с Диском. Содержимое хранится в самом поле. Эта страница про него.
  • «Файл (диск)» — поле ссылается на объект Диска. С такими файлами работают через эндпоинты /v1/files/*, см. Работа с файлами Диска.

Тип поля виден в ответе GET /v1/{entity}/fields.

Где применимо

Механика одинакова для всех перечисленных сущностей CRM.

Имя поля

Точное имя файлового поля возвращает GET /v1/{entity}/fields — написание различается по сущностям, см. Пользовательские поля (UF). Признак множественности приходит в списке определений полей, в поле multiple со значением Y или N: GET /v1/userfields/:entity для сделок, лидов, контактов, компаний и предложений, GET /v1/items/:entityTypeId/userfields для смарт-процессов и счетов.

Как поле выглядит в ответе

GET /v1/deals/:id возвращает файловое поле внутри конверта { success, data } — само поле лежит в data, а не в корне ответа.

JSON
{
  "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

Terminal
curl -s -o result.pdf "URL_MACHINE_FROM_RESPONSE"

JavaScript

javascript
import { writeFile } from 'node:fs/promises'

const res = await fetch(urlMachine)
await writeFile('result.pdf', Buffer.from(await res.arrayBuffer()))

PHP

php
<?php
file_put_contents('result.pdf', file_get_contents($urlMachine));

Для ключа-вебхука авторизация вшита в адрес и работает сразу. Для ключа OAuth-приложения адрес действует ограниченное время. Скачивайте файл сразу после чтения записи, а если прошло время, перечитайте запись и возьмите свежий urlMachine.

Шаг 2. Записать файл

Запись заменяет содержимое поля целиком. Если в поле уже лежат документы, а вы передали одну новую пару, прежние файлы будут удалены. Чтобы добавить файл к существующим, перечислите их идентификаторы вместе с новой парой — как показано в конце этого шага.

Файл передаётся парой из имени и содержимого в формате base64.

Поле с одним значением принимает одну пару.

JSON
{ "ufCrm_1a2b3c": ["contract.pdf", "BASE64_CONTENT"] }

Множественное поле принимает массив пар.

JSON
{ "ufCrm_1a2b3c": [["contract.pdf", "BASE64_CONTENT"], ["act.pdf", "BASE64_CONTENT"]] }

Примеры ниже показывают множественное поле. Для поля с одним значением передайте одну пару без внешнего массива.

cURL

Terminal
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

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
<?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);

Ответ возвращает запись целиком, и файловое поле в нём — уже новый набор. По нему и сверяйте результат записи.

JSON
{
  "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=..."
      }
    ]
  }
}

Файл больше допустимого портал отклоняет:

JSON
{
  "success": false,
  "error": {
    "code": "BITRIX_ERROR",
    "message": "Максимальный размер файла превышен"
  }
}

Чтобы сохранить уже прикреплённые файлы и добавить новый, передайте идентификаторы существующих файлов вместе с новой парой.

JSON
{ "ufCrm_1a2b3c": [{ "id": 35845 }, ["new.pdf", "BASE64_CONTENT"]] }

Шаг 3. Удалить файл

Передайте в поле только те файлы, которые нужно оставить. Пустой массив очищает поле с одним значением. У множественного поля пустой массив прежний набор не меняет — перечислите идентификаторы остающихся файлов, как показано в конце шага 2.

JSON
{ "ufCrm_1a2b3c": [] }

cURL

Terminal
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

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 отклонил значение, чаще всего файл больше допустимого: уменьшите файл или перенесите его на Диск.

Полный перечень кодов — Ошибки.

Полный код

Скрипт добавляет файл в поле, не потеряв то, что там уже лежит: читает запись, забирает идентификаторы прикреплённых файлов и отправляет их обратно вместе с новой парой. Это единственный запускаемый артефакт страницы — примеры шагов выше показывают отдельные вызовы.

javascript
// 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}`)

Скрипт идемпотентен по количеству, но не по содержимому: повторный запуск добавит второй экземпляр того же файла. Если запускаете по расписанию, сверяйте имя файла с уже прикреплёнными и пропускайте совпадения.

Смотрите также