ASL RAQAM Connect: руководство для партнёров

Версия API: v1. Версия документа: 0.9.2 от 14.10.2026, предварительная.

Документ для разработчиков онлайн-касс (E-POS), ТСД и учётных систем, которые подключают проверку кодов маркировки через ASL RAQAM Connect. Полная машиночитаемая спецификация лежит в openapi.yaml в формате OpenAPI 3.1.

Статус. API v1 реализован и проходит автоматические тесты на эмуляторе ASL BELGISI. Боевой и тестовый адреса будут сообщены при подключении: в документе стоит https://connect.aslraqam.uz, это рабочее название. Что ещё в разработке, перечислено в разделе «Что будет позже».

Содержание

  1. Что делает Connect
  2. Быстрый старт за 5 шагов
  3. Общие правила API
  4. Подключение мерчанта
  5. Проверка кодов: POST /check
  6. Вердикты
  7. События кассы: POST /events
  8. Остатки, коды, расхождения
  9. Синхронизация
  10. Инвентаризация
  11. Вебхуки
  12. Использование и тарификация
  13. Ошибки и лимиты
  14. Песочница: тестовые коды
  15. Примеры кода
  16. Что будет позже
  17. Журнал изменений

1. Что делает Connect

Касса сканирует код маркировки, а Connect за доли секунды отвечает, можно ли продавать товар и почему.

Кассе не нужно работать с ASL BELGISI напрямую: ключи, лимиты, повторы и сбои ASL берёт на себя Connect.

Касса / ТСД ──HTTPS──▶  ASL RAQAM Connect  ──▶  ASL BELGISI (ключ магазина)
      ▲                        │
      └──── вебхуки ◀──────────┘

Главное правило: касса никогда не останавливает продажу из-за Connect. Если Connect не ответил за 3 секунды или вернул 5xx, касса продаёт и отправляет событие продажи позже (см. 3.6).


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

В примерах используется песочница и тестовый ключ ck_test_…. Песочница работает на эмуляторе ASL с заранее подготовленными кодами (раздел 14).

export CONNECT=https://sandbox.connect.aslraqam.uz/api/v1   # адрес сообщим при подключении
export KEY=ck_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Шаг 1. Проверьте доступ.

curl -s $CONNECT/health
# {"status":"ok"}

Шаг 2. Зарегистрируйте магазин (мерчанта) по ИНН.

curl -s -X POST $CONNECT/merchants \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tin": "300000001", "name": "Магазин «Барака»"}'

Шаг 3. Передайте каталог маркируемых товаров (GTIN).

curl -s -X PUT $CONNECT/merchants/300000001/catalog \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode": "replace", "items": [{"gtin": "04780012345677", "name": "Вода 0,5 л"}]}'

Шаг 4. Подключите ASL BELGISI: передайте API-ключ магазина и его согласие.

curl -s -X PUT $CONNECT/merchants/300000001/asl-key \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"key": "<API-ключ ASL магазина>",
       "consent": {"accepted": true, "version": "2026-10-v1", "user": "Директор А. Каримов"}}'
# {"status":"active","organization":"…","product_groups":["14"],"expires_on":"2027-06-30T23:59:59+00:00","key_last4":"0001"}

Сразу после этого Connect начинает первую синхронизацию баланса. Когда она закончится, придёт вебхук sync.completed.

Шаг 5. Проверьте код на кассе.

curl -s -X POST $CONNECT/check \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  --max-time 3 \
  -d '{"merchant_tin": "300000001", "store_id": "store-01", "context": "SALE",
       "codes": ["010478001234567721OKOKOK00001"]}'
{
  "request_id": "01a13a69-0dcb-793f-9de1-319ece312fb9",
  "results": [{
    "index": 0,
    "ki": "010478001234567721OKOKOK00001",
    "verdict": "OK", "color": "green", "can_sell": true,
    "served_from": "ledger", "stale": false,
    "message": {"ru": "Можно продавать", "uz": "Sotish mumkin"},
    "hint": {"ru": "", "uz": ""},
    "product": {"gtin": "04780012345677"},
    "package": null,
    "asl": {"status": "INTRODUCED"},
    "details": {},
    "billable": false
  }],
  "summary": {"total": 1, "green": 1, "yellow": 0, "red": 0, "grey": 0, "can_sell_all": true},
  "connection": {"status": "active"}
}

После продажи отправьте событие SALE (раздел 7). На этом минимальная интеграция готова.


