REST API development for a 1C-Bitrix website

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
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Developing a REST API for a 1C-Bitrix Website

Bitrix has a built-in REST API for Bitrix24, but for a "1C-Bitrix: Site Management" website there is no native REST — it must be built. This need arises regularly: a mobile application requires catalog data, a third-party service wants to receive orders, a React or Vue frontend needs to be fed data without page reloads. Let's walk through how to build an API on D7 that handles load and does not turn into a mess six months later.

Architecture: Controller + Router

The foundation of the API is controllers based on \Bitrix\Main\Engine\Controller. Each controller is responsible for one resource:

/local/modules/my.api/lib/
├── Controllers/
│   ├── ProductController.php    → GET /api/v1/products
│   ├── OrderController.php      → GET/POST /api/v1/orders
│   ├── CategoryController.php   → GET /api/v1/categories
│   └── AuthController.php       → POST /api/v1/auth/token
├── Services/
│   ├── ProductService.php
│   └── OrderService.php
├── Transformers/
│   ├── ProductTransformer.php   → response formatting
│   └── OrderTransformer.php
└── Middleware/
    ├── AuthMiddleware.php
    └── RateLimitMiddleware.php

Controller Implementation

namespace MyApi\Controllers;

use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\ActionFilter;
use MyApi\Services\ProductService;
use MyApi\Middleware\AuthMiddleware;

class ProductController extends Controller
{
    public function configureActions(): array
    {
        return [
            'list'   => ['prefilters' => [new AuthMiddleware()]],
            'detail' => ['prefilters' => [new AuthMiddleware()]],
            'create' => ['prefilters' => [new AuthMiddleware(), new ActionFilter\HttpMethod(['POST'])]],
        ];
    }

    public function listAction(int $page = 1, int $perPage = 20, string $category = ''): array
    {
        $service = new ProductService();
        $result  = $service->getList($page, $perPage, $category);

        return [
            'data'  => $result['items'],
            'meta'  => [
                'total'    => $result['total'],
                'page'     => $page,
                'per_page' => $perPage,
                'pages'    => ceil($result['total'] / $perPage),
            ],
        ];
    }

    public function detailAction(int $id): array
    {
        $service = new ProductService();
        $product = $service->getById($id);

        if (!$product) {
            $this->addError(new \Bitrix\Main\Error('Product not found', 404));
            return [];
        }

        return ['data' => $product];
    }
}

Authentication

API keys — the simplest option for server-to-server integrations:

namespace MyApi\Middleware;

use Bitrix\Main\Engine\ActionFilter\Base;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;

class AuthMiddleware extends Base
{
    public function onBeforeAction(Event $event): ?EventResult
    {
        $request = \Bitrix\Main\Application::getInstance()->getContext()->getRequest();
        $apiKey  = $request->getHeader('X-Api-Key');

        $validKey = \Bitrix\Main\Config\Option::get('my.api', 'api_key');
        if ($apiKey !== $validKey) {
            $this->addError(new \Bitrix\Main\Error('Unauthorized', 401));
            return new EventResult(EventResult::ERROR);
        }

        return null;
    }
}

JWT — for user requests (mobile app, SPA). The firebase/php-jwt library via Composer in /local/:

$decoded = \Firebase\JWT\JWT::decode(
    substr($authHeader, 7), // strip 'Bearer '
    new \Firebase\JWT\Key($secret, 'HS256')
);
$userId = (int)$decoded->sub;

Tokens are stored on the client side. Refresh tokens are saved in b_option or in a custom ORM table bound to the user.

Response Format

A consistent format is the foundation of predictable API behavior:

// Successful response
{
    "status": "ok",
    "data": { ... },
    "meta": { "total": 150, "page": 1 }
}

// Error
{
    "status": "error",
    "errors": [
        { "code": "NOT_FOUND", "message": "Product not found" }
    ]
}

The Bitrix controller formats the response automatically, but the default format is opinionated. For full control — override processAfterAction:

protected function processAfterAction(Action $action, $result)
{
    $response = \Bitrix\Main\Application::getInstance()->getContext()->getResponse();
    $response->addHeader('Content-Type', 'application/json; charset=utf-8');

    if ($this->getErrors()) {
        echo json_encode([
            'status' => 'error',
            'errors' => array_map(fn($e) => [
                'code'    => $e->getCode(),
                'message' => $e->getMessage(),
            ], $this->getErrors()),
        ], JSON_UNESCAPED_UNICODE);
    } else {
        echo json_encode([
            'status' => 'ok',
            'data'   => $result,
        ], JSON_UNESCAPED_UNICODE);
    }

    exit;
}

CORS

For an API called from a different domain:

// At the beginning of the controller or in a separate middleware
$response = \Bitrix\Main\Application::getInstance()->getContext()->getResponse();
$response->addHeader('Access-Control-Allow-Origin', 'https://app.example.com');
$response->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Api-Key');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

Documentation: OpenAPI / Swagger

An API without documentation is an API only for the person who wrote it. Endpoint descriptions in OpenAPI 3.0 format are created manually or generated from annotations (via libraries such as zircote/swagger-php). Swagger UI is deployed as a static page in /local/swagger/.

Rate Limiting

Request counter based on Redis or via b_option (for low load):

$key    = 'api_ratelimit_' . md5($apiKey);
$count  = (int)\Bitrix\Main\Data\Cache::createInstance()->get($key);
if ($count > 1000) { // 1000 requests per hour
    http_response_code(429);
    exit(json_encode(['error' => 'Rate limit exceeded']));
}
// increment via Redis or custom counter

Timeline

Task Timeline
Basic REST API (3–5 resources, API key, JSON responses) 1.5–2 weeks
API with JWT authentication, user permissions, documentation 3–5 weeks
Full-featured API with versioning, rate limiting, tests, CI 6–10 weeks

A REST API on Bitrix is built from standard building blocks — controllers, ORM, cache. The complexity lies not in the technology but in the design: correct endpoints, consistent response formats, handling edge cases. A poorly designed API becomes a burden that drags down both sides of the integration.