POS Partner API
Публичный API торговой точки для интеграции внешних POS-партнёров (QRPay-сервисы безналичной оплаты).
Base URL
| Env | URL |
|---|---|
| Sandbox / Test | https://erp-test.nirbi.ru/partners/v1 |
| Production | выдаётся при подключении к конкретной франшизе |
Все эндпоинты — JSON. Money в копейках (минимальные единицы валюты, ISO 4217 minor units): 20100 = 201 ₽.
Test-credentials и список тестовых ТТ выдаются вместе с client_id/client_secret. Примеры в этом документе используют реальные id из sandbox БД — можно дёргать сразу после получения JWT.
Authentication
OAuth 2.0 client_credentials. Партнёр получает client_id / client_secret при подключении.
POST /auth/token
curl -X POST https://erp-test.nirbi.ru/partners/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"client_id": "<выдаём>",
"client_secret": "<выдаём>"
}'Response 200:
{
"access_token": "eyJhbGciOiJIUzI1NiI...",
"expires_in": 86400,
"token_type": "Bearer",
"scope": ["orders.read", "orders.close", "orders.refund", "tables.read", "webhooks.manage"]
}Все последующие запросы:
Authorization: Bearer <access_token>
JWT валиден 24 часа.
Endpoints
GET /tables
Список столов ТТ.
Query:
| Param | Required | Описание |
|---|---|---|
store_id | yes | UUID торговой точки |
status | no | free / occupied / reserved (CSV) |
Example:
curl 'https://erp-test.nirbi.ru/partners/v1/tables?store_id=bf5291f8-58e5-429b-8156-77beadd06fb4&status=occupied' \
-H 'Authorization: Bearer <token>'Response 200:
{
"data": [
{
"id": "17d5214f-fd1e-4f43-abfe-2accb68705ec",
"store_id": "bf5291f8-58e5-429b-8156-77beadd06fb4",
"number": 2,
"label": null,
"capacity": 4,
"status": "occupied",
"current_order_id": "50c45f4f-3d75-43b5-9aa2-035ba3bb1edc",
"occupied_at": "2026-04-29T18:14:03+03:00"
}
]
}GET /tables/{tableId}/active-order
Текущий открытый заказ стола (status ∈ new / accepted / ready).
Example:
curl https://erp-test.nirbi.ru/partners/v1/tables/1fe9fe95-f287-4dbc-bf32-9597d6d95126/active-order \
-H 'Authorization: Bearer <token>'Response 200 — Order shape (см. GET /orders/{id}).
Response 404 — стол свободен либо заказ уже закрыт.
GET /orders/{id}
Один заказ по UUID.
Example:
curl https://erp-test.nirbi.ru/partners/v1/orders/50c45f4f-3d75-43b5-9aa2-035ba3bb1edc \
-H 'Authorization: Bearer <token>'Response 200 — Order shape:
{
"data": {
"id": "50c45f4f-3d75-43b5-9aa2-035ba3bb1edc",
"order_number": "007",
"store_id": "bf5291f8-58e5-429b-8156-77beadd06fb4",
"table_id": "17d5214f-fd1e-4f43-abfe-2accb68705ec",
"table_number": 2,
"status": "closed",
"order_type": "dine_in",
"currency": "RUB",
"total": 20100,
"items": [
{
"id": "...",
"product_name": "E2E Phase2 add-items",
"quantity": 2,
"unit_price": 9950,
"total_price": 19900,
"modifiers": [],
"vat": "vat20"
},
{
"id": "...",
"product_name": "Тестовая пицца",
"quantity": 1,
"unit_price": 100,
"total_price": 200,
"modifiers": [
{ "group_name": "Сыр", "option_name": "Чеддер", "price": 100 }
],
"vat": "vat20"
}
],
"payment": {
"method": "card",
"paid_amount": 20100,
"paid_at": "2026-04-29T18:14:03+03:00"
},
"discount": { "amount": 0, "reason": null },
"tip": { "amount": 0, "waiter_id": null, "waiter_name": null },
"customer": { "id": null, "phone": null },
"created_at": "2026-04-29T18:00:00+03:00",
"updated_at": "2026-04-29T18:14:03+03:00"
}
}Status values: new → accepted → ready → closed. Дополнительно cancelled.
POST /orders/{id}/close-with-payment
Партнёр принял оплату от клиента и закрывает заказ.
Example:
curl -X POST https://erp-test.nirbi.ru/partners/v1/orders/942f32ac-2db5-4f09-8ade-14e9b06c276e/close-with-payment \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"client_payment_id": "netmonet-tx-7894521",
"amount": 10000,
"method": "qr_partner",
"partner_provider": "netmonet",
"partner_payment_id": "NM-2026-04-29-001",
"tip_amount": 1000,
"waiter_id": "84dfe9b8-e509-4375-8906-6be67cfa8208"
}'| Поле | Required | Описание |
|---|---|---|
client_payment_id | yes | Idempotency key. Повторный запрос с тем же id возвращает 200 без побочных эффектов |
amount | yes | Должно совпадать с order.total. Иначе 422 AMOUNT_MISMATCH |
method | yes | qr_partner |
partner_provider | yes | Идентификатор поставщика (выдаётся при подключении) |
partner_payment_id | yes | ID транзакции в системе партнёра |
tip_amount | no | Чаевые если переданы поверх total |
waiter_id | no | UUID получателя чаевых |
card_brand, card_last4, rrn | no | Если получены — пробрасываются в фискальный чек |
metadata | no | Произвольный JSON, сохраняется в audit |
Response 200:
{
"data": {
"id": "942f32ac-2db5-4f09-8ade-14e9b06c276e",
"status": "closed",
"paid_at": "2026-04-29T20:15:00+03:00",
"payment": {
"method": "qr_partner",
"amount": 10000,
"partner_provider": "netmonet",
"partner_payment_id": "NM-2026-04-29-001"
},
"fiscal_receipt": {
"fn": "9999078900000000",
"fd": "12345",
"fp": "1234567890",
"url": "https://lk.platformaofd.ru/web/noauth/cheque?fn=...&fd=...&fp=..."
}
}
}POST /orders/{id}/refund
Полный или частичный возврат после закрытия.
Example (частичный):
curl -X POST https://erp-test.nirbi.ru/partners/v1/orders/50c45f4f-3d75-43b5-9aa2-035ba3bb1edc/refund \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"client_refund_id": "netmonet-rfd-12345",
"amount": 9950,
"type": "partial",
"reason": "Гость вернул одну позицию",
"refund_cart": [
{
"name": "E2E Phase2 add-items",
"quantity": 1,
"price": 9950,
"sum": 9950,
"vat": "vat20"
}
],
"partner_refund_id": "NM-RFD-77123"
}'| Поле | Required | Описание |
|---|---|---|
client_refund_id | yes | Idempotency key |
amount | yes | ≤ оставшееся к возврату по заказу |
type | yes | full / partial |
refund_cart | если partial | Корзина возврата для фискального чека (54-ФЗ) |
partner_refund_id | yes | ID операции возврата у партнёра |
Response 200:
{
"data": {
"id": "rfd-bc7e3a91-...",
"order_id": "50c45f4f-3d75-43b5-9aa2-035ba3bb1edc",
"amount": 9950,
"type": "partial",
"status": "done",
"refunded_at": "2026-04-29T20:30:00+03:00",
"fiscal_receipt": { "fd": "12346", "fp": "1234567890" }
}
}Webhooks
Партнёр регистрирует endpoint — мы шлём POST на изменения статуса заказов.
POST /webhooks
curl -X POST https://erp-test.nirbi.ru/partners/v1/webhooks \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://api.partner.example/erp-callback",
"events": ["order.ready", "order.closed", "order.cancelled", "refund.done"],
"secret": "32-byte-base64-or-hex-string"
}'Response 201:
{ "data": { "id": "wh-uuid", "url": "...", "events": [...] } }Webhook delivery
POST на партнёрский URL. Headers:
Content-Type: application/json
X-Signature: sha256=<HMAC-SHA256 от raw body, секрет из регистрации>
X-Event-Id: <event uuid>
Body:
{
"event": "order.closed",
"event_id": "evt-c5a91b2e-...",
"occurred_at": "2026-04-29T20:15:00+03:00",
"data": {
"order_id": "942f32ac-2db5-4f09-8ade-14e9b06c276e",
"store_id": "84ed7103-a48b-49d4-ac39-10a772cd5e83",
"total": 10000,
"paid_at": "2026-04-29T20:15:00+03:00",
"payment_method": "qr_partner"
}
}Партнёр должен ответить 2xx в течение 5 секунд. Иначе ретраи: 10s / 30s / 2m / 10m / 1h / 6h / 24h (max 7 попыток).
События
| event | data shape |
|---|---|
order.ready | { order_id, store_id, total } |
order.closed | { order_id, store_id, total, paid_at, payment_method } |
order.cancelled | { order_id, store_id, cancel_reason } |
refund.done | { order_id, refund_id, amount, type } |
Errors
Единый формат:
{
"error": {
"code": "AMOUNT_MISMATCH",
"message": "amount must equal order.total",
"details": [{ "field": "amount", "message": "expected 20100, got 10000" }]
}
}| HTTP | code | когда |
|---|---|---|
| 400 | VALIDATION_ERROR | Невалидное body / параметры |
| 401 | UNAUTHORIZED | Нет / истёкший / невалидный JWT |
| 403 | INSUFFICIENT_SCOPE | JWT без нужного scope |
| 404 | ORDER_NOT_FOUND / TABLE_NOT_FOUND | Нет такого id |
| 409 | ORDER_ALREADY_CLOSED | Заказ уже закрыт |
| 422 | AMOUNT_MISMATCH | amount не совпал с order.total |
| 422 | WAITER_NOT_FOUND | Указан несуществующий waiter_id |
| 422 | REFUND_AMOUNT_EXCEEDED | Сумма возврата превышает остаток |
| 429 | RATE_LIMITED | Превышен rate limit |
| 500 | INTERNAL_ERROR | Внутренняя ошибка |
Idempotency
Все write-операции (close-with-payment, refund) принимают client_*_id. Повторный запрос с тем же id:
- предыдущий завершился
2xx→ возвращаем тот же ответ, side-effects не выполняются - предыдущий упал на ошибке → можно ретраить с тем же id
Срок хранения idempotency-ключей: 30 дней.
Rate limits
Per-client_id:
- 60 RPS — на read-операции
- 20 RPS — на write-операции (per-store)
При превышении — 429 RATE_LIMITED с заголовком Retry-After (секунды).
Currency
Только RUB. Все money — копейки (целое число).
Sandbox sample data
В тестовом окружении уже есть данные для проверки запросов после получения JWT:
| Что | UUID |
|---|---|
ТТ «Шаурма Арбат» (store_id) | bf5291f8-58e5-429b-8156-77beadd06fb4 |
ТТ «test» (store_id) | 84ed7103-a48b-49d4-ac39-10a772cd5e83 |
| Стол №2 (occupied, ТТ Арбат) | 17d5214f-fd1e-4f43-abfe-2accb68705ec |
| Стол №1 (occupied, ТТ test) | 1fe9fe95-f287-4dbc-bf32-9597d6d95126 |
| Закрытый заказ #007 (201 ₽, card) | 50c45f4f-3d75-43b5-9aa2-035ba3bb1edc |
| Открытый dine-in заказ #002 (100 ₽) | 942f32ac-2db5-4f09-8ade-14e9b06c276e |
Сотрудник «Иван Петров» (waiter_id) | 84dfe9b8-e509-4375-8906-6be67cfa8208 |