POS Partner API

Публичный API торговой точки для интеграции внешних POS-партнёров (QRPay-сервисы безналичной оплаты).

Base URL

EnvURL
Sandbox / Testhttps://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:

ParamRequiredОписание
store_idyesUUID торговой точки
statusnofree / 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: newacceptedreadyclosed. Дополнительно 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_idyesIdempotency key. Повторный запрос с тем же id возвращает 200 без побочных эффектов
amountyesДолжно совпадать с order.total. Иначе 422 AMOUNT_MISMATCH
methodyesqr_partner
partner_provideryesИдентификатор поставщика (выдаётся при подключении)
partner_payment_idyesID транзакции в системе партнёра
tip_amountnoЧаевые если переданы поверх total
waiter_idnoUUID получателя чаевых
card_brand, card_last4, rrnnoЕсли получены — пробрасываются в фискальный чек
metadatanoПроизвольный 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_idyesIdempotency key
amountyes≤ оставшееся к возврату по заказу
typeyesfull / partial
refund_cartесли partialКорзина возврата для фискального чека (54-ФЗ)
partner_refund_idyesID операции возврата у партнёра

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 попыток).

События

eventdata 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" }]
  }
}
HTTPcodeкогда
400VALIDATION_ERRORНевалидное body / параметры
401UNAUTHORIZEDНет / истёкший / невалидный JWT
403INSUFFICIENT_SCOPEJWT без нужного scope
404ORDER_NOT_FOUND / TABLE_NOT_FOUNDНет такого id
409ORDER_ALREADY_CLOSEDЗаказ уже закрыт
422AMOUNT_MISMATCHamount не совпал с order.total
422WAITER_NOT_FOUNDУказан несуществующий waiter_id
422REFUND_AMOUNT_EXCEEDEDСумма возврата превышает остаток
429RATE_LIMITEDПревышен rate limit
500INTERNAL_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