openapi: 3.0.3
info:
  title: eCommerce Exchange API
  version: 1.0
  description: |
    HTTPS JSON API for catalog, cart, checkout, shopper accounts, and partner orders.
    
    MojoShop is the reference storefront. Institutions can whitelabel against this same `/api/v1` contract. Public catalog payloads never include vendor identity.
    
    Required on JSON writes: `Content-Type: application/json` and `Accept: application/json`.
    Successful payloads wrap in `data`. Paginated lists also include Laravel `links` and `meta`.
    
    **Auth:** shoppers use `Authorization: Bearer {token}` from register/login (Sanctum token named `storefront`). Partners use `X-Api-Key`. Carts send `X-Cart-Token` (also returned on cart responses).
    
    **Idempotency:** checkout and partner orders accept optional `idempotency_key` in the JSON body. Reuse the same key only when retrying the same request.
    
    **Vendor webhooks** are configured in `/vendor` (Webhooks). There is no public API to register URLs. Outbound events `order.paid`, `fulfillment.packed`, `fulfillment.shipped`, and `fulfillment.delivered` are signed with HMAC-SHA256 of `{timestamp}.{raw_body}` using headers `X-MojoShop-Event`, `X-MojoShop-Timestamp`, and `X-MojoShop-Signature`.
    
    Rate limits: 60/min default; tighter on register, login, password reset, checkout, and partners. Over the limit returns HTTP 429 with `Retry-After`.
servers:
  - url: "https://ecom.mojolabx.com"
    description: Current application (APP_URL)
tags:
  - name: Catalog
  - name: Cart
  - name: Checkout
  - name: Orders
  - name: Account
  - name: Partners
