API замовника складу (3PL)
API для поклажодавців логістичного оператора на ApexSFA: залишки й рух, заявки на відвантаження й приймання, документи й акти, вебхуки подій, імпорт файлів.
API для компаній, чий товар зберігає й відвантажує логістичний оператор на ApexSFA (поклажодавців). Ваша облікова система чи ERP напряму бачить залишки й рух свого товару, створює заявки на відвантаження й приймання, забирає документи й акти послуг, а про події дізнається з вебхуків.
Базова адреса: https://api.apexsfa.com/api/v1/consignor. Доступ відкривається, коли оператор складу підключив вам кабінет замовника.
| Що | Як |
|---|---|
| Автентифікація | Authorization: Bearer cst_… — токен поклажодавця з кабінету |
| Формат | JSON у UTF-8; дати YYYY-MM-DD, час — ISO 8601 з часовим поясом |
| Обгортка відповіді | {"data": …, "meta": …}; списки — ще й pagination (див. 1.3) |
| Помилки | єдиний формат {"error": {code, message, details, request_id}} (див. 1.3) |
| Події | вебхуки з підписом X-Apex-Signature (розділ 9.6) |
9.1. Токени й дозволи
Видача
Токени створює сам замовник у кабінеті: Кабінет замовника → API та інтеграції → Новий токен. Вкажіть назву, дозволи, за потреби — термін дії й список IP-адрес, з яких дозволено запити. Створити токен може і співробітник оператора складу від імені поклажодавця.
- Токен має вигляд
cst_+ 43 випадкові символи, наприкладcst_Zm9vYmFyLWV4YW1wbGUtdG9rZW4tMDEyMzQ1Njc4OWFi(вигаданий). - Відкритий токен показується один раз. ApexSFA зберігає лише його хеш; у списку токенів видно перші 12 символів, дату останнього запиту й термін дії.
- Токен належить одному поклажодавцю: інтеграція бачить і змінює лише його товар, точки, заявки, приймання, документи й акти. Чужі обʼєкти для неї не існують — запит до них повертає
404. - Відкликання — у кабінеті, кнопка «Відкликати»; токен перестає діяти одразу. Для заміни без простою створіть другий токен, переключіть клієнт і відкличте перший.
- До 20 активних токенів на поклажодавця.
GET /api/v1/consignor/me HTTP/1.1
Host: api.apexsfa.com
Authorization: Bearer cst_Zm9vYmFyLWV4YW1wbGUtdG9rZW4tMDEyMzQ1Njc4OWFiДозволи (scopes)
| Дозвіл | Що відкриває |
|---|---|
stock.read | залишки (/stock), каталог (/products) |
movements.read | рух товару (/movements) |
orders.read | заявки на відвантаження й точки доставки — перегляд, трек |
orders.write | створення й скасування заявок, нові точки доставки, імпорт заявок файлом (включає перегляд) |
inbound.read | заявки на приймання (ASN), факт приймання, акт розбіжностей |
inbound.write | створення заявок на приймання, імпорт файлом (включає перегляд) |
documents.read | складські документи й акти інвентаризації |
acts.read | акти наданих послуг і їхні PDF |
webhooks | керування вебхуками через API |
Запит без потрібного дозволу — 403 FORBIDDEN. Каталог /products доступний з будь-яким із дозволів stock.read, orders.read, orders.write, inbound.write.
Коли токен не приймається — 401
Немає заголовка або токен невідомий; токен відкликано; минув термін дії; запит прийшов з IP, якого немає в списку дозволених; поклажодавця деактивовано. Якщо в оператора складу вимкнено модуль кабінету замовника — 403.
9.2. Ліміти й пагінація
| Ліміт | Значення |
|---|---|
| Запити | 120 за 60 с на токен; понад — 429 із заголовком Retry-After: 60 |
| Сторінка списку | page від 1, page_size до 500 |
| Файл імпорту | до 5 МБ, Excel (.xlsx) або CSV (; чи ,, UTF-8) |
| Період у фільтрах «з–по» | до 366 днів |
Актуальні ліміти й дозволи токена повертає GET /me:
{
"data": {
"consignor": { "id": "0192f3c1-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "ТОВ «Приклад Дистрибуція»", "code": "PRK" },
"token_id": "0192f3c1-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
"token_name": "ERP головного офісу",
"token_prefix": "cst_Zm9vYmFy",
"scopes": ["inbound.read", "inbound.write", "orders.read", "orders.write", "stock.read"],
"expires_at": null,
"limits": { "requests_per_minute": 120, "page_size_max": 500, "import_max_bytes": 5242880 },
"webhook_events": ["order.status", "inbound.arrived", "inbound.completed", "delivery.proof", "act.issued"]
},
"meta": { "request_id": null, "timestamp": "2026-11-03T09:00:00.000000" }
}9.3. Залишки, рух, каталог, точки
GET /stock — залишки
Параметри: warehouse_id, updated_since (ISO-час: лише товари, залишок яких змінювався після цього моменту — зручно для інкрементальної синхронізації), page, page_size (за замовчуванням 200). Дозвіл stock.read.
Рядок — склад × товар × партія (з терміном придатності, якщо партію ведуть):
{
"data": [
{
"consignor_id": "0192f3c1-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"warehouse_id": "0192f3c1-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
"warehouse_name": "Склад Київ-Північ",
"product_id": "0192f3c1-4d5e-7f6a-8b7c-8d9e0f1a2b3c",
"sku": "PRK-0001",
"product_name": "Вода мінеральна 1,5 л",
"unit": "шт",
"batch_id": "0192f3c1-5e6f-7a7b-8c8d-9e0f1a2b3c4d",
"batch_number": "L-2610",
"expiry_date": "2027-04-30",
"quantity": "480",
"weight_kg": "720"
}
],
"pagination": { "page": 1, "page_size": 200, "total": 1, "pages": 1, "has_next": false },
"meta": { "request_id": null, "timestamp": "2026-11-03T09:00:00.000000" }
}Кількості й вага — рядки з десятковим числом (без втрати точності).
GET /movements?from=2026-11-01&to=2026-11-03 — рух товару
Обовʼязкові from, to; необовʼязковий warehouse_id. Дозвіл movements.read. Кожен рядок: at, kind (arrival, order_delivery, order_cancel, transfer_out, transfer_in, transfer_cancel, writeoff, writeoff_cancel), kind_label, direction (in/out), склад, товар, партія, quantity, номер документа.
GET /products — каталог
Параметри: q (пошук за назвою, артикулом, кодом, штрихкодом), page, page_size. Рядок: id, sku, code, barcode, name, unit, weight_kg, available (вільний залишок на всіх складах).
У заявках товар можна вказувати product_id, а в заявках на приймання й файлах — також кодом, артикулом або штрихкодом.
GET /points, POST /points — точки доставки
GET — дозвіл orders.read; параметри q, page, page_size. POST — дозвіл orders.write:
{
"code": "SHOP-0042",
"name": "Магазин «Приклад» №42",
"address": "м. Київ, вул. Прикладна, 42",
"city": "Київ",
"phone": "+380501234567",
"contact_person": "Олена Приклад"
}code — ваш код точки; повторний POST з тим самим code поверне наявну точку з кодом 200 замість 201.
9.4. Заявки на відвантаження
Заявка з API проходить ті самі правила, що й заявка з кабінету: лише товар вашого каталогу, точка — ваша (або нова адреса, або самовивіз), далі — підтвердження оператором, збирання, рейс, доставка.
POST /orders — нова заявка
Дозвіл orders.write.
{
"external_id": "ERP-SO-2026-001842",
"client_id": "0192f3c1-6f7a-7b8c-8d9e-0f1a2b3c4d5e",
"warehouse_id": null,
"delivery_mode": "delivery",
"requested_date": "2026-11-05",
"requested_window": "09:00–13:00",
"recipient": { "name": "Олена Приклад", "phone": "+380501234567" },
"notes": "Вʼїзд з двору",
"lines": [
{ "product_id": "0192f3c1-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "quantity": "48" }
]
}| Поле | Опис |
|---|---|
external_id | номер документа у вашій системі, до 100 символів — ключ ідемпотентності |
client_id | точка доставки; замість неї — new_point (name, address, city, phone, contact_person) або delivery_mode: "pickup" (самовивіз) |
warehouse_id | склад відвантаження; не вказано — обирає оператор |
lines | 1–500 рядків: product_id, quantity (> 0, до 3 знаків після коми) |
Відповідь 201 — картка заявки (як у GET /orders/{id}). Повторний запит з тим самим external_id нічого не створює: повертає наявну заявку з кодом 200 і заголовком Idempotent-Replayed: true. Безпечно повторювати запит після мережевої помилки чи таймауту.
GET /orders, GET /orders/{id}
Список: from, to (дата заявки), status (клієнтський статус), external_id, page, page_size (до 500). Дозвіл orders.read або orders.write.
{
"data": {
"id": "0192f3c1-7a8b-7c9d-8e0f-1a2b3c4d5e6f",
"external_id": "ERP-SO-2026-001842",
"order_number": "ЗВ-000318",
"order_date": "2026-11-03",
"status": "approved",
"client_status": "accepted",
"client_status_label": "Прийнято",
"client_name": "Магазин «Приклад» №42",
"address": "м. Київ, вул. Прикладна, 42",
"delivery_mode": "delivery",
"requested_date": "2026-11-05",
"requested_window": "09:00–13:00",
"lines_count": 1,
"quantity": "48",
"trip_number": null,
"trip_date": null,
"delivered_at": null,
"can_cancel": true,
"lines": [
{ "product_id": "0192f3c1-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "sku": "PRK-0001", "product_name": "Вода мінеральна 1,5 л", "unit": "шт", "quantity": "48" }
]
}
}Клієнтські статуси client_status: draft, accepted (прийнято), picking (збирається), picked (зібрано), in_transit (в дорозі), delivered (доставлено), return (повернення), cancelled (скасовано). Поле status — внутрішній стан, орієнтуйтеся на client_status.
GET /orders/{id}/track — де машина
Рейс, водій, остання позиція й маршрут, доки заявка в дорозі (available: true).
POST /orders/{id}/cancel
Тіло (необовʼязково): {"reason": "Клієнт відмовився"}. Скасувати можна, доки склад не почав збирати заявку; пізніше — 409 CONFLICT.
POST /orders/import — заявки файлом
multipart/form-data, поле file (Excel або CSV). Перший рядок — заголовки; колонки розпізнаються за назвою:
| Колонка | Назви в заголовку (будь-яка з) | Обовʼязкова |
|---|---|---|
| Номер документа | «Номер заявки», «Документ», external_id, order | ні — без неї весь файл = одна заявка |
| Точка | «Код точки», «Точка» | так, або «Адреса» |
| Адреса | «Адреса», address | якщо точку не знайдено за кодом — створюється нова |
| Товар | «Код», «Артикул», «Штрихкод», sku, barcode | так |
| Кількість | «Кількість», qty | так |
| Дата доставки | «Дата», date | ні |
| Коментар | «Коментар», «Примітка», comment | ні |
Номер заявки;Код точки;Адреса;Код товару;Кількість
ERP-SO-1843;SHOP-0042;;PRK-0001;24
ERP-SO-1843;SHOP-0042;;PRK-0002;12
ERP-SO-1844;;м. Львів, пл. Прикладна, 1;PRK-0001;6Рядки з однаковим номером — одна заявка; номер стає її external_id, тож повторне завантаження того самого файлу не задвоює заявки. Відповідь — звіт:
{
"data": {
"imported": 1,
"duplicates": 0,
"ids": ["0192f3c1-8b9c-7d0e-8f1a-2b3c4d5e6f70"],
"errors": [
{ "document": "ERP-SO-1844", "row": 4, "message": "Товар «PRK-0009» не знайдено в каталозі" }
]
}
}Документ з помилкою не створюється цілком; решта документів файлу — створюються.
9.5. Заявки на приймання (ASN)
POST /inbound-orders
Дозвіл inbound.write.
{
"external_id": "ERP-PO-2026-000977",
"warehouse_id": "0192f3c1-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
"supplier_name": "ТОВ «Приклад Виробництво»",
"supplier_doc_number": "ТН-000977",
"supplier_doc_date": "2026-11-03",
"planned_at": "2026-11-04T09:00:00+02:00",
"carrier": "ТОВ «Приклад Транс»",
"vehicle_plate": "AA0000AA",
"driver_name": "Іван Приклад",
"seal_numbers": ["SEAL-001"],
"temperature_mode": "chilled",
"temperature_min": "2",
"temperature_max": "6",
"lines": [
{ "product_code": "PRK-0001", "expected_qty": "480", "expected_batch": "L-2610", "expected_expiry": "2027-04-30" },
{ "barcode": "4820000000000", "expected_qty": "120" },
{ "product_id": "0192f3c1-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "expected_qty": "24", "pallets": "1" }
]
}- Товар рядка —
product_id, абоproduct_code(код, артикул), абоbarcode; шукаємо лише у вашому каталозі. Товар, якого в каталозі ще немає, можна передати зproduct_name— склад прийме його за назвою. temperature_mode:ambient,chilled,frozen.- Ідемпотентність: ключ —
external_id; без нього —supplier_doc_number(серед нескасованих заявок). Повтор повертає наявну заявку з кодом200іIdempotent-Replayed: true.
GET /inbound-orders, GET /inbound-orders/{id}
Список: status (можна кілька: ?status=received&status=discrepancy), from, to (плановий день або день створення), external_id, supplier_doc_number, page, page_size. Картка містить план і факт приймання по кожному рядку:
{
"data": {
"id": "0192f3c1-9c0d-7e1f-8a2b-3c4d5e6f7081",
"external_id": "ERP-PO-2026-000977",
"number": "ПР-000041",
"status": "discrepancy",
"warehouse_name": "Склад Київ-Північ",
"supplier_doc_number": "ТН-000977",
"arrived_at": "2026-11-04T08:47:12+02:00",
"arrival_status": "completed",
"discrepancy_status": "open",
"act_pdf_url": "/api/v1/consignor/inbound-orders/0192f3c1-9c0d-7e1f-8a2b-3c4d5e6f7081/act.pdf",
"lines": [
{
"product_code": "PRK-0001",
"product_name": "Вода мінеральна 1,5 л",
"expected_qty": "480",
"fact_qty": "476",
"damaged_qty": "4",
"diff_qty": "-4",
"discrepancy_reason": "damage"
}
]
}
}Статуси: planned → arrived (транспорт прибув) → receiving → received (без розбіжностей) або discrepancy (є розбіжності) → closed; cancelled. Причини розбіжностей: shortage (недостача), surplus (надлишок), damage (брак), expiry (термін), wrong_item (пересорт).
GET /inbound-orders/{id}/act.pdf
Акт приймання з розбіжностями (пломби, температура, план/факт, підписи) — коли приймання завершено.
POST /inbound-orders/import — заявки на приймання файлом
multipart/form-data: file (Excel/CSV), warehouse_id (обовʼязково), supplier_doc_number і planned_date (необовʼязково — для файлу без колонки номера). Колонки рядків — як у завантаженні рядків заявки на порталі: «Код»/«Артикул»/«Штрихкод», «Назва», «Кількість», «Партія», «Термін придатності», «Од.», «Палет»; додатково «Номер накладної»/«Документ» — групує рядки в окремі заявки (номер стає external_id і supplier_doc_number) і «Дата» — плановий день. Відповідь — той самий звіт {imported, duplicates, ids, errors}.
9.6. Документи й акти послуг
| Запит | Дозвіл | Опис |
|---|---|---|
GET /documents?from&to&kind | documents.read | приходи, інвентаризації, списання, переміщення (kind: arrival, inventory, writeoff, transfer) |
GET /documents/inventory/{id}/pdf | documents.read | акт інвентаризації (коли has_pdf: true) |
GET /acts?from&to&status | acts.read | акти наданих послуг: issued, sent, paid; суми, ПДВ, сплачено |
GET /acts/{id}/pdf | acts.read | PDF акта |
{
"data": [
{
"id": "0192f3c1-0d1e-7f2a-8b3c-4d5e6f708192",
"number": "АКТ-2026-10-007",
"act_date": "2026-11-01",
"period_start": "2026-10-01",
"period_end": "2026-10-31",
"status": "issued",
"subtotal": "18450.00",
"vat": "3690.00",
"total": "22140.00",
"currency": "UAH",
"paid_amount": "0.00",
"issued_at": "2026-11-01T10:12:00+02:00",
"pdf_url": "/api/v1/consignor/acts/0192f3c1-0d1e-7f2a-8b3c-4d5e6f708192/pdf"
}
],
"pagination": { "page": 1, "page_size": 50, "total": 1, "pages": 1, "has_next": false }
}9.7. Вебхуки
Вебхуки налаштовуються в кабінеті (API та інтеграції → Новий вебхук) або через API з дозволом webhooks: GET /webhooks, POST /webhooks ({"url": "https://…", "events": [...]} — у відповіді secret, показується один раз), DELETE /webhooks/{id}, GET /webhooks/{id}/deliveries (останні 20 доставок). Адреса — лише HTTPS; до 5 вебхуків на поклажодавця.
Події
| Подія | Коли | data |
|---|---|---|
order.status | заявку прийнято, почали збирати, зібрано, в дорозі, доставлено, відхилено, скасовано | order_id, order_number, external_id, status (клієнтський), stage, reason, at |
inbound.arrived | транспорт прибув на приймання | inbound_order_id, number, external_id, supplier_doc_number, status, arrived_at, vehicle_plate |
inbound.completed | приймання завершено | те саме + arrival_id, has_discrepancy, act_number, act_pdf_url |
delivery.proof | водій зафіксував доставку, часткову доставку чи відмову | proof_id, order_id, order_number, external_id, result (delivered/partial/refused), recipient_name, refusal_reason, delivered_at, returned_lines |
act.issued | виставлено акт послуг | act_id, number, act_date, period_start, period_end, subtotal, vat, total, currency, pdf_url |
webhook.test | тестовий виклик з кабінету | message, sent_at |
Запит
POST <ваш url>
Content-Type: application/json
User-Agent: ApexSFA-Consignor-Webhook/1.0
X-Apex-Signature: t=1793782800,v1=5f0c2b8e9d1a4c7f3e6b0a9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b
X-Apex-Event: order.status
X-Apex-Delivery-Id: 0192f3c1-1e2f-7a3b-8c4d-5e6f708192a3{
"attempt": 1,
"consignor_id": "0192f3c1-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"created_at": "2026-11-05T11:20:31.512000+00:00",
"data": {
"at": "2026-11-05T11:20:31.498000+00:00",
"external_id": "ERP-SO-2026-001842",
"order_id": "0192f3c1-7a8b-7c9d-8e0f-1a2b3c4d5e6f",
"order_number": "ЗВ-000318",
"reason": null,
"stage": "in_delivery",
"status": "in_transit"
},
"event": "order.status",
"event_id": "0192f3c1-1e2f-7a3b-8c4d-5e6f708192a3"
}event_id однаковий в усіх повторах однієї події — використовуйте його як ключ ідемпотентності на своєму боці.
Відповідь і повтори
- Відповідайте
2xxшвидко (до 10 с); обробку робіть асинхронно. - Будь-який інший код, таймаут чи помилка зʼєднання — повтор: до 5 спроб з паузами 1 хв → 5 хв → 30 хв → 2 год.
- Після 20 невдалих спроб поспіль вебхук вимикається; причина видно в кабінеті. Увімкніть його знову, коли приймач запрацює.
- Порядок доставки не гарантований — орієнтуйтеся на
data.at/ час події й поточний стан через API.
Перевірка підпису
X-Apex-Signature: t=<unix-час у секундах>,v1=<hex>
v1 = HEX( HMAC-SHA256( key = секрет вебхука (UTF-8), message = "<t>.<сире тіло запиту>" ) )Алгоритм той самий, що й у вихідних вебхуках: підписуються рівно ті байти, що прийшли; запити з t, старшим за 5 хв, відкидайте; порівнюйте в постійному часі.
import hashlib, hmac, time
def verify_apex_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
ts, received = int(parts["t"]), parts["v1"]
except (KeyError, ValueError):
return False
if tolerance and abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)// Express: app.post("/apex/webhook", express.raw({ type: "application/json" }), handler)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyApexSignature(rawBody, header, secret, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const ts = Number(parts.t);
if (!ts || !parts.v1) return false;
if (tolerance && Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}9.8. Помилки
| HTTP | Коли |
|---|---|
| 400 | невалідні дані: товар поза каталогом, порожня заявка, некоректна дата чи період |
| 401 | немає токена, токен невідомий, відкликаний, прострочений або запит з недозволеного IP |
| 403 | токену бракує дозволу; модуль кабінету замовника не підключено |
| 404 | обʼєкт не знайдено або він належить іншому поклажодавцю |
| 409 | дія неможлива в поточному стані (напр. скасування заявки, яку вже збирають) |
| 422 | порушення бізнес-правила (напр. змінити заявку на приймання після початку приймання) |
| 429 | перевищено ліміт запитів — повторіть після Retry-After |
9.9. Файли через SFTP
Для обміну файлами заявок і ASN без програмування доступний SFTP-обмін за запитом: ми відкриваємо поклажодавцю теку на SFTP-сервері ApexSFA, файли з неї обробляються так само, як POST /orders/import і POST /inbound-orders/import, а звіт кладеться поруч. Запит — через оператора складу або форму «Запросити доступ» на apexsfa.com/developers. Підключення — за ключем ed25519, як у розділі 5.