PDF
Розділ: API замовника складу

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 активних токенів на поклажодавця.
HTTP
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:

JSON
{
  "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.

Рядок — склад × товар × партія (з терміном придатності, якщо партію ведуть):

JSON
{
  "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:

JSON
{
  "code": "SHOP-0042",
  "name": "Магазин «Приклад» №42",
  "address": "м. Київ, вул. Прикладна, 42",
  "city": "Київ",
  "phone": "+380501234567",
  "contact_person": "Олена Приклад"
}

code — ваш код точки; повторний POST з тим самим code поверне наявну точку з кодом 200 замість 201.


9.4. Заявки на відвантаження

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

POST /orders — нова заявка

Дозвіл orders.write.

JSON
{
  "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склад відвантаження; не вказано — обирає оператор
lines1–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.

JSON
{
  "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ні
CSV
Номер заявки;Код точки;Адреса;Код товару;Кількість
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, тож повторне завантаження того самого файлу не задвоює заявки. Відповідь — звіт:

JSON
{
  "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.

JSON
{
  "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. Картка містить план і факт приймання по кожному рядку:

JSON
{
  "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&kinddocuments.readприходи, інвентаризації, списання, переміщення (kind: arrival, inventory, writeoff, transfer)
GET /documents/inventory/{id}/pdfdocuments.readакт інвентаризації (коли has_pdf: true)
GET /acts?from&to&statusacts.readакти наданих послуг: issued, sent, paid; суми, ПДВ, сплачено
GET /acts/{id}/pdfacts.readPDF акта
JSON
{
  "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

Запит

HTTP
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
JSON
{
  "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)

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.