API Elbuz

11 хв

API дає зовнішній програмі доступ до даних вашого акаунта: товари, прайс-листи, контрагенти, документи, довідники. Це стаття-довідник: усі приклади нижче виконані на живому акаунті, і відповіді в них справжні.

Стаття розрахована на розробника. Адміністратору достатньо першого розділу: завести ключ і передати його тому, хто пише інтеграцію.

Крок 1. Ключ доступу

Вікно
Налаштування системи → розділ «Інші налаштування» → плитка «API та токени доступу»
Що створюєте
пару «користувач + ключ доступу» для однієї зовнішньої програми
Скільки
окремий ключ на кожну інтеграцію
Розділ «Інші налаштування» з плиткою «API та токени доступу»
1Плитка «API та токени доступу»
2Розділ «Інші налаштування»
  1. Плитка «API та токени доступу» — звідси відкривається список ключів API.
  2. Розділ «Інші налаштування» — сюди зібрані вікна, яких немає в головному меню.
Поле картки ключаПараметр у запитіЩо робить
«Користувач»usernameімʼя для входу
«Ключ доступу»keyсекрет; кнопка «Генерувати» створює його за вас
«Активність»вимкнений ключ не пускає ні на вхід, ні за вже виданим токеном
«Тільки для читання»будь-яка зміна даних відповідає помилкою; читання працює
«IP-адреси для доступу»перелік через кому, пробіл або з нового рядка. Порожньо — звідусіль
«Дата закінчення доступу»ключ працює по цю дату включно; порожня дата — безстроково
«Примітка»кому й навіщо виданий ключ

Три поля закривають доступ миттєво, а не з наступного входу. «Активність», «Дата закінчення доступу» й «Тільки для читання» перечитуються з картки ключа на кожному запиті, тому діють і на вже виданий токен: вимкнули ключ або настав наступний день після дати — поточний токен перестає працювати без перелогіну. Це зручно, коли ключ треба відкликати негайно.

Крок 2. Швидкий старт

Мінімальний робочий скрипт: вхід, запит даних, друк результату. Підставте свою адресу, імʼя користувача та ключ.

<?php
$host = 'https://ваш-акаунт.elbuz.com';   // адреса вашого акаунта
$user = 'site';                           // «Користувач» з картки ключа
$key  = 'ВАШ_КЛЮЧ';                       // «Ключ доступу»

function elbuz_call($host, $route, array $post) {
    $ch = curl_init($host . '/admin/index.php?route=' . $route);
    curl_setopt_array($ch, array(
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query($post),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 120,
    ));
    $body = curl_exec($ch);
    return json_decode($body, true);
}

// 1. вхід — отримуємо токен
$login = elbuz_call($host, 'api/login', array('username' => $user, 'key' => $key));
if (empty($login['api_token'])) {
    exit('Не вийшло увійти: ' . json_encode($login, JSON_UNESCAPED_UNICODE));
}
$token = $login['api_token'];

// 2. запит даних
$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'category',
    'max_rows'    => 2,
    'limit_field' => 'name',
));

print_r($res);

Відповідь на вхід:

{
    "success": "Сеанс API успішно розпочато!",
    "api_token": "b439dd5c3b386d36464bef450d"
}

Відповідь на запит категорій — дослівно те, що повернув сервер:

{
    "item_list": {
        "83": {
            "category_id": "83",
            "parent_id": "11",
            "name": "Варочные поверхности"
        },
        "84": {
            "category_id": "84",
            "parent_id": "11",
            "name": "Встраиваемые вытяжки"
        }
    },
    "meta": {
        "item_scope": "category",
        "total_row": 234,
        "page_total": 117,
        "page_next": 2,
        "page_current": 1,
        "request_time": "00:00:00"
    }
}

Зверніть увагу: у запиті було limit_field=name, а у відповіді ще й category_id із parent_id. Так і має бути — обовʼязкові поля довідника дописуються завжди.

Основи протоколу

ЩоЯк
Базова адресаадреса акаунта + /admin/index.php?route= + маршрут
Методтільки POST
Тіло запитузвичайна форма, application/x-www-form-urlencoded
ВідповідьJSON із заголовком Content-Type: application/json
Код HTTPзавжди 200 — навіть на помилку
Токенпараметр api_token у тілі POST або в адресі

Не орієнтуйтеся на код HTTP: помилка теж приходить із кодом 200. Розбирайте структуру: помилка — це JSON-масив рядків, успішна відповідь — обʼєкт із item_list або meta.

Час життя токена