3. Общие правила API

3.1. Адрес и протокол

Боевой https://connect.aslraqam.uz/api/v1 (рабочее название)
Песочница https://sandbox.connect.aslraqam.uz/api/v1 (рабочее название)
Протокол только HTTPS, TLS 1.2+
Формат JSON, UTF-8
Время ISO 8601 с часовым поясом, рекомендуем UTC: 2026-10-14T09:15:00Z
Деньги целые числа в тийинах (1 сум = 100 тийинов)

3.2. Авторизация

Authorization: Bearer ck_live_…    # боевой ключ
Authorization: Bearer ck_test_…    # тестовый ключ
Право (scope) Что разрешает
merchants:write регистрация и изменение мерчантов, точек, каталога, ключа ASL; закрытие расхождений
checks:write POST /check
events:write POST /events, отмена событий
reports:read чтение мерчантов, остатков, кодов, расхождений, синхронизаций, инвентаризаций
sync:write запуск синхронизации вручную
inventory:write инвентаризация
webhooks:write управление вебхуками
usage:read статистика использования

3.3. Мерчант и точки

3.4. Идемпотентность

Хорошая практика для событий кассы: строить ключ из данных чека, например sale-{terminal_id}-{external_ref}. Тогда повторная отправка из офлайн-очереди не создаст дубль.

3.5. Трассировка, язык, совместимость

3.6. Таймауты и fail-open (обязательно)

Правило Значение
Таймаут ожидания /check 3 секунды
Что делать при таймауте, 5xx или отсутствии сети разрешить продажу и сохранить событие в локальную очередь
Когда отправлять событие из очереди при появлении связи, с тем же Idempotency-Key
Насколько старые события принимаются до 30 дней; старше 7 дней принимаются с пометкой late

Сбои ASL BELGISI не приводят к ошибкам /check. Если ASL недоступен, Connect отвечает 200 с вердиктом UNKNOWN (⚪, can_sell: true) или ответом по леджеру с флагом stale: true. Ошибки 5xx возможны только при сбое самого Connect.

3.7. Пагинация

Списки возвращают {"items": [...], "next_cursor": "…"}. Следующая страница запрашивается с параметром ?cursor=<next_cursor>, limit не больше 1000 (по умолчанию 100). next_cursor: null означает конец списка.


4. Подключение мерчанта

4.1. Порядок

  1. POST /merchants регистрирует мерчанта, статус pending.
  2. PUT /merchants/{tin}/catalog передаёт список маркируемых GTIN, которые магазин продаёт. Каталог можно передать и после подключения: новые GTIN подгрузятся автоматически.
  3. PUT /merchants/{tin}/asl-key передаёт API-ключ ASL магазина и его согласие. Connect проверяет ключ (до 10 секунд), сохраняет его в зашифрованном виде и запускает первую синхронизацию.
  4. Вебхук merchant.connected приходит сразу, sync.completed — после загрузки баланса.

4.2. Ключ ASL магазина

Магазин выпускает API-ключ в личном кабинете ASL BELGISI. Пользователю ключа нужна роль «Наблюдатель» с привилегией «Коды маркировки».

PUT /merchants/300000001/asl-key
{
  "key": "<API-ключ ASL>",
  "rotation_mode": "manual",
  "consent": {"accepted": true, "version": "2026-10-v1", "user": "Директор А. Каримов"}
}
Поле Обяз. Описание
key да API-ключ ASL BELGISI. Передаётся один раз; не храните его у себя
consent.accepted да true: магазин согласился, что Connect обращается к ASL от его имени
consent.version да версия текста согласия, который вы показали магазину
consent.user нет кто дал согласие
rotation_mode нет manual (по умолчанию): новый ключ передаёт партнёр. auto и resign пригодятся, когда заработает вход по ЭЦП (раздел 16)

Что проверяет Connect: формат ключа, принадлежность ИНН мерчанта, роли (пробный запрос к ASL), организацию и товарные группы. Ответ:

{"status": "active", "organization": "ООО «Барака»", "product_groups": ["14"],
 "expires_on": "2027-06-30T23:59:59+00:00", "key_last4": "0001"}

Ошибки подключения (HTTP 422):

