For AI agents: markdown of this page — /docs-content-en/recipes/crm-files.md documentation index — /llms.txt

Working with files in CRM fields

Difficulty: medium | Scopes: crm | Stack: cURL / JavaScript / PHP

Read, download and write files in user fields of the «File» type on deals, leads, contacts, companies and smart processes.

What you need

  • A Vibecode API key with the crm scope
  • Node.js 18 or newer for the JavaScript examples

In all examples $VIBE_URL is the base address https://vibecode.bitrix24.com and $VIBE_API_KEY is your API key.

How the solution works

  1. Download the file from the address returned in the record field.
  2. Write the file as a pair of a name and Base64-encoded content.
  3. Remove what is no longer needed, leaving in the field only the files that must stay there.

Before that you need to know two things about the field — its type and its exact name. Both are covered in the sections below, before the steps.

Two types of file fields

Bitrix24 has two different types of file fields, and they work differently.

  • «File» — the field is not linked to Drive. The content is stored in the field itself. This page is about it.
  • «File (Drive)» — the field references a Drive object. Such files are handled through the /v1/files/* endpoints, see Working with Drive files.

The field type is visible in the response of GET /v1/{entity}/fields.

Where it applies

The mechanics are the same for all the CRM entities listed below.

  • DealsGET / PATCH /v1/deals/:id
  • LeadsGET / PATCH /v1/leads/:id
  • ContactsGET / PATCH /v1/contacts/:id
  • CompaniesGET / PATCH /v1/companies/:id
  • QuotesGET / PATCH /v1/quotes/:id
  • InvoicesGET / PATCH /v1/invoices/:id
  • Smart processesGET / PATCH /v1/items/:entityTypeId/:id

Field name

The name of a file field looks like ufCrm_<code>. The exact name and the multiple flag are returned by GET /v1/{entity}/fields.

How the field looks in the response

GET /v1/deals/:id returns the file field inside the { success, data } envelope — the field itself sits in data, not at the root of the response.

JSON
{
  "ufCrm_1a2b3c": [
    {
      "id": 35845,
      "url": "https://portal.bitrix24.com/bitrix/services/main/ajax.php?action=crm.controller.item.getFile&entityTypeId=2&id=100&fieldName=UF_CRM_1A2B3C&fileId=35845",
      "urlMachine": "https://portal.bitrix24.com/rest/1/WEBHOOK/crm.controller.item.getFile/?token=..."
    }
  ]
}

Each object contains two addresses for different purposes.

  • url — for opening in a browser. Requires an active user session.
  • urlMachine — for programmatic download. Authorization is included in the address itself, so treat it like a key: keep it out of logs, do not pass it to third parties and do not store it in tasks or tickets.

The step examples show individual calls and rely on variables declared earlier in the scenario.

Step 1. Download the file

Take urlMachine from the response and send a GET to it. The response contains the binary content of the file.

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

For a webhook key the authorization is built into the address and works right away. For an OAuth application key the address is valid for a limited time. Download the file right after reading the record, and if some time has passed, read the record again and take a fresh urlMachine.

Step 2. Write the file

A write replaces the whole content of the field. If the field already holds documents and you send a single new pair, the previous files are deleted. To add a file to the existing ones, list their identifiers together with the new pair — as shown at the end of this step.

A file is passed as a pair of a name and content in base64 format.

A single field accepts one pair.

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

A multiple field accepts an array of pairs.

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

The multiple flag is returned by GET /v1/{entity}/fields. The examples below show a multiple field. For a single field pass one pair without the outer array.

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.com'
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);

The response returns the whole record, and the file field in it is already the new set. Check the result of the write against it.

JSON
{
  "success": true,
  "data": {
    "id": 100,
    "ufCrm_1a2b3c": [
      {
        "id": 35849,
        "url": "https://portal.bitrix24.com/bitrix/services/main/ajax.php?action=crm.controller.item.getFile&entityTypeId=2&id=100&fieldName=UF_CRM_1A2B3C&fileId=35849",
        "urlMachine": "https://portal.bitrix24.com/rest/1/WEBHOOK/crm.controller.item.getFile/?token=..."
      }
    ]
  }
}

A file over the limit is rejected by Bitrix24:

JSON
{
  "success": false,
  "error": {
    "code": "BITRIX_ERROR",
    "message": "Maximum file size exceeded"
  }
}

To keep the already attached files and add a new one, pass the identifiers of the existing files together with the new pair.

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

Step 3. Delete a file

Pass only the files that must remain in the field. An empty array clears the field completely.

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.com'
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: [] }),
})

Limitations

Size of a single file. The request body is limited to 1 MB, and base64 increases the size by about a third, so the practical limit for one file is around 750 KB. For larger files use the «File (Drive)» field and upload to Drive.

Refusals of this scenario. ENTITY_NOT_FOUND — there is no record with that identifier, check the identifier and the entity type. READONLY_FIELD — the field is not writable, check it in the response of GET /v1/{entity}/fields. BITRIX_ERROR — Bitrix24 rejected the value, most often the file is over the limit: make the file smaller or move it to Drive.

The full list of error codes — Errors.

Full code

The script adds a file to the field without losing what is already there: it reads the record, takes the identifiers of the attached files and sends them back together with the new pair. This is the only runnable artifact of the page — the step examples above show individual calls.

javascript
// attach.mjs — add a file to a «File» field of a CRM record, keeping the previous ones
// The .mjs extension is required: the script uses top-level await, and Node reads
// a .js file without "type": "module" as CommonJS and fails while parsing it.
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'

const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.com'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('Environment variable VIBE_API_KEY is not set')

const ENTITY = 'deals'            // deals | leads | contacts | companies | quotes | invoices
const ENTITY_ID = 100
const FIELD = 'ufCrm_1a2b3c'      // field name from 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 ?? `request rejected (${res.status})`)
  return body.data
}

// A write replaces the whole content of the field, so the previous files have to be
// listed by their identifiers. Without this step they disappear silently.
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 > 750 * 1024) {
  throw new Error(`${basename(FILE_PATH)} is over 750 KB — use a «File (Drive)» field`)
}

const updated = await apiCall(`${VIBE_URL}/v1/${ENTITY}/${ENTITY_ID}`, {
  method: 'PATCH',
  headers,
  body: JSON.stringify({
    [FIELD]: [...keep, [basename(FILE_PATH), bytes.toString('base64')]],
  }),
})

// The account answers with success even for a write that changed nothing, so the
// number of files is checked against the response instead of being assumed.
const after = Array.isArray(updated[FIELD]) ? updated[FIELD].length : 0
console.log(`was ${existing.length}, now ${after}`)

The script is idempotent by count but not by content: a repeat run adds a second copy of the same file. If you run it on a schedule, compare the file name with the already attached ones and skip the matches.

See also