openapi: 3.0.3 info: title: Bober BBQ Mini App API description: > Backend API for the Bober BBQ Telegram Mini App, served under /api by the Flask blueprint `api_bp` (bober_bbq/api/__init__.py). See docs/API.md for the full narrative reference, including caveats and edge-case notes — this file covers the same routes in machine-readable form but is secondary to that document. version: "1.0.0" servers: - url: /api tags: - name: menu - name: settings - name: cart - name: orders - name: checkout - name: payments - name: me components: securitySchemes: TelegramInitData: type: apiKey in: header name: X-Telegram-Init-Data description: > Raw Telegram Mini App initData string, validated per Telegram's WebApp initData algorithm (HMAC-SHA256 against the bot token, <=24h old). In Flask debug mode only, a `?dev_user_id=` query param bypasses this when validation fails. GuestSession: type: apiKey in: cookie name: session description: > Guest fallback used by every route below whose security lists both schemes (see require_customer_auth in docs/API.md's Authentication section): if TelegramInitData is missing or fails validation, the request is NOT rejected — it's served on behalf of a cookie-bound guest TelegramUser instead (created transparently on first contact). As a result, none of those routes ever return 401. parameters: DevUserId: name: dev_user_id in: query required: false schema: type: integer description: > Dev-mode-only auth bypass. Only honored when the failed initData check occurs AND the Flask app is running with debug=True. responses: Unauthorized: description: Missing or invalid Telegram initData content: application/json: schema: type: object properties: error: type: string example: invalid or missing Telegram init data schemas: Category: type: object properties: id: { type: integer } name: { type: string } slug: { type: string } sort_order: { type: integer } Allergen: type: object properties: code: { type: string } label: { type: string } emoji: { type: string } Product: type: object properties: id: { type: integer } category_id: { type: integer } name: { type: string } description: { type: string } weight: { type: string, example: "300 г" } price: { type: integer, description: "Whole UAH" } image_url: { type: string } allergens: type: array items: { $ref: '#/components/schemas/Allergen' } CartItem: type: object properties: product_id: { type: integer } name: { type: string } weight: { type: string } image_url: { type: string } price: { type: integer } quantity: { type: integer } line_total: { type: integer } Cart: type: object properties: items: type: array items: { $ref: '#/components/schemas/CartItem' } items_total: { type: integer } count: { type: integer } Theme: type: object properties: accent: { type: string } accent_2: { type: string } accent_grad: { type: string } accent_ink: { type: string } PublicSettings: type: object properties: cafe_name: { type: string } logo_url: { type: string } phone: { type: string, nullable: true } address: { type: string, nullable: true } work_hours_from: { type: string, example: "11:00" } work_hours_to: { type: string, example: "21:00" } is_open: { type: boolean } instagram_url: { type: string, nullable: true } maps_url: { type: string, nullable: true } delivery_fee: { type: integer } free_delivery_threshold: { type: integer } min_delivery_order_amount: { type: integer } pickup_discount_percent: { type: integer } force_closed: { type: boolean } force_closed_message: { type: string } announcement_enabled: { type: boolean } announcement_message: { type: string } theme: { $ref: '#/components/schemas/Theme' } has_active_promos: { type: boolean } OrderItemLine: type: object properties: name: { type: string } quantity: { type: integer } price: { type: integer } Order: type: object properties: id: { type: integer } number: { type: string, description: '"№1042" or "#42" fallback' } order_type: { type: string, enum: [delivery, pickup, dine_in] } status: type: string enum: [new, confirmed, cooking, ready, courier, completed, cancelled] status_label: { type: string } payment_method: { type: string, enum: [cash, card], nullable: true } payment_status: { type: string, enum: [unpaid, paid] } items_total: { type: integer } delivery_fee: { type: integer } discount_amount: { type: integer } promo_code: { type: string, nullable: true } promo_discount_amount: { type: integer } total: { type: integer } created_at: { type: string, format: date-time, nullable: true } pickup_time: { type: string, nullable: true } address_street: { type: string, nullable: true } address_house: { type: string, nullable: true } table_number: { type: string, nullable: true } can_cancel: { type: boolean } can_pay: { type: boolean } items: type: array items: { $ref: '#/components/schemas/OrderItemLine' } ValidationErrorMissing: type: object properties: error: { type: string, example: validation } missing: type: array items: { type: string } ValidationErrorMessage: type: object properties: error: { type: string, example: validation } message: { type: string } SimpleError: type: object properties: error: { type: string } message: { type: string } NotFoundError: type: object properties: error: { type: string, example: not_found } CheckoutResult: type: object properties: order_id: { type: integer } order_number: { type: string } total: { type: integer } payment_method: { type: string, enum: [cash, card] } pay_url: { type: string, nullable: true } card_unavailable_message: type: string nullable: true description: > Non-null only when payment_method is "card" and the active payment provider was unreachable/unconfigured — the same translated text a real Telegram customer gets via a bot message, surfaced here too so a guest (who has no Telegram chat to message) still sees it. track_url: type: string nullable: true description: > Same-origin SPA path, e.g. "/webapp/?track=" — not an absolute URL. Lets anyone with the link check order status via GET /checkout/track/{token} even with no session cookie. DeliveryCheckoutBody: type: object required: [name, phone, address_city, address_street, address_house, payment_method] properties: name: { type: string, minLength: 2 } phone: { type: string } address_city: { type: string } address_street: { type: string } address_house: { type: string } address_apartment: { type: string, nullable: true } address_comment: { type: string, nullable: true } payment_method: { type: string, enum: [cash, card] } cash_change_for: { type: integer, nullable: true } promo_code: { type: string, nullable: true } PickupCheckoutBody: type: object required: [name, phone, pickup_time, payment_method] properties: name: { type: string, minLength: 2 } phone: { type: string } pickup_time: { type: string, pattern: '^([01]?\d|2[0-3]):[0-5]\d$', example: "18:30" } payment_method: { type: string, enum: [cash, card] } promo_code: { type: string, nullable: true } DineInCheckoutBody: type: object required: [name, phone, payment_method] properties: name: { type: string, minLength: 2 } phone: { type: string } table_number: { type: string, nullable: true } payment_method: { type: string, enum: [cash, card] } promo_code: { type: string, nullable: true } paths: /categories: get: tags: [menu] summary: List active menu categories security: [] responses: '200': description: OK content: application/json: schema: type: array items: { $ref: '#/components/schemas/Category' } /products: get: tags: [menu] summary: List active products security: [] responses: '200': description: OK content: application/json: schema: type: array items: { $ref: '#/components/schemas/Product' } /settings: get: tags: [settings] summary: Public cafe configuration security: [] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/PublicSettings' } /cart: get: tags: [cart] summary: Get the caller's cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Cart' } delete: tags: [cart] summary: Clear the caller's cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK (empty cart) content: application/json: schema: { $ref: '#/components/schemas/Cart' } /cart/items: post: tags: [cart] summary: Add a product to the cart (or increment its quantity) security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: type: object properties: product_id: { type: integer } quantity: { type: integer, default: 1, minimum: 1 } required: [product_id] responses: '201': description: Created / updated content: application/json: schema: { $ref: '#/components/schemas/Cart' } '400': description: quantity < 1 content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } '404': description: product not found or inactive content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } /cart/items/{product_id}: parameters: - name: product_id in: path required: true schema: { type: integer } patch: tags: [cart] summary: Set a cart line's quantity (deletes the line if <= 0) security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: false content: application/json: schema: type: object properties: quantity: { type: integer, default: 1 } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Cart' } '404': description: item not in cart content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } delete: tags: [cart] summary: Remove one line item (no-op if absent) security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Cart' } /orders: get: tags: [orders] summary: List the caller's most recent orders (max 50) security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: type: array items: { $ref: '#/components/schemas/Order' } /orders/{order_id}/cancel: parameters: - name: order_id in: path required: true schema: { type: integer } post: tags: [orders] summary: Cancel an order the caller owns (only while new/confirmed) security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Order' } '400': description: not cancellable in current status content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } '404': description: not found (or not the caller's order) content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' } /orders/{order_id}/repeat: parameters: - name: order_id in: path required: true schema: { type: integer } post: tags: [orders] summary: Re-add a past order's items to the current cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: type: object properties: cart: { $ref: '#/components/schemas/Cart' } skipped: type: array items: { type: string } description: Product names skipped because no longer active/existing '404': description: not found (or not the caller's order) content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' } /orders/{order_id}/pay: parameters: - name: order_id in: path required: true schema: { type: integer } post: tags: [orders] summary: (Re-)generate a Monobank payment link for a card order security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: type: object properties: pay_url: { type: string, nullable: true } '400': description: not payable / already paid / Monobank not configured content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } '404': description: not found (or not the caller's order) content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' } '502': description: Monobank API call failed content: application/json: schema: { $ref: '#/components/schemas/SimpleError' } /checkout/delivery: post: tags: [checkout] summary: Place a delivery order from the current cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DeliveryCheckoutBody' } responses: '201': description: Order created content: application/json: schema: { $ref: '#/components/schemas/CheckoutResult' } '400': description: > cart_empty | closed | min_order_amount | promo_invalid | validation (missing fields, invalid payment_method, cash_change_for not a number, name/phone invalid) content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationErrorMissing' - $ref: '#/components/schemas/ValidationErrorMessage' - $ref: '#/components/schemas/SimpleError' /checkout/pickup: post: tags: [checkout] summary: Place a pickup order from the current cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PickupCheckoutBody' } responses: '201': description: Order created content: application/json: schema: { $ref: '#/components/schemas/CheckoutResult' } '400': description: > cart_empty | closed | promo_invalid | validation (missing fields, invalid payment_method, name/phone invalid, bad pickup_time format) content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationErrorMissing' - $ref: '#/components/schemas/ValidationErrorMessage' - $ref: '#/components/schemas/SimpleError' /checkout/dine-in: post: tags: [checkout] summary: Place a dine-in order from the current cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DineInCheckoutBody' } responses: '201': description: Order created content: application/json: schema: { $ref: '#/components/schemas/CheckoutResult' } '400': description: > cart_empty | closed | promo_invalid | validation (missing fields, invalid payment_method, name/phone invalid) content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationErrorMissing' - $ref: '#/components/schemas/ValidationErrorMessage' - $ref: '#/components/schemas/SimpleError' /checkout/promo/validate: post: tags: [checkout] summary: Preview-validate a promo code against the current cart security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: { type: string } responses: '200': description: Valid promo code content: application/json: schema: type: object properties: valid: { type: boolean, example: true } code: { type: string } discount_percent: { type: integer } discount_amount: { type: integer } '400': description: Invalid/inapplicable promo code (also used for blank code) content: application/json: schema: type: object properties: valid: { type: boolean, example: false } message: { type: string } /checkout/prefill: get: tags: [checkout] summary: Most recent name/phone and delivery address for the caller security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: OK content: application/json: schema: type: object properties: has_orders: { type: boolean } name: { type: string, nullable: true } phone: { type: string, nullable: true } delivery: type: object nullable: true properties: address_city: { type: string, nullable: true } address_street: { type: string, nullable: true } address_house: { type: string, nullable: true } address_apartment: { type: string, nullable: true } /checkout/payment-status/{order_id}: parameters: - name: order_id in: path required: true schema: { type: integer } get: tags: [checkout] summary: Poll whether a card order has been paid security: [{ TelegramInitData: [] }, { GuestSession: [] }] responses: '200': description: > {"paid": true} once confirmed paid, otherwise {"paid": false} (including transient Monobank check failures — indistinguishable from genuinely unpaid). content: application/json: schema: type: object properties: paid: { type: boolean } '404': description: not found (or not the caller's order) content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' } /checkout/track/{token}: parameters: - name: token in: path required: true schema: { type: string } get: tags: [checkout] summary: Look up an order's status by its tracking token (no auth) description: > The only checkout_bp route with no security requirement at all — Order.tracking_token itself (a secrets.token_urlsafe(16) value generated for every order, guest or Telegram) is the sole credential, same trust level as a payment provider's emailed receipt link. Not scoped to any TelegramUser. security: [] responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Order' } '404': description: no order with that tracking_token content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' } /me/locale: patch: tags: [me] summary: Persist the caller's UA/EN language choice security: [{ TelegramInitData: [] }, { GuestSession: [] }] requestBody: required: true content: application/json: schema: type: object properties: locale: { type: string, example: uk } required: [locale] responses: '200': description: > OK — returns the normalized locale actually stored (invalid codes silently fall back to "uk" rather than erroring). content: application/json: schema: type: object properties: locale: { type: string, enum: [uk, en] } /payments/monobank/webhook: post: tags: [payments] summary: Monobank server-to-server payment status webhook security: [] description: > Authenticated via the X-Sign header (ECDSA/SHA256 signature verified against Monobank's published public key), not Telegram initData. parameters: - name: X-Sign in: header required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object properties: invoiceId: { type: string } status: { type: string } responses: '200': description: Acknowledged (processed, or invoiceId unknown) content: application/json: schema: type: object properties: ok: { type: boolean, example: true } '400': description: invalid signature or missing invoiceId content: application/json: schema: { $ref: '#/components/schemas/NotFoundError' }