Код Что сказать магазину
ASL_KEY_INVALID ключ неверный, отозван или истёк: выпустите новый в кабинете ASL
ASL_KEY_WRONG_TIN ключ принадлежит другой организации
ASL_INSUFFICIENT_ROLES не хватает роли «Наблюдатель» с привилегией «Коды маркировки»
ASL_UNAVAILABLE (503) ASL BELGISI временно недоступен, повторите позже
RATE_LIMITED (429) больше 10 попыток подключения за сутки

Если у работающего мерчанта новый ключ не прошёл проверку, Connect продолжает работать со старым.

4.3. Статус подключения

GET /merchants/{tin}/connection:

{"status": "active", "method": "manual", "key_last4": "0001",
 "expires_on": "2027-06-30T23:59:59+00:00", "key_expires_in_days": 259,
 "last_checked_at": "2026-10-14T12:34:57Z", "check_status": "ok", "missing_roles": [],
 "rotation_mode": "manual", "product_groups": ["14"], "consent_version": "2026-10-v1",
 "edo_status": null}
status Значение
pending зарегистрирован, ключ ещё не передан
active работает
key_expiring ключ истекает в ближайшие 14 дней: попросите магазин выпустить новый
key_invalid ключ отозван или истёк, проверка работает только по леджеру
wrong_tin, insufficient_roles ключ не подходит (см. 4.2)
suspended приостановлен оператором
disconnected мерчант отключён

Напоминания об истечении ключа приходят вебхуком merchant.connection.expiring за 14, 7, 3 и 1 день.

4.4. Настройки мерчанта

PATCH /merchants/{tin}:

{"name": "Магазин «Барака»", "policy": "strict",
 "policy_overrides": {"NOT_INTRODUCED": "block", "PACKAGE_CODE": "warn"}}

4.5. Отключение

DELETE /merchants/{tin}: ключ ASL удаляется сразу, данные леджера хранятся 90 дней для отчётов.

4.6. Виджет для мерчанта: сессия

Экраны подключения, остатков, расхождений и инвентаризации можно встроить в интерфейс кассы виджетом (сам виджет — в разработке, API сессий уже работает).

  1. Ваш сервер запрашивает одноразовый токен (право widget:issue):
POST /widget/sessions
{"merchant_tin": "300000001", "store_id": "store-01", "locale": "uz",
 "user": {"id": "u-17", "name": "Азиз", "role": "owner"},
 "scopes": ["connect", "reports", "inventory", "settings"]}
→ 201 {"token": "wt_…", "url": "https://connect.aslraqam.uz/widget/", "expires_at": "…"}
  1. Токен живёт 15 минут и обменивается один раз. Передавайте его в iframe через postMessage, а не в адресе страницы: адреса попадают в логи.
  2. Виджет сам обменивает токен на сессию (POST /widget/exchange) и держит её только в памяти. Сессия ограничена одним мерчантом: чужой ИНН даёт 404.
scopes Что открывает Кому
connect подключение ASL только owner
settings режим, переопределения, отключение только owner
reports обзор, остатки, коды, расхождения, синхронизация; закрыть расхождение может только owner все
inventory инвентаризация все
invoices ЭСФ (после подключения Didox) все

Проверка кодов доступна в виджете всегда. Ключ партнёра в браузер не попадает никогда.


5. Проверка кодов: POST /check

5.1. Запрос

{
  "merchant_tin": "300000001",
  "store_id": "store-01",
  "context": "SALE",
  "receipt_draft_id": "draft-1001",
  "codes": ["010478001234567721OKOKOK00001"],
  "options": {"verify_authenticity": "off"}
}
Поле Обяз. Описание
merchant_tin да ИНН мерчанта
store_id да ID точки в вашей системе
context да SALE, RETURN, RECEIVE, INVENTORY, INFO
codes да 1–100 кодов в любом виде
receipt_draft_id нет ID черновика чека: по нему ловятся повторные сканы одного кода в чеке
options.verify_authenticity нет off (по умолчанию), async, sync: проверка подлинности по криптохвосту

Коды можно передавать как есть со сканера: с символами GS (\u001d), со скобками (01)…(21)…, с префиксом ]d2, с криптохвостом. Connect сам нормализует код до КИ (GTIN и серийный номер). Криптохвост используется только для проверки подлинности, нигде не хранится и никогда не возвращается.

5.2. Контексты

context Когда Особенности
SALE пробитие товара в чек основной контекст
RETURN возврат от покупателя «продан этим магазином» означает OK; непроданный код даёт RETURN_NOT_SOLD
RECEIVE приёмка товара код упаковки раскрывается в состав (package.children), все вложенные считаются «в наличии»
INVENTORY инвентаризация возвращает состав упаковки
INFO справка о коде, например в карточке товара ничего не меняет

