MojoShop

OpenAPI

OpenAPI 3.0 specification for the eCommerce Exchange API. Import the raw YAML file into Postman, Insomnia, or your codegen tool.

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 whitelabel on their own hostname; the server resolves the institution from the request host and filters catalog, cart, and checkout to that institution’s approved vendor allowlist. Shared shopper accounts work across institution hosts. Public catalog and order payloads may include shop `{ name, slug, fulfillment_mode }` but never `vendor_id`, email, payout, commission, markup %, or webhook fields. Soft anonymity for browsing: the main marketplace catalog UI may omit shop names even when the JSON includes `shop`. `fulfillment_mode` is `delivery`, `pickup`, or `both` and limits which whole-order `shipping_method` values checkout may use for that shop.
    
    **Pricing:** Catalog, cart, checkout, and shopper order amounts are **shopper sale** prices — seller list × (1 + shop markup%). `min_price` / `max_price` and `sort=price_*` use those sale prices. Seller settlement is list − commission; platform keeps markup + commission. Public shopper JSON never exposes list price, `markup_rate`, or `vendor_amount` (those appear only on seller outbound webhooks).
    
    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`). Checkout and delivery quotes require that Bearer token. Partners use `X-Api-Key`. Carts send `X-Cart-Token` (also returned on cart responses) and stay guest-capable until checkout.
    
    **Delivery pin:** delivery quotes, delivery checkout, and saved shopper addresses require `shipping_address.lat` and `shipping_address.lng`. Resolve them with `GET /api/v1/geocode/search` or `GET /api/v1/geocode/reverse` (same Nominatim proxy the MojoShop map pin uses). Pickup checkout may omit `shipping_address`.
    
    **Fulfillment modes:** each shop exposes `fulfillment_mode` (`delivery`|`pickup`|`both`). Checkout and partner `shipping_method` must be allowed by every shop in the cart; delivery quotes also reject pickup-only shops.
    
    **Idempotency:** checkout and partner orders accept optional `idempotency_key` in the JSON body. Keys are scoped per signed-in shopper or partner API client. 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`. Item `unit_price` / fulfillment `subtotal` are shopper sale amounts; `vendor_amount` is your settle share (list − commission). See `/apidoc` for the full payload shape including `base_unit_price` and `markup_*`.
    
    Rate limits: 60/min default; tighter on register (10), login/reviews (20), password reset (6), checkout (30), and partners (30). 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: vendor
          in: query
          description: Approved shop slug
          schema:
            type: string
            maxLength: 100
        - name: vendor_id
          in: query
          schema:
            type: integer
        - name: min_price
          in: query
          description: Minimum shopper sale price (list × (1 + shop markup%))
          schema:
            type: number
            minimum: 0
        - name: max_price
          in: query
          description: Maximum shopper sale price
          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
          description: Default for API clients is newest. Shopper storefront uses random. price_asc / price_desc use shopper sale prices. random uses optional seed for stable pagination.
          schema:
            type: string
            enum:
              - random
              - newest
              - popular
              - rating
              - price_asc
              - price_desc
              - name
        - name: seed
          in: query
          description: Integer seed for sort=random so page 2 keeps the same shuffle
          schema:
            type: integer
            minimum: 1
            maximum: 2147483646
        - 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/shops/{slug}":
    get:
      tags:
        - Catalog
      summary: Public shop profile
      operationId: getShop
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Approved shop
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Shop"
        "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 product cards
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ProductCard"
        "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/geocode/search:
    get:
      tags:
        - Checkout
      summary: Forward-geocode an address into a drop-off pin
      operationId: geocodeSearch
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 3
            maxLength: 255
        - name: country
          in: query
          schema:
            type: string
            minLength: 2
            maxLength: 2
            default: GH
      responses:
        "200":
          description: Pin (`data` may be null)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/GeocodeHit"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
        "502":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/geocode/reverse:
    get:
      tags:
        - Checkout
      summary: Reverse-geocode a map pin into address fields
      operationId: geocodeReverse
      parameters:
        - name: lat
          in: query
          required: true
          schema:
            type: number
            format: double
            minimum: -90
            maximum: 90
        - name: lng
          in: query
          required: true
          schema:
            type: number
            format: double
            minimum: -180
            maximum: 180
      responses:
        "200":
          description: Address (`data` may be null)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/GeocodeHit"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
        "502":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /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
      security:
        - cartToken: []
        - bearerAuth: []
        - cartToken: []
          bearerAuth: []
      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
      security:
        - cartToken: []
        - bearerAuth: []
        - cartToken: []
          bearerAuth: []
      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
      security:
        - cartToken: []
        - bearerAuth: []
        - cartToken: []
          bearerAuth: []
      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/shipping-quote:
    post:
      tags:
        - Checkout
      summary: Preview Swoove delivery fee
      operationId: quoteShipping
      security:
        - bearerAuth: []
          cartToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ShippingQuoteRequest"
      responses:
        "200":
          description: Quote (`data.shipping_total`, `data.legs`)
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "422":
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ValidationError"
        "503":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/checkout:
    post:
      tags:
        - Checkout
      summary: Checkout cart
      operationId: checkout
      security:
        - bearerAuth: []
          cartToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CheckoutRequest"
      responses:
        "201":
          description: Order created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CheckoutResponse"
        "200":
          description: Idempotent replay
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CheckoutResponse"
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "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
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/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 SMS OTP (email uses confirmation link)
      operationId: verifyCustomer
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                phone_code:
                  type: string
                  description: 6-digit SMS OTP
                email_code:
                  type: string
                  description: Rejected — use the signed email link instead
      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 email confirmation link and/or SMS 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/change-email:
    post:
      tags:
        - Account
      summary: Correct mistyped email while phone is verified and email is pending
      operationId: changeCustomerEmail
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Email updated; confirmation link sent when Postmark is connected
        "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/change-phone:
    post:
      tags:
        - Account
      summary: Correct mistyped phone while phone is still unverified
      operationId: changeCustomerPhone
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phone
              properties:
                phone:
                  type: string
                  example: 0240000000
      responses:
        "200":
          description: Phone updated; new SMS code sent when Hubtel is connected
        "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/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 (10 per page)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/OrderList"
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/account/push-vapid:
    get:
      tags:
        - Account
      summary: Web Push VAPID public key
      operationId: getPushVapidPublicKey
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Public key for PushManager.subscribe
        "401":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "403":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
        "503":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
  /api/v1/account/push-subscription:
    post:
      tags:
        - Account
      summary: Save Web Push subscription
      operationId: storePushSubscription
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - endpoint
                - keys
              properties:
                endpoint:
                  type: string
                  format: uri
                keys:
                  type: object
                  required:
                    - p256dh
                    - auth
                  properties:
                    p256dh:
                      type: string
                    auth:
                      type: string
                content_encoding:
                  type: string
                  enum:
                    - aesgcm
                    - aes128gcm
      responses:
        "201":
          description: Subscription saved
        "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"
        "503":
          description: Error
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Message"
    delete:
      tags:
        - Account
      summary: Delete Web Push subscription
      operationId: deletePushSubscription
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - endpoint
              properties:
                endpoint:
                  type: string
                  format: uri
      responses:
        "200":
          description: Subscription removed
        "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/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
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CheckoutResponse"
        "200":
          description: Idempotent replay
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CheckoutResponse"
        "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
    ShopCard:
      type: object
      nullable: true
      properties:
        name:
          type: string
        slug:
          type: string
        fulfillment_mode:
          type: string
          enum:
            - delivery
            - pickup
            - both
          description: How this shop fulfills orders. Checkout shipping_method must be allowed by every shop in the cart.
    Shop:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        fulfillment_mode:
          type: string
          enum:
            - delivery
            - pickup
            - both
          description: How this shop fulfills orders.
    ProductCard:
      type: object
      description: Slim catalog card for list, related, and flash embeds. No description, variants list, or gallery. Includes default_variant_id and has_options for quick-add. Prices are shopper sale amounts. Never vendor_id, unit_cost, list price, markup_rate, email, or payout fields.
      properties:
        id:
          type: integer
        category_id:
          type: integer
          nullable: true
        name:
          type: string
        slug:
          type: string
        brand:
          type: string
          nullable: true
        status:
          type: string
        image_path:
          type: string
          nullable: true
        from_price:
          type: string
          nullable: true
          description: Lowest active-variant shopper sale price
        compare_at_price:
          type: string
          nullable: true
        available_quantity:
          type: integer
        default_variant_id:
          type: integer
          nullable: true
          description: Cheapest in-stock active variant for quick-add
        has_options:
          type: boolean
          description: True when more than one active variant — open product detail to choose
        rating_avg:
          type: number
        rating_count:
          type: integer
        sold_count:
          type: integer
        discount_percent:
          type: integer
          nullable: true
        shop:
          "$ref": "#/components/schemas/ShopCard"
        category:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
            slug:
              type: string
    Product:
      type: object
      description: Product detail. Includes shop { name, slug, fulfillment_mode }, variants, and images. Prices are shopper sale amounts. Never vendor_id, unit_cost, list price, markup_rate, email, or payout fields.
      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
          description: Lowest active-variant shopper sale price
        compare_at_price:
          type: string
          nullable: true
          description: Compare-at with the same shop markup applied
        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
        shop:
          "$ref": "#/components/schemas/ShopCard"
        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. price / compare_at_price are shopper sale amounts. Never includes unit_cost or list price.
      properties:
        id:
          type: integer
        sku:
          type: string
        name:
          type: string
        price:
          type: string
          description: Shopper sale price (list × (1 + shop markup%))
        compare_at_price:
          type: string
          nullable: true
          description: Compare-at with shop markup applied
        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/ProductCard"
        links:
          type: object
        meta:
          type: object
    CartItem:
      type: object
      properties:
        id:
          type: integer
        product_variant_id:
          type: integer
        quantity:
          type: integer
        unit_price:
          type: number
          nullable: true
          description: Shopper sale unit price
        line_total:
          type: number
          nullable: true
        product:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
            slug:
              type: string
            image_path:
              type: string
              nullable: true
            shop:
              "$ref": "#/components/schemas/ShopCard"
        variant:
          "$ref": "#/components/schemas/ProductVariant"
    CartResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            token:
              type: string
            currency:
              type: string
            items:
              type: array
              items:
                "$ref": "#/components/schemas/CartItem"
        meta:
          type: object
          properties:
            cart_token:
              type: string
    Shipment:
      type: object
      nullable: true
      description: Shopper Track shipment. Differs from seller outbound webhooks (which include provider / shipped_at / delivered_at).
      properties:
        status:
          type: string
          nullable: true
        tracking_number:
          type: string
          nullable: true
        carrier:
          type: string
          nullable: true
        handover_code:
          type: string
          nullable: true
        tracking_url:
          type: string
          nullable: true
    OrderItem:
      type: object
      properties:
        id:
          type: integer
        product_id:
          type: integer
          nullable: true
        product_variant_id:
          type: integer
          nullable: true
        product_name:
          type: string
        variant_name:
          type: string
          nullable: true
        sku:
          type: string
          nullable: true
        quantity:
          type: integer
        unit_price:
          type: string
          description: Shopper sale unit price
        line_total:
          type: string
        shop:
          "$ref": "#/components/schemas/ShopCard"
        image_path:
          type: string
          nullable: true
    Fulfillment:
      type: object
      description: Shopper packing list. Includes shop { name, slug, fulfillment_mode }; never vendor_id or secrets.
      properties:
        id:
          type: integer
        number:
          type: string
        status:
          type: string
        shipping_method:
          type: string
          enum:
            - delivery
            - pickup
          nullable: true
        subtotal:
          type: string
        shop:
          "$ref": "#/components/schemas/ShopCard"
        items:
          type: array
          items:
            "$ref": "#/components/schemas/OrderItem"
        pickup:
          type: object
          nullable: true
          description: Present when shipping_method is pickup. Includes shop name/slug; never lat/lng.
        shipment:
          "$ref": "#/components/schemas/Shipment"
    PaymentSummary:
      type: object
      nullable: true
      properties:
        id:
          type: integer
        gateway:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
        amount:
          type: string
          nullable: true
        currency:
          type: string
          nullable: true
        external_reference:
          type: string
          nullable: true
        paid_at:
          type: string
          format: date-time
          nullable: true
    Order:
      type: object
      description: Shopper order. Nested shops may include name/slug/fulfillment_mode. Never vendor_id, email, payout, commission, markup %, or vendor_amount.
      properties:
        id:
          type: integer
        number:
          type: string
        status:
          type: string
        customer_name:
          type: string
          nullable: true
        customer_email:
          type: string
          nullable: true
        customer_phone:
          type: string
          nullable: true
        shipping_address:
          type: object
          nullable: true
        billing_address:
          type: object
          nullable: true
        customer_notes:
          type: string
          nullable: true
        shipping_method:
          type: string
          enum:
            - delivery
            - pickup
          nullable: true
        currency:
          type: string
        subtotal:
          type: string
        shipping_total:
          type: string
        tax_total:
          type: string
        grand_total:
          type: string
        paid_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        items:
          type: array
          items:
            "$ref": "#/components/schemas/OrderItem"
        fulfillments:
          type: array
          items:
            "$ref": "#/components/schemas/Fulfillment"
        payment:
          "$ref": "#/components/schemas/PaymentSummary"
    OrderList:
      type: object
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/Order"
        links:
          type: object
        meta:
          type: object
    CheckoutResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            order:
              "$ref": "#/components/schemas/Order"
            payment:
              type: object
              properties:
                id:
                  type: integer
                status:
                  type: string
                amount:
                  type: string
                currency:
                  type: string
                checkout_url:
                  type: string
                  nullable: true
                external_reference:
                  type: string
                  nullable: true
    ShippingAddress:
      type: object
      required:
        - line1
        - city
        - country
        - region
        - lat
        - lng
      description: Required for delivery quotes, delivery checkout, and saved shopper addresses. Pickup checkout may omit shipping_address entirely. lat/lng come from GET /api/v1/geocode/search or /geocode/reverse.
      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
        lat:
          type: number
          format: double
          minimum: -90
          maximum: 90
          description: Drop-off pin latitude (OSM map).
        lng:
          type: number
          format: double
          minimum: -180
          maximum: 180
          description: Drop-off pin longitude (OSM map).
    GeocodeHit:
      type: object
      nullable: true
      properties:
        lat:
          type: number
          format: double
        lng:
          type: number
          format: double
        display_name:
          type: string
        address:
          type: object
          additionalProperties: true
    CheckoutRequest:
      type: object
      required:
        - customer_name
        - customer_email
        - shipping_method
        - accept_terms
      properties:
        shipping_method:
          type: string
          enum:
            - delivery
            - pickup
          description: Whole-order method. Must be allowed by every cart shop’s fulfillment_mode (delivery|pickup|both).
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
          nullable: true
          description: Required for delivery.
        shipping_address:
          "$ref": "#/components/schemas/ShippingAddress"
          description: Required for delivery (including lat/lng). Optional for pickup.
        billing_address:
          type: object
          nullable: true
          description: Optional; stored on the order when provided.
        customer_notes:
          type: string
          maxLength: 1000
          nullable: true
        accept_terms:
          type: boolean
        idempotency_key:
          type: string
          maxLength: 255
          nullable: true
    ShippingQuoteRequest:
      type: object
      required:
        - customer_name
        - customer_email
        - customer_phone
        - shipping_address
      properties:
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
        shipping_address:
          "$ref": "#/components/schemas/ShippingAddress"
    ExternalOrderRequest:
      type: object
      required:
        - cart_token
        - customer_name
        - customer_email
        - shipping_method
        - external_reference
        - accept_terms
      properties:
        cart_token:
          type: string
        shipping_method:
          type: string
          enum:
            - delivery
            - pickup
          description: Whole-order method. Must be allowed by every cart shop’s fulfillment_mode (delivery|pickup|both).
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
          nullable: true
        shipping_address:
          "$ref": "#/components/schemas/ShippingAddress"
        billing_address:
          type: object
          nullable: true
        external_reference:
          type: string
        accept_terms:
          type: boolean
        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