API ответственного хранения (Storage API v1)

Storage API — это способ подключить ваш склад на FULEX к внешней WMS или ERP, чтобы не вести топологию, остатки и заявки вручную в кабинете. По смыслу это как API-токен продавца в Wildberries или Ozon: вы выпускаете ключ и даёте внешней системе доступ к данным своего склада.

Прежде чем начать

Интеграция работает сразу на production (https://fulex.pro) — отдельной тестовой среды для интеграторов нет. Чтобы проверить подключение, ничего не сломав, используйте ключ «только чтение» и dry_run=true на изменяющих запросах (см. «Как протестировать, ничего не сломав»). Оплата, оферта, сделка и подпись актов остаются в кабинете FULEX — API их не заменяет.

Часть 1 — для всех

Что это и кому нужно

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

API нужен, если топология и остатки уже живут во внешней WMS / ERP, и вы хотите, чтобы они автоматически синхронизировались с FULEX. Тогда сотрудник не переносит данные руками, а внешняя система общается с платформой напрямую.

Кому что делать:

  • Менеджеру / владельцу склада — прочитать «Что можно и чего нельзя», «Быстрый старт» и «Ключ доступа»: этого хватает, чтобы завести доступ и понять границы.
  • Разработчику — «Справочник для разработчика»: базовый URL, заголовки, endpoints, webhooks и ошибки.

Что можно и чего нельзя через API

Через API можноТолько в кабинете FULEX
Импортировать топологию (зоны и ячейки) из WMSОплата и оферта
Синхронизировать занятость ячеекСоздание сделки и подпись актов
Получать адресные заявки, confirm / rejectОтклик на заявку из общего каталога
Фиксировать приёмку и отгрузкуУправление ключами и webhook (нужна сессия исполнителя)
Получать события (polling и webhooks)Оспаривание, санкции, поддержка

API — это про данные склада, а не про деньги. Денежных операций в нём нет, и он не заменяет юридические шаги сделки в кабинете.

Быстрый старт за 5 шагов

Всё выполняется сразу на production. Чтобы пройти путь безопасно, начните с ключа «только чтение» и проверяйте каждый изменяющий запрос через dry_run:

  1. Кабинет исполнителя → «API и интеграции» → выберите склад → создайте ключ и сразу сохраните его (показывается один раз).
  2. GET /api/v1/storage/facility — убедитесь, что ключ привязан к нужному складу.
  3. Импортируйте небольшую топологию: topology/import?dry_run=true → исправьте ошибки → повторите с dry_run=false и новым Idempotency-Key.
  4. Загрузите занятость (PUT .../cells/occupancy) и включите чтение событий (polling).
  5. Прогоните тестовую заявку: событие → confirm/reject → приёмка/отгрузка. Сверьте кабинет и WMS.

Когда маппинг сверён на dry_run без ошибок — переключите ключ на полный доступ (или выпустите боевой) и включайте запись. Подробный чек-лист — в разделе «Полный чек-лист запуска».

Ключ доступа: как получить и хранить

Ключ создаётся в кабинете для конкретного склада и определяет, что этому подключению разрешено (см. права/scopes). Один ключ = один склад; на разные склады нужны разные ключи.

Три правила, которые важно знать заранее
  • Полное значение ключа показывается один раз при создании. Потерянный ключ посмотреть нельзя — создайте новый (rotate) и отзовите старый (revoke).
  • Не кладите ключ в Git, переписку или Postman-коллекцию — только в secret / переменную окружения.
  • Ключ даёт доступ только к своему складу. Другие склады исполнителя этим ключом недоступны.

Управление ключами и webhook выполняется под сессией исполнителя (не под Bearer API-ключом):

  • POST /api/executor/storage/onboarding/api — API-черновик склада + первый ключ
  • POST /api/executor/storage/facilities/{facility_id}/api-keys — новый ключ к существующему складу
  • POST .../api-keys/{key_id}/rotate · POST .../api-keys/{key_id}/revoke — перевыпуск / отзыв

Как протестировать, ничего не сломав

Отдельной тестовой среды для интеграторов нет — все запросы идут сразу на production. Но проверить подключение можно безопасно, не затрагивая реальные данные склада:

  • Ключ «только чтение» — выпустите read-only ключ и прогоните все GET-запросы (склад, ячейки, заявки, события): проверите авторизацию, scopes, маппинг полей и пагинацию без единой записи.
  • dry_run=true — на изменяющих запросах (импорт топологии, занятость) сервер валидирует тело и возвращает счётчики, но ничего не пишет. Так вы сверяете формат и маппинг ячеек перед реальной загрузкой.
  • Тест вебхука — из кабинета отправьте событие storage.webhook.test на ваш URL и проверьте приём и HMAC-подпись.
  • Локальный симулятор — scripts/storage_api_simulator.py повторяет поведение API: стройте и отлаживайте интеграцию у себя локально, затем переключитесь на боевой ключ.

Полный цикл с записью (реальная приёмка/отгрузка) прогоняйте на одной небольшой настоящей операции — уже после того, как read-only и dry_run прошли без ошибок.

Словарь терминов

ТерминПростыми словами
Ключ (Bearer)Пароль подключения. Передаётся в заголовке Authorization в каждом запросе.
Scope (право)Что конкретному ключу разрешено: только читать или ещё и записывать.
external_idВаш постоянный ID объекта из WMS (зоны, ячейки). По нему FULEX узнаёт объект при повторной синхронизации.
Idempotency-KeyМетка одной операции. Позволяет безопасно повторить запрос, не создав дубль.
dry_run«Проверка вхолостую»: запрос проверяется, но ничего не меняет. Удобно перед реальной записью.
ТопологияСтруктура склада: зоны (стеллажи/ряды) и ячейки внутри них.
ЗанятостьКакие ячейки заняты и чьим товаром.
PollingПериодический опрос событий: вы сами запрашиваете «что нового».
WebhookНаоборот: FULEX сам присылает событие на ваш HTTPS-адрес.
Часть 2 — справочник для разработчика

Базовый URL

ПараметрЗначение
Base URLhttps://fulex.pro
Префикс API/api/v1/storage

Полный адрес endpoint — это Base URL + префикс + путь, например: https://fulex.pro/api/v1/storage/facility. Отдельного тестового домена/песочницы нет — все запросы идут на production; как проверять безопасно, см. «Как протестировать, ничего не сломав».

Авторизация и заголовки

Во всех внешних запросах к API:

Authorization: Bearer fulex_storage_...
Accept: application/json
Content-Type: application/json
Idempotency-Key: 01JXYZ...   ← для всех POST/PUT/PATCH

Передавать executor_id в теле не нужно — склад берётся из ключа. Денежных операций в этом API нет.

Права (scopes)

При создании ключа выбирается набор прав. Пресет «Полный» даёт все scopes ниже; «Только чтение» — без write.

ScopeЧто открывает
facility:readКарточка склада
topology:writeИмпорт зон и ячеек
cells:readЧтение ячеек и занятости
cells:writeСинхронизация занятости
applications:readСписок адресных заявок
applications:writeConfirm / reject заявки
events:readPolling событий
operations:writeПриёмка и завершение отгрузки

Общие типы данных

Тип / правилоКак принимаем
id (UUID FULEX)Строка UUID, стабильный идентификатор на стороне платформы
external_idВаш постоянный ID из WMS, строка до 200 символов, непустая. При retry тот же ID — не генерируйте новый UUID на каждый повтор
ВремяRFC 3339 UTC, например 2026-07-31T09:00:00Z
КоличестваЦелые неотрицательные числа
ОбъёмЧисло (decimal) в м³
Размеры / весЧисла в метрах и кг (width_m, depth_m, height_m, max_weight_kg)
СпискиQuery: limit 1–200, cursor; в ответе next_cursor или null
dry_runQuery boolean. true — только проверка, без записи. После успеха повторите то же тело с false и новым Idempotency-Key
Idempotency-KeyОбязателен на изменяющих запросах. Один ключ = одна логическая операция. Тот же ключ + то же тело → безопасный повтор. Тот же ключ + другое тело → 409

Склад

GET /api/v1/storage/facility · scope facility:read

Возвращает профиль склада, привязанного к ключу (включая layout и статус витрины). Другие склады исполнителя этим ключом недоступны.

Топология (зоны и ячейки)

Прочитать текущее

GET /api/v1/storage/cells · scope cells:read — зоны с ячейками.

Импорт

POST /api/v1/storage/topology/import?dry_run=true|false · scope topology:write · нужен Idempotency-Key

Режим — replace draft: вы присылаете желаемый полный список. Объект, которого нет в импорте, при применении архивируется, только если он не занят, не в резерве и не в открытой операции — иначе 409 topology_in_use.

Тело запроса (TopologyImport):

{
  "zones": [
    {
      "external_id": "ZONE-A",
      "name": "Зона A",
      "aisle": "A",
      "cells": [
        {
          "external_id": "A-01-01",
          "code": "A-01-01",
          "level": "1",
          "volume_m3": 1.7,
          "width_m": 1.2,
          "depth_m": 0.8,
          "height_m": 1.8,
          "max_weight_kg": 800
        }
      ]
    }
  ]
}
ПолеОбяз.ТипСмысл
zonesдамассивСписок зон (стеллажей/рядов)
zones[].external_idдаstring ≤200ID зоны в вашей WMS
zones[].nameнетstringЧеловекочитаемое имя
zones[].aisleнетstringПроход / ряд
zones[].cellsдамассивЯчейки зоны
cells[].external_idдаstring ≤200ID ячейки в WMS (уникален в рамках склада)
cells[].codeнетstringКод на плане / этикетке
cells[].levelнетstringЯрус
cells[].volume_m3нетnumberПолезный объём, м³
cells[].width_m / depth_m / height_mнетnumberГабариты, м (если объём не задан явно)
cells[].max_weight_kgнетnumberМакс. вес, кг

Ответ (и dry run, и apply): счётчики created_zones, created_cells, updated_cells, archived_cells, protected_cells, флаг dry_run, актуальный layout.

Занятость

Полная начальная загрузка из WMS

PUT /api/v1/storage/cells/occupancy?dry_run=true|false · scope cells:write · Idempotency-Key

Заменяет начальную занятость, пришедшую из WMS. Ячейки, которых нет в entries, становятся свободными для WMS-загрузки. Размещения активных сделок FULEX не снимаются и защищены.

{
  "entries": [
    {
      "external_id": "A-01-01",
      "owner_label": "ООО Пример",
      "pallet_number": "PAL-900"
    }
  ]
}
ПолеОбяз.ТипСмысл
entriesдамассивСписок занятых ячеек
external_idдаstringЯчейка из топологии
owner_labelнетstringВладелец партии / компания
pallet_numberнетstringНомер паллеты / партии

Одна ячейка

PATCH /api/v1/storage/cells/{external_id}/occupancy · scope cells:write · Idempotency-Key

{
  "status": "occupied",
  "owner_label": "ООО Пример",
  "pallet_number": "PAL-901"
}

status — только free или occupied. Ячейки активных сделок FULEX менять нельзя.

Также: GET /api/v1/storage/cells/occupancy — текущая эффективная загрузка.

Заявки

GET /api/v1/storage/applications?cursor=&limit=50 · scope applications:read

Список адресных заявок на этот склад. В карточке: id FULEX, объём, срок, тип товара, дедлайн подтверждения, статус. Контакты выдаются только в объёме, разрешённом процессом сделки.

МетодПутьТелоСмысл
POST /applications/{application_id}/confirm — Подтвердить операционную возможность принять товар. Это не оплата и не hard hold
POST /applications/{application_id}/reject опционально reason, note, stale_capacity (boolean) Отклонить. stale_capacity=true — сигнал, что вместимость в витрине устарела

Обе операции требуют Idempotency-Key и scope applications:write.

Отдельно событие storage.request.published — новая заявка в общем каталоге хранения; отклик на неё делается в кабинете, не через confirm.

Приёмка и отгрузка

Scope: operations:write. Фиксация факта в WMS ≠ подпись акта заказчиком.

Приёмка

POST /api/v1/storage/deals/{deal_id}/receipt · Idempotency-Key

{
  "pallets": 10
}

или явное размещение:

{
  "external_cell_ids": ["A-01-01", "A-01-02"]
}

Передайте либо pallets (целое ≥ 1, авторазмещение), либо external_cell_ids (массив ваших ID ячеек).

Отгрузка

POST /api/v1/storage/shipment-requests/{shipment_id}/complete · Idempotency-Key

{
  "external_cell_ids": ["A-01-01", "A-01-02"]
}

external_cell_ids обязателен — ячейки, которые освобождаются после фактической отгрузки. Запрос на отгрузку сначала создаётся в FULEX.

События: polling и webhooks

Polling

GET /api/v1/storage/events?cursor=&limit=100 · scope events:read

Доставка at-least-once: дедуплицируйте по event.id. Сохраняйте next_cursor только после обработки всей страницы. При первом запуске cursor можно не передавать.

Поля события: id (UUID), type, facility_id, resource_id, payload (объект), occurred_at, опционально cursor.

typeКогда
storage.application.createdАдресная заявка на ваш склад
storage.request.publishedЗаявка в общем каталоге хранения
storage.application.confirmed / rejectedПосле confirm/reject
storage.deal.paidСделка оплачена в FULEX
storage.topology.importedПрименён импорт топологии
storage.occupancy.updatedОбновлена занятость
storage.receipt.completedЗафиксирована приёмка
storage.shipment.requested / completedЗапрос/завершение отгрузки
storage.webhook.testТестовая доставка из кабинета

Webhooks

Настройка в кабинете: POST /api/executor/storage/facilities/{facility_id}/webhooks — URL только HTTPS, список event_types. Secret подписи показывается один раз.

Заголовки доставки:

  • X-Fulex-Webhook-Id
  • X-Fulex-Event-Type
  • X-Fulex-Webhook-Timestamp (unix seconds)
  • X-Fulex-Webhook-Signature — v1= + hex HMAC-SHA256

Строка подписи: <timestamp>.<сырое тело>. Проверяйте HMAC по raw body до парсинга JSON, constant-time compare, окно времени ~5 минут. Ответьте 2xx за 10 секунд. Иначе до 8 повторов с растущей задержкой (10 с → до 1 ч). Polling остаётся резервом.

Ошибки и повторы

{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "request_id": "req_01JXYZ",
    "details": [{ "field": "cells[0].status", "reason": "unsupported value" }]
  }
}
HTTPКогдаПовтор
400 / 422Формат или бизнес-валидацияНет — исправьте данные
401 / 403Ключ, scope, складНет
404Ресурс вне области ключаНет
409Idempotency / topology_in_use / защищённая ячейкаПеречитайте состояние
429Лимит (~120 req/min на ключ)Да, после Retry-After
5xxВременная ошибкаДа, backoff + jitter

