Integration of 1C-Bitrix with the Akeneo PIM system

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

Integration of 1C-Bitrix with Akeneo PIM System

Akeneo is a specialized product information management (PIM) system. When a catalog contains thousands of products with dozens of attributes, translations in multiple languages, and variants for different channels, storing everything directly in the Bitrix infoblock becomes inconvenient. Akeneo handles data enrichment and validation, while Bitrix handles sales and transactions. Integration is built on the Akeneo REST API.

Data Flow Architecture

Typical flow:

1C (product catalog, prices, inventory)
            ↓
         Akeneo
   (descriptions, attributes, media, translations)
            ↓
         Bitrix
    (catalog, cart, orders)

Akeneo is the "master" of product content. 1C is the source for SKUs, prices, and inventory. Bitrix is the sales channel that reads data from Akeneo.

Synchronization direction: Akeneo → Bitrix. Reverse flow (Bitrix → Akeneo) is rarely used — only for transmitting analytics (views, orders) if needed.

Akeneo REST API

Akeneo provides REST API with OAuth 2.0 (client credentials):

POST /api/oauth/v1/token
GET  /api/rest/v1/products?search={"enabled":[{"operator":"=","value":true}]}
GET  /api/rest/v1/products/{code}
GET  /api/rest/v1/product-models
GET  /api/rest/v1/categories
GET  /api/rest/v1/attributes
GET  /api/rest/v1/media-files/{code}/download

Token acquisition:

class AkeneoClient
{
    private string $baseUrl;
    private string $token;

    public function __construct(
        string $baseUrl,
        string $clientId,
        string $secret,
        string $username,
        string $password
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->token   = $this->authenticate($clientId, $secret, $username, $password);
    }

    private function authenticate(
        string $clientId,
        string $secret,
        string $username,
        string $password
    ): string {
        $http = new \Bitrix\Main\Web\HttpClient();
        $http->setHeader('Authorization',
            'Basic ' . base64_encode($clientId . ':' . $secret));
        $http->setHeader('Content-Type', 'application/json');

        $response = $http->post(
            $this->baseUrl . '/api/oauth/v1/token',
            json_encode([
                'grant_type' => 'password',
                'username'   => $username,
                'password'   => $password,
            ])
        );

        $data = json_decode($response, true);
        return $data['access_token'] ?? throw new \RuntimeException('Akeneo auth failed');
    }

    public function getProducts(int $page = 1, int $limit = 100): array
    {
        $http = new \Bitrix\Main\Web\HttpClient();
        $http->setHeader('Authorization', 'Bearer ' . $this->token);

        $response = $http->get(
            $this->baseUrl . '/api/rest/v1/products'
            . '?page=' . $page . '&limit=' . $limit
            . '&with_attribute_options=true'
        );

        return json_decode($response, true)['_embedded']['items'] ?? [];
    }

    public function getMediaFile(string $code): string
    {
        $http = new \Bitrix\Main\Web\HttpClient();
        $http->setHeader('Authorization', 'Bearer ' . $this->token);
        return $http->get($this->baseUrl . '/api/rest/v1/media-files/' . $code . '/download');
    }
}

Akeneo Attribute Mapping → Bitrix

Each Akeneo attribute must be mapped to a Bitrix infoblock property. The mapping is stored in a configuration file or HL-block:

// /local/config/akeneo-mapping.php
return [
    // 'akeneo_attribute_code' => 'BITRIX_PROPERTY_CODE'
    'description'         => 'DETAIL_TEXT',          // Special field
    'short_description'   => 'PREVIEW_TEXT',
    'brand'               => 'BRAND',
    'weight'              => 'WEIGHT',
    'color'               => 'COLOR',
    'material'            => 'MATERIAL',
    'care_instructions'   => 'CARE',
    'country_of_origin'   => 'COUNTRY_ORIGIN',
];

Akeneo attributes can be localized (different values for different locales). Locale mapping:

$localeMap = [
    'ru_RU' => 'ru',
    'en_US' => 'en',
    'de_DE' => 'de',
];

Synchronization: Agent Logic

A Bitrix agent triggers synchronization on schedule. For 50,000+ products — incremental mode (only changed since last sync):