Токен — це ідентифікатор сеансу. Сеанси, до яких не зверталися більше години, видаляються; кожен запит із токеном продовжує сеанс. Тому постійно працююча інтеграція входить один раз, а новий api/login робить лише у відповідь на помилку доступу.

function elbuz_get($host, $user, $key, &$token, array $post) {
    $post['api_token'] = $token;
    $res = elbuz_call($host, 'api/scope/get', $post);

    // помилка приходить масивом рядків — можливо, сеанс закінчився
    if (isset($res[0]) && is_string($res[0])) {
        $login = elbuz_call($host, 'api/login', array('username' => $user, 'key' => $key));
        if (empty($login['api_token'])) return $res;   // справа не в сеансі
        $token = $login['api_token'];
        $post['api_token'] = $token;
        $res = elbuz_call($host, 'api/scope/get', $post);
    }
    return $res;
}

Обмеження частоти

У ключа є межа звернень — за замовчуванням 30 за хвилину. Влаштована вона як мінімальний проміжок: 60 поділити на це число, тобто за замовчуванням не частіше ніж раз на дві секунди. Накопичити паузу й видати пачку запитів поспіль не можна — відхилений буде кожен, що прийшов раніше за проміжок.

{"Error": "Rate limit exceeded! Time passed:0.0582 Time minimal request:0.1"}

У відповіді видно обидва числа: скільки минуло від минулого запиту й скільки потрібно. Формат тут інший, ніж у решти помилок, — ключ Error з великої літери й обʼєкт замість масиву. Обробляйте обидва варіанти:

if (isset($res['Error'])) {                    // межа частоти
    sleep(2);
    // повторити той самий запит
}
if (isset($res[0]) && is_string($res[0])) {    // звичайна помилка API
    throw new RuntimeException($res[0]);
}

З інтерфейсу межа не міняється — це питання до технічної підтримки.

Мова відповідей

Тексти приходять мовою співробітника, від імені якого працює ключ — це власник акаунта. А помилка доступу без токена приходить мовою бази за замовчуванням, бо співробітник ще не визначений. Тобто два повідомлення одного API можуть бути різними мовами. Ще один аргумент розбирати відповідь за структурою, а не за текстом.

Довідник операцій

МаршрутПризначенняОбовʼязкові параметри
api/loginотримати токенusername, key
api/scope/describeопис полів довідникаapi_token, scope
api/scope/getотримати записиapi_token, scope
api/scope/addдодати запис або пачкуapi_token, scope + поля запису
api/scope/updateоновитиapi_token, scope, ідентифікатор
api/scope/deleteвидалитиapi_token, scope, ідентифікатор
api/scope/searchпошук товарів пошуковим індексомapi_token, scope=product

Довідник задають параметром scope або дописують у маршрут — обидва варіанти рівнозначні:

api/scope/get          + scope=currency
api/scope/get/currency

Довідники за операціями

Набори різні. Це перше, що варто звірити перед проєктуванням інтеграції: читати можна майже все, змінювати — далеко не все.

ОпераціяКоди довідників
getcategory, product, product_price, attribute, attribute_block, language, article, manufacturer, stock_status, warehouse, store, company, contractor, contractor_group, currency, document, document_type, document_status, document_payment_status, document_delivery_status, print_template
addcategory, product, product_price, manufacturer, contractor, document, document_extended, user, company, attribute, attribute_block, article, contractor_price, mail_send
updatecategory, product, currency, contractor, document
deletecategory, product, contractor, attribute_block, attribute, manufacturer, article, document
describeproduct, product_price, manufacturer, contractor, document, user, company
searchproduct

product_price є в додаванні, але немає в оновленні: рядки прайсу через API створюються, а змінюються тільки завантаженням прайс-листа. Плануйте інтеграцію з урахуванням цього.

describe: звідки брати назви полів

Назви полів у запитах — не ті, що підписані в інтерфейсі. describe повертає реєстр полів: внутрішню назву, людський підпис і тип.

$res = elbuz_call($host, 'api/scope/describe', array(
    'api_token'   => $token,
    'scope'       => 'contractor',
    'limit_field' => 'datafield,datafield_type,grid_field_name',
));
{
    "item_list": {
        "347": {"row_id": "347", "datafield": "name",         "datafield_type": "string", "grid_field_name": "Назва"},
        "349": {"row_id": "349", "datafield": "phones",       "datafield_type": "string", "grid_field_name": "Телефон"},
        "560": {"row_id": "560", "datafield": "status_name",  "datafield_type": "string", "grid_field_name": "Статус"},
        "562": {"row_id": "562", "datafield": "manager_name", "datafield_type": "string", "grid_field_name": "Відповідальний менеджер"}
    },
    "meta": {"item_scope": "contractor", "total_row": 118, "page_total": 2, "page_next": 2, "page_current": 1, "request_time": "00:00:00"}
}