Изменяющий запрос повторяйте только с тем же Idempotency-Key и тем же телом. В логах сохраняйте request_id, не ключ API.

Полный чек-лист запуска

  1. Кабинет → «API и интеграции» → склад или API-черновик → выпустите ключ (для проверки — «только чтение»).
  2. GET /api/v1/storage/facility — сверьте склад.
  3. Маленький topology/import?dry_run=true, исправьте ошибки, затем dry_run=false с новым Idempotency-Key.
  4. То же для PUT .../cells/occupancy.
  5. Включите polling, сохраните cursor; при необходимости настройте webhook и проверьте HMAC.
  6. Прогоните заявку на небольшой реальной операции: событие → confirm/reject → receipt/shipment.
  7. Сверьте кабинет и WMS. После успешной проверки переключите ключ на полный доступ (или выпустите боевой) для операций с записью.
Часть 3 — справка

Частые вопросы (FAQ)

Нужен ли программист, чтобы пользоваться API?

Чтобы подключить API — да: настройку запросов, polling и webhooks делает разработчик или интегратор вашей WMS. Но выпустить ключ, выбрать права и понять границы можно и без кода — это разделы «Быстрый старт» и «Ключ доступа». Если склад небольшой, API вообще не обязателен — всё делается в кабинете.

Я потерял ключ. Можно его восстановить?

