openapi: 3.1.0
info:
  title: Mercazul Inventory Integration API
  version: 1.0.0
  description: |
    Programmatic inventory integration for Mercazul sellers.

    The API is the platform's Supabase data layer exposed over REST (PostgREST).
    Every request is scoped by Row-Level Security: you can only read and write
    rows that belong to your own seller company.

    **Authentication** — every request needs TWO headers:
    - `apikey`: the platform publishable (anon) key — shown on the
      [integration guide](/help/api-integration) page.
    - `Authorization: Bearer <access_token>` — a user JWT obtained from the
      sign-in endpoint below, using your seller account credentials.

    **Recommended flow for inventory sync**
    1. Sign in (`POST /auth/v1/token?grant_type=password`) → `access_token`.
    2. Push your catalog with `POST /rest/v1/rpc/seller_import_products`
       (idempotent upsert keyed on your SKU).
    3. Poll `GET /rest/v1/product_import_jobs` for the import result.
    4. For lightweight stock/price updates between full syncs, PATCH
       `/rest/v1/products` and `/rest/v1/inventory` directly.

    **Note** — newly inserted products start in `draft` status and go through
    marketplace moderation before they are publicly visible. Updates to
    existing products keep the current status.

    **SFTP channel (live)** — sellers who prefer file drops over API calls can
    deliver the same CSV contract via SFTP: host `sftp.izzatissa.site`, port
    `2022`, SSH-key authentication (Ed25519; account provisioned by support —
    email support@mercazul.com with your public key). Files dropped in
    `inbox/` are imported automatically (checked every 2 minutes) and a
    per-file result report is written to `reports/`; jobs appear in the seller
    dashboard import history. Limits: 2 MB and 2000 data rows per file. See the
    [integration guide](/help/api-integration) for details.
servers:
  - url: https://{project-ref}.supabase.co
    description: Mercazul platform API (Supabase). The concrete base URL is shown on the integration guide page.
    variables:
      project-ref:
        default: YOUR-PROJECT-REF
        description: Platform project reference (part of the base URL shown in the integration guide).
tags:
  - name: Auth
    description: Obtain an access token for your seller account.
  - name: Bulk import
    description: Idempotent catalog + stock upsert (same contract as CSV upload).
  - name: Products
    description: Direct product reads and field-level updates (RLS-scoped).
  - name: Inventory
    description: Stock levels per product / variant.
security:
  - ApiKeyAuth: []
    BearerAuth: []