paths:
  /api/v1/products:
    get:
      tags:
        - Catalog
      summary: List published products
      operationId: listProducts
      parameters:
        - name: q
          in: query
          schema:
            type: string
            maxLength: 200
        - name: category_id
          in: query
          schema:
            type: integer
        - name: min_price
          in: query
          schema:
            type: number
            minimum: 0
        - name: max_price
          in: query
          schema:
            type: number
            minimum: 0
        - name: in_stock
          in: query
          schema:
            type: boolean
        - name: on_sale
          in: query
          schema:
            type: boolean
        - name: min_rating
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 5
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - newest
              - popular
              - rating
              - price_asc
              - price_desc
              - name
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 48
            default: 24
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Paginated products
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ProductList"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  "/api/v1/products/{id}":
    get:
      tags:
        - Catalog
      summary: Product detail
      operationId: getProduct
      parameters:
        - "$ref": "#/components/parameters/ProductId"
      responses:
        "200":
          description: Product
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Product"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  "/api/v1/products/{id}/related":
    get:
      tags:
        - Catalog
      summary: Related products
      operationId: listRelatedProducts
      parameters:
        - "$ref": "#/components/parameters/ProductId"
      responses:
        "200":
          description: Related products
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Product"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  "/api/v1/products/{id}/reviews":
    get:
      tags:
        - Catalog
      summary: List reviews
      operationId: listProductReviews
      parameters:
        - "$ref": "#/components/parameters/ProductId"
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Paginated reviews
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
    post:
      tags:
        - Catalog
      summary: Leave a review
      operationId: createProductReview
      security:
        - bearerAuth: []
      parameters:
        - "$ref": "#/components/parameters/ProductId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - rating
              properties:
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                body:
                  type: string
                  maxLength: 1000
                  nullable: true
      responses:
        "201":
          description: Review created
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/categories:
    get:
      tags:
        - Catalog
      summary: Active categories
      operationId: listCategories
      responses:
        "200":
          description: Categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Category"
  /api/v1/flash-banner:
    get:
      tags:
        - Catalog
      summary: Promo banner slides
      operationId: getFlashBanner
      responses:
        "200":
          description: Banner payload
  /api/v1/cart:
    get:
      tags:
        - Cart
      summary: Get or create cart
      operationId: getCart
      security:
        - cartToken: []
        - bearerAuth: []
        - cartToken: []
          bearerAuth: []
      responses:
        "200":
          description: Cart
          headers:
            X-Cart-Token:
              schema:
                type: string
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CartResponse"
  /api/v1/cart/items:
    post:
      tags:
        - Cart
      summary: Add cart item
      operationId: addCartItem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - product_variant_id
                - quantity
              properties:
                product_variant_id:
                  type: integer
                quantity:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Updated cart
          headers:
            X-Cart-Token:
              schema:
                type: string
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  "/api/v1/cart/items/{id}":
    patch:
      tags:
        - Cart
      summary: Update cart item
      operationId: updateCartItem
      parameters:
        - "$ref": "#/components/parameters/CartItemId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - quantity
              properties:
                quantity:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Updated cart
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
    delete:
      tags:
        - Cart
      summary: Remove cart item
      operationId: removeCartItem
      parameters:
        - "$ref": "#/components/parameters/CartItemId"
      responses:
        "200":
          description: Updated cart
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/checkout:
    post:
      tags:
        - Checkout
      summary: Checkout cart
      operationId: checkout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CheckoutRequest"
      responses:
        "201":
          description: Order created
        "200":
          description: Idempotent replay
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
        "502":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "503":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  "/api/v1/orders/{number}":
    get:
      tags:
        - Orders
      summary: Guest order lookup
      operationId: getOrder
      parameters:
        - "$ref": "#/components/parameters/OrderNumber"
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
      responses:
        "200":
          description: Order
        "404":
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/auth/register:
    post:
      tags:
        - Account
      summary: Register shopper
      operationId: registerCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - email
                - phone
                - password
                - password_confirmation
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                phone:
                  type: string
                password:
                  type: string
                  minLength: 8
                password_confirmation:
                  type: string
      responses:
        "201":
          description: Registered
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/auth/login:
    post:
      tags:
        - Account
      summary: Shopper login
      operationId: loginCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: Token issued
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/auth/verify:
    post:
      tags:
        - Account
      summary: Verify email or SMS OTP
      operationId: verifyCustomer
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email_code:
                  type: string
                phone_code:
                  type: string
      responses:
        "200":
          description: Verified
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/auth/resend-verification:
    post:
      tags:
        - Account
      summary: Resend OTP
      operationId: resendCustomerVerification
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                channel:
                  type: string
                  enum:
                    - email
                    - phone
                    - both
      responses:
        "200":
          description: Sent
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/auth/forgot-password:
    post:
      tags:
        - Account
      summary: Request password reset
      operationId: forgotPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/auth/reset-password:
    post:
      tags:
        - Account
      summary: Reset password
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - token
                - password
                - password_confirmation
              properties:
                email:
                  type: string
                  format: email
                token:
                  type: string
                password:
                  type: string
                  minLength: 8
                password_confirmation:
                  type: string
      responses:
        "200":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/auth/logout:
    post:
      tags:
        - Account
      summary: Revoke storefront token
      operationId: logoutCustomer
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/auth/me:
    get:
      tags:
        - Account
      summary: Current shopper
      operationId: getMe
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Profile
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
    patch:
      tags:
        - Account
      summary: Update shopper profile
      operationId: updateMe
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - email
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
                phone:
                  type: string
                  nullable: true
                shipping_address:
                  nullable: true
                  allOf:
                    - "$ref": "#/components/schemas/ShippingAddress"
      responses:
        "200":
          description: Updated profile
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
  /api/v1/account/orders:
    get:
      tags:
        - Account
      summary: Shopper order history
      operationId: listAccountOrders
      security:
        - bearerAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Paginated orders
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/external/orders:
    post:
      tags:
        - Partners
      summary: Create externally paid order
      operationId: createExternalOrder
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ExternalOrderRequest"
      responses:
        "201":
          description: Paid order created
        "200":
          description: Idempotent replay
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum
      description: Shopper token from POST /api/v1/auth/login or /auth/register.
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Partner API key issued in /admin.
    cartToken:
      type: apiKey
      in: header
      name: X-Cart-Token
      description: Guest or account cart token. Created on first cart call if omitted.
  schemas:
    Message:
      type: object
      properties:
        message:
          type: string
    ValidationError:
      type: object
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    Category:
      type: object
      properties:
        id:
          type: integer
        parent_id:
          type: integer
          nullable: true
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        sort_order:
          type: integer
    Product:
      type: object
      description: Public product. Never includes vendor identity or unit_cost.
      properties:
        id:
          type: integer
        category_id:
          type: integer
          nullable: true
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        brand:
          type: string
          nullable: true
        manufacturer:
          type: string
          nullable: true
        status:
          type: string
        image_path:
          type: string
          nullable: true
        from_price:
          type: string
          nullable: true
        compare_at_price:
          type: string
          nullable: true
        available_quantity:
          type: integer
        rating_avg:
          type: number
        rating_count:
          type: integer
        sold_count:
          type: integer
        discount_percent:
          type: integer
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true
        category:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
            slug:
              type: string
            parent:
              type: object
              nullable: true
              properties:
                id:
                  type: integer
                name:
                  type: string
                slug:
                  type: string
        variants:
          type: array
          items:
            "$ref": "#/components/schemas/ProductVariant"
    ProductVariant:
      type: object
      description: Public variant. Never includes unit_cost.
      properties:
        id:
          type: integer
        sku:
          type: string
        name:
          type: string
        price:
          type: string
        compare_at_price:
          type: string
          nullable: true
        weight_kg:
          type: string
          nullable: true
        unit_of_measure:
          type: string
          nullable: true
        dimensions:
          type: string
          nullable: true
        options:
          type: object
          nullable: true
        is_active:
          type: boolean
        available_quantity:
          type: integer
    ProductList:
      type: object
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/Product"
        links:
          type: object
        meta:
          type: object
    CartResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            token:
              type: string
            currency:
              type: string
            items:
              type: array
        meta:
          type: object
          properties:
            cart_token:
              type: string
    ShippingAddress:
      type: object
      required:
        - line1
        - city
        - country
      properties:
        line1:
          type: string
        line2:
          type: string
          nullable: true
        city:
          type: string
        region:
          type: string
          description: Required for GH. GhanaRegion enum value such as greater_accra.
        postal_code:
          type: string
          nullable: true
          description: GhanaPost GPS code when available.
        country:
          type: string
          minLength: 2
          maxLength: 2
    CheckoutRequest:
      type: object
      required:
        - customer_name
        - customer_email
        - shipping_address
        - accept_terms
      properties:
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
          nullable: true
        shipping_address:
          "$ref": "#/components/schemas/ShippingAddress"
        customer_notes:
          type: string
          maxLength: 1000
          nullable: true
        accept_terms:
          type: boolean
        idempotency_key:
          type: string
          maxLength: 255
          nullable: true
    ExternalOrderRequest:
      type: object
      required:
        - cart_token
        - customer_name
        - customer_email
        - shipping_address
        - external_reference
      properties:
        cart_token:
          type: string
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
          nullable: true
        shipping_address:
          "$ref": "#/components/schemas/ShippingAddress"
        external_reference:
          type: string
        idempotency_key:
          type: string
          maxLength: 255
          nullable: true
  parameters:
    ProductId:
      name: id
      in: path
      required: true
      schema:
        type: integer
    CartItemId:
      name: id
      in: path
      required: true
      schema:
        type: integer
    OrderNumber:
      name: number
      in: path
      required: true
      schema:
        type: string