Це той самий реєстр, який видно у вікні «Налаштувати сітку» з перемикачем «Тех. назви». Без limit_field кожне поле приходить із трьома десятками службових властивостей — беріть три потрібні колонки.

get: вибірка даних

Параметри

ПараметрЗа замовчуваннямЩо робить
scopeкод довідника, обовʼязковий
page1номер сторінки
max_rows100записів на сторінці. Стеля 500 у хмарі, 10 000 в офлайн-версії; більше просто обрізається
language_idмова акаунтамова текстових полів
limit_fieldусі поляперелік потрібних полів через кому
extendвкладені дані: фото, характеристики, позиції документа
filterscount і даліумови відбору

Обовʼязкові поля

Навіть якщо ви їх не просили, ці поля приходять завжди — без них відповідь неможливо зіставити з вашими даними:

ДовідникДописуються завжди
productproduct_id, name, price
product_pricesupply_product_id, contractor_id, price_id, product_id, sku_manufacturer, manufacturer, name, price, stock_status_id, field_ssd_stock_status_name
categorycategory_id, parent_id, name
documentdocument_id, contractor_id, type_id, document_type_name, document_number
рештавласний ідентифікатор і name

Сторінки

Записи приходять обʼєктом, а не масивом: ключ — ідентифікатор запису. У блоці meta лежить усе для обходу, ознака кінця — page_next дорівнює нулю.

$page = 1;
$all  = array();

do {
    $res = elbuz_call($host, 'api/scope/get', array(
        'api_token'   => $token,
        'scope'       => 'product',
        'max_rows'    => 500,          // стеля для хмари
        'limit_field' => 'product_id,name,price,quantity',
        'page'        => $page,
    ));

    if (!isset($res['item_list'])) break;        // помилка або порожня вибірка

    $all += $res['item_list'];                   // ключі = ID, дублів не буде
    $page = (int)$res['meta']['page_next'];

    sleep(2);                                     // межа частоти за замовчуванням
} while ($page > 0);

echo 'отримано: ' . count($all);

На довіднику валют із тринадцяти записів по пʼять на сторінку цей цикл робить рівно три запити: третя сторінка повертає page_next: 0, і обхід завершується.

Порожня вибірка — це не порожній item_list, а помилка «Елемент не знайдено». Якщо фільтр нікого не пропустив, перевірка isset($res['item_list']) обірве цикл — саме тому вона стоїть до розбору.

Фільтри

Для product і document працює той самий відбір, що й у сітках інтерфейсу: filterscount — скільки умов, далі для кожної четвірка параметрів із номером, починаючи з нуля.

ПараметрЗначення
filterdatafield0поле, як у describe
filtervalue0значення
filtercondition0EQUAL, NOT_EQUAL, CONTAINS, STARTS_WITH, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL
filteroperator00 — зʼєднати з наступною умовою через «і», 1 — через «або»

Товари, змінені після заданої дати:

$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'        => $token,
    'scope'            => 'product',
    'max_rows'         => 2,
    'limit_field'      => 'product_id,name,quantity,date_modified',
    'filterscount'     => 1,
    'filterdatafield0' => 'date_modified',
    'filtervalue0'     => '2026-01-01',
    'filtercondition0' => 'GREATER_THAN_OR_EQUAL',
    'filteroperator0'  => 0,
));
{
    "item_list": {
        "6068": {
            "product_id": "6068",
            "price": "1200.00000000",
            "quantity": "0",
            "date_modified": "2026-09-12 14:54:23",
            "name": "Ardesto LESH-Y305"
        }
    },
    "meta": {
        "item_scope": "product",
        "total_row": 126999,
        "page_total": 63500,
        "page_next": 2,
        "page_current": 1,
        "request_time": "00:00:00"
    }
}

Фільтрувати можна й за характеристиками: імʼя поля — attribute_ плюс номер характеристики, його дає describe.

Вкладені дані

Фото, характеристики й позиції документа лежать не в самому записі — їх домовляють параметром extend через кому.

ДовідникЗначення extendПоле у відповіді
productattribute, image, file, video, categoryextend_attribute, extend_image, extend_file, extend_video, extend_category
product_priceattribute, imageті самі
documentcontractor, productextend_contractor, extend_product
$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'product',
    'max_rows'    => 1,
    'limit_field' => 'product_id,name',
    'extend'      => 'image,category',
));
{
    "item_list": {
        "6068": {
            "product_id": "6068",
            "price": "1200.00000000",
            "name": "Ardesto LESH-Y305",
            "extend_image": [
                {
                    "product_image_id": "201587",
                    "image": "100/68/product_image_6068_201587.jpg",
                    "sort_order": "1",
                    "image_original": "https://…/product_image_6068_201587.jpg",
                    "image_cloud_url": ""
                }
            ],
            "extend_category": [ {"category_id": "1201"} ]
        }
    }
}