paths:
  /auth/v1/token:
    post:
      tags: [Auth]
      summary: Sign in (password grant) and get an access token
      description: |
        Exchanges your seller account email + password for a JWT.
        Send the returned `access_token` as `Authorization: Bearer …` on all
        other endpoints. Tokens expire (default 1 hour) — refresh with the
        returned `refresh_token` or sign in again.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: grant_type
          in: query
          required: true
          schema:
            type: string
            enum: [password, refresh_token]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                password: { type: string }
                refresh_token:
                  type: string
                  description: Only for grant_type=refresh_token.
            examples:
              password:
                value: { email: "seller@example.com", password: "••••••••" }
      responses:
        "200":
          description: Token issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, example: bearer }
                  expires_in: { type: integer, example: 3600 }
                  refresh_token: { type: string }
        "400":
          description: Invalid credentials.

  /rest/v1/rpc/seller_import_products:
    post:
      tags: [Bulk import]
      summary: Bulk upsert products + stock (inventory sync)
      description: |
        The primary inventory-sync endpoint. Accepts an array of rows using the
        same 10-field contract as the CSV upload. Rows are upserted keyed on
        `(your company, sku)`:
        - unknown `sku` → product is **inserted** (status `draft`, pending moderation)
        - known `sku` → product fields and stock are **updated**

        `category` must match an existing category name or slug (case-insensitive).
        Rows that fail server validation are reported per-row in `error_rows`
        without aborting the rest of the batch. An import job record is always
        created for auditability.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [p_rows]
              properties:
                p_rows:
                  type: array
                  items: { $ref: "#/components/schemas/ImportRow" }
                p_filename:
                  type: [string, "null"]
                  description: Label stored on the import job (e.g. "api-sync-2026-06-12").
                p_invalid_rows:
                  type: array
                  description: Optional client-side-rejected rows, counted in the job stats.
                  items: { type: object }
                  default: []
            examples:
              sync:
                value:
                  p_filename: api-sync-2026-06-12
                  p_rows:
                    - sku: "WIDGET-001"
                      name_es: "Widget industrial 10cm"
                      name_en: "Industrial widget 10cm"
                      description_es: "Widget de acero inoxidable para uso industrial."
                      description_en: "Stainless-steel widget for industrial use."
                      category: "hardware"
                      price: 12.50
                      moq: 100
                      stock: 5400
                      currency: "USD"
      responses:
        "200":
          description: Import processed (check per-row errors in the payload).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ImportResult" }
        "401":
          description: Missing/invalid token.
        "400":
          description: Malformed payload (e.g. p_rows not an array) or no seller company on the account.

  /rest/v1/product_import_jobs:
    get:
      tags: [Bulk import]
      summary: List your import jobs (history + per-row errors)
      description: |
        PostgREST query semantics: filter and order via query string, e.g.
        `?order=created_at.desc&limit=10`.
      parameters:
        - name: select
          in: query
          schema: { type: string, example: "*" }
        - name: order
          in: query
          schema: { type: string, example: "created_at.desc" }
        - name: limit
          in: query
          schema: { type: integer, example: 10 }
      responses:
        "200":
          description: Your company's import jobs (RLS-scoped).
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ImportJob" }

  /rest/v1/products:
    get:
      tags: [Products]
      summary: List products
      description: |
        Returns rows visible to your token under RLS (your own catalog plus
        publicly approved products). Common filters:
        `?sku=eq.WIDGET-001`, `?select=id,sku,name_en,price,currency,status`,
        `?status=eq.approved`.
      parameters:
        - name: select
          in: query
          schema: { type: string, example: "id,sku,name_en,price,currency,status" }
        - name: sku
          in: query
          schema: { type: string, example: "eq.WIDGET-001" }
        - name: order
          in: query
          schema: { type: string, example: "updated_at.desc" }
        - name: limit
          in: query
          schema: { type: integer, example: 50 }
      responses:
        "200":
          description: Product rows.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Product" }
    patch:
      tags: [Products]
      summary: Update product fields by filter (e.g. price by SKU)
      description: |
        PostgREST bulk update: rows matching the query-string filter are
        updated with the JSON body. RLS restricts writes to your own catalog.
        Add header `Prefer: return=representation` to get the updated rows back.
      parameters:
        - name: sku
          in: query
          schema: { type: string, example: "eq.WIDGET-001" }
        - name: Prefer
          in: header
          schema: { type: string, example: "return=representation" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                price: { type: number, example: 13.25 }
                min_order_qty: { type: integer, example: 50 }
            additionalProperties: true
      responses:
        "200":
          description: Updated rows (with Prefer return=representation).
        "204":
          description: Updated (no body).

  /rest/v1/inventory:
    get:
      tags: [Inventory]
      summary: Read stock levels
      parameters:
        - name: select
          in: query
          schema: { type: string, example: "product_id,quantity,reserved_quantity" }
        - name: product_id
          in: query
          schema: { type: string, example: "eq.<product-uuid>" }
      responses:
        "200":
          description: Inventory rows (RLS-scoped).
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/InventoryRecord" }
    patch:
      tags: [Inventory]
      summary: Set stock quantity by product
      parameters:
        - name: product_id
          in: query
          required: true
          schema: { type: string, example: "eq.<product-uuid>" }
        - name: Prefer
          in: header
          schema: { type: string, example: "return=representation" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity: { type: integer, example: 4800 }
      responses:
        "200": { description: Updated rows. }
        "204": { description: Updated (no body). }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: apikey
      description: Platform publishable (anon) key — shown on the integration guide page.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Seller account access token from POST /auth/v1/token.
  schemas:
    ImportRow:
      type: object
      description: One catalog row — identical contract to the CSV upload columns.
      required: [sku, name_es, name_en, description_es, description_en, category, price, moq, stock, currency]
      properties:
        sku:
          type: string
          description: Your unique product code — the upsert key within your catalog.
        name_es: { type: string, description: Product name (Spanish). }
        name_en: { type: string, description: Product name (English). }
        description_es: { type: string, description: Description (Spanish). }
        description_en: { type: string, description: Description (English). }
        category:
          type: string
          description: Existing category name or slug (case-insensitive). Unknown categories reject the row.
        price:
          type: number
          minimum: 0
          description: Unit price (>= 0).
        moq:
          type: integer
          minimum: 1
          description: Minimum order quantity (integer >= 1).
        stock:
          type: integer
          minimum: 0
          description: Available stock (integer >= 0). Replaces the stored quantity.
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
          description: 3-letter ISO 4217 code, e.g. USD.
        sell_unit:
          type: string
          enum: [individual, box, both]
          description: >-
            Optional. How the product is sold. Blank or omitted ⇒ `individual`
            (a loose unit, fully back-compatible). `box` ⇒ sold only as a pre-packed
            box. `both` ⇒ available as individual units and as a box.
        units_per_box:
          type: integer
          minimum: 1
          description: >-
            Required when sell_unit is `box` or `both`; must equal the sum of the
            counts in size_breakdown. Omit for `individual`.
        size_breakdown:
          type: string
          description: >-
            Required when sell_unit is `box` or `both`. Compact `size:qty` pairs
            joined by commas. A size curve uses multiple non-empty sizes
            (`38:1,39:2,40:3,41:2,42:1`); a single-size box uses one pair
            (`40:24`); a non-sized box uses an empty size key (`:24`). The qty
            values must sum to units_per_box. composition_type is inferred: one
            pair ⇒ single_size, more than one ⇒ size_curve. Must be empty/omitted
            when sell_unit is `individual`.
        size_system:
          type: string
          description: >-
            Optional free-text size system label for boxes (e.g. EU, US, UK).
    ImportResult:
      type: object
      properties:
        job_id: { type: string, format: uuid }
        total_rows: { type: integer }
        inserted_count: { type: integer }
        updated_count: { type: integer }
        invalid_rows: { type: integer }
        error_rows:
          type: array
          description: Per-row server-side failures (row number, sku, error message).
          items:
            type: object
            properties:
              row: { type: integer }
              sku: { type: [string, "null"] }
              error: { type: string }
    ImportJob:
      type: object
      properties:
        id: { type: string, format: uuid }
        filename: { type: [string, "null"] }
        total_rows: { type: integer }
        valid_rows: { type: integer }
        invalid_rows: { type: integer }
        inserted_count: { type: integer }
        updated_count: { type: integer }
        status: { type: string, enum: [processing, completed, failed] }
        message: { type: [string, "null"] }
        error_rows: { type: array, items: { type: object } }
        created_at: { type: string, format: date-time }
    Product:
      type: object
      properties:
        id: { type: string, format: uuid }
        company_id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }
        sku: { type: [string, "null"] }
        name_es: { type: [string, "null"] }
        name_en: { type: [string, "null"] }
        price: { type: [number, "null"] }
        currency: { type: [string, "null"] }
        min_order_qty: { type: [integer, "null"] }
        status: { type: string, enum: [draft, pending, approved, rejected] }
        updated_at: { type: string, format: date-time }
    InventoryRecord:
      type: object
      properties:
        product_id: { type: string, format: uuid }
        quantity: { type: integer }
        reserved_quantity: { type: integer }
        reorder_level: { type: integer }
