openapi: 3.0.3 info: title: Bonex API version: "1.0.0" description: | REST API программы лояльности **Bonex** для интеграции в сторонние системы и мобильные приложения. ## Способы интеграции - **Mobile API** — приложение само авторизует клиента (SMS-код или Telegram), получает Bearer-токен и обращается к `/client/*`. - **Partner API** — у партнёра есть свой бэкенд: server-to-server вызовы с HMAC-подписью (SSO, продажи, списание бонусов). - **Integration API** — синхронизация остатков из 1С / ERP. ## Авторизация - `X-Tenant-Token` — обязателен во всех запросах, идентифицирует магазин (64 символа). - `Authorization: Bearer ` — персональный токен клиента (Mobile/Client API). - `X-Partner-Timestamp` + `X-Partner-Signature` — HMAC-SHA256 подпись (Partner API). ## Формат ответа Успех: `{ "success": true, "data": ... }`. Ошибка: `{ "success": false, "error": "..." }`. Подробные сценарии — в `MOBILE_API.md`, `PARTNER_API.md`, `INTEGRATIONS_API.md`. contact: name: Bonex url: https://bonex.one servers: - url: https://api.bonex.one/api/v1 description: Production - url: http://localhost:8081/api/v1 description: Локальная разработка tags: - name: Auth (Mobile) description: Авторизация клиента в мобильном приложении (SMS-код, Telegram) - name: Client (Mobile) description: Данные клиента под Bearer-токеном (профиль, история, акции, push) - name: Catalog description: Брендинг магазина, акции, список филиалов - name: Partner (server-to-server) description: Серверная интеграция с HMAC-подписью (SSO, продажи, списание) - name: Integration description: Синхронизация остатков (1С / ERP) security: - TenantToken: [] paths: /auth/send-otp: post: tags: [Auth (Mobile)] summary: Отправить SMS-код security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [phone] properties: phone: type: string maxLength: 20 example: "+992901234567" responses: "200": description: Код отправлен content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: message: type: string example: "OTP отправлен." "401": { $ref: '#/components/responses/Unauthorized' } "422": { $ref: '#/components/responses/ValidationError' } /auth/verify-otp: post: tags: [Auth (Mobile)] summary: Вход по коду (существующий клиент) security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [phone, code] properties: phone: { type: string, example: "+992901234567" } code: { type: string, minLength: 6, maxLength: 6, example: "123456" } responses: "200": description: Токен выдан content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string, example: "12|aBcDeFplainTextToken" } client_id: { type: integer, example: 42 } "404": description: Клиент не найден — требуется регистрация content: application/json: schema: { $ref: '#/components/schemas/ApiError' } "422": { $ref: '#/components/responses/ValidationError' } /auth/register: post: tags: [Auth (Mobile)] summary: Регистрация нового клиента security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [phone, code] properties: phone: { type: string, example: "+992901234567" } code: { type: string, example: "123456" } full_name: { type: string, nullable: true, example: "Парвиз Солиев" } referral_code: { type: string, nullable: true, maxLength: 32, example: "AB12CD34" } responses: "201": description: Клиент создан, токен выдан content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string, example: "13|eFgHplainTextToken" } client_id: { type: integer, example: 43 } "422": { $ref: '#/components/responses/ValidationError' } /auth/telegram: post: tags: [Auth (Mobile)] summary: Вход через Telegram (Mini App) security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [init_data] properties: init_data: type: string description: window.Telegram.WebApp.initData responses: "200": description: Токен и клиент content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } client: { $ref: '#/components/schemas/Client' } "400": { $ref: '#/components/responses/BadRequest' } "401": { $ref: '#/components/responses/Unauthorized' } "404": description: Аккаунт не привязан — используйте /auth/telegram-contact content: application/json: schema: { $ref: '#/components/schemas/ApiError' } /auth/telegram-contact: post: tags: [Auth (Mobile)] summary: Первый вход через Telegram (привязка по контакту) security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [init_data, contact] properties: init_data: { type: string } contact: type: string description: Подписанный объект контакта из Telegram referral_code: { type: string, nullable: true, maxLength: 32 } responses: "200": description: Существующий клиент — токен выдан content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } client: { $ref: '#/components/schemas/Client' } "201": description: Новый клиент создан — токен выдан content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } client: { $ref: '#/components/schemas/Client' } "401": { $ref: '#/components/responses/Unauthorized' } "422": { $ref: '#/components/responses/ValidationError' } /client/settings: get: tags: [Catalog] summary: Брендинг магазина (без авторизации клиента) security: - TenantToken: [] responses: "200": description: Настройки брендинга content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Settings' } "404": { $ref: '#/components/responses/NotFound' } /client/profile: get: tags: [Client (Mobile)] summary: Профиль и баланс security: - TenantToken: [] BearerAuth: [] responses: "200": description: Профиль клиента content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Client' } "401": { $ref: '#/components/responses/Unauthorized' } put: tags: [Client (Mobile)] summary: Обновить профиль security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: full_name: { type: string, nullable: true, maxLength: 255 } birth_date: { type: string, format: date, nullable: true, example: "1990-05-01" } gender: { type: string, nullable: true, maxLength: 20, example: "male" } responses: "200": description: Обновлённый профиль content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Client' } "422": { $ref: '#/components/responses/ValidationError' } /client/transactions: get: tags: [Client (Mobile)] summary: История бонусов (начисления/списания) security: - TenantToken: [] BearerAuth: [] parameters: - $ref: '#/components/parameters/Page' responses: "200": description: Постраничный список транзакций content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: allOf: - $ref: '#/components/schemas/Paginated' - properties: data: type: array items: { $ref: '#/components/schemas/BonusTransaction' } /client/purchases: get: tags: [Client (Mobile)] summary: История покупок security: - TenantToken: [] BearerAuth: [] parameters: - $ref: '#/components/parameters/Page' - name: per_page in: query schema: { type: integer, minimum: 1, maximum: 50, default: 20 } responses: "200": description: Постраничный список покупок content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: allOf: - $ref: '#/components/schemas/Paginated' - properties: data: type: array items: { $ref: '#/components/schemas/Purchase' } /client/promotions: get: tags: [Client (Mobile)] summary: Акции (персональные при авторизации) security: - TenantToken: [] BearerAuth: [] responses: "200": description: Список акций content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: array items: { $ref: '#/components/schemas/Promotion' } /client/stores: get: tags: [Client (Mobile)] summary: Список магазинов/филиалов security: - TenantToken: [] BearerAuth: [] responses: "200": description: Список филиалов content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: array items: { $ref: '#/components/schemas/Store' } /client/referral: get: tags: [Client (Mobile)] summary: Реферальная программа security: - TenantToken: [] BearerAuth: [] responses: "200": description: Данные реферальной программы content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Referral' } /client/link-telegram: post: tags: [Client (Mobile)] summary: Привязать Telegram к аккаунту security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [init_data] properties: init_data: { type: string } responses: "200": description: Привязано content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: linked: { type: boolean, example: true } "400": { $ref: '#/components/responses/BadRequest' } "401": { $ref: '#/components/responses/Unauthorized' } "422": { $ref: '#/components/responses/ValidationError' } /client/device-token: post: tags: [Client (Mobile)] summary: Зарегистрировать push-токен (FCM) security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: { type: string, maxLength: 512 } platform: { type: string, enum: [android, ios], nullable: true } responses: "200": description: Токен зарегистрирован content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: registered: { type: boolean, example: true } delete: tags: [Client (Mobile)] summary: Удалить push-токен security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: { type: string, maxLength: 512 } responses: "200": description: Токен удалён content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: deleted: { type: boolean, example: true } /partner/auth: post: tags: [Partner (server-to-server)] summary: SSO — обмен телефона на Bearer-токен клиента description: | Бэкенд партнёра вызывает после входа пользователя у себя. Требует HMAC-подпись. security: - TenantToken: [] PartnerTimestamp: [] PartnerSignature: [] requestBody: required: true content: application/json: schema: type: object required: [phone] properties: phone: { type: string, example: "+992901234567" } full_name: { type: string, nullable: true } create_if_missing: type: boolean default: true description: Создать клиента, если его нет (иначе 404) responses: "200": description: Найден существующий клиент content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } created: { type: boolean, example: false } client: { $ref: '#/components/schemas/Client' } "201": description: Клиент создан content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } created: { type: boolean, example: true } client: { $ref: '#/components/schemas/Client' } "401": { $ref: '#/components/responses/Unauthorized' } "403": { $ref: '#/components/responses/Forbidden' } "404": { $ref: '#/components/responses/NotFound' } /partner/client/{identifier}: get: tags: [Partner (server-to-server)] summary: Поиск клиента и баланса security: - TenantToken: [] PartnerTimestamp: [] PartnerSignature: [] parameters: - name: identifier in: path required: true description: URL-encoded телефон (%2B992...), EAN-13 или клубная карта schema: { type: string } example: "%2B992901234567" - name: check_amount in: query description: Сумма заказа для расчёта max_redeem_amount schema: { type: number } example: 200 responses: "200": description: Клиент с данными лояльности content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/CashierClient' } "403": { $ref: '#/components/responses/Forbidden' } "404": { $ref: '#/components/responses/NotFound' } /partner/sale: post: tags: [Partner (server-to-server)] summary: Продажа — начисление (+ опционально списание) security: - TenantToken: [] PartnerTimestamp: [] PartnerSignature: [] requestBody: required: true content: application/json: schema: type: object required: [client_phone, total_amount] properties: client_phone: { type: string, example: "+992901234567" } total_amount: { type: number, minimum: 0.01, example: 955.00 } gross_amount: { type: number, nullable: true, example: 1000.00 } redeem_mode: { type: string, enum: [payment_type, discount], nullable: true } bonus_redeem_amount: { type: number, minimum: 0, nullable: true, example: 45.00 } external_sale_id: type: string nullable: true description: Ваш ID заказа — идемпотентность (повтор → 422) example: "order-2026-000123" receipt_number: { type: string, nullable: true, maxLength: 64 } payment_method: { type: string, nullable: true, maxLength: 50 } sale_date: { type: string, format: date-time, nullable: true } lines: type: array nullable: true maxItems: 200 items: { $ref: '#/components/schemas/SaleLineInput' } responses: "201": description: Продажа создана content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Sale' } "403": { $ref: '#/components/responses/Forbidden' } "404": { $ref: '#/components/responses/NotFound' } "422": { $ref: '#/components/responses/ValidationError' } /partner/redeem: post: tags: [Partner (server-to-server)] summary: Списание бонусов (онлайн-оплата) description: Доступно, только если включено «списание из приложения» (иначе 403). security: - TenantToken: [] PartnerTimestamp: [] PartnerSignature: [] requestBody: required: true content: application/json: schema: type: object required: [client_phone, amount, total_amount] properties: client_phone: { type: string, example: "+992901234567" } amount: { type: number, minimum: 0.01, example: 50, description: Сколько бонусов списать } total_amount: { type: number, minimum: 0.01, example: 200, description: Сумма заказа } external_sale_id: { type: string, nullable: true, maxLength: 255 } responses: "200": description: Бонусы списаны content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: transaction_id: { type: integer, example: 5012 } amount_redeemed: { type: number, example: 50 } new_balance: { type: number, example: 102.75 } sale_id: { type: integer, example: 1002 } "403": { $ref: '#/components/responses/Forbidden' } "404": { $ref: '#/components/responses/NotFound' } "422": { $ref: '#/components/responses/ValidationError' } /integration/inventory/sync: post: tags: [Integration] summary: Синхронизация остатков (1С / ERP) description: Только X-Tenant-Token. Используется для персональных push по неликвиду. security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [items] properties: items: type: array maxItems: 5000 items: type: object required: [sku] properties: sku: { type: string, maxLength: 64, example: "SKU-001" } name: { type: string, nullable: true, maxLength: 255 } category_code: { type: string, nullable: true, maxLength: 64 } interest_tag: { type: string, nullable: true, maxLength: 64 } quantity: { type: number, minimum: 0, nullable: true } quantity_on_hand: { type: number, minimum: 0, nullable: true } overstock_threshold: { type: number, minimum: 0, nullable: true } days_without_sale: { type: integer, minimum: 0, nullable: true } deactivate_missing: { type: boolean, nullable: true, default: false } responses: "200": description: Результат синхронизации content: application/json: schema: { $ref: '#/components/schemas/SuccessEnvelope' } "422": { $ref: '#/components/responses/ValidationError' } components: securitySchemes: TenantToken: type: apiKey in: header name: X-Tenant-Token description: 64-символьный токен магазина (panel → Интеграции → Учётные данные API) BearerAuth: type: http scheme: bearer description: Персональный токен клиента (выдаётся при авторизации) PartnerTimestamp: type: apiKey in: header name: X-Partner-Timestamp description: Unix-время запроса в секундах (расхождение ≤ 300 с) PartnerSignature: type: apiKey in: header name: X-Partner-Signature description: | HMAC-SHA256(secret) от строки "{timestamp}.{METHOD}.{path}.{rawBody}", результат hex. parameters: Page: name: page in: query description: Номер страницы (пагинация) schema: { type: integer, minimum: 1, default: 1 } responses: Unauthorized: description: Не авторизован (нет/неверный токен или подпись) content: application/json: schema: { $ref: '#/components/schemas/ApiError' } Forbidden: description: Доступ запрещён (магазин приостановлен, клиент заблокирован, функция выключена) content: application/json: schema: { $ref: '#/components/schemas/ApiError' } NotFound: description: Не найдено content: application/json: schema: { $ref: '#/components/schemas/ApiError' } BadRequest: description: Некорректный запрос content: application/json: schema: { $ref: '#/components/schemas/ApiError' } ValidationError: description: Ошибка валидации или бизнес-правила content: application/json: schema: { $ref: '#/components/schemas/ApiError' } schemas: SuccessEnvelope: type: object properties: success: { type: boolean, example: true } data: {} ApiError: type: object properties: success: { type: boolean, example: false } error: { type: string, example: "Текст ошибки." } Paginated: type: object properties: data: type: array items: {} current_page: { type: integer, example: 1 } last_page: { type: integer, example: 4 } total: { type: integer, example: 73 } Tier: type: object properties: id: { type: integer, example: 2 } name: { type: string, example: "Gold" } color: { type: string, example: "#C9A227" } Client: type: object properties: id: { type: integer, example: 42 } phone: { type: string, example: "+992901234567" } barcode_ean13: { type: string, example: "2000000000420" } club_card_number: { type: string, nullable: true, example: "001-0000123" } account_number: { type: string, nullable: true, example: "ACC-000042" } full_name: { type: string, nullable: true, example: "Парвиз Солиев" } birth_date: { type: string, format: date, nullable: true, example: "1990-05-01" } gender: { type: string, nullable: true, example: "male" } bonus_balance: { type: number, example: 150.0 } welcome_bonus_balance: { type: number, example: 0.0 } welcome_expires_at: { type: string, format: date-time, nullable: true } birthday_bonus_balance: { type: number, example: 0.0 } birthday_expires_at: { type: string, format: date-time, nullable: true } total_spent: { type: number, example: 1200.0 } tier: { $ref: '#/components/schemas/Tier' } telegram_username: { type: string, nullable: true, example: "parviz" } has_mobile_app: { type: boolean, example: true } is_blocked: { type: boolean, example: false } last_visit_at: { type: string, format: date-time, nullable: true } CashierClient: type: object properties: id: { type: integer, example: 42 } phone: { type: string, example: "+992901234567" } barcode_ean13: { type: string, example: "2000000000420" } club_card_number: { type: string, nullable: true } account_number: { type: string, nullable: true } full_name: { type: string, nullable: true, example: "Парвиз Солиев" } bonus_balance: { type: number, example: 150.0 } redeemable_balance: { type: number, nullable: true, example: 120.0 } held_balance: { type: number, nullable: true, example: 30.0 } welcome_bonus_balance: { type: number, example: 0.0 } total_spent: { type: number, example: 1200.0 } tier: { $ref: '#/components/schemas/Tier' } is_blocked: { type: boolean, example: false } last_visit_at: { type: string, format: date-time, nullable: true } loyalty: type: object nullable: true properties: bonus_percent: { type: number, example: 5 } max_redeem_percent: { type: number, example: 30 } redeem_hold_seconds: { type: integer, example: 86400 } redeem_hold_human: { type: string, example: "24 ч" } redeemable_balance: { type: number, example: 120 } held_balance: { type: number, example: 30 } min_purchase_amount: { type: number, example: 10 } currency: { type: string, example: "TJS" } max_redeem_amount: { type: number, example: 60, description: Только если передан check_amount } BonusTransaction: type: object properties: id: { type: integer, example: 5012 } type: { type: string, example: "accrual", description: "accrual, redeem, welcome, birthday, referral_inviter, referral_invitee, expire ..." } amount: { type: number, example: 7.5 } amount_unit: { type: string, example: "бонус" } balance_after: { type: number, example: 157.5 } sale_amount: { type: number, nullable: true, example: 1000.0 } purchase_currency: { type: string, nullable: true, example: "TJS" } note: { type: string, nullable: true, example: "Начисление за покупку" } created_at: { type: string, format: date-time } Purchase: type: object properties: id: { type: integer, example: 1001 } kind: { type: string, example: "purchase" } title: { type: string, example: "Покупка" } receipt_number: { type: string, nullable: true, example: "000123" } sale_date: { type: string, format: date-time } total_amount: { type: number, example: 1000.0 } currency: { type: string, example: "TJS" } bonus_accrued: { type: number, example: 7.5 } bonus_redeemed: { type: number, example: 45.0 } store_name: { type: string, nullable: true, example: "Магазин на Рудаки" } store_address: { type: string, nullable: true, example: "пр. Рудаки, 12" } lines: type: array items: { $ref: '#/components/schemas/SaleLine' } SaleLine: type: object properties: sku: { type: string, nullable: true, example: "SKU-001" } name: { type: string, nullable: true, example: "Молоко 1л" } quantity: { type: number, example: 2.0 } unit_price: { type: number, example: 20.0 } line_amount: { type: number, example: 40.0 } SaleLineInput: type: object properties: sku: { type: string, nullable: true, maxLength: 64 } category_code: { type: string, nullable: true, maxLength: 64 } name: { type: string, nullable: true, maxLength: 255 } quantity: { type: number, minimum: 0, nullable: true } line_amount: { type: number, minimum: 0, nullable: true } unit_price: { type: number, minimum: 0, nullable: true } Promotion: type: object properties: id: { type: integer, example: 7 } name: { type: string, example: "Двойной кэшбэк на молочное" } description: { type: string, nullable: true } type: { type: string, example: "category_multiplier" } condition: { type: object, nullable: true } reward: { type: object, nullable: true } starts_at: { type: string, format: date-time, nullable: true } ends_at: { type: string, format: date-time, nullable: true } personalized: { type: boolean, example: true } Store: type: object properties: id: { type: integer, example: 1 } name: { type: string, example: "Магазин на Рудаки" } address: { type: string, nullable: true, example: "пр. Рудаки, 12" } Referral: type: object properties: code: { type: string, example: "AB12CD34" } share_text: { type: string } telegram_start_param: { type: string, example: "ref_AB12CD34" } invite_url: { type: string, nullable: true, example: "https://t.me/yourbot?start=ref_AB12CD34" } invited_count: { type: integer, example: 5 } completed_count: { type: integer, example: 3 } bonuses_earned: { type: number, example: 60.0 } Settings: type: object properties: store_name: { type: string, example: "Сеть «Хорошо»" } logo_url: { type: string, nullable: true, example: "https://cdn.bonex.one/logos/42.png" } primary_color: { type: string, nullable: true, example: "#0E7C66" } currency: { type: string, example: "TJS" } bonus_unit: { type: string, example: "бонус" } Sale: type: object properties: id: { type: integer, example: 1001 } client_id: { type: integer, example: 42 } total_amount: { type: number, example: 955 } bonus_accrued: { type: number, example: 47.75 } bonus_redeemed: { type: number, example: 45 } external_sale_id: { type: string, nullable: true, example: "order-2026-000123" } sale_date: { type: string, format: date-time } new_balance: { type: number, example: 152.75 }