openapi: 3.0.3 info: title: Bonex API version: "1.0.0" description: | REST API of the **Bonex** loyalty platform for integration into third-party systems and mobile applications. ## Integration models - **Mobile API** — the app authenticates the customer itself (SMS code or Telegram), receives a Bearer token and calls `/client/*`. - **Partner API** — the partner has its own backend: server-to-server calls with an HMAC signature (SSO, sales, bonus redemption). - **Integration API** — inventory sync from 1C / ERP. ## Authentication - `X-Tenant-Token` — required on every request, identifies the store (64 chars). - `Authorization: Bearer ` — the customer's personal token (Mobile/Client API). - `X-Partner-Timestamp` + `X-Partner-Signature` — HMAC-SHA256 signature (Partner API). ## Response format Success: `{ "success": true, "data": ... }`. Error: `{ "success": false, "error": "..." }`. Detailed scenarios — see `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: Local development tags: - name: Auth (Mobile) description: Customer authentication in the mobile app (SMS code, Telegram) - name: Client (Mobile) description: Customer data under a Bearer token (profile, history, promotions, push) - name: Catalog description: Store branding, promotions, list of branches - name: Partner (server-to-server) description: Server integration with HMAC signature (SSO, sales, redemption) - name: Integration description: Inventory sync (1C / ERP) security: - TenantToken: [] paths: /auth/send-otp: post: tags: [Auth (Mobile)] summary: Send SMS code security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [phone] properties: phone: type: string maxLength: 20 example: "+992901234567" responses: "200": description: Code sent content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: message: type: string example: "OTP sent." "401": { $ref: '#/components/responses/Unauthorized' } "422": { $ref: '#/components/responses/ValidationError' } /auth/verify-otp: post: tags: [Auth (Mobile)] summary: Log in with code (existing customer) 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: Token issued 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: Customer not found — registration required content: application/json: schema: { $ref: '#/components/schemas/ApiError' } "422": { $ref: '#/components/responses/ValidationError' } /auth/register: post: tags: [Auth (Mobile)] summary: Register a new customer 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: "John Smith" } referral_code: { type: string, nullable: true, maxLength: 32, example: "AB12CD34" } responses: "201": description: Customer created, token issued 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: Log in via 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: Token and customer 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: Account not linked — use /auth/telegram-contact content: application/json: schema: { $ref: '#/components/schemas/ApiError' } /auth/telegram-contact: post: tags: [Auth (Mobile)] summary: First Telegram login (link by contact) security: - TenantToken: [] requestBody: required: true content: application/json: schema: type: object required: [init_data, contact] properties: init_data: { type: string } contact: type: string description: Signed contact object from Telegram referral_code: { type: string, nullable: true, maxLength: 32 } responses: "200": description: Existing customer — token issued content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: token: { type: string } client: { $ref: '#/components/schemas/Client' } "201": description: New customer created — token issued 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: Store branding (no customer auth) security: - TenantToken: [] responses: "200": description: Branding settings 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: Profile and balance security: - TenantToken: [] BearerAuth: [] responses: "200": description: Customer profile 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: Update profile 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: Updated profile 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: Bonus history (accruals/redemptions) security: - TenantToken: [] BearerAuth: [] parameters: - $ref: '#/components/parameters/Page' responses: "200": description: Paginated list of transactions 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: Purchase history 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: Paginated list of purchases 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: Promotions (personalized when authenticated) security: - TenantToken: [] BearerAuth: [] responses: "200": description: List of promotions 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: List of stores/branches security: - TenantToken: [] BearerAuth: [] responses: "200": description: List of branches 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: Referral program security: - TenantToken: [] BearerAuth: [] responses: "200": description: Referral program data content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: { $ref: '#/components/schemas/Referral' } /client/link-telegram: post: tags: [Client (Mobile)] summary: Link Telegram to account security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [init_data] properties: init_data: { type: string } responses: "200": description: Linked 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: Register push token (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: Token registered content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessEnvelope' - properties: data: type: object properties: registered: { type: boolean, example: true } delete: tags: [Client (Mobile)] summary: Delete push token security: - TenantToken: [] BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: { type: string, maxLength: 512 } responses: "200": description: Token deleted 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 — exchange phone for a customer Bearer token description: | The partner backend calls this after the user logs in on their side. Requires an HMAC signature. 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: Create the customer if absent (otherwise 404) responses: "200": description: Existing customer found 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: Customer created 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: Look up customer and balance security: - TenantToken: [] PartnerTimestamp: [] PartnerSignature: [] parameters: - name: identifier in: path required: true description: URL-encoded phone (%2B992...), EAN-13 or club card schema: { type: string } example: "%2B992901234567" - name: check_amount in: query description: Order amount to compute max_redeem_amount schema: { type: number } example: 200 responses: "200": description: Customer with loyalty data 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: Sale — accrual (+ optional redemption) 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: Your order ID — idempotency (duplicate → 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: Sale created 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: Redeem bonuses (online payment) description: Available only if "redeem from app" is enabled (otherwise 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: How many bonuses to redeem } total_amount: { type: number, minimum: 0.01, example: 200, description: Order amount } external_sale_id: { type: string, nullable: true, maxLength: 255 } responses: "200": description: Bonuses redeemed 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: Inventory sync (1C / ERP) description: X-Tenant-Token only. Used for personalized push on slow-moving stock. 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: Sync result 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-char store token (panel → Integrations → API credentials) BearerAuth: type: http scheme: bearer description: Customer's personal token (issued on authentication) PartnerTimestamp: type: apiKey in: header name: X-Partner-Timestamp description: Unix time of the request in seconds (skew ≤ 300 s) PartnerSignature: type: apiKey in: header name: X-Partner-Signature description: | HMAC-SHA256(secret) of the string "{timestamp}.{METHOD}.{path}.{rawBody}", hex output. parameters: Page: name: page in: query description: Page number (pagination) schema: { type: integer, minimum: 1, default: 1 } responses: Unauthorized: description: Unauthorized (missing/invalid token or signature) content: application/json: schema: { $ref: '#/components/schemas/ApiError' } Forbidden: description: Forbidden (store suspended, customer blocked, feature disabled) content: application/json: schema: { $ref: '#/components/schemas/ApiError' } NotFound: description: Not found content: application/json: schema: { $ref: '#/components/schemas/ApiError' } BadRequest: description: Bad request content: application/json: schema: { $ref: '#/components/schemas/ApiError' } ValidationError: description: Validation or business-rule error 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: "Error message." } 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: "John Smith" } 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: "johnsmith" } 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: "John Smith" } 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 h" } 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: Present only if check_amount is passed } 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: "bonus" } 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: "Accrual for purchase" } 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: "Purchase" } 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: "Rudaki Store" } store_address: { type: string, nullable: true, example: "12 Rudaki Ave" } 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: "Milk 1L" } 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: "Double cashback on dairy" } 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: "Rudaki Store" } address: { type: string, nullable: true, example: "12 Rudaki Ave" } 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: "Khorosho Retail" } 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: "bonus" } 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 }