5.3. Ответ

Каждому входному коду соответствует один элемент results[], в том же порядке.

Поле Описание
index позиция кода во входном массиве
ki нормализованный КИ или null, если код не распознан
verdict вердикт (раздел 6)
color green / yellow / red / grey
can_sell рекомендация с учётом режима и переопределений
served_from ledger — из леджера, live — живой запрос в ASL, cache, fallback — ASL не ответил вовремя, input — ошибка во входных данных
stale true: ответ по устаревшим данным, потому что ASL был недоступен
message {"ru", "uz"} — короткий текст для экрана кассира, до 40 символов
hint {"ru", "uz"} — подсказка, что делать
product {"gtin"}
package для кода упаковки: type, children_count; children[] только в RECEIVE, INVENTORY, INFO
asl {"status"} — статус кода в ASL (INTRODUCED, WITHDRAWN…)
details уточнения: withdrawal_reason, sold_elsewhere, problem (почему код не распознан), reason (для NOT_ON_BALANCE)
billable тарифицирован ли этот код
summary total, green, yellow, red, grey, can_sell_all
connection.status статус подключения мерчанта: касса может показать предупреждение, например «ключ истекает»

Пример: код упаковки в контексте RECEIVE.

{
  "index": 0, "ki": "00467800123456BOX00001",
  "verdict": "OK", "color": "green", "can_sell": true, "served_from": "live",
  "package": {"type": "BOX_LV_1", "children_count": 3,
              "children": ["010478001234567721CHILD000001", "010478001234567721CHILD000002",
                           "010478001234567721CHILD000003"]},
  "asl": {"status": "INTRODUCED"}
}

5.4. Как отображать на кассе

5.5. Сколько ждать

Ответ из леджера приходит за десятки миллисекунд. Живой запрос в ASL ограничен 1,5 секунды. Если ASL не уложился, Connect возвращает ответ по леджеру (stale) или UNKNOWN, а проверку заканчивает в фоне. Если в фоне код окажется 🔴, придёт вебхук check.updated.


6. Вердикты

Вердикт Цвет can_sell мягкий / строгий Когда Текст кассиру (RU)
OK 🟢 да / да код на балансе магазина, в обороте, не продан Можно продавать
NOT_ON_BALANCE 🟡 да / нет код в обороте, но числится на другом участнике: поставщик не передал коды Код не на вашем балансе
NOT_INTRODUCED 🟡 да / нет код магазина, но не введён в оборот Код не в обороте
PACKAGE_CODE 🟡 да / нет отсканирован код упаковки, а не единицы товара Это код упаковки
IN_OUTGOING_INVOICE 🟡 да / нет код в исходящей ЭСФ покупателю (после подключения Didox) Код в исходящей ЭСФ
RETURN_NOT_SOLD 🟡 да / нет только RETURN: код не продавался Код не продавался
SOLD_LOCALLY 🔴 нет / нет по событиям кассы уже продан в этом магазине Уже продан у вас
ALREADY_SOLD 🔴 нет / нет в ASL выведен из оборота продажей Код уже продан
WITHDRAWN 🔴 нет / нет выведен из оборота по другой причине Выведен из оборота
WRITTEN_OFF 🔴 нет / нет списан Код списан
NOT_FOUND 🔴 нет / нет кода нет в ASL Код не найден
NOT_AUTHENTIC 🔴 нет / нет не прошёл проверку подлинности Код не подлинный
INVALID_FORMAT 🔴 нет / нет код повреждён или считан неверно Код считан неверно
DUPLICATE_IN_REQUEST 🔴 нет / нет код уже есть в запросе или в черновике чека Уже есть в чеке
UNKNOWN ⚪ да / да проверить не удалось: ASL недоступен, таймаут Проверка недоступна
NOT_CONNECTED ⚪ да / да мерчант не подключён или ключ недействителен, а данных в леджере нет Проверка не подключена
SERVICE_SUSPENDED ⚪ да / да доступ партнёра приостановлен Проверка приостановлена

Полные тексты и подсказки на обоих языках приходят в ответе /check (message и hint), хранить их у себя не нужно.

