Developing Bitrix24 REST Applications in TypeScript
The Bitrix24 REST API is feature-rich but convoluted. IDE autocompletion for methods like crm.deal.list, tasks.task.add, and im.message.send only works if you have types. Without TypeScript, developing a Bitrix24 REST application means constantly switching to the documentation and catching undefined errors at runtime.
Developing Bitrix24 REST Applications in TypeScript
Bitrix24 REST Application Architecture
Three types of applications in the Bitrix24 ecosystem:
-
Web application (iframe) — loads inside the Bitrix24 interface in an iframe. JavaScript/TypeScript with access to the
BX24.jsSDK. - Server-side application — PHP/Node.js, runs independently, communicates with Bitrix24 via REST over OAuth.
- Widget — a compact application in the sidebar or CRM.
TypeScript applies to all three cases, but with different entry points.
Typing the BX24 SDK
There is no official TypeScript package for the BX24 SDK. We write the declaration ourselves:
// types/bx24.d.ts
declare global {
const BX24: {
init(callback: () => void): void;
isAdmin(): boolean;
getAuth(): BX24Auth;
refreshAuth(callback: (auth: BX24Auth) => void): void;
callMethod(
method: string,
params?: Record<string, unknown>,
callback?: (result: BX24CallResult) => void
): void;
callBatch(
calls: Record<string, [string, Record<string, unknown>?]>,
callback: (result: Record<string, BX24CallResult>) => void,
bHaltOnError?: boolean
): void;
resizeWindow(width: number, height: number): void;
closeApplication(): void;
placement: {
info(): BX24PlacementInfo;
call(command: string, params?: Record<string, unknown>): void;
};
};
}
interface BX24Auth {
access_token: string;
refresh_token: string;
expires_in: number;
domain: string;
member_id: string;
}
interface BX24CallResult {
status(): number;
data(): unknown;
error(): string | false;
more(): boolean;
next(): void;
total(): number;
}
interface BX24PlacementInfo {
placement: string;
options: Record<string, string>;
}
export {};
Types for CRM Data
// types/crm.ts
export interface BX24Deal {
ID: string;
TITLE: string;
STAGE_ID: string;
OPPORTUNITY: string;
CURRENCY_ID: string;
ASSIGNED_BY_ID: string;
DATE_CREATE: string;
DATE_MODIFY: string;
CONTACT_ID: string | null;
COMPANY_ID: string | null;
COMMENTS: string | null;
UF_CRM_CUSTOM_FIELD?: string; // custom fields via UF_
[key: string]: unknown; // additional fields
}
export interface BX24Task {
id: string;
title: string;
description: string;
status: string;
responsible: { id: string; name: string };
deadline: string | null;
createdDate: string;
ufTaskWebdavFiles?: string[]; // custom task fields
}
export type StageId =
| 'NEW' | 'PREPARATION' | 'PREPAYMENT_INVOICE'
| 'EXECUTING' | 'FINAL_INVOICE' | 'WON' | 'LOSE';
Typed Wrapper Around BX24 callMethod
// api/bx24client.ts
export function callMethod<T>(
method: string,
params: Record<string, unknown> = {}
): Promise<T[]> {
return new Promise((resolve, reject) => {
const results: T[] = [];
const handleResult = (result: ReturnType<typeof BX24.callMethod extends (...args: unknown[]) => infer R ? R : never>) => {
if (result.error()) {
reject(new Error(String(result.error())));
return;
}
const data = result.data() as T[];
results.push(...(Array.isArray(data) ? data : [data as T]));
if (result.more()) {
result.next(); // automatic pagination
} else {
resolve(results);
}
};
BX24.callMethod(method, params, handleResult);
});
}
// Usage
import type { BX24Deal } from '@/types/crm';
const deals = await callMethod<BX24Deal>('crm.deal.list', {
filter: { STAGE_ID: 'NEW' },
select: ['ID', 'TITLE', 'OPPORTUNITY', 'ASSIGNED_BY_ID'],
order: { DATE_CREATE: 'DESC' },
});
result.more() + result.next() is the BX24 SDK pagination mechanism. The wrapper automatically traverses all pages and returns the complete array.
Batch Requests for Performance
Each callMethod is a separate HTTP request. For applications with high API load, use callBatch:
export function callBatch<T extends Record<string, unknown>>(
calls: Record<string, [string, Record<string, unknown>?]>
): Promise<T> {
return new Promise((resolve, reject) => {
BX24.callBatch(calls, (results) => {
const output = {} as T;
let hasError = false;
for (const [key, result] of Object.entries(results)) {
if (result.error()) {
hasError = true;
console.error(`Batch error for "${key}":`, result.error());
} else {
(output as Record<string, unknown>)[key] = result.data();
}
}
if (hasError) reject(new Error('Batch had errors'));
else resolve(output);
});
});
}
// Load a deal with related data in a single request
const data = await callBatch<{
deal: BX24Deal;
contact: BX24Contact;
history: BX24Activity[];
}>({
deal: ['crm.deal.get', { id: dealId }],
contact: ['crm.contact.get', { id: contactId }],
history: ['crm.activity.list', { filter: { OWNER_ID: dealId, OWNER_TYPE_ID: '2' } }],
});
React + TypeScript Application in a Bitrix24 iframe
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
BX24.init(() => {
const container = document.getElementById('app');
if (!container) return;
const root = createRoot(container);
root.render(<App />);
// Auto-resize iframe height
const resizeObserver = new ResizeObserver(() => {
BX24.resizeWindow(
document.body.scrollWidth,
document.body.scrollHeight
);
});
resizeObserver.observe(document.body);
});
Timeline
| Task | Duration |
|---|---|
| TypeScript setup, BX24 SDK and CRM entity types | 1–2 days |
| Simple iframe application (view/edit CRM data) | 3–5 days |
| Full-featured React application in Bitrix24 | 2–4 weeks |
| Server-side Node.js/TypeScript application with OAuth | 1–2 weeks |







