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, это рабочее название. Что ещё в разработке, перечислено в разделе «Что будет позже».
Содержание
- Что делает Connect
- Быстрый старт за 5 шагов
- Общие правила API
- Подключение мерчанта
- Проверка кодов:
POST /check - Вердикты
- События кассы:
POST /events - Остатки, коды, расхождения
- Синхронизация
- Инвентаризация
- Вебхуки
- Использование и тарификация
- Ошибки и лимиты
- Песочница: тестовые коды
- Примеры кода
- Что будет позже
- Журнал изменений
1. Что делает Connect
Касса сканирует код маркировки, а Connect за доли секунды отвечает, можно ли продавать товар и почему.
- Проверка до продажи. Код на балансе магазина? В обороте? Не продан ли раньше? Не код ли это
упаковки? Ответ приходит цветом (🟢 🟡 🔴 ⚪), флагом
can_sellи готовым текстом для кассира на русском и узбекском. - Леджер кодов магазина. Connect по ключу магазина выгружает из ASL BELGISI все коды на его балансе и обновляет их каждую ночь. Большинство проверок отвечаются из леджера: быстро и без нагрузки на ASL.
- Учёт движения. Касса сообщает о продажах, возвратах, приёмке, списании и перемещении. 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_… # тестовый ключ
- Ключ показывается один раз при выдаче. Мы храним только его хэш и восстановить ключ не можем. Потеряли ключ — выпустим новый, а старый отзовём.
- Ключу можно ограничить права (scopes) и разрешённые IP-адреса.
- Для замены без простоя у партнёра одновременно может быть два действующих ключа.
- Ключ хранится на сервере партнёра. Никогда не передавайте его в браузер или мобильное приложение кассира.
| Право (scope) | Что разрешает |
|---|---|
merchants:write |
регистрация и изменение мерчантов, точек, каталога, ключа ASL; закрытие расхождений |
checks:write |
POST /check |
events:write |
POST /events, отмена событий |
reports:read |
чтение мерчантов, остатков, кодов, расхождений, синхронизаций, инвентаризаций |
sync:write |
запуск синхронизации вручную |
inventory:write |
инвентаризация |
webhooks:write |
управление вебхуками |
usage:read |
статистика использования |
3.3. Мерчант и точки
- Мерчант определяется ИНН: в пути
/merchants/{tin}/…, в/checkи/eventsв полеmerchant_tin. ИНН состоит из 9 цифр (юрлицо) или 14 цифр (ПИНФЛ ИП). - Чужой или незарегистрированный ИНН возвращает 404
MERCHANT_NOT_FOUND, а не 403: так не раскрывается, кто подключён. store_id— это ID точки в вашей системе, строка до 64 символов. Регистрировать точку заранее не обязательно: при первом упоминании она создаётся автоматически. Название и адрес можно задать черезPUT /merchants/{tin}/stores/{store_id}.
3.4. Идемпотентность
- Заголовок
Idempotency-Keyс уникальной строкой, лучше UUID, обязателен для всех POST/PUT/PATCH/DELETE. Исключение —/check: там заголовок необязателен, но желателен. - Ключ хранится 24 часа.
- Повтор запроса с тем же ключом и тем же телом возвращает тот же ответ с заголовком
Idempotent-Replayed: true. Так можно безопасно повторить запрос после обрыва связи. - Тот же ключ с другим телом даёт 409
IDEMPOTENCY_CONFLICT. Тот же ответ приходит, если первый запрос ещё выполняется (details.in_progress: true). - Если запрос завершился ошибкой, ключ освобождается и запрос можно повторить с тем же ключом.
Хорошая практика для событий кассы: строить ключ из данных чека, например
sale-{terminal_id}-{external_ref}. Тогда повторная отправка из офлайн-очереди не создаст дубль.
3.5. Трассировка, язык, совместимость
X-Request-Id: если вы его прислали, он вернётся в ответе; если нет, сервер создаст свой. Он есть в каждом ответе и в каждой ошибке. Укажите его, когда пишете в поддержку.Accept-Language: ru | uzвыбирает язык поляmessageв ошибках. Узбекский текст всегда приходит вmessage_uz. В результатах проверки всегда есть оба языка.- Игнорируйте неизвестные поля. Новые поля добавляются без смены версии.
- Несовместимые изменения выходят только в
/v2;/v1после этого работает ещё 6 месяцев.
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. Порядок
POST /merchantsрегистрирует мерчанта, статусpending.PUT /merchants/{tin}/catalogпередаёт список маркируемых GTIN, которые магазин продаёт. Каталог можно передать и после подключения: новые GTIN подгрузятся автоматически.PUT /merchants/{tin}/asl-keyпередаёт API-ключ ASL магазина и его согласие. Connect проверяет ключ (до 10 секунд), сохраняет его в зашифрованном виде и запускает первую синхронизацию.- Вебхук
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"}}
policy:soft(по умолчанию) илиstrict. В строгом режиме 🟡-вердикты даютcan_sell: false.nullозначает настройку партнёра по умолчанию.policy_overrides: реакция на конкретный вердикт,allow,warnилиblock. Можно уточнить по причине:"NOT_ON_BALANCE:INVOICE_NOT_SIGNED": "warn". Порядок приоритета: причина важнее вердикта, настройка мерчанта важнее настройки партнёра.- 🔴-вердикты нельзя переключить в
allow: такой запрос вернёт 400.
4.5. Отключение
DELETE /merchants/{tin}: ключ ASL удаляется сразу, данные леджера хранятся 90 дней для отчётов.
4.6. Виджет для мерчанта: сессия
Экраны подключения, остатков, расхождений и инвентаризации можно встроить в интерфейс кассы виджетом (сам виджет — в разработке, API сессий уже работает).
- Ваш сервер запрашивает одноразовый токен (право
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": "…"}
- Токен живёт 15 минут и обменивается один раз. Передавайте его в iframe через
postMessage, а не в адресе страницы: адреса попадают в логи. - Виджет сам обменивает токен на сессию (
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. Как отображать на кассе
- Показывайте
messageкрупно, аhintмелко под ним. Язык выбирайте по настройкам кассы. - Цвет берите из
color, решение «пробивать или нет» — изcan_sell. can_sell— рекомендация. Касса может пропустить 🟡 с подтверждением старшего кассира. Если товар продан вопреки рекомендации, передайте это вitems[].overrideсобытия продажи (раздел 7).- ⚪ (
UNKNOWN,NOT_CONNECTED,SERVICE_SUSPENDED) всегда разрешает продажу.
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}
- Событие принимается всегда, даже при плохих вердиктах: касса уже провела операцию. Connect фиксирует факт и при необходимости открывает расхождение.
rejected_items[]— позиции, которые не удалось принять (INVALID_FORMAT,DUPLICATE_IN_EVENT,INVALID_QUANTITY,INVALID_UNIT,INVALID_ITEM). Остальные позиции приняты.warnings[]:SOLD_WITH_RED_VERDICT(продан код с 🔴-вердиктом),SOLD_TWICE(второйSALEбез возврата).- События офлайн-касс могут приходить с опозданием и не по порядку. Connect применяет их в порядке
occurred_at.
Отмена события (аннулированный чек): 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=>in= |
по каждому 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}]}
- Недостача открывает расхождение
D6, чужие коды —D7. - С
apply: trueнайденные коды переходят в «в наличии», ненайденные — в «недостача». - По завершении приходит вебхук
inventory.finished. - Отчёт в Excel:
GET /inventory-sessions/{id}?format=xlsx— листы «Сводка», «По GTIN», «Недостача», «Чужие коды», «Продано, но найдено».
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. Доставка
- Ваш сервер должен ответить 2xx за 10 секунд. Тяжёлую обработку делайте асинхронно.
- Повторы через 1 мин, 5 мин, 15 мин, 1 ч, 3 ч, 6 ч, 12 ч, 24 ч, затем доставка получает статус
failed. - Если адрес не принимает события 3 дня, он отключается, а мы связываемся с вами.
- Гарантия «хотя бы один раз»: отбрасывайте дубли по
id. Порядок событий не гарантирован, ориентируйтесь наcreated_at.
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. Чек-лист перед запуском в бой
- [ ] Таймаут
/check3 секунды; при таймауте, 5xx или 429 продажа разрешается. - [ ] Офлайн-очередь событий с
Idempotency-Key, построенным из данных чека. - [ ]
occurred_atс часовым поясом; события старше 30 дней не принимаются. - [ ]
receipt_draft_idпередаётся, чтобы ловить повторный скан одного кода в чеке. - [ ] Аннулированный чек отменяется через
/events/{id}/reverse. - [ ] Продажа вопреки 🟡/🔴 передаётся в
items[].override. - [ ] Вебхуки: проверяется подпись по сырому телу, дубли отбрасываются по
id, ответ 2xx за 10 секунд. - [ ] Неизвестные поля в ответах игнорируются.
- [ ] Ключи
ck_live_хранятся только на сервере; ключ ASL магазина у себя не хранится. - [ ] Логи содержат
X-Request-Id.
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 из ответа.
Контакты технической поддержки сообщим при подключении.