Нет. Полное значение ключа показывается только один раз при создании. Создайте новый ключ (rotate) и отзовите старый (revoke) в разделе «API и интеграции».

Заменяет ли API кабинет FULEX?

Нет. Оплата, оферта, создание сделки и подпись актов остаются в кабинете. API синхронизирует данные склада (топология, занятость, заявки, приёмка/отгрузка), но не проводит деньги и юридические шаги.

Чем confirm заявки отличается от оплаты?

confirm подтверждает только операционную возможность принять товар. Это не оплата и не жёсткий резерв — деньги и сделка идут своим порядком в кабинете.

Сколько ключей можно создать и на сколько складов?

Ключ выпускается на один склад. На несколько складов — несколько ключей. К одному складу можно держать несколько ключей (например, отдельный «только чтение»).

Обязателен ли webhook?

Нет. Можно работать только на polling (GET /events). Webhook — это доставка событий вам на HTTPS-адрес; polling при этом остаётся резервом.

Как проверить интеграцию, ничего не сломав?

Отдельной тестовой среды нет — всё на production, но есть безопасные способы: ключ «только чтение» (все GET без записи), dry_run=true на изменяющих запросах (валидация без записи), тест вебхука из кабинета и локальный симулятор scripts/storage_api_simulator.py. Полный цикл с записью — на небольшой реальной операции. Подробнее — раздел «Как протестировать, ничего не сломав».

Машинный контракт: OpenAPI docs/openapi/storage-api-v1.yaml, ТЗ docs/SPEC_STORAGE_API_v1.md, Postman docs/postman/, симулятор scripts/storage_api_simulator.py. Старый Partner API (capacity/holds) — отдельный контур, не алиас v1.

См. также: ответственное хранение в кабинете.