Набір полів вкладеного блока теж керований: extend_limit_field_attribute, extend_limit_field_image, extend_limit_field_product і подібні. За замовчуванням у характеристик це attribute_id, attribute_name, attribute_value, у позицій документа — product_id, sku, mpn, name, quantity, price, price_sum, ean, isbn.

З параметром extend форма відповіді змінюється: item_list перестає бути обʼєктом із ключами-ідентифікаторами й приходить звичайним списком. Код, написаний без extend, зламається рівно тоді, коли хтось його додасть. Розбирайте обидві форми одразу:

$rows = $res['item_list'];
$rows = isset($rows[0]) ? $rows : array_values($rows);   // однаково для обох форм
foreach ($rows as $row) { /* … */ }

add, update, delete

Відповідь у всіх трьох однакова й коротка:

{"meta": {"item_scope": "product", "result": 1024, "request_time": "00:00:00"}}

У result лежить те, що повернула операція: для додавання — ідентифікатор нового запису, для решти — ознака успіху. Окремого поля з текстом помилки валідації немає: якщо запис не створився, result буде порожнім.

Пакетні операції

Замість полів одного запису передається item_list — список записів:

$res = elbuz_call($host, 'api/scope/add', array(
    'api_token' => $token,
    'scope'     => 'product',
    'item_list' => array(
        array('name' => 'Товар 1', 'price' => 100, 'sku' => 'A-001'),
        array('name' => 'Товар 2', 'price' => 200, 'sku' => 'A-002'),
    ),
));
ДовідникОпераціяЗаписів за раз
productдодавання й оновленнядо 10 000
product_priceдодаваннядо 100

Пакет — не просто зручність. З межею в один запит на дві секунди десять тисяч товарів поштучно — це майже шість годин, пакетом — один запит.

Позначка джерела

Параметр source_code в додаванні позначає, звідки прийшли дані; позначка потрапляє в історію запису, і потім видно, що товар завела саме ця інтеграція, а не людина. Без параметра ставиться jumper.

Видалення

Ідентифікатор передається в полі з іменем довідника: category_id, product_id, manufacturer_id, attribute_id, attribute_block_id, article_id, document_id.

Передавайте лише те поле, що відповідає довіднику. Якщо потрібного ідентифікатора немає, запит не завершиться помилкою — обробка піде далі за списком довідників і може дійти до видалення документа, якщо в тілі запиту трапиться document_id. Відповідь при цьому назве той довідник, який ви просили. Не складайте універсальне тіло запиту з ідентифікаторами різних довідників.

Проведені документи не видаляються — така спроба повертає порожній результат.

Помилки

Помилки api/scope/* приходять масивом з одного рядка:

["Увага: у вас немає дозволу на зміну даних!"]
ПовідомленняПричинаЩо робити
немає дозволу на доступ до APIтокен не переданий, сеанс закінчився або ключ вимкненоповторити api/login; якщо не допомогло — перевірити «Активність»
немає дозволу на зміну даниху ключа стоїть «Тільки для читання»зняти прапорець у картці ключа
Не задано код довідниканемає параметра scopeдодати scope або дописати його в маршрут
Невідомий код довідникадовідник не підтримується цією операцієюзвірити з таблицею довідників
Елемент не знайденовибірка порожняперевірити фільтр; це не збій
{"error":{"key":…}}вхід: не той користувач або ключзвірити пару з карткою ключа
{"error":{"ip":…}}вхід: адреса не в спискуадреса названа в тексті помилки — вписати її в ключ
{"Error":"Rate limit exceeded…"}запити частіше за дозволений проміжокпауза й повтор

Кожен запит записується в журнал роботи як операція типу «api» разом із тілом відповіді — це найпростіший спосіб побачити, що саме робила інтеграція.

Готові рецепти

Інкрементальна синхронізація каталогу

Замість того щоб щоночі викачувати весь каталог, забирайте змінене з минулого разу.

$since = file_exists('last_sync.txt') ? trim(file_get_contents('last_sync.txt')) : '2000-01-01';
$started = date('Y-m-d H:i:s');
$page = 1;

do {
    $res = elbuz_call($host, 'api/scope/get', array(
        'api_token'        => $token,
        'scope'            => 'product',
        'max_rows'         => 500,
        'limit_field'      => 'product_id,name,price,quantity,sku',
        'page'             => $page,
        'filterscount'     => 1,
        'filterdatafield0' => 'date_modified',
        'filtervalue0'     => $since,
        'filtercondition0' => 'GREATER_THAN_OR_EQUAL',
        'filteroperator0'  => 0,
    ));

    if (!isset($res['item_list'])) break;   // «Елемент не знайдено» = змін немає

    foreach ($res['item_list'] as $id => $row) {
        // ваша логіка: оновити товар у своїй системі
    }

    $page = (int)$res['meta']['page_next'];
    sleep(2);
} while ($page > 0);

file_put_contents('last_sync.txt', $started);   // час СТАРТУ, а не завершення

Зберігайте час старту прогону: те, що змінилося під час обходу, тоді потрапить у наступний прогін, а не загубиться між ними.

Пакетне завантаження товарів

$products = array( /* ваш масив товарів */ );