Причины NOT_ON_BALANCE (details.reason) появятся после подключения Didox: INVOICE_NOT_SIGNED, INVOICE_SIGNED_PENDING, INVOICE_SIGNED_STUCK, INVOICE_REJECTED, NO_INVOICE. Без Didox поле отсутствует.


7. События кассы: POST /events

Касса сообщает о каждой операции с маркированным товаром. По этим данным Connect ведёт остатки и находит расхождения.

{
  "merchant_tin": "300000001",
  "store_id": "store-01",
  "type": "SALE",
  "external_ref": "CHK-000123",
  "fiscal_sign": "123456789012",
  "terminal_id": "POS-1",
  "occurred_at": "2026-10-14T09:15:00Z",
  "items": [{"code": "010478001234567721OKOKOK00001", "price_tiyin": 450000}]
}
Поле Обяз. Описание
type да SALE, RETURN, RECEIVE, WRITE_OFF, MOVE, INVENTORY_ADJUST
external_ref да номер чека или документа у вас
occurred_at да время операции на кассе, с часовым поясом; не старше 30 дней и не в будущем
fiscal_sign, terminal_id нет фискальный признак и номер кассы
to_store_id для MOVE точка назначения
reason для WRITE_OFF DAMAGE, EXPIRED, LOSS, OTHER — только для локального учёта, в ASL не отправляется
items[] да 1–1000 позиций
items[].code да код в любом виде
items[].quantity, items[].unit нет по умолчанию 1 и pcs; для весовых товаров kg, l
items[].price_tiyin нет цена за единицу
items[].override нет продано вопреки вердикту: {"verdict", "reason", "approved_by"}

Ответ 201:

{"event_id": 2, "accepted_items": 1, "rejected_items": [],
 "warnings": [{"index": 0, "warning": "SOLD_WITH_RED_VERDICT", "verdict": "ALREADY_SOLD"}],
 "late": false}

Отмена события (аннулированный чек): POST /events/{event_id}/reverse. Повторная отмена не даёт ошибки и возвращает тот же результат.

{"event_id": 1, "reversed": true, "reversed_at": "2026-10-14T12:34:57Z", "restored": 1}

8. Остатки, коды, расхождения

Метод Что возвращает
GET /merchants/{tin}/stock?store_id=&gtin= по каждому GTIN: on_balance (на балансе в ASL и в обороте), in_stock_local (в наличии по событиям), sold_30d, open_discrepancies, last_sync_at
GET /merchants/{tin}/codes?gtin=&owned=&state=&store_id= коды из леджера
GET /merchants/{tin}/codes/{код} карточка кода: данные ASL, местное состояние, последние 20 событий, открытые расхождения
GET /merchants/{tin}/discrepancies?type=&status=&severity=&store_id= расхождения
PATCH /merchants/{tin}/discrepancies/{id} закрыть: {"status": "resolved" \| "ignored", "comment": "…"}

Выгрузка в файл. У stock, codes и discrepancies есть параметр format=csv|xlsx: все страницы с учётом фильтров приходят одним файлом (до 100 000 строк; больше — 422 EXPORT_TOO_LARGE, сузьте фильтр). CSV — UTF-8 с BOM и разделителем ;, открывается в Excel без настройки.

curl -s -H "Authorization: Bearer $KEY" -o stock.xlsx "$CONNECT/merchants/300000001/stock?format=xlsx"

Пример остатков:

{"items": [{"gtin": "04780012345677", "name": "Вода 0,5 л", "on_balance": 5, "in_stock_local": 4,
            "sold_30d": 1, "open_discrepancies": 0, "last_sync_at": "2026-10-14T12:34:57Z"}],
 "next_cursor": null}

Типы расхождений

Код Название Важность Что делать магазину
D1 Продан не со своего баланса высокая запросить у поставщика ЭСФ с кодами; риск штрафа
D2 Висит на балансе без движения больше 180 дней низкая провести инвентаризацию
D3 Ушёл с баланса без продажи через кассу средняя проверить: продан через другую кассу, передан, списан
D4 Продан дважды высокая проверить чеки
D5 Продажа не дошла до ASL за 72 часа высокая проверить отправку чека в ОФД
D6 Недостача на инвентаризации средняя найти товар или списать
D7 Чужой код на полке высокая запросить у поставщика передачу кодов

Расхождение закрывается автоматически, когда условие исчезло, например поставщик передал коды. Новое расхождение высокой важности приходит вебхуком discrepancy.created.


9. Синхронизация

