Developing a typed SDK for the 1C-Bitrix API

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1189
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    813
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Developing a Typed SDK for the 1C-Bitrix API

Writing integrations with 1C-Bitrix without an SDK means constant copy-pasting of authorization code, manual request serialization, and string method names with no autocomplete. After six months the project turns into an unreadable tangle of curl_exec() with json_decode(). A typed SDK solves this: each API method is a separate class or method with PHPDoc, IDE autocomplete, a single configuration point, and built-in error handling.

What an SDK for Bitrix Is

In this context, an SDK is a PHP library that encapsulates interaction with one of the APIs:

  1. REST API of Bitrix24 (cloud and on-premise) — methods like crm.lead.add, tasks.task.update.
  2. 1C-Bitrix (site) API — internal classes CIBlockElement, \Bitrix\Sale\Order, D7 ORM.
  3. Webhook API — incoming and outgoing Bitrix24 webhooks.

An SDK is most commonly developed for the Bitrix24 REST API, which is used by external applications and integrations.

Structure of a Typed REST API SDK

src/
├── Client/
│   ├── BitrixClient.php          # HTTP client (Guzzle/native curl)
│   ├── TokenStorage.php          # OAuth token storage
│   └── RetryMiddleware.php       # Retry logic for 503/rate limit
├── Api/
│   ├── Crm/
│   │   ├── LeadApi.php
│   │   ├── DealApi.php
│   │   ├── ContactApi.php
│   │   └── CompanyApi.php
│   ├── Tasks/
│   │   ├── TaskApi.php
│   │   └── CommentApi.php
│   └── Catalog/
│       ├── ProductApi.php
│       └── PriceApi.php
├── Dto/
│   ├── Crm/
│   │   ├── Lead.php
│   │   ├── LeadCreateRequest.php
│   │   └── LeadListRequest.php
│   └── Common/
│       ├── Pagination.php
│       └── Filter.php
├── Exception/
│   ├── BitrixApiException.php
│   ├── AuthException.php
│   └── RateLimitException.php
└── BitrixSdk.php                 # Entry point / DI container

HTTP Client

namespace BitrixSdk\Client;

class BitrixClient
{
    private string $baseUrl;
    private TokenStorage $tokenStorage;
    private \GuzzleHttp\Client $http;

    public function __construct(string $baseUrl, TokenStorage $tokenStorage)
    {
        $this->baseUrl      = rtrim($baseUrl, '/') . '/rest/';
        $this->tokenStorage = $tokenStorage;
        $this->http         = new \GuzzleHttp\Client([
            'timeout'         => 30,
            'connect_timeout' => 10,
        ]);
    }

    public function call(string $method, array $params = []): array
    {
        $token  = $this->tokenStorage->getAccessToken();
        $url    = $this->baseUrl . $method;

        try {
            $response = $this->http->post($url, [
                'json' => array_merge($params, ['auth' => $token]),
            ]);

            $data = json_decode($response->getBody()->getContents(), true);

            if (!empty($data['error'])) {
                $this->handleError($data);
            }

            return $data['result'] ?? $data;

        } catch (\GuzzleHttp\Exception\ClientException $e) {
            $statusCode = $e->getResponse()->getStatusCode();
            if ($statusCode === 401) {
                // Try refreshing the token
                $this->tokenStorage->refresh();
                return $this->call($method, $params);
            }
            throw new BitrixApiException("HTTP {$statusCode}: " . $e->getMessage(), $statusCode);
        }
    }

    private function handleError(array $data): void
    {
        $errorCode = $data['error'] ?? 'UNKNOWN';
        $errorDesc = $data['error_description'] ?? '';

        if ($errorCode === 'QUERY_LIMIT_EXCEEDED') {
            // Rate limit — delay and retry
            throw new RateLimitException($errorDesc);
        }

        if (in_array($errorCode, ['NO_AUTH_FOUND', 'expired_token', 'invalid_token'])) {
            throw new AuthException($errorDesc);
        }

        throw new BitrixApiException("{$errorCode}: {$errorDesc}");
    }

    /** Batch request */
    public function batch(array $commands): array
    {
        $cmdParams = [];
        foreach ($commands as $key => $command) {
            $cmdParams[$key] = $command->toApiParam();
        }

        $result = $this->call('batch', ['cmd' => $cmdParams]);
        return $result['result'] ?? [];
    }
}

DTOs and Typed Methods

Instead of working with raw arrays — Data Transfer Objects:

namespace BitrixSdk\Dto\Crm;

class LeadCreateRequest
{
    public string $title;
    public ?string $name        = null;
    public ?string $lastName    = null;
    public ?string $phone       = null;
    public ?string $email       = null;
    public string $sourceId     = 'WEB';
    public ?string $comments    = null;
    public int $responsibleId   = 0;
    /** @var array<string, string> */
    public array $utmFields     = [];