function syncAkeneoProductsAgent(): string
{
    $lastSync = \Bitrix\Main\Config\Option::get('akeneo_sync', 'last_sync', '');
    $client   = new AkeneoClient(
        AKENEO_URL, AKENEO_CLIENT_ID, AKENEO_SECRET,
        AKENEO_USER, AKENEO_PASSWORD
    );

    $mapping    = include '/local/config/akeneo-mapping.php';
    $syncTime   = date('c');
    $page       = 1;

    do {
        $products = $client->getProducts($page, 100);
        foreach ($products as $akeneoProduct) {
            syncSingleProduct($akeneoProduct, $mapping, $client);
        }
        $page++;
    } while (count($products) === 100);

    \Bitrix\Main\Config\Option::set('akeneo_sync', 'last_sync', $syncTime);

    return __FUNCTION__ . '();';
}

function syncSingleProduct(array $product, array $mapping, AkeneoClient $client): void
{
    $sku     = $product['identifier']; // Akeneo product code = article
    $enabled = $product['enabled'];

    // Find product by article
    $existing = CIBlockElement::GetList(
        [],
        ['IBLOCK_ID' => CATALOG_IBLOCK_ID, 'PROPERTY_CML2_ARTICLE' => $sku]
    )->Fetch();

    $el     = new CIBlockElement();
    $fields = [
        'IBLOCK_ID' => CATALOG_IBLOCK_ID,
        'ACTIVE'    => $enabled ? 'Y' : 'N',
        'NAME'      => getAkeneoValue($product['values']['name'] ?? [], 'ru_RU'),
    ];

    // Process properties via mapping
    $properties = [];
    foreach ($mapping as $akeneoCode => $bitrixCode) {
        $value = getAkeneoValue($product['values'][$akeneoCode] ?? [], 'ru_RU');
        if ($value !== null) {
            if (in_array($bitrixCode, ['DETAIL_TEXT', 'PREVIEW_TEXT', 'NAME'])) {
                $fields[$bitrixCode] = $value;
            } else {
                $properties[$bitrixCode] = $value;
            }
        }
    }

    if ($existing) {
        $el->Update($existing['ID'], $fields);
        CIBlockElement::SetPropertyValuesEx($existing['ID'], CATALOG_IBLOCK_ID, $properties);
    } else {
        $fields['IBLOCK_SECTION_ID'] = resolveCategoryId($product['categories'][0] ?? null);
        $newId = $el->Add($fields);
        if ($newId) {
            CIBlockElement::SetPropertyValuesEx($newId, CATALOG_IBLOCK_ID, $properties);
        }
    }
}

function getAkeneoValue(array $values, string $locale): mixed
{
    foreach ($values as $entry) {
        if (($entry['locale'] === $locale || $entry['locale'] === null)
            && $entry['scope'] === null) {
            return $entry['data'];
        }
    }
    return null;
}

Media File Synchronization

Images in Akeneo are stored as media files. An attribute of type pim_catalog_image returns a file code (_media_links). Download and save to Bitrix:

function syncProductImage(int $productId, string $mediaCode, AkeneoClient $client): void
{
    $cacheKey = 'akeneo_media_' . md5($mediaCode);
    $cache    = \Bitrix\Main\Data\Cache::createInstance();

    if ($cache->initCache(86400 * 7, $cacheKey, '/akeneo/media')) {
        return; // already downloaded
    }

    $imageContent = $client->getMediaFile($mediaCode);
    $ext          = pathinfo($mediaCode, PATHINFO_EXTENSION) ?: 'jpg';
    $tmpFile      = sys_get_temp_dir() . '/' . $productId . '_' . uniqid() . '.' . $ext;

    file_put_contents($tmpFile, $imageContent);

    $fileId = \CFile::MakeFileArray($tmpFile);
    \CIBlockElement::SetPropertyValues($productId, CATALOG_IBLOCK_ID, $fileId, 'MORE_PHOTO');

    unlink($tmpFile);

    $cache->startDataCache(86400 * 7, $cacheKey, '/akeneo/media');
    $cache->endDataCache([]);
}

Category Synchronization

Akeneo category hierarchy is synchronized to infoblock sections:

GET /api/rest/v1/categories?parent=master

The response contains code, parent, labels. Recursively create infoblock sections via CIBlockSection::Add(), storing the mapping akeneo_codeIBLOCK_SECTION_ID in the HL-block AkeneoMapping.

Implementation Timeline

Scope Components Timeline
1,000–5,000 SKU, basic attributes Client + mapping + agent 1–2 weeks
10,000–50,000 SKU + media files + i18n Incremental sync + processing queue 3–4 weeks
Two-way synchronization + product models Full bi-directional exchange + Webhook 5–7 weeks