Connect сам загружает баланс магазина из ASL: полностью при подключении, затем каждую ночь (GTIN с движением — ежедневно, остальные — раз в неделю). Новые GTIN из каталога подгружаются днём.

Ручной запуск — не чаще 1 раза в час на мерчанта:

POST /merchants/{tin}/sync          {"gtins": ["04780012345677"]}   ← необязательно; без поля — все GTIN
→ 202 {"sync_job_id": "01a13a68-…"}

GET /sync-jobs/{id}
→ {"id": "…", "kind": "manual", "status": "done", "gtins_total": 1, "gtins_done": 1,
   "codes_upserted": 6, "codes_left_balance": 3, "discrepancies_found": 2, "error": null}

status: queued, running, done, partial (часть GTIN не загрузилась), failed. Повторный запуск раньше чем через час вернёт 429 с details.retry_at. По окончании приходит вебхук sync.completed (для первой и ручной синхронизации) или sync.failed (для любой).


10. Инвентаризация

1. POST /merchants/{tin}/inventory-sessions   {"store_id": "store-01", "scope": "all"}
                                              (или "scope": {"gtins": ["04780012345677"]})
   → 201 {"id": "<session_id>", "status": "open"}

2. POST /inventory-sessions/{id}/scans        {"codes": [...до 1000...], "device_id": "TSD-7"}
   отменить скан: {"void": ["<ki>", ...]}

3. POST /inventory-sessions/{id}/finish       {"apply": false}

4. GET  /inventory-sessions/{id}               отчёт в любой момент
{"session_id": "…", "applied": false, "found_on_balance": 3,
 "shortage": ["…"], "shortage_count": 2,
 "foreign": ["010478001234567721FOREIGN0001"], "foreign_count": 1,
 "found_but_sold": [], "unknown_store_codes": 1,
 "by_gtin": [{"gtin": "04780012345677", "expected": 5, "found": 3}]}

11. Вебхуки

11.1. Подписка

POST /webhooks  {"url": "https://pos.example.uz/hooks/connect",
                 "events": ["merchant.connected", "sync.completed", "discrepancy.created", "check.updated"]}
→ 201 {"id": "…", "secret": "…"}          ← секрет показывается один раз (повтор запроса — secret: null)
GET    /webhooks
DELETE /webhooks/{id}
POST   /webhooks/{id}/test                ← тестовое событие webhook.test

Адрес должен быть HTTPS.

11.2. События

Событие Когда
merchant.connected ключ ASL проверен, мерчант подключён
merchant.connection.expiring до истечения ключа ASL 14, 7, 3, 1 день
merchant.connection.failed ключ истёк или отозван, чужой ИНН, не хватает ролей
sync.completed / sync.failed завершилась синхронизация
discrepancy.created новое расхождение высокой важности
discrepancy.resolved расхождение закрыто
check.updated фоновая проверка изменила вердикт кода на 🔴 после ответа кассе
inventory.finished завершена инвентаризация
billing.period.closed сформирован счёт за месяц
edo.*, invoice.* события Didox и ЭСФ (после подключения Didox)

11.3. Формат

POST /hooks/connect HTTP/1.1
Content-Type: application/json
X-Connect-Signature: t=1760445325,v1=5f2b…
X-Connect-Event: merchant.connected
X-Connect-Delivery: 01a13a69-…

{"id": "evt_01a13a690cf578b8", "type": "merchant.connected",
 "created_at": "2026-10-14T12:35:25Z", "api_version": "v1", "merchant_tin": "300000001",
 "data": {"organization": "ООО «Барака»", "product_groups": ["14"], "expires_on": "2027-06-30T23:59:59+00:00"}}

11.4. Проверка подписи (обязательно)

v1 = HMAC-SHA256(secret, "<t>." + сырое тело запроса) в шестнадцатеричном виде. Сообщения старше 5 минут отбрасывайте. Подпись считайте по сырому телу, а не по повторно сериализованному JSON.

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        ts = int(parts["t"])
    except (ValueError, KeyError):
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Примеры на PHP и Node.js — в разделе 15.

11.5. Доставка


12. Использование и тарификация

GET /usage?date_from=2026-10-01&date_to=2026-10-31 (период до 92 дней):

{"items": [{"day": "2026-10-14", "merchant_tin": "300000001", "store_id": "store-01",
            "checks_total": 14, "checks_billable": 9, "events_total": 1}],
 "totals": {"checks_total": 14, "checks_billable": 9, "events_total": 1, "active_stores": 1}}