    public function toApiArray(): array
    {
        $fields = [
            'TITLE'     => $this->title,
            'SOURCE_ID' => $this->sourceId,
        ];

        if ($this->name)     $fields['NAME']      = $this->name;
        if ($this->lastName) $fields['LAST_NAME']  = $this->lastName;
        if ($this->comments) $fields['COMMENTS']   = $this->comments;

        if ($this->phone) {
            $fields['PHONE'] = [['VALUE' => $this->phone, 'VALUE_TYPE' => 'WORK']];
        }
        if ($this->email) {
            $fields['EMAIL'] = [['VALUE' => $this->email, 'VALUE_TYPE' => 'WORK']];
        }

        foreach ($this->utmFields as $key => $value) {
            $fields[$key] = $value;
        }

        return $fields;
    }
}

class Lead
{
    public int $id;
    public string $title;
    public ?string $name;
    public ?string $lastName;
    public string $status;
    public float $opportunity;
    public \DateTimeImmutable $dateCreate;

    public static function fromApiArray(array $data): self
    {
        $lead              = new self();
        $lead->id          = (int)$data['ID'];
        $lead->title       = $data['TITLE'];
        $lead->name        = $data['NAME'] ?? null;
        $lead->lastName    = $data['LAST_NAME'] ?? null;
        $lead->status      = $data['STATUS_ID'];
        $lead->opportunity = (float)($data['OPPORTUNITY'] ?? 0);
        $lead->dateCreate  = new \DateTimeImmutable($data['DATE_CREATE']);
        return $lead;
    }
}

API Class for Leads

namespace BitrixSdk\Api\Crm;

class LeadApi
{
    public function __construct(private BitrixClient $client) {}

    public function add(LeadCreateRequest $request): int
    {
        $result = $this->client->call('crm.lead.add', [
            'fields' => $request->toApiArray(),
        ]);
        return (int)$result;
    }

    public function get(int $id): Lead
    {
        $data = $this->client->call('crm.lead.get', ['id' => $id]);
        return Lead::fromApiArray($data);
    }

    /**
     * @return Lead[]
     */
    public function list(array $filter = [], array $select = [], int $start = 0): array
    {
        $params = ['filter' => $filter, 'start' => $start];
        if ($select) $params['select'] = $select;

        $data = $this->client->call('crm.lead.list', $params);
        return array_map(fn($item) => Lead::fromApiArray($item), $data);
    }

    public function update(int $id, array $fields): bool
    {
        return (bool)$this->client->call('crm.lead.update', [
            'id'     => $id,
            'fields' => $fields,
        ]);
    }
}

SDK Entry Point

namespace BitrixSdk;

class BitrixSdk
{
    private BitrixClient $client;
    private ?Crm\LeadApi $leadApi     = null;
    private ?Crm\DealApi $dealApi     = null;
    private ?Tasks\TaskApi $taskApi   = null;

    public static function create(string $webhookUrl): self
    {
        $sdk         = new self();
        $storage     = new Client\WebhookTokenStorage($webhookUrl);
        $sdk->client = new Client\BitrixClient($webhookUrl, $storage);
        return $sdk;
    }

    public static function createOAuth(string $domain, string $clientId, string $clientSecret, TokenStorage $storage): self
    {
        $sdk         = new self();
        $sdk->client = new Client\BitrixClient("https://{$domain}", $storage);
        return $sdk;
    }

    public function crm(): CrmFacade
    {
        return new CrmFacade($this->client);
    }

    public function tasks(): TasksFacade
    {
        return new TasksFacade($this->client);
    }
}

// Usage
$sdk  = BitrixSdk::create('https://company.bitrix24.ru/rest/1/webhook_token/');
$lead = new LeadCreateRequest();
$lead->title = 'Request from website';
$lead->name  = 'John';
$lead->phone = '+79001234567';
$leadId = $sdk->crm()->leads()->add($lead);

Rate Limiting and Queue

Bitrix24 limits requests: 2 requests/second for cloud portals. When exceeded — HTTP 503 with X-RateLimit-Reset. The SDK must handle this:

class RetryMiddleware
{
    private const MAX_RETRIES   = 3;
    private const RETRY_DELAYS  = [1, 2, 5]; // seconds

    public function handle(callable $next, string $method, array $params): array
    {
        for ($attempt = 0; $attempt <= self::MAX_RETRIES; $attempt++) {
            try {
                return $next($method, $params);
            } catch (RateLimitException $e) {
                if ($attempt === self::MAX_RETRIES) throw $e;
                sleep(self::RETRY_DELAYS[$attempt]);
            }
        }
        throw new BitrixApiException('Max retries exceeded');
    }
}

For bulk operations (importing 10,000 leads) — use the Bitrix batch method (up to 50 commands at a time) and a queue (Laravel Queue, RabbitMQ, Redis Queue).

Development Timelines

Option Scope Timeline
Basic SDK CRM methods, HTTP client, DTOs, error handling 8–12 days
Extended + Tasks, catalog, webhooks, rate limiting 14–20 days
Full SDK with tests + PHPUnit tests, CI/CD, Packagist package 20–30 days