Para agentes de IA: markdown desta página — /docs-content-en/recipes/crm-files.md índice da documentação — /llms.txt

Os artigos da documentação estão disponíveis atualmente em inglês.

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 only the files that must stay in the field.

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 covers this type.
  • "File (Drive)" — the field references a Drive object. These 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 exact name of a file field is returned by GET /v1/{entity}/fields — the spelling differs between entities, see User fields (UF). Whether the field accepts multiple values is reported in the list of field definitions, in the multiple field with the value Y or N: GET /v1/userfields/:entity for deals, leads, contacts, companies and quotes, GET /v1/items/:entityTypeId/userfields for smart processes and invoices.

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, 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-value 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 examples below show a multiple field. For a single-value 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 a single-value field. On a multiple field an empty array leaves the previous set untouched — list the identifiers of the files that stay, as shown at the end of step 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.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 body of a create or update request is limited to 40 MiB, and base64 increases the size by about a third, so the practical limit for one file is just under 30 MiB. A body over the limit is rejected with the PAYLOAD_TOO_LARGE code.

Bitrix24 itself accepts more, so this ceiling is ours. Mind the clock too: the call to Bitrix24 is capped at 60 seconds with no retry, so a file right at the edge may fail with BITRIX_TIMEOUT on a slow Bitrix24 account — leave some headroom. For files of tens of megabytes use the "File (Drive)" field and upload to Drive.

Refusals in 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 > 25 * 1024 * 1024) {
  throw new Error(`${basename(FILE_PATH)} is over 25 MiB — 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')]],
  }),
})

// Bitrix24 returns 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