Что не тарифицируется: - запросы с тестовым ключом ck_test_; - вердикты INVALID_FORMAT, DUPLICATE_IN_REQUEST, UNKNOWN, NOT_CONNECTED, SERVICE_SUSPENDED; - повторная проверка того же кода в той же точке в течение 10 минут.

Условия тарифа (абонплата за активную точку, квота проверок, БРВ) прописываются в договоре. Данные за текущий день обновляются при запросе.


13. Ошибки и лимиты

13.1. Формат ошибки

{"error": {"code": "MERCHANT_NOT_FOUND", "message": "Мерчант не найден",
           "message_uz": "Merchant topilmadi",
           "request_id": "01a13a69-0f6e-78d2-b7b7-05a7e9b6c832", "details": {}}}
HTTP Код Когда
400 VALIDATION_ERROR неверные поля; details.fields — какие именно
401 UNAUTHORIZED нет ключа, ключ неверный или отозван
403 FORBIDDEN_SCOPE у ключа нет нужного права (details.required)
403 PARTNER_SUSPENDED доступ партнёра приостановлен; /check при этом работает и отвечает SERVICE_SUSPENDED
403 IP_NOT_ALLOWED запрос с неразрешённого IP
404 NOT_FOUND, MERCHANT_NOT_FOUND
409 IDEMPOTENCY_CONFLICT ключ идемпотентности уже использован с другим телом
409 MERCHANT_EXISTS мерчант уже зарегистрирован
413 PAYLOAD_TOO_LARGE тело больше 256 КБ для /check или 5 МБ для остальных
422 TOO_MANY_CODES больше 100 кодов в /check
422 MERCHANT_NOT_READY для операции нужен ключ ASL
422 EXPORT_TOO_LARGE в выгрузке больше 100 000 строк — сузьте фильтр
422 ASL_KEY_INVALID, ASL_KEY_WRONG_TIN, ASL_INSUFFICIENT_ROLES см. 4.2
429 RATE_LIMITED превышен лимит; ждите Retry-After секунд
500 INTERNAL_ERROR сбой Connect: сообщите нам request_id
503 ASL_UNAVAILABLE только при подключении ключа

13.2. Лимиты

Что Лимит по умолчанию
Все запросы одного ключа 100 в секунду, всплеск до 200
/check по одному мерчанту 20 в секунду
Кодов в одном /check 100
Позиций в одном событии 1000
Сканов в одном запросе инвентаризации 1000
Ручная синхронизация 1 раз в час на мерчанта
Попытки подключения ключа ASL 10 в сутки на мерчанта

Лимиты настраиваются индивидуально. В каждом ответе есть заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.


14. Песочница: тестовые коды

В песочнице Connect работает с эмулятором ASL BELGISI. Тестовый мерчант — ИНН 300000001, тестовый GTIN — 04780012345677. API-ключ ASL для песочницы выдаём вместе с ключом партнёра.

Вердикты в контексте SALE:

Код Вердикт
010478001234567721OKOKOK00001 OK 🟢
010478001234567721FOREIGN0001 NOT_ON_BALANCE 🟡
010478001234567721NOTINTR0001 NOT_INTRODUCED 🟡
00467800123456BOX00001 PACKAGE_CODE 🟡 (в RECEIVE — OK и 3 вложенных кода)
010478001234567721SOLD0000001 ALREADY_SOLD 🔴, продан этим магазином
010478001234567721ELSEWH00001 ALREADY_SOLD 🔴, продан другим участником (details.sold_elsewhere)
010478001234567721WDRAWN00001 WITHDRAWN 🔴
010478001234567721WOFF0000001 WRITTEN_OFF 🔴
010478001234567721MISSING0001 NOT_FOUND 🔴
not-a-code INVALID_FORMAT 🔴
любой код дважды в одном запросе второй — DUPLICATE_IN_REQUEST 🔴

Таблица проверяется автоматическим тестом при каждой сборке документации.


15. Примеры кода

15.1. Python: проверка с fail-open

import uuid
import httpx

CONNECT = "https://connect.aslraqam.uz/api/v1"
client = httpx.Client(base_url=CONNECT, headers={"Authorization": "Bearer ck_live_…"},
                      timeout=httpx.Timeout(3.0))