foreach (array_chunk($products, 1000) as $chunk) {
    $res = elbuz_call($host, 'api/scope/add', array(
        'api_token'   => $token,
        'scope'       => 'product',
        'source_code' => 'my_erp',    // позначка джерела в історії записів
        'item_list'   => $chunk,
    ));

    if (isset($res['Error'])) { sleep(2); continue; }   // межа частоти — повторити пачку
    sleep(2);
}

Тисяча записів у пачці — компроміс: межа 10 000, але чим більша пачка, тим довша відповідь і тим болючіший повтор, якщо запит обірвався.

Віддати сайту залишки й ціни

Найчастіша задача — і найдешевша: потрібні три поля, тож відповідь виходить маленькою, а сторінок мало.

$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'product',
    'max_rows'    => 500,
    'limit_field' => 'sku,price,quantity',   // product_id і name додадуться самі
));

Коли API не потрібне

Перш ніж писати інтеграцію, перевірте готові механізми — вони закривають більшість задач без розробника:

API лишається для своєї логіки: нестандартний сайт, внутрішня система обліку, обмін, якого немає в готових форматах.

Розбір частих проблем

Що ви бачитеЧому такЩо зробити
Працювало, а через годину пересталосеанс токена закінчився без зверненьперелогінитися при помилці доступу
«Rate limit exceeded» на кожному другому запитізапити йдуть частіше за проміжокпауза між запитами або пакетні операції
Приходить рівно 100 записіврозмір сторінки за замовчуваннямmax_rows до 500 і обхід по page_next
Обхід сторінок зацикливсяне перевіряється page_nextнуль означає кінець
У відповіді зайві поляобовʼязкові поля довідника дописуються завждитак і має бути
Після додавання extend зламався розбірitem_list став списком замість обʼєктарозбирати обидві форми
Порожня вибірка приходить помилкоютак відповідає get, коли нічого не знайшлосяперевіряти наявність item_list
Оновлення рядків прайсу не працюєproduct_price немає в списку оновленняоновлювати прайс завантаженням файлу
Читання працює, запис відхиляєтьсяу ключа «Тільки для читання»; прапорець діє на всі маршрути, зокрема старізняти прапорець — діє одразу, без перелогіну
Ключ перестав пускати, хоча активнийнастав день після «Дати закінчення доступу»посунути дату або очистити поле — тоді ключ безстроковий

Часті питання

Де в програмі знайти ключі API?

Налаштування системи → розділ «Інші налаштування» → плитка «API та токени доступу». Окремого пункту в головному меню немає.

Чи треба логінитися перед кожним запитом?

Ні. Сеанс живе, поки ним користуються, і видаляється після години без звернень. Новий вхід робіть у відповідь на помилку доступу.

Скільки запитів на хвилину дозволено?

За замовчуванням тридцять, і рахуються вони як мінімальний проміжок: не частіше ніж раз на дві секунди. Пачку підряд зробити не можна, навіть після довгої паузи.

Як дізнатися справжні назви полів?

Операцією api/scope/describe для потрібного довідника — вона повертає внутрішню назву, підпис і тип кожного поля.

Як синхронізувати каталог, не викачуючи його цілком?

Фільтром за date_modified із датою минулої синхронізації. Зберігайте час старту прогону, а не завершення.

Як завантажити багато товарів за раз?

Параметром item_list в операції додавання: товарів каталогу — до десяти тисяч за запит, рядків прайсу — до ста.

Чи можна орієнтуватися на код відповіді HTTP?

Ні, він завжди 200 — і на успіх, і на помилку. Розбирайте тіло: помилка приходить масивом рядків, успіх містить item_list або meta.

Суміжні теми

Чи була стаття корисною?