Обмін з обліковою системою
Як облікова система передає в ApexSFA довідники, залишки й продажі та забирає заявки, оплати, приходи й рейси: конверти, pull/ack, повтори.
Цей розділ описує, як облікова система дистрибʼютора обмінюється даними з ApexSFA: як передати в ApexSFA довідники, залишки, продажі й борги і як забрати з ApexSFA заявки торгових, оплати, приходи, інвентаризації, заявки на обладнання та рейси.
- Базовий URL:
https://api.apexsfa.com/api/v1 - Формат: JSON, UTF-8,
Content-Type: application/json, лишеPOST. - Автентифікація — підпис запиту ключем обміну (див. 2.2).
Примітка. Шляхи містять технічний сегмент
/webhooks/1c/— це назва каналу в API, змінювати його не треба. Він однаковий для будь-якої облікової системи.
Не хочете програмувати обмін? Для облікових систем BAS і сумісних є готова обробка обміну.
3.1. Модель обміну
Обидва напрямки ініціює облікова система — їй не потрібна публічна адреса, вистачить вихідного HTTPS:
| Напрямок | Як | Ендпоінти |
|---|---|---|
| Облікова система → ApexSFA | облікова система надсилає «конверти» | POST /api/v1/webhooks/1c/inbound/batch, POST /api/v1/webhooks/1c/inbound |
| ApexSFA → облікова система | облікова система сама забирає документи з черги (pull) і підтверджує обробку (ack) | POST /api/v1/webhooks/1c/outbox/pull, POST /api/v1/webhooks/1c/outbox/ack |
Облікова система ApexSFA
──────────────── ───────
довідники, ціни, залишки, ── inbound/batch ──▶ записує в тенант
продажі, борги, каса, рейси
заявки, оплати, приходи, ◀── outbox/pull ─── черга документів
інвентаризації, рейси ─── outbox/ack ───▶ позначка «синхронізовано»Тенант в обміні
Кожна точка синхронізації (філія компанії, яка веде свою базу обліку) — окремий тенант ApexSFA зі своїм tenant_id і своїм ключем обміну. Усе, що надіслано з ключем тенанта, лягає в цей тенант; коди філій у payload конвертів (branch_external_id тощо) не впливають на те, куди лягають дані. Черга документів для забору теж одна на тенант — її забирає одна база обліку.
3.2. Автентифікація
Кожен запит до /api/v1/webhooks/1c/* містить:
| Заголовок | Обовʼязковий | Опис |
|---|---|---|
X-Tenant-Id | так* | UUID тенанта. *Можна не передавати, якщо в JSON-тілі є поле tenant_id. |
X-Signature | так | t=<unix-час>,v2=<hex> — підпис тіла ключем обміну тенанта. |
Content-Type | так | application/json |
v2 = lowercase_hex( SHA256( "<t>" + "." + <тіло запиту як є, байти UTF-8> + "." + <ключ обміну> ) )
X-Signature: t=<t>,v2=<v2>Повний опис, приклади на трьох мовах і контрольний приклад — у 2.2.
Коли повертається 401
Відповідь 401 з тілом {"error": {"code": "INVALID_CREDENTIALS", "message": "...", "details": [], "request_id": "..."}}:
message | Причина |
|---|---|
Invalid X-Tenant-Id | заголовок не є UUID |
Cannot resolve tenant / Tenant required | тенант не передано ні в заголовку, ні в тілі |
… integration not configured for tenant | обмін для тенанта не підключено або вимкнено |
Tenant is suspended | тенант призупинено — обмін зупинено |
Invalid signature | невірний підпис, час поза ±300 с, повтор підпису або не той ключ |
3.3. Облікова система → ApexSFA: конверти
Усі дані з облікової системи передаються конвертами:
| Поле | Тип | Обовʼязкове | Опис |
|---|---|---|---|
entity_type | string | так | Тип сутності (перелік — у 3a). Невідомий тип → 422. |
operation | string | так | create | update | upsert | delete. На практиці надсилайте upsert; delete — деактивація або зняття запису (поведінка залежить від типу). |
external_id | string, 1..50 | так | Ідентифікатор обʼєкта в обліковій системі (зазвичай GUID). Ключ ідемпотентності: повторний конверт з тим самим entity_type + external_id оновлює той самий запис, дубля не буде. |
payload | object | ні (за замовч. {}) | Поля сутності — див. 3a. |
Типи сутностей коротко:
- довідники:
branch,warehouse,supplier,supplier_point,price_type,manufacturer,product_category,product,product_external_code,counterparty,client(торгова точка),agent(торговий),client_assignment,driver,vehicle,transport_list,equipment_type,equipment_unit,manufacturer_price_type,manufacturer_payment_term; - ціни й залишки:
price_list_item,product_stock,product_stock_snapshot,wms_stock,stock_document,excise_marks; - продажі, борги, каса:
sale,goods_document,goods_period,debt_document,debt_reset,cash_document,cash_balance,cash_period,order_state,order_state_period; - логістика й рейси:
trip,logistics_order,logistics_manufacturer,logistics_own_manufacturer,logistics_counterparty,logistics_point,logistics_warehouse,logistics_product_category,logistics_product,logistics_product_code,logistics_stock.
POST /api/v1/webhooks/1c/inbound/batch
Пакет конвертів — основний спосіб передачі.
| Поле | Тип | Обовʼязкове | Опис |
|---|---|---|---|
tenant_id | UUID | ні | Якщо немає заголовка X-Tenant-Id. |
partial | bool | ні, за замовч. true | true — кожен конверт обробляється окремо, помилка одного не скасовує інших. false — «все або нічого»: перша помилка відкочує весь пакет. |
items | array | так | 1..5000 конвертів. Рекомендуємо пакети по 1000. |
Порядок конвертів у пакеті має значення: обробка йде послідовно, тому довідники-«батьки» ставте раніше за залежні записи (див. порядок первинного завантаження).
Порада. Завжди надсилайте дані через
/inbound/batchзpartial: true— так кожен рядок отримує власний результат, а збій одного не зупиняє інші.
Відповідь 200:
{
"data": {
"accepted_count": 2,
"failed_count": 1,
"results": [
{"index": 0, "entity_type": "warehouse", "external_id": "8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c",
"accepted": true, "entity_id": "0192e004-5555-7ccc-8ddd-4c5d6e7f8091", "operation_performed": "upsert",
"error": null, "details": null},
{"index": 1, "entity_type": "client", "external_id": "5b1f2c3d-4e5f-11ee-9a0b-0050569a1b2c",
"accepted": true, "entity_id": "0192e001-2222-7ccc-8ddd-1e2f3a4b5c6d", "operation_performed": "upsert",
"error": null, "details": null},
{"index": 2, "entity_type": "supplier_point", "external_id": "00000000-0000-0000-0000-000000000000",
"accepted": false, "entity_id": null, "operation_performed": null,
"error": "Supplier … not found — push the supplier first", "details": null}
]
}
}| Поле результату | Опис |
|---|---|
index | Позиція конверта у вхідному масиві (з 0). |
entity_type, external_id | Як у конверті. |
accepted | true — записано. |
entity_id | UUID запису в ApexSFA (може бути null для службових конвертів). |
operation_performed | Операція, яку виконано. |
error | Текст помилки для відхиленого конверта. |
details | Лічильники й примітки обробника, напр. {"skipped": "client_not_found"} — рядок проігноровано, бо точки ще немає. |
Помилка окремого конверта в режимі partial=true не змінює HTTP-код: відповідь лишається 200, перевіряйте failed_count і results[].accepted.
| Код | Тіло | Коли |
|---|---|---|
| 200 | {"data": {"accepted_count": 0, "failed_count": 0, "results": []}} | items порожній — це не помилка. |
| 401 | INVALID_CREDENTIALS | див. 3.2. |
| 422 | VALIDATION_ERROR | Пакет не пройшов перевірку формату (невідомий entity_type, порожній external_id, невірна operation, понад 5000 елементів — batch too large). Нічого не записано. |
| 422 | BATCH_ATOMIC_FAILURE + accepted_so_far, failed_at_index | Лише partial=false: конверт failed_at_index не прийнято, увесь пакет відкочено. |
| 429 | RATE_LIMIT_EXCEEDED + заголовок Retry-After: 60 | Перевищено ліміт — повторіть той самий пакет після паузи. |
| 5xx | INTERNAL_ERROR | Збій сервера — повторіть пакет пізніше. |
POST /api/v1/webhooks/1c/inbound
Один конверт. Тіло — сам конверт (+ необовʼязковий tenant_id):
{"entity_type": "warehouse", "operation": "upsert", "external_id": "8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c",
"payload": {"name": "Склад Основний"}}Відповідь 200: {"data": {"accepted": true, "entity_id": "…", "operation_performed": "upsert", "details": null}}.
Відповідь 422 — конверт відхилено (запис не знайдено, порушено бізнес-правило): {"error": {"accepted": false, "error": "…"}}.
Порядок первинного завантаження
Під час першого підключення передавайте дані в такому порядку:
- Філія, склади, постачальники (+ їхні точки), типи цін.
- Групи номенклатури, виробники, номенклатура.
- Контрагенти, торгові, точки, привʼязки точок до торгових, обладнання, ціни.
- Залишки.
- Продажі, борги, рухи товару, каса.
- Рейси.
Деталі за кожною сутністю — у 3a.
3.4. ApexSFA → облікова система: pull і ack
Документи, які мають потрапити в облікову систему, ApexSFA кладе в чергу тенанта. Облікова система за розкладом (рекомендовано кожні 5–15 хв):
- викликає
outbox/pullі отримує пачку елементів; - створює або оновлює документи в себе;
- викликає
outbox/ackз результатом по кожному елементу; - якщо
has_more = true— повторює з кроку 1.
POST /api/v1/webhooks/1c/outbox/pull
| Поле | Тип | Обовʼязкове | Опис |
|---|---|---|---|
tenant_id | UUID | ні | Якщо немає заголовка X-Tenant-Id. |
batch_size | int, 1..100 | ні, за замовч. 50 | Скільки елементів видати. |
entity_types | string[], ≤20 | ні | Забрати лише ці типи, напр. ["order", "order_payment"]. Без поля — усі типи. |
Відповідь 200 (повний приклад — examples/exchange-outbox-pull-response.json):
{
"data": {
"items": [
{
"queue_id": "0192f3a1-7c2e-7b10-9a4d-5e6f7a8b9c01",
"entity_type": "order",
"entity_id": "0192f3a0-1111-7aaa-8bbb-0c0d0e0f1a2b",
"operation": "create",
"attempt": 1,
"payload": {"order_number": "ORD-20261027-0042", "...": "..."}
}
],
"has_more": false
}
}| Поле | Опис |
|---|---|
queue_id | Ідентифікатор елемента черги — його повертаєте в ack. |
entity_type | Тип документа: order, logistics_order, order_payment, arrival, stock_document, equipment_request, delivery_trip. |
entity_id | UUID документа в ApexSFA. Для одного документа може бути кілька елементів черги (напр. create, а потім cancel). |
operation | create | update | cancel (див. нижче). |
attempt | Номер видачі цього елемента (1, 2, …). |
payload | Знімок документа — див. 3.5. |
has_more | true — у черзі лишились елементи, заберіть наступну пачку. |
Порядок видачі: за пріоритетом, потім від старіших до новіших.
Повторна видача. Виданий елемент закріплюється за вашою обліковою системою до ack. Якщо ack не прийшов протягом 5 хвилин, елемент знову потрапляє у видачу (attempt зростає).
Увага. Ідемпотентність на вашому боці обовʼязкова. Той самий
queue_id(і той самий документ) може прийти повторно — якщоackзагубився, запізнився понад 5 хв або ви отримали відповідьpull, але впали доack. Шукайте вже створений документ за ключем з payload (order_number,payment_number,arrival_id,request_id,trip_number/external_id) і не створюйте дубль; уackвідповідайтеokз тим самимexternal_id. Якщо ви вже відповідали на елемент помилкою — на повтор відповідайте тією самою помилкою.
operation:
| Значення | Що зробити |
|---|---|
create | Створити документ (або знайти вже створений — повторна видача). |
update | Оновити раніше створений документ (рейс, логістична заявка зі зміненою датою, прихід або документ складу — подія завершення). |
cancel | Документ скасовано в ApexSFA. Рекомендована поведінка: непроведений документ помітити на видалення; якщо документ уже проведено й скасувати автоматично не можна — відповісти ack з status=error, permanent=true і поясненням, щоб оператор розібрався вручну. Повторна відміна вже скасованого — ok. |
Примітка. Якщо
pullповернув200, а ви не змогли обробити відповідь (обрив звʼязку) — нічого робити не треба: елементи повернуться у видачу через 5 хв.
Помилки: 401 — див. 3.2; 429 — зробіть паузу 5–60 с. Тримайте batch_size у межах 1..100, а results в ack — 1..200.
POST /api/v1/webhooks/1c/outbox/ack
Приклад тіла — examples/exchange-outbox-ack-request.json.
| Поле | Тип | Обовʼязкове | Опис |
|---|---|---|---|
tenant_id | UUID | ні | Якщо немає заголовка X-Tenant-Id. |
results | array, 1..200 | так | Результати по елементах. |
results[].queue_id | UUID | так | З pull. |
results[].status | ok | error | так | Результат обробки. |
results[].external_id | string, ≤200 | ні | Ідентифікатор створеного документа в обліковій системі (GUID). Зберігається при першому успішному ack. |
results[].error_message | string, ≤2000 | ні | Текст помилки — його бачить оператор на порталі. |
results[].permanent | bool, за замовч. false | ні | true — остаточна помилка (дані невірні, автоматичний повтор не допоможе). |
{
"results": [
{"queue_id": "0192f3a1-7c2e-7b10-9a4d-5e6f7a8b9c01", "status": "ok", "external_id": "d18f9a01-b2c3-11ee-9a0b-0050569a1b2c"},
{"queue_id": "0192f3a1-7c2e-7b10-9a4d-5e6f7a8b9c02", "status": "error", "error_message": "Не знайдено точку", "permanent": false}
]
}Відповідь 200: {"data": {"acked": 2, "synced": 1, "retrying": 1, "failed": 0, "not_found": 0}}
| Лічильник | Опис |
|---|---|
acked | Скільки результатів застосовано. |
synced | Скільки елементів позначено доставленими (включно з повторними ok). |
retrying | Помилки, що будуть повторені за розкладом. |
failed | Остаточні помилки. |
not_found | queue_id не знайдено в черзі цього тенанта. |
Відповідь завжди 200, навіть якщо частина результатів — помилки; дивіться лічильники.
Що відбувається після ack:
ok→ елемент «доставлено»; документ у ApexSFA отримує позначку синхронізації й вашexternal_id. Для заявки з кабінету покупця (B2B) першийok— це підтвердження: покупець отримує повідомлення «Підтверджено».errorбезpermanent→ повтор за розкладом (60 с, 5 хв, 30 хв, 2 год, далі 12 год); після вичерпання спроб (6) — стан «помилка», далі елементом займається оператор на порталі.errorзpermanent=true→ стан «помилка», оператор бачить ваш текст.- Повторний
ackдля вже доставленого елемента — безпечний.
3.5. Що лежить у черзі: payload за типами
Загальні правила:
- Суми, кількості, ціни, ставки ПДВ — рядки з десятковою крапкою (
"1250.00"). Виняток — уdelivery_tripвага, сума й відстань — числа. *_code_1c,*_external_id,external_id_1c— ідентифікатори (GUID) обʼєктів облікової системи, які раніше прийшли в ApexSFA конвертами. Якщо обʼєкт заведено лише в ApexSFA —null.- Payload — знімок на момент постановки в чергу.
entity_type | Коли потрапляє в чергу | operation | Ключ для пошуку дубля |
|---|---|---|---|
order | заявку торгового погоджено; повторна відправка з порталу; скасування погодженої заявки | create, cancel | order_number |
logistics_order | логістичну заявку погоджено; зміна дати документа після забору | create, update, cancel | order_number |
order_payment | оплату прийнято; оплату скасовано | create, cancel | payment_number |
arrival | прихід товару завершено | update | arrival_id |
stock_document | інвентаризацію завершено | update | document_id / external_id |
equipment_request | заявку на встановлення або зняття обладнання виконано | create | request_id |
delivery_trip | рейс передано в облік із порталу; зміна складу рейсу; скасування | create, update, cancel | external_id, далі trip_number |
Заявки, що прийшли з облікової системи, назад у чергу не потрапляють.
order — заявка торгового (продаж або повернення)
| Поле | Тип | Опис |
|---|---|---|
order_id | UUID | Ідентифікатор заявки в ApexSFA. |
order_number | string | Номер заявки (напр. ORD-20261027-0042). Ключ ідемпотентності. |
order_type | string | sale — продаж, return — повернення від покупця. |
return_basis | object|null | Для повернення: документ-підстава — {debt_document_id, external_id_1c, doc_number, doc_date}. |
payment_form | string|null | Вид оплати: cash | non_cash | null. |
comment | string|null | Коментар торгового. |
order_date | date | Дата заявки, YYYY-MM-DD. |
requested_delivery_date | date|null | Бажана дата доставки. |
client_id | UUID | Точка в ApexSFA. |
client_code_1c | string|null | Ідентифікатор точки в обліковій системі. |
agent_id | UUID | Торговий — автор заявки. |
agent | object|null | {name, code_1c} — ПІБ і ідентифікатор торгового в обліковій системі. |
price_list | object|null | Прайс заявки: {id, name, code}. |
status | string | Статус заявки на момент постановки в чергу (approved; для cancel — cancelled). |
subtotal, discount_amount, vat_amount, total_amount | decimal-string | Суми заявки. |
currency | string | Валюта, напр. UAH. |
warehouse | object|null | Склад відвантаження: {id, name, code_1c}. |
manufacturer | object|null | Виробник першого рядка: {id, name, code_1c}. |
manufacturers | array | Усі виробники рядків (без повторів). |
multi_manufacturer | bool | true, якщо в заявці кілька виробників. |
lines[] | array | Рядки (див. нижче). |
Рядок lines[]:
| Поле | Тип | Опис |
|---|---|---|
line_number | int | Номер рядка. |
product_id | UUID|null | Товар у ApexSFA. |
logistics_product_id | UUID|null | Позиція каталогу логістики (для логістичних заявок). |
product_code_1c | string|null | Ідентифікатор номенклатури в обліковій системі. |
product_sku | string|null | Артикул. |
product_name | string | Назва. |
quantity | decimal-string | Кількість (в одиницях). |
pack_size | int | Кількість в упаковці (1, якщо не задано). |
packages | decimal-string|null | Кількість упаковок (для позицій каталогу логістики). |
unit_price | decimal-string | Ціна. |
vat_rate | decimal-string | Ставка ПДВ, %. |
line_total | decimal-string | Сума рядка. |
is_promo | bool | Акційний рядок. |
promo_name | string|null | Назва акції. |
manufacturer_name, manufacturer_code_1c | string|null | Виробник рядка. |
price_list | object|null | Прайс, з якого взято ціну рядка: {id, name, code}. |
Для operation=cancel payload має той самий формат (повний знімок заявки на момент скасування).
logistics_order — логістична заявка
Формат як у order (без manufacturers/multi_manufacturer), плюс:
| Поле | Тип | Опис |
|---|---|---|
logistics | bool | Завжди true. |
manufacturer | object|null | Виробник — власник вантажу: {id, name, code_1c}. |
source_document | object | Для заявок із файлів заказів: {kind: "network_order_file", file, template, doc_number, doc_date, po_number, po_date, counterparty_name, counterparty_egrpou, delivery_point_code, contract}. |
Логістичні заявки зручно забирати окремим викликом pull з entity_types: ["logistics_order"].
order_payment — оплата (прибутковий касовий ордер)
| Поле | Тип | Опис |
|---|---|---|
payment_id | UUID | Ідентифікатор оплати. |
payment_number | string | Номер оплати. Ключ ідемпотентності. |
client_id | UUID | Точка. |
client_code_1c | string|null | Ідентифікатор точки в обліковій системі. |
agent_id | UUID | Хто прийняв оплату. |
amount | decimal-string | Сума. |
currency | string | Валюта. |
method | string | cash | bank_transfer | card | check | other. |
collected_at | datetime|null | Момент прийому оплати, ISO 8601, UTC. |
reference | string|null | Довідковий номер (чек, платіжка). |
status | string | completed, для cancel — cancelled. |
allocations[] | array | Розподіл на заявки ApexSFA: {order_id, order_date, amount}. |
debt_documents[] | array | Розподіл на документи боргу облікової системи: {external_id_1c, doc_number, amount}. |
arrival — прихід товару
| Поле | Тип | Опис |
|---|---|---|
event | string | arrival.completed. |
arrival_id | UUID | Ідентифікатор приходу. Ключ ідемпотентності. |
arrival_date | date|null | Дата приходу. |
branch_external_id | string|null | Ідентифікатор філії в обліковій системі. |
warehouse_external_id | string|null | Склад. |
supplier_external_id, supplier_point_external_id | string|null | Постачальник і його точка. |
price_type_external_id | string|null | Тип цін. |
completed_at | datetime|null | Момент завершення (UTC). |
lines[] | array | {line_number, product_id, product_code, product_article, product_name, barcode, quantity, price, batch_id, manufacture_date}. |
stock_document — результат інвентаризації
| Поле | Тип | Опис |
|---|---|---|
event | string | stock_document.completed. |
document_id | UUID | Документ у ApexSFA. |
external_id | string|null | Ідентифікатор вихідного документа облікової системи (якщо інвентаризацію передано з обліку). |
doc_number, doc_type | string|null | Номер і вид документа. |
kind | string | detailed — до партій і дат; simple — поштучно по рядках. |
branch_external_id, warehouse_external_id | string|null | Філія й склад. |
inventory_date | date|null | Дата інвентаризації. |
completed_at | datetime|null | Момент завершення (UTC). |
source | string|null | Джерело документа. |
unmatched_scans | array | Відскановані коди, яких не знайдено. |
lines[] | array | {line_number, product_code, product_article, product_name, barcode, batch_id, batch_document, batch_date, quantity, price, fact_quantity, transfer_quantity, write_off_quantity, transfer_date}. |
equipment_request — заявка на встановлення або зняття обладнання
| Поле | Тип | Опис |
|---|---|---|
request_id | UUID | Ідентифікатор заявки. Ключ ідемпотентності. |
kind | string | install | remove. |
client_code_1c | string|null | Точка. |
type_external_id | string|null | Тип обладнання. |
type_name | string|null | Назва типу. |
unit_external_id | string|null | Конкретна одиниця обладнання. |
quantity | string | Завжди "1". |
comment | string|null | Коментар. |
requested_at, approved_at | datetime|null | Створення й погодження (UTC). |
delivery_trip — рейс
Потрапляє в чергу, якщо для тенанта увімкнено передачу рейсів в облік і диспетчер натиснув у рейсі «Передати в облік».
| Поле | Тип | Опис |
|---|---|---|
trip_id | UUID | Рейс у ApexSFA. |
trip_number | string, ≤15 | Номер рейсу для облікової системи. Ключ ідемпотентності в межах філії (разом з external_id). |
portal_number | string | Номер рейсу на порталі. |
trip_date, shipping_date | date | Дата рейсу / відвантаження. |
branch_code | string|null | Ідентифікатор філії в обліковій системі. |
warehouse_code | string|null | Склад. |
vehicle_external_id | string|null | Автомобіль. |
driver_external_id, forwarder_external_id | string|null | Водій, експедитор. |
comment | string | Примітка. |
distance_km | number|null | Плановий пробіг. |
weight, amount | number | Загальні вага й сума. |
external_id | string | Лише якщо рейс уже має ваш GUID з попереднього ack. |
points[] | array | Зупинки: {position, kind, document_external_id, doc_number, order_number, client_external_id, weight, amount}; kind = sale (документ продажу з обліку) або order (заявка ApexSFA). |
cancel: {trip_id, trip_number, external_id}. Якщо рейс змінили після забору, зміна приходить окремим елементом update.
3.6. Повтори й журнал
- Облікова система → ApexSFA: сервер не повторює за вас. Відхилений конверт (
accepted=false) виправте й надішліть ще раз — повтор з тим самимexternal_idбезпечний. На429— повторіть пакет післяRetry-After; на5xxабо обрив — повторіть пакет цілком. - ApexSFA → облікова система: див. поведінку після
ackвище. - Журнал обміну видно на порталі (розділ «Журнал дій», фільтр «Обмін з обліковою системою»): скільки прийнято й відхилено за типами, номери забраних документів, тексти помилок.
- Якщо черга не порожня, а успішного обміну не було понад 30 хв, адміністратору приходить лист «Обмін з обліковою системою зупинився».
- На порталі можна повторно поставити заявку в чергу («Повторно в облікову систему») і передати рейс в облік.
За потреби можливий режим, коли ApexSFA сам надсилає документи на HTTPS-адресу вашої системи (push) замість pull, — узгоджується під час підключення.
3.7. Ліміти
| Що | Значення |
|---|---|
Запитів на /api/v1/webhooks/1c/* | 600 за 60 с на пару «тенант + IP» |
Конвертів у пакеті inbound/batch | до 5000 (рекомендовано 1000) |
external_id конверта | до 50 символів |
outbox/pull → batch_size | 1..100 (за замовч. 50) |
outbox/pull → entity_types | до 20 типів |
outbox/ack → results | 1..200 |
ack.external_id / ack.error_message | ≤200 / ≤2000 символів |
| Розбіжність часу підпису | ±300 с |
Повторна видача без ack | через 5 хв |
| Спроб доставки елемента | 6 |
| Перекриття ключів при ротації | 24 год |
3.8. Дати й часові пояси
У черзі (ApexSFA → облік):
- Дати без часу (
order_date,requested_delivery_date,arrival_date,trip_date,inventory_date) —YYYY-MM-DD, календарна дата. - Моменти часу (
collected_at,completed_at,requested_at,approved_at) — ISO 8601 у UTC (2026-10-27T09:41:12.512000+00:00). Переводьте в локальний час на своєму боці.
У конвертах (облік → ApexSFA):
- Дати без часу —
YYYY-MM-DD. - Дата й час — завжди з явним зсувом:
2026-10-27T12:00:00+03:00або…Z. - Періоди
date_from/date_to(уgoods_period,cash_period) — від 00:00 першого до 24:00 останнього дня за київським часом.
3.9. Приклади
Надіслати пакет довідників
BASE="https://api.apexsfa.com/api/v1"
TENANT="3f2a9c1e-5b7d-4e2a-9c1f-0a1b2c3d4e5f"
SECRET="<ключ обміну>"
BODY='{"partial":true,"items":[{"entity_type":"warehouse","operation":"upsert","external_id":"8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c","payload":{"name":"Склад Основний"}}]}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY.$SECRET" | sha256sum | cut -d' ' -f1)
curl -sS -X POST "$BASE/webhooks/1c/inbound/batch" \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: $TENANT" \
-H "X-Signature: t=$TS,v2=$SIG" \
--data-binary "$BODY"import hashlib, json, time, requests
BASE = "https://api.apexsfa.com/api/v1"
TENANT = "3f2a9c1e-5b7d-4e2a-9c1f-0a1b2c3d4e5f"
SECRET = "<ключ обміну>"
def post_signed(path: str, payload: dict) -> dict:
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
ts = int(time.time())
sig = hashlib.sha256(f"{ts}.{body}.{SECRET}".encode()).hexdigest()
resp = requests.post(
f"{BASE}{path}",
data=body.encode("utf-8"),
headers={"Content-Type": "application/json", "X-Tenant-Id": TENANT,
"X-Signature": f"t={ts},v2={sig}"},
timeout=300,
)
resp.raise_for_status()
return resp.json()["data"]
result = post_signed("/webhooks/1c/inbound/batch", {"partial": True, "items": [
{"entity_type": "warehouse", "operation": "upsert",
"external_id": "8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c", "payload": {"name": "Склад Основний"}},
]})
print(result["accepted_count"], result["failed_count"])import { createHash } from "node:crypto";
const BASE = "https://api.apexsfa.com/api/v1";
const TENANT = "3f2a9c1e-5b7d-4e2a-9c1f-0a1b2c3d4e5f";
const SECRET = process.env.APEX_SECRET;
async function postSigned(path, payload) {
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000);
const sig = createHash("sha256").update(`${ts}.${body}.${SECRET}`, "utf8").digest("hex");
const resp = await fetch(BASE + path, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Tenant-Id": TENANT, "X-Signature": `t=${ts},v2=${sig}` },
body,
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
return (await resp.json()).data;
}
const result = await postSigned("/webhooks/1c/inbound/batch", {
partial: true,
items: [{ entity_type: "warehouse", operation: "upsert",
external_id: "8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c", payload: { name: "Склад Основний" } }],
});
console.log(result.accepted_count, result.failed_count);Забрати заявки й підтвердити
BODY="{\"batch_size\":100,\"entity_types\":[\"order\",\"order_payment\"],\"request_nonce\":\"$(uuidgen)\"}"
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY.$SECRET" | sha256sum | cut -d' ' -f1)
curl -sS -X POST "$BASE/webhooks/1c/outbox/pull" \
-H "Content-Type: application/json" -H "X-Tenant-Id: $TENANT" \
-H "X-Signature: t=$TS,v2=$SIG" --data-binary "$BODY"
BODY='{"results":[{"queue_id":"0192f3a1-7c2e-7b10-9a4d-5e6f7a8b9c01","status":"ok","external_id":"d18f9a01-b2c3-11ee-9a0b-0050569a1b2c"}]}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY.$SECRET" | sha256sum | cut -d' ' -f1)
curl -sS -X POST "$BASE/webhooks/1c/outbox/ack" \
-H "Content-Type: application/json" -H "X-Tenant-Id: $TENANT" \
-H "X-Signature: t=$TS,v2=$SIG" --data-binary "$BODY"import uuid
while True:
data = post_signed("/webhooks/1c/outbox/pull", {
"batch_size": 100, "entity_types": ["order", "order_payment"],
"request_nonce": str(uuid.uuid4()),
})
results = []
for item in data["items"]:
try:
doc_id = create_or_find_document(item) # ваша логіка, ідемпотентна за order_number
results.append({"queue_id": item["queue_id"], "status": "ok", "external_id": doc_id})
except Exception as exc:
results.append({"queue_id": item["queue_id"], "status": "error",
"error_message": str(exc)[:2000]})
if results:
print(post_signed("/webhooks/1c/outbox/ack", {"results": results}))
if not data["has_more"]:
breakimport { randomUUID } from "node:crypto";
for (;;) {
const data = await postSigned("/webhooks/1c/outbox/pull", {
batch_size: 100, entity_types: ["order", "order_payment"], request_nonce: randomUUID(),
});
const results = [];
for (const item of data.items) {
try {
const docId = await createOrFindDocument(item); // ваша логіка, ідемпотентна за order_number
results.push({ queue_id: item.queue_id, status: "ok", external_id: docId });
} catch (err) {
results.push({ queue_id: item.queue_id, status: "error", error_message: String(err).slice(0, 2000) });
}
}
if (results.length) console.log(await postSigned("/webhooks/1c/outbox/ack", { results }));
if (!data.has_more) break;
}Використовуйте --data-binary (а не -d), щоб curl не змінив тіло після підпису. На macOS замість sha256sum — shasum -a 256. Повний мінімальний клієнт на Python — examples/exchange_client.py.