def check(tin: str, store: str, codes: list[str], draft_id: str) -> dict | None:
    """None означает, что Connect недоступен: продаём (fail-open)."""
    try:
        r = client.post("/check", json={"merchant_tin": tin, "store_id": store, "context": "SALE",
                                        "receipt_draft_id": draft_id, "codes": codes})
        if r.status_code == 200:
            return r.json()
        if r.status_code >= 500 or r.status_code == 429:
            return None
        r.raise_for_status()          # 4xx — ошибка интеграции, её надо исправить
    except httpx.TransportError:      # таймаут, нет сети
        return None

def send_sale(tin: str, store: str, receipt_no: str, terminal: str, occurred_at: str, items: list[dict]):
    # Ключ из данных чека: повтор из офлайн-очереди не создаст дубль
    key = f"sale-{terminal}-{receipt_no}"
    r = client.post("/events", headers={"Idempotency-Key": key}, json={
        "merchant_tin": tin, "store_id": store, "type": "SALE", "external_ref": receipt_no,
        "terminal_id": terminal, "occurred_at": occurred_at, "items": items}, timeout=10)
    r.raise_for_status()
    return r.json()

15.2. PHP: проверка

<?php
function connect_check(string $tin, string $store, array $codes): ?array {
    $ch = curl_init('https://connect.aslraqam.uz/api/v1/check');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT_MS => 3000,                       // fail-open через 3 секунды
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('CONNECT_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'merchant_tin' => $tin, 'store_id' => $store, 'context' => 'SALE', 'codes' => $codes,
        ]),
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($body === false || $code >= 500 || $code == 429) {
        return null;                                      // продаём, событие отправим позже
    }
    return json_decode($body, true);
}

// Проверка подписи вебхука
function connect_verify(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        [$k, $v] = array_pad(explode('=', $p, 2), 2, '');
        $parts[$k] = $v;
    }
    if (!isset($parts['t'], $parts['v1']) || abs(time() - (int)$parts['t']) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1']);
}
// $raw = file_get_contents('php://input');
// connect_verify($secret, $_SERVER['HTTP_X_CONNECT_SIGNATURE'] ?? '', $raw);

15.3. JavaScript (Node.js 18+)

const CONNECT = "https://connect.aslraqam.uz/api/v1";

export async function check(tin, store, codes) {
  try {
    const r = await fetch(`${CONNECT}/check`, {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.CONNECT_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({ merchant_tin: tin, store_id: store, context: "SALE", codes }),
      signal: AbortSignal.timeout(3000),          // fail-open через 3 секунды
    });
    if (r.status >= 500 || r.status === 429) return null;
    if (!r.ok) throw new Error(`Connect ${r.status}: ${await r.text()}`);
    return await r.json();
  } catch (e) {
    if (e.name === "TimeoutError" || e.name === "TypeError") return null;   // таймаут / сеть
    throw e;
  }
}

// Проверка подписи вебхука (express: app.post(path, express.raw({type: "application/json"}), …))
import crypto from "node:crypto";
export function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const ts = Number(parts.t);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
  const got = Buffer.from(parts.v1 || "", "utf8");
  const exp = Buffer.from(expected, "utf8");
  return got.length === exp.length && crypto.timingSafeEqual(got, exp);
}

15.4. Чек-лист перед запуском в бой


16. Что будет позже

Возможность Статус
Подключение магазина входом по ЭЦП (E-IMZO) без ручного ключа в разработке, ждём данные от ASL
Didox: входящие и исходящие ЭСФ, причины NOT_ON_BALANCE, методы /edo, /invoices в разработке
Встраиваемый виджет для мерчанта (интерфейс; API сессий готов — 4.6) в разработке
Кабинет партнёра: ключи, журнал вебхуков, повторная отправка в разработке

Методы из этой таблицы появятся в /v1 без изменения существующих.


17. Журнал изменений

Дата Версия Изменения
14.10.2026 0.9.2 Сессии виджета: POST /widget/sessions, /widget/exchange (4.6)
14.10.2026 0.9.1 Выгрузка format=csv\|xlsx для остатков, кодов, расхождений; отчёт инвентаризации в XLSX
14.10.2026 0.9 Первая версия документа: API v1 (мерчанты, ключ ASL, /check, /events, отчёты, синхронизация, инвентаризация, вебхуки, использование)

Поддержка: при обращении приложите X-Request-Id из ответа. Контакты технической поддержки сообщим при подключении.