Adding Custom Methods to the REST API of the On-Premise Version of Bitrix24
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
The on-premise version of Bitrix24 gives access to the server-side code, so the REST API can be extended: you can add your own methods and your own permissions — scopes. A custom method is called the same way as a standard one: at an address such as /rest/mycompany.warehouse.get, with regular authorization by a webhook or an application token.
The mechanism fits the cases where the data resides in the on-premise Bitrix24 itself and no standard method covers it. Typical tasks:
- expose the data of your own module or table: warehouse stock, price lists, records of an accounting system
- collect in a single call the data that would otherwise have to be retrieved with several requests and merged on the application side
- give an external service narrow access: your own scope with two or three methods instead of permissions for the entire CRM
A custom method can be added only in the on-premise version — the cloud Bitrix24 provides no access to the server-side code. In the cloud, the same tasks are handled with standard tools: custom fields and CRM smart processes to retain your own data, or an external service that the application calls directly.
What you need:
- an on-premise Bitrix24 with the rest module installed
- access to the server file system with permission to edit
/bitrix/php_interface/init.php. These are server administrator permissions, not Bitrix24 administrator permissions - knowledge of PHP and the Bitrix24 core API
To make a custom method work, three steps are required:
- Register the event handler that describes the method
- Write the handler function that does the work
- Grant access to your scope and reset the permission cache
How It Works
Four elements are involved in the mechanism:
| Element | Role |
|---|---|
The OnRestServiceBuildDescription event of the rest module |
Builds the description of all REST API methods. Your handler adds your own scopes and methods to that description |
| Method description | An array with the callback and options keys. It tells the rest module which PHP function to call for the method |
| Method handler function | Does the work and returns the data that goes into the response |
The \CRestServer object |
An instance of the current REST server. It is passed to the handler function and provides access to the request and authorization data |
A request to a custom method is processed as follows:
- The rest module fires the
OnRestServiceBuildDescriptionevent and merges the arrays returned by all handlers of the event. The outcome is a combined description of methods grouped by scope - The module looks for the called method in that description. If the method is not there, the onFindMethodDescription event fires and can supply a description on the fly. If it returns nothing either, the request fails with the
ERROR_METHOD_NOT_FOUNDerror - The module checks the authorization and the access to the scope the method belongs to. If the application or the webhook does not have the required scope, the call fails with the
insufficient_scopeerror - The module calls the handler function and passes three parameters to it
- The value returned by the function goes into the
resultfield of the response. An exception is converted into a REST error with theerroranderror_descriptionfields
The descriptions of different event handlers are merged recursively. Three rules follow from this:
- identical scope names are safe, the methods of both handlers end up in the same scope
- an identical method name in two descriptions does not raise an error: the call still works, but only one of the handlers runs and the other one is silently ignored
- for the same reason, a custom description can silently override a standard method: one of the two handlers runs, and which one is not determined in advance. Such an override cannot be relied upon, whereas someone else's scenario can be broken by accident
Start method names with the name of your scope — that way they collide neither with custom methods of others nor with standard ones.
The rest module checks only the authorization and the access to the scope. It does not check permissions for specific data: if the application has been granted your scope, the method will be called. Check permissions inside the handler function yourself. By the time of the call, the $USER object is already initialized with the user the token or the webhook is bound to — rely on it and on the API of your own module.
Step 1. Register the Event Handler
The OnRestServiceBuildDescription event handler is registered in the /bitrix/php_interface/init.php file with the AddEventHandler function. The class with the methods of your API is declared in the same file or included in init.php as a separate file — before the AddEventHandler call. The function takes the module name, the event name, and the callable of the handler:
AddEventHandler(
'rest',
'OnRestServiceBuildDescription',
['MyClass', 'onRestServiceBuildDescription']
);
OnRestServiceBuildDescription is an event of the old format, which is why it is registered with AddEventHandler and not through the event manager of the current core.
The handler returns an array with the following structure:
return [
'scope_name' => [
'scope_name.object.action' => [
'callback' => ['MyClass', 'firstMethod'],
'options' => [],
],
'scope_name.object.other_action' => [
'callback' => ['MyClass', 'secondMethod'],
'options' => [],
],
],
];
| Key | Description |
|---|---|
| Scope name | An arbitrary permission name, scope_name in the example. How to grant access to your scope is described in step 3. A method can also be placed into a standard scope, crm for example — then any application with that permission calls it, and access cannot be restricted separately. Create your own scope for your own data |
| Method name | An arbitrary method name, scope_name.object.action in the example. Case does not matter: the rest module converts method names to lowercase. The traditional naming convention is scope name, object, action |
callback |
The PHP callable type. Use the same forms as the standard handlers: a function name or an array of the form [class, method] |
options |
An array of additional method configurations. One key is currently supported — private. With the value true, the method does not appear in the methods output but remains available for calls. This is how service and internal methods that should not be listed are marked |
A method can be made available to any application without a separate permission. To do this, specify the \CRestUtil::GLOBAL_SCOPE constant instead of a scope name.
A method in \CRestUtil::GLOBAL_SCOPE is called by any application and any webhook of this Bitrix24, and access to it cannot be restricted with permissions. Place there only the methods that are safe to open to everyone. For everything else, create your own scope.
Step 2. Write the Handler Function
The handler function receives three parameters:
| Parameter | Type | Description |
|---|---|---|
$query |
array | An associative array of call parameters without the authorization parameters. The start parameter is removed from it |
$start |
int | The value of the start parameter from the request. Zero if the parameter is not passed. Used for pagination |
$server |
\CRestServer | The object of the current REST server |
The $server object provides access to the request and authorization data:
| Method | What It Returns |
|---|---|
getScope() |
The scope in which the called method was found |
getMethod() |
The name of the called method in lowercase |
getQuery() |
The same array of call parameters that arrives in the handler as the first parameter |
getAuthType() |
The authorization type of the call: oauth — an application, apauth — an inbound webhook, sessionauth — a call authorized by the session of the current user |
getAppId() |
The identifier of the application on behalf of which the call is made. Returns null for a webhook |
The handler function can:
- return an array or a scalar value — it goes into the
resultfield of the response and is converted to the json or xml format - throw an exception — it is caught and returned to the client as a REST error
How to Return an Error
To set the HTTP status of an error, throw the \Bitrix\Rest\RestException exception. The constructor takes three parameters: the error message, the error code, and the HTTP status.
throw new \Bitrix\Rest\RestException(
'Parameter id is required',
'WAREHOUSE_ID_REQUIRED',
\CRestServer::STATUS_WRONG_REQUEST
);
The message goes into the error_description field of the response, and the code — into the error field. If the status is not specified, the response is returned with the 400 Bad Request status.
HTTP statuses are defined by the constants of the \CRestServer class. Only error statuses are passed into an exception — the first two constants refer to a successful response and are not used in RestException.
| Constant | HTTP status |
|---|---|
STATUS_OK |
200 OK |
STATUS_CREATED |
201 Created |
STATUS_WRONG_REQUEST |
400 Bad Request |
STATUS_UNAUTHORIZED |
401 Unauthorized |
STATUS_PAYMENT_REQUIRED |
402 Payment Required |
STATUS_FORBIDDEN |
403 Forbidden |
STATUS_NOT_FOUND |
404 Not Found |
STATUS_TO_MANY_REQUESTS |
429 Too Many Requests |
STATUS_INTERNAL |
500 Internal Server Error |
Any other exception is converted into a REST error as well. By default this is the 400 Bad Request status and the ERROR_CORE code, but if the exception has a code of its own, that code goes into the response. Core exceptions are handled separately: \Bitrix\Main\ArgumentException produces the ERROR_ARGUMENT code, and an SQL error produces the ERROR_CORE code and the 500 Internal Server Error status.
If the core generated an old-style error through $APPLICATION->ThrowException() before the exception was thrown, it overwrites the code and the message in the response. Make sure that no unhandled old core errors are left in the method handler function.
Step 3. Grant Access to Your Scope
Your scope appears in the list of permissions when a local application or an inbound webhook is created — in the Applications > Developer resources section. It is displayed by its code, mycompany for example: it has no title in the interface language.
The list of scopes is cached for seven days. The cache is force-reset only when modules are installed or removed, and an event handler in init.php is not a module. Therefore, after adding a new scope, reset the cache by calling \Bitrix\Rest\Engine\ScopeManager::cleanCache().
The call is needed once, after a new scope is registered. Do not leave it in init.php: there it fires on every request to Bitrix24 and resets the cache permanently. Run it once — for example, as a separate script in the site root:
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
\Bitrix\Main\Loader::includeModule('rest');
\Bitrix\Rest\Engine\ScopeManager::cleanCache();
Open the script in a browser once and delete it from the server. If the Bitrix24 cache is stored in files, a full cache cleanup gives the same result: the /bitrix/admin/cache.php page of the administrative section, the Delete Cache Files tab, the All option.
The same cleanCache() call also resets the cache of method.get results — it resides in the same cache directory. There is no need to clear it separately.
Verify the result with the calls:
- method.get with the
nameparameter — it returnsisExistingandisAvailable: whether the method is registered and whether it is available with the current permissions - scope with the
fullparameter — the new scope must appear in the full list of permissions. Without parameters, the method returns only the permissions already granted to the application or the webhook
How to read the method.get response:
isExisting: false— the method is not registered. Check whether the event handler is connected and whether the cache is resetisExisting: trueandisAvailable: false— the method exists, but the required scope is not granted to the application or the webhook. When called, such a method returns theinsufficient_scopeerror
Example: A Custom Scope and Method
The code registers the mycompany scope and the mycompany.warehouse.get method in it. The method checks a required parameter and the user permissions, and then returns the data.
class MyCompanyRestApi
{
public static function onRestServiceBuildDescription(): array
{
return [
'mycompany' => [
'mycompany.warehouse.get' => [
'callback' => [__CLASS__, 'getWarehouse'],
'options' => [],
],
],
];
}
public static function getWarehouse($query, $start, \CRestServer $server): array
{
global $USER;
$warehouseId = (int)($query['id'] ?? 0);
if ($warehouseId <= 0)
{
throw new \Bitrix\Rest\RestException(
'Parameter id is required',
'WAREHOUSE_ID_REQUIRED',
\CRestServer::STATUS_WRONG_REQUEST
);
}
// the handler checks data permissions itself, the rest module does not do it
if (!$USER->IsAdmin())
{
throw new \Bitrix\Rest\RestException(
'Access to warehouse data denied',
'ACCESS_DENIED',
\CRestServer::STATUS_FORBIDDEN
);
}
return [
'id' => $warehouseId,
'title' => 'Central warehouse',
'scope' => $server->getScope(),
];
}
}
AddEventHandler(
'rest',
'OnRestServiceBuildDescription',
['MyCompanyRestApi', 'onRestServiceBuildDescription']
);
Requests to a custom method are built by the general REST rules — they are described in the How a Request Is Executed article. A successful call:
GET /rest/mycompany.warehouse.get?auth=**put_access_token_here**&id=12
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"result": {
"id": 12,
"title": "Central warehouse",
"scope": "mycompany"
},
"time": {
"start": 1791540000.123456,
"finish": 1791540000.234567,
"duration": 0.111111,
"processing": 0.021,
"date_start": "2026-10-09T12:00:00+02:00",
"date_finish": "2026-10-09T12:00:00+02:00"
}
}
A call without the required parameter, in the xml format:
GET /rest/mycompany.warehouse.get.xml?auth=**put_access_token_here**
HTTP/1.1 400 Bad Request
Content-Type: text/xml; charset=utf-8
<?xml version="1.0" ?>
<response>
<error>WAREHOUSE_ID_REQUIRED</error>
<error_description>Parameter id is required</error_description>
</response>
Pagination in Custom Methods
If the method returns a list, inherit the class from \IRestService and use its getNavData and setNavData methods. They build the same pagination as the standard methods: the page size is 50 records, the position of the next page is returned in the next field, and the total number — in the total field. Your method behaves the same way as the standard list methods — their common contract is described in the Features of List Methods article.
getNavData takes two parameters: the start value from the request and the flag of an ORM class. With the value true the method returns an array with the limit and offset keys for ORM methods, and with the value false — an array with the nPageSize and iNumPage keys for the methods of the old core.
setNavData takes the selected records and a pagination array with the keys count — the total number of records, and offset — the offset of the current page. The method adds the next and total fields to the result, which the rest module lifts to the top level of the response.
\Bitrix\Main\Loader::includeModule('rest');
class MyCompanyRestList extends \IRestService
{
public static function onRestServiceBuildDescription(): array
{
return [
'mycompany' => [
'mycompany.user.list' => [
'callback' => [__CLASS__, 'getUserList'],
'options' => [],
],
],
];
}
public static function getUserList($query, $start, \CRestServer $server): array
{
$navData = static::getNavData($start, true);
$result = \Bitrix\Main\UserTable::getList([
'filter' => $query['filter'] ?? [],
'select' => $query['select'] ?? ['ID', 'NAME', 'LAST_NAME'],
'order' => $query['order'] ?? ['ID' => 'ASC'],
'limit' => $navData['limit'],
'offset' => $navData['offset'],
'count_total' => true,
]);
return static::setNavData(
$result->fetchAll(),
[
'count' => $result->getCount(),
'offset' => $navData['offset'],
]
);
}
}
AddEventHandler(
'rest',
'OnRestServiceBuildDescription',
['MyCompanyRestList', 'onRestServiceBuildDescription']
);
The \Bitrix\Main\Loader::includeModule('rest') call is required before the class declaration: without it the \IRestService class is not loaded yet and the code in init.php fails with an error.
Example request:
GET /rest/mycompany.user.list?auth=**put_access_token_here**&order[ID]=ASC&filter[<ID]=1000&select[]=ID&select[]=NAME&start=50
In the response, next appears only if there is a next page. The result array in the example is shortened to two records out of fifty:
{
"result": [
{"ID": "51", "NAME": "Klaus"},
{"ID": "52", "NAME": "Petra"}
],
"next": 100,
"total": 137
}
How to Supply a Method Description on the Fly
The onFindMethodDescription event allows you to describe a method at the moment of the call instead of in advance. It is needed when the set of methods is unknown at startup: for example, when the method name is built from the name of an object created by a user.
The handler receives two parameters: the name of the called method in lowercase and the requested scope. To handle the call, return an array with the scope key and the method description. To decline handling, return null — then the rest module polls the remaining handlers.
AddEventHandler('rest', 'onFindMethodDescription', 'myCompanyFindMethodDescription');
function myCompanyFindMethodDescription($method, $scope)
{
if (mb_strpos($method, 'mycompany.dynamic.') !== 0)
{
return null;
}
return [
'scope' => 'mycompany',
'callback' => ['MyCompanyDynamicRest', 'getDynamicItem'],
'options' => [],
];
}
The MyCompanyDynamicRest class with the getDynamicItem method is written the same way as the method handler from step 2: the same three input parameters, the same way to return data or an error.
Such methods do not appear in the methods output: they are not present in the combined description.