openapi: 3.1.0
info:
  title: NEXOS Partner API
  version: 2.3.4
  description: |
    NEXOS Partner API — programmatic access to NexosIQ invoice and receipt
    extraction, CostIQ line-item categorization, vendor management, and
    NexosIQ Search (semantic search) for white-label partners.

    ## Authentication

    All endpoints require an API key in the `X-API-Key` header. Keys are
    issued per partner from the admin dashboard and can be scoped to
    specific capabilities (extraction, storage, webhooks).

    ```
    X-API-Key: pnr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    ## Tiers

    - **Extraction API** ($0.50/doc) — NexosIQ extraction only
    - **Extraction + Storage** ($0.85/doc) — includes cloud archive and
      NexosIQ Search across your partner's documents
    - **Platform Embed** (custom) — full white-label NEXOS embedded in
      the partner's product via deep links + SAML/OIDC SSO

    ## Rate limits

    - 1,000 requests/minute per API key on Extraction API
    - 5,000 requests/minute on Extraction + Storage
    - Platform Embed: unlimited

  contact:
    name: NEXOS Partner Support
    email: partners@nexosscan.com
    url: https://nexosscan.com/partners
  license:
    name: Proprietary
    url: https://nexosscan.com/terms
  x-powered-by: NexosIQ
  x-vendor: Titan Innovations LLC

servers:
  - url: https://api.nexosscan.com
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Invoices
    description: Upload documents for NexosIQ extraction. 99% accuracy.
  - name: Receipts
    description: Upload receipts for NexosIQ extraction with CostIQ categorization.
  - name: Inventory
    description: Per-store inventory items, counts, and summary. Scopes — `inventory:read`, `inventory:write`.
  - name: Scheduling
    description: Schedules, employees, time-off, and labor reporting. Scopes — `scheduling:read`, `scheduling:write`.
  - name: Vendors
    description: Vendor management and price tracking.
  - name: Search
    description: NexosIQ Search — semantic search across all indexed documents.
  - name: Webhooks
    description: Register endpoints for real-time event delivery.
  - name: Tenants
    description: Platform Embed tier only — manage sub-tenants under your partner account.
  - name: Embed SSO
    description: Auto-provision a tenant user and exchange a one-time URL for a logged-in deep-link experience.

paths:
  /api/v1/partner/invoices/upload:
    post:
      tags: [Invoices]
      summary: Upload an invoice for NexosIQ extraction
      description: |
        Submit an invoice (PDF, JPG, PNG, WEBP) for extraction. NexosIQ processes
        the document synchronously and returns structured data within 60 seconds.

        Multi-page PDFs are handled automatically. Multi-vendor PDFs are
        auto-split into separate invoice records.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Invoice file (max 25 MB)
                tenantId:
                  type: string
                  format: uuid
                  description: Tenant ID if submitting on behalf of a sub-tenant (Platform Embed only)
                storeId:
                  type: string
                  format: uuid
                  description: Store ID to assign the invoice to
      responses:
        "201":
          description: Invoice extracted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractedInvoice"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/partner/invoices/{id}:
    get:
      tags: [Invoices]
      summary: Get an extracted invoice by ID
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Invoice details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractedInvoice"

  /api/v1/partner/receipts/upload:
    post:
      tags: [Receipts]
      summary: Upload a receipt for NexosIQ extraction with CostIQ categorization
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                useType:
                  type: string
                  enum: [business, personal, mixed]
                  default: business
      responses:
        "201":
          description: Receipt extracted with CostIQ categories assigned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractedReceipt"

  /api/v1/partner/search:
    get:
      tags: [Search]
      summary: NexosIQ Search — semantic search across indexed documents
      description: |
        Semantic search over indexed tenant documents — 512-dimension vector
        embeddings stored in pgvector. Finds invoices, receipts, and vendors
        by meaning, not just keywords.

        Example queries:
        - "Korean food distributor"
        - "cleaning supplies"
        - "invoices over $500 from January"
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
          description: Search query
        - name: type
          in: query
          schema:
            type: string
            enum: [invoice, receipt, vendor]
          description: Filter to a single entity type
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
      responses:
        "200":
          description: Ranked results by semantic similarity
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: "#/components/schemas/SearchResult"
                      query: { type: string }
                      totalResults: { type: integer }

  /api/v1/partner/vendors:
    get:
      tags: [Vendors]
      summary: List vendors
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
      responses:
        "200":
          description: Vendor list
          content:
            application/json:
              schema:
                type: object
                properties:
                  vendors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Vendor"

  /api/v1/partner/webhooks:
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: |
        Subscribes an HTTPS endpoint to NEXOS events. Payloads are
        signed with HMAC-SHA256 using your webhook secret.

        Events:
        - `invoice.created`
        - `invoice.processed`
        - `invoice.failed`
        - `receipt.processed`
        - `vendor.created`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - invoice.created
                      - invoice.processed
                      - invoice.failed
                      - receipt.processed
                      - vendor.created
      responses:
        "201":
          description: Webhook registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  secret:
                    type: string
                    description: HMAC secret — shown once, store it securely
                  url: { type: string }
                  events: { type: array, items: { type: string } }

  /api/v1/partner/tenants:
    post:
      tags: [Tenants]
      summary: Create a sub-tenant under your partner account (Platform Embed only)
      description: |
        Provisions a new NEXOS tenant under your partner account. Returns
        a setup link that can be deep-linked into your product for the tenant
        to complete onboarding (optionally with SAML/OIDC SSO).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                maxLocations: { type: integer, default: 1 }
                ssoProvider:
                  type: string
                  enum: [saml, oidc, null]
      responses:
        "201":
          description: Tenant provisioned
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenantId: { type: string, format: uuid }
                  setupUrl: { type: string, format: uri }

  # ===========================================================================
  # Embed SSO — auto-provision a tenant user and exchange a one-time URL.
  # ===========================================================================
  /api/v1/partner/tenants/{tenantId}/embed/auth:
    post:
      tags: [Embed SSO]
      summary: Auto-provision user + return a one-time embed URL
      description: |
        Provisions (or reuses) a user in the tenant identified by tenantId,
        scoped to the store you pass via externalStoreId. Returns a one-time
        URL that auto-logs the user into the requested page. Token expires in
        5 minutes. Use this for in-app embedding via webview, iframe, or
        new-tab open.
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name, externalStoreId]
              properties:
                email: { type: string, format: email }
                name: { type: string }
                role:
                  type: string
                  default: store_manager
                  enum: [account_owner, executive, cfo, regional_director, area_director, location_manager, store_manager, accounting, supervisor]
                externalStoreId:
                  type: string
                  description: Your ID for the store (set via POST /tenants/{tenantId}/stores).
                page:
                  type: string
                  default: dashboard
                  description: Which NEXOS page to land on. e.g. dashboard, invoices, inventory, scheduling, search, pl, reports.
      responses:
        "200":
          description: Embed token issued (5-minute TTL).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      embedToken: { type: string }
                      embedUrl: { type: string, format: uri }

  # ===========================================================================
  # Inventory — list items + counts, submit counts, summary.
  # Scopes: inventory:read (all GETs), inventory:write (POST counts).
  # ===========================================================================
  /api/v1/partner/tenants/{tenantId}/inventory/items:
    get:
      tags: [Inventory]
      summary: List inventory items for a tenant (optionally filtered by store)
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Inventory item list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items: { type: object }

  /api/v1/partner/tenants/{tenantId}/inventory/counts/{storeId}:
    get:
      tags: [Inventory]
      summary: Latest counts for each item at a store
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Counts joined to inventory items + default cost
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      counts:
                        type: array
                        items: { type: object }

  /api/v1/partner/tenants/{tenantId}/inventory/counts:
    post:
      tags: [Inventory]
      summary: Submit a new inventory count
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [storeId, itemId, quantity]
              properties:
                storeId: { type: string, format: uuid }
                itemId: { type: string, format: uuid }
                quantity: { type: number }
                unit: { type: string, description: "What you counted in (case, each, lb, etc.)" }
                notes: { type: string }
      responses:
        "201":
          description: Count submitted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      count: { type: object }

  /api/v1/partner/tenants/{tenantId}/inventory/summary:
    get:
      tags: [Inventory]
      summary: Inventory summary — item count, last-counted-at, total value
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Summary across all (or one) of the tenant's stores.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      itemCount: { type: integer }
                      lastCountedAt: { type: string, format: date-time, nullable: true }
                      totalInventoryValue: { type: number }

  # ===========================================================================
  # Scheduling — schedules, shifts, employees, time-off, labor report.
  # Scopes: scheduling:read (all GETs), scheduling:write (POST time-off).
  # ===========================================================================
  /api/v1/partner/tenants/{tenantId}/scheduling/schedules:
    get:
      tags: [Scheduling]
      summary: List schedules for a tenant
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: query
          schema: { type: string, format: uuid }
        - name: status
          in: query
          schema: { type: string, enum: [draft, published, archived] }
      responses:
        "200":
          description: Schedule list (latest 100, by week descending).
          content:
            application/json:
              schema: { type: object }

  /api/v1/partner/tenants/{tenantId}/scheduling/schedules/{scheduleId}:
    get:
      tags: [Scheduling]
      summary: Get one schedule with all its shifts (joined to employees)
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: scheduleId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Schedule + shifts.
          content:
            application/json:
              schema: { type: object }

  /api/v1/partner/tenants/{tenantId}/scheduling/employees:
    get:
      tags: [Scheduling]
      summary: List employees on the tenant's roster
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: query
          schema: { type: string, format: uuid }
        - name: includeInactive
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Employee list.
          content:
            application/json:
              schema: { type: object }

  /api/v1/partner/tenants/{tenantId}/scheduling/time-off:
    get:
      tags: [Scheduling]
      summary: List time-off requests for the tenant
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: status
          in: query
          schema: { type: string, enum: [pending, approved, denied] }
        - name: employeeId
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Time-off request list (latest 200).
          content:
            application/json:
              schema: { type: object }
    post:
      tags: [Scheduling]
      summary: Submit a time-off request on behalf of an employee
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [employeeId, startDate, endDate]
              properties:
                employeeId: { type: string, format: uuid }
                startDate: { type: string, format: date }
                endDate: { type: string, format: date }
                reason: { type: string }
      responses:
        "201":
          description: Request created in pending state.
          content:
            application/json:
              schema: { type: object }

  /api/v1/partner/tenants/{tenantId}/scheduling/labor-report:
    get:
      tags: [Scheduling]
      summary: Labor report — scheduled vs actual hours, labor % of sales
      description: |
        Pairs scheduled hours from schedule_shifts with actual punches from
        time_punches and POS revenue from pos_sales_daily. Defaults to
        first-of-month → today UTC. Returns the same shape the dashboard
        Labor Report uses: summary tiles, weekly Sun-Sat breakdown, per-employee.
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
        - name: storeId
          in: query
          schema: { type: string, format: uuid }
        - name: startDate
          in: query
          schema: { type: string, format: date }
        - name: endDate
          in: query
          schema: { type: string, format: date }
      responses:
        "200":
          description: Labor report payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      summary: { type: object }
                      weekly: { type: array, items: { type: object } }
                      employees: { type: array, items: { type: object } }

  # ===========================================================================
  # NexosIQ Search — semantic search per tenant.
  # Scope: search:read.
  # ===========================================================================
  /api/v1/partner/tenants/{tenantId}/search:
    post:
      tags: [Search]
      summary: Run NexosIQ Search across this tenant's documents
      description: |
        Same Voyage AI + pgvector pipeline the dashboard uses. Returns ranked
        results enriched with vendor / total / date so the partner doesn't
        need a follow-up call per match.
      parameters:
        - $ref: "#/components/parameters/TenantIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query: { type: string, minLength: 2 }
                type:
                  type: string
                  enum: [invoice, receipt, vendor]
                limit:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 10
      responses:
        "200":
          description: Ranked + enriched results.
          content:
            application/json:
              schema: { type: object }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    TenantIdPath:
      name: tenantId
      in: path
      required: true
      description: UUID of the tenant (corporation) the partner key has access to.
      schema: { type: string, format: uuid }

  schemas:
    ExtractedInvoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        vendorName: { type: string }
        invoiceNumber: { type: string }
        invoiceDate: { type: string, format: date }
        dueDate: { type: string, format: date }
        subtotal: { type: number }
        tax: { type: number }
        total: { type: number }
        currency: { type: string, example: "USD" }
        extractionConfidence:
          type: number
          minimum: 0
          maximum: 100
        status: { type: string, enum: [processing, processed, failed] }
        lineItems:
          type: array
          items:
            $ref: "#/components/schemas/LineItem"
        costiqCategories:
          type: array
          items: { type: string }
          description: Unique CostIQ category labels present on this invoice
        createdAt: { type: string, format: date-time }

    ExtractedReceipt:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchantName: { type: string }
        receiptDate: { type: string, format: date }
        subtotal: { type: number }
        tax: { type: number }
        tip: { type: number }
        total: { type: number }
        taxCategory: { type: string, example: "Meals" }
        useType: { type: string, enum: [business, personal, mixed] }
        lineItems:
          type: array
          items:
            $ref: "#/components/schemas/LineItem"

    LineItem:
      type: object
      properties:
        description: { type: string }
        sku: { type: string }
        quantity: { type: number }
        unit: { type: string }
        unitPrice: { type: number }
        total: { type: number }
        costiqCategory:
          type: string
          description: One of 19 CostIQ categories (food_cost, supply_cost, equipment, etc.)

    Vendor:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        normalizedName: { type: string }
        invoiceCount: { type: integer }
        totalSpent: { type: number }

    SearchResult:
      type: object
      properties:
        type: { type: string, enum: [invoice, receipt, vendor] }
        id: { type: string, format: uuid }
        similarity:
          type: integer
          description: Semantic similarity percentage (0-100)
        preview: { type: string }
        vendorName: { type: string }
        merchantName: { type: string }
        total: { type: number }
        date: { type: string, format: date }

  responses:
    BadRequest:
      description: Request validation failed
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              retryAfter: { type: integer }
