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_code ↔ IBLOCK_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 |







