Тема
Пять операций
4.1. Найти клиента
GET /api/integration/pos/consumers/resolve?code=482915
GET /api/integration/pos/consumers/resolve?phone=%2B998901234567Передайте один из двух параметров:
| Параметр | Что это | Замечания |
|---|---|---|
code | шесть цифр с экрана покупателя. В QR из приложения закодированы именно они и ничего больше: сканер выдаёт строку из шести цифр, её и передавайте. Кассир может и ввести цифры, которые продиктовал клиент | Основной способ. Код живёт 5 минут, потом перестаёт действовать; пока он жив, приложение показывает один и тот же. Неверный или истёкший код — 404 |
phone | номер в формате E.164 (+998...), знак + кодируйте в URL | Запасной способ для ручного поиска. Владелец мерчанта может отключить поиск по телефону — тогда 422 с текстом «Поиск по номеру телефона отключён владельцем. Попросите код клиента или QR»; рассчитывайте на code |
Если переданы оба параметра, используется code, а phone игнорируется. Не передан ни один — 422. Постоянный идентификатор consumerId в этот запрос не передаётся: он приходит в ответе.
Это обязательный шаг перед продажей с клиентом. Успешный ответ сервер запоминает («клиент опознан у этого мерчанта»). Предпросмотр и продажа с consumerId принимаются, только если клиента опознали этим запросом не раньше чем за 7 суток, а если в чеке или предпросмотре есть списание баллов (redeemPoints > 0) — не раньше чем за 15 минут; иначе — 422 с одним и тем же текстом, существует такой клиент или нет («Клиент не опознан на этой кассе: попросите показать QR или назвать код ещё раз»). Подставить consumerId «из головы» или из старого списка нельзя. Кэшировать consumerId на кассе можно, но перед чеком со списанием баллов вызывайте GET /consumers/resolve заново, а если с последнего поиска прошло больше 7 суток — и перед обычным чеком. Срок кода (5 минут) на это не влияет: опознание действует по тем же 7 суткам и 15 минутам, даже когда сам код уже истёк. Лимиты: не больше 30 запросов поиска в минуту на сотрудника ключа и 120 на мерчанта, а неудачных поисков по коду (404) — не больше 10 в минуту на сотрудника ключа (дальше 429 с Retry-After). Живой кассир ошибается кодом редко; лимит нужен, чтобы шесть цифр нельзя было перебирать.
bash
curl -s "https://connect.uget.uz/api/integration/pos/consumers/resolve?code=482915" -H "X-Api-Key: $UGET_KEY"
curl -s "https://connect.uget.uz/api/integration/pos/consumers/resolve?phone=%2B998901234567" -H "X-Api-Key: $UGET_KEY"json
{ "consumerId": 1024, "name": "Дилшод", "phone": "+998901234567", "points": "45000.00" }points — баланс баллов клиента у этого мерчанта, не общий по платформе. Клиента нет в системе → 404. Это нормальный случай: продайте чек как анонимный (см. «Провести продажу»).
4.2. Показать итоги до пробития
POST /api/integration/pos/sales/previewНичего не записывает. Вызывайте столько раз, сколько нужно — при каждом изменении корзины.
bash
curl -s -X POST https://connect.uget.uz/api/integration/pos/sales/preview -H "X-Api-Key: $UGET_KEY" -H "Content-Type: application/json" -d '{"consumerId":1024,"idempotencyKey":"preview","redeemPoints":"5000.00","items":[{"productId":512,"qty":"2","price":"25000.00"}]}'json
{
"goods": "50000.00",
"discount": "5000.00",
"payable": "45000.00",
"giftsToIssue": 0,
"pointsBalance": "45000.00",
"maxRedeemPoints": "22500.00",
"pointsToAccrue": "2250.00"
}| Поле | Что показать кассиру |
|---|---|
goods | стоимость товаров до скидок |
discount | общая скидка: акции мерчанта и сертификат/купон вместе (как discountAmount в ответе продажи) |
promoDiscount | скидка только по акциям мерчанта |
certificateDiscount | скидка по сертификату/купону из certificateCode; считается от остатка после акций. 0 — кода нет, он неверный или не нужен |
certificateNotApplied | COVERED_BY_PROMO — акции уже закрыли чек, сертификат не списывается и остаётся у клиента |
certificateLoss | сколько номинала сертификата сгорит: номинал больше остатка чека отдаёт только остаток, и только если сертификат после этой активации не останется у клиента. У многоразового с остатком активаций — 0 |
payable | к оплате после всех скидок (акции и сертификат), до списания баллов |
giftsToIssue | сколько подарков выдать |
pointsBalance | баллов у клиента |
maxRedeemPoints | предел списания в этом чеке — не давайте кассиру ввести больше |
pointsToAccrue | сколько начислится |
Это оценка, а не обязательство. Правило акции может закончиться, а баланс — измениться на другой кассе в ту же секунду. Итог фиксирует только регистрация продажи, и он может отличаться. Кроме того, предпросмотр не запускает антифрод — в редком случае начисление при проводке окажется меньше показанного. Не сверяйте эти числа программно и не отказывайте в продаже из-за расхождения.
idempotencyKey в предпросмотре не используется, но поле обязательно по схеме — передавайте любую непустую строку.
4.3. Провести продажу
POST /api/integration/pos/salesbash
curl -s -X POST https://connect.uget.uz/api/integration/pos/sales -H "X-Api-Key: $UGET_KEY" -H "Content-Type: application/json" -d '{"consumerId":1024,"idempotencyKey":"3b0c1e2a-6f1e-4a71-9c1e-7d3a2f5b8c41","redeemPoints":"5000.00","certificateCode":"WELCOME2026","items":[{"productId":512,"qty":"2","price":"25000.00"}]}'Ответ 201 Created:
json
{
"saleId": 90210,
"goodsAmount": "50000.00",
"discountAmount": "5000.00",
"giftsIssued": 0,
"redeemedPoints": "5000.00",
"cashPaid": "40000.00",
"fiscalReceiptRef": null,
"accrualOperationId": 77431
}cashPaid = товары − скидка − баллы. Именно эту сумму берите деньгами.
Поля запроса:
| Поле | Обяз. | Примечание |
|---|---|---|
idempotencyKey | да | UUID, до 80 символов. См. «Идемпотентность» — это главное поле контракта. |
items | да | Непустой список. qty и price — положительные, productId необязателен. |
consumerId | нет | null — анонимная продажа: чек пройдёт, но без баллов и подарков. Если указан — клиента должны были опознать через GET /consumers/resolve не раньше чем за 7 суток (при redeemPoints > 0 — за 15 минут), иначе 422. |
redeemPoints | нет | Не больше maxRedeemPoints из предпросмотра. |
certificateCode | нет | Код сертификата или купона, если клиент его предъявил. |
Точки продажи и смены в запросе нет намеренно: точка берётся из ключа, а смену ваша касса ведёт у себя. Z-отчёт uget по такой точке строится по продажам за период.
4.4. Вернуть чек (целиком или частью)
POST /api/integration/pos/sales/{saleId}/refund
X-Api-Key: <ключ>
Content-Type: application/json
{ "reason": "Возврат товара", "idempotencyKey": "refund-90210-1", "items": [ { "lineIndex": 1, "qty": 2 } ] }Все поля необязательны.
| Поле | Что значит |
|---|---|
reason | Причина со слов кассира, до 200 символов |
idempotencyKey | Ключ возврата, до 100 символов. Передавайте всегда: без него повтор запроса после обрыва связи создаст второй частичный возврат |
items | Позиции к возврату: lineIndex — номер позиции в чеке с нуля (порядок items в запросе продажи, номера видны в GET /sales/{saleId}), qty — сколько единиц, не больше остатка, до трёх знаков после запятой. Не передан — вернуть весь чек (или весь остаток, если частичные возвраты уже были) |
Возврат целиком (без items) работает как раньше: начисленный кешбек снимается, списанные баллы возвращаются клиенту, оплата картой возвращается, использованный сертификат возвращается. Если на этом чеке клиенту пришёл приветственный бонус uget, он тоже снимается и снова ждёт следующей покупки клиента; если клиент бонус уже потратил, снимается остаток, а бонус второй раз не выдаётся. pointsWithdrawn — сколько баллов фактически снято, не больше баланса клиента. Старые кассы, присылающие только reason или пустое тело, не требуют изменений.
Возврат частью (items):
- Сумма возвращённой позиции —
количество × цена. Начисленный за неё кешбек снимается пропорционально её доле в чеке (с округлением вверх до сотой, не больше баланса клиента), баллы, которыми клиент оплатил чек, пропорционально возвращаются, наличные к выдаче —cashToReturn. - Возвратов по одному чеку может быть несколько. Суммарно нельзя вернуть больше чека; последний возврат добирает остаток до копейки, погрешность округления не накапливается. Превышение —
422с объяснением (какая позиция, сколько осталось). - Указать позицию дважды в
items, несуществующий номер или нулевое количество нельзя —422.
Ответ:
json
{
"saleId": 90210, "refundId": 88, "refundNumber": 1, "kind": "PARTIAL",
"idempotencyKey": "refund-90210-1", "replayed": false,
"partial": true, "saleFullyRefunded": false,
"refundedAmount": "30000.00", "cashToReturn": "30000.00",
"cashbackWithdrawn": "3000.00", "pointsWithdrawn": "3000.00", "pointsReturned": "0.00",
"pointsBalanceBefore": "7000.00", "pointsBalanceAfter": "4000.00",
"items": [ { "lineIndex": 1, "qty": "1", "amount": "30000.00" } ],
"remainingAmount": "40000.00",
"remainingItems": [ { "lineIndex": 0, "qty": "2", "amount": "20000.00" }, { "lineIndex": 2, "qty": "5", "amount": "20000.00" } ],
"cardRefunded": false, "certificateReturned": false
}pointsBalanceBefore / pointsBalanceAfter — баланс баллов клиента у этого мерчанта до и после именно этого возврата; null, если чек пробит без клиента. partial: false и saleFullyRefunded: true — после этого возврата в чеке ничего не осталось.
Повтор с тем же idempotencyKey по тому же чеку возвращает тот же ответ с "replayed": true и ничего не создаёт. Тот же ключ с другими позициями — 422. Новый ключ по полностью возвращённому чеку — 422 «Чек уже возвращён».
Где частичный возврат невозможен. Пропорциональный пересчёт не подменяет то, что уже выдано клиенту на руки, поэтому для чека, на котором есть скидка по акции (в том числе «N+M» и бесплатный товар), погашенный сертификат или купон, выданный подарок или штампы, денежный кешбек на карту, кешбек по правилу на отдельные товары, оплата картой через uGet, фискализация платформой через настоящий ОФД или начисление не по кешбеку чека (бонус новичка, реферальное), а также для чека без записанного расчёта скидок — ответ 422: «Частичный возврат по этому чеку невозможен: …. Верните чек целиком (запрос без items) и пробейте новый чек на оставшиеся товары». Целиком такой чек возвращается, как раньше. Если ваш чек в магазине идёт без акций, ограничений нет.
Вернуть можно только чек своей точки: ключ выпущен на одну точку, и чужой чек того же мерчанта отвечает 404 — подтверждать существование чужих чеков незачем. Повторный возврат уже полностью возвращённого чека — 422. Порог согласования. Если владелец включил в кабинете согласование возвратов и задал порог, то сумма возвратов по чеку нарастающим итогом (уже возвращённое плюс этот возврат) сверяется с порогом: чек, возвращённый мелкими частями, обойти его не может. Возврат выше порога отвечает 409 «Требуется согласование владельца» (type — …/errors/approval-required, поля operationType, amount — накопленная сумма, threshold — порог) и ничего не меняет. Это не повтор по ключу: в отличие от 409 при регистрации продажи, успехом он не считается. Через внешний API согласовать возврат нельзя — оформляет владелец в кабинете или на кассе uGet.
После частичного возврата остаток возвращайте этим же запросом (без items): возврат этого чека целиком из кабинета или кассы uGet будет отклонён, пока в чеке есть частичные возвраты.
4.5. Состояние чека
GET /api/integration/pos/sales/{saleId}
X-Api-Key: <ключ>json
{
"saleId": 90210,
"clientRef": "3b0c1e2a-6f1e-4a71-9c1e-7d3a2f5b8c41",
"consumerId": 1024,
"totalAmount": "70000.00",
"refunded": false,
"refundedAt": null,
"occurredAt": "2026-09-05T14:22:11Z",
"partiallyRefunded": true,
"refundedAmount": "30000.00",
"remainingAmount": "40000.00",
"items": [
{ "lineIndex": 0, "productId": null, "name": "", "qty": "2", "price": "10000.00", "amount": "20000.00",
"refundedQty": "0", "remainingQty": "2", "refundedAmount": "0.00" },
{ "lineIndex": 1, "productId": null, "name": "", "qty": "1", "price": "30000.00", "amount": "30000.00",
"refundedQty": "1", "remainingQty": "0", "refundedAmount": "30000.00" }
],
"refunds": [
{ "refundId": 88, "refundNumber": 1, "kind": "PARTIAL", "idempotencyKey": "refund-90210-1",
"amount": "30000.00", "cashToReturn": "30000.00", "cashbackWithdrawn": "3000.00",
"pointsReturned": "0.00", "saleFullyRefunded": false, "refundedAt": "2026-09-05T15:01:00Z" }
]
}refunded — чек возвращён полностью; partiallyRefunded — часть уже возвращена и вернуть ещё есть что. lineIndex — номер позиции для возврата по позициям. Возвраты целиком, оформленные до появления частичных, в refunds не перечисляются — о них говорит refunded.
Нужен для сверки: связь оборвалась на ответе, касса не знает, прошёл чек или нет. Спросить дешевле, чем пробивать повторно (хотя идемпотентность защитит и от повтора).