openapi: 3.0.3
info:
  title: PeptideClients Public API
  version: 1.0.0
  description: |
    Operator-managed REST API for the PeptideClients workspace.

    Authenticate every request with `Authorization: Bearer pck_live_*`. Mint
    keys at `https://app.peptideclients.com/settings/api-keys`. Read-only
    keys can hit every `GET`; read-write keys additionally can fire test
    webhook events.

    Pagination uses keyset cursors: pass `cursor_at` and `cursor_id` from
    the previous page's `next_cursor` (or omit for page 1). `limit` is
    1-100, default 50.

  contact:
    name: PeptideClients support
    email: support@peptideclients.com
  license:
    name: Proprietary

servers:
  - url: https://api.peptideclients.com
    description: Custom domain (operator follow-up DNS step)
  - url: https://bxbdqmtldkhgwpnwecox.supabase.co/functions/v1/public-api
    description: Supabase Edge Function fallback (works without DNS)

tags:
  - name: orders
  - name: vendors
  - name: clients
  - name: products
  - name: webhooks
  - name: meta

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key (pck_live_*)
  parameters:
    cursorAt:
      name: cursor_at
      in: query
      description: Cursor sort value from a previous response's `next_cursor.at`.
      schema: { type: string, format: date-time }
    cursorId:
      name: cursor_id
      in: query
      description: Cursor tie-breaker from a previous response's `next_cursor.id`.
      schema: { type: string, format: uuid }
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    pathId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
  schemas:
    Cursor:
      type: object
      nullable: true
      properties:
        at: { type: string, format: date-time }
        id: { type: string, format: uuid }
    Order:
      type: object
      properties:
        id: { type: string, format: uuid }
        number: { type: string }
        status: { type: string }
        client_id: { type: string, format: uuid }
        currency: { type: string }
        subtotal_cents: { type: integer, format: int64 }
        total_cents: { type: integer, format: int64 }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    OrderDetail:
      allOf:
        - $ref: '#/components/schemas/Order'
        - type: object
          properties:
            discount_cents: { type: integer, format: int64 }
            tax_cents: { type: integer, format: int64 }
            shipping_cents: { type: integer, format: int64 }
            amount_paid_cents: { type: integer, format: int64 }
    OrdersList:
      type: object
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/Order' }
        next_cursor: { $ref: '#/components/schemas/Cursor' }
    Vendor:
      type: object
      properties:
        id: { type: string, format: uuid }
        display_name: { type: string }
        company_name: { type: string }
        vendor_type: { type: string }
        status: { type: string }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        website: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    VendorDetail:
      allOf:
        - $ref: '#/components/schemas/Vendor'
        - type: object
          properties:
            payment_terms: { type: string }
            default_currency: { type: string }
            notes: { type: string, nullable: true }
            tags: { type: array, items: { type: string } }
    VendorsList:
      type: object
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/Vendor' }
        next_cursor: { $ref: '#/components/schemas/Cursor' }
    Client:
      type: object
      properties:
        id: { type: string, format: uuid }
        display_name: { type: string }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        status: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ClientsList:
      type: object
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/Client' }
        next_cursor: { $ref: '#/components/schemas/Cursor' }
    Product:
      type: object
      properties:
        id: { type: string, format: uuid }
        vendor_id: { type: string, format: uuid }
        vendor_slug: { type: string }
        slug: { type: string }
        name: { type: string }
        product_type: { type: string }
        price_cents: { type: integer, format: int64 }
        currency: { type: string }
        inventory_status: { type: string }
        published: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ProductDetail:
      allOf:
        - $ref: '#/components/schemas/Product'
        - type: object
          properties:
            description_md: { type: string, nullable: true }
            image_urls: { type: array, items: { type: string } }
    ProductsList:
      type: object
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/Product' }
        next_cursor: { $ref: '#/components/schemas/Cursor' }
    WebhookTestResponse:
      type: object
      properties:
        delivery_id: { type: string, format: uuid }
    Error:
      type: object
      properties:
        error: { type: string }
        detail: { type: string, nullable: true }
  responses:
    Unauthorized:
      description: Missing or invalid api key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Insufficient scope (e.g. read_only hitting a write endpoint).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Row not found in this org's scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Per-key rate limit exceeded (600 req/min in V1).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

security:
  - bearerAuth: []

paths:
  /v1/health:
    get:
      tags: [meta]
      security: []
      summary: Liveness probe (unauthenticated)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }

  /v1/openapi.json:
    get:
      tags: [meta]
      security: []
      summary: Return this OpenAPI document as JSON
      responses:
        '200':
          description: OK

  /v1/orders:
    get:
      tags: [orders]
      summary: List orders for the calling org
      parameters:
        - $ref: '#/components/parameters/cursorAt'
        - $ref: '#/components/parameters/cursorId'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrdersList' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/orders/{id}:
    get:
      tags: [orders]
      summary: Get one order
      parameters:
        - $ref: '#/components/parameters/pathId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/vendors:
    get:
      tags: [vendors]
      summary: List vendors (org's supplier CRM)
      parameters:
        - $ref: '#/components/parameters/cursorAt'
        - $ref: '#/components/parameters/cursorId'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VendorsList' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/vendors/{id}:
    get:
      tags: [vendors]
      summary: Get one vendor by UUID
      description: |
        `vendors.vendors` rows have no slug column, so the path uses UUID.
        See `vendors.id` returned by `GET /v1/vendors`.
      parameters:
        - $ref: '#/components/parameters/pathId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VendorDetail' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/clients:
    get:
      tags: [clients]
      summary: List CRM clients
      parameters:
        - $ref: '#/components/parameters/cursorAt'
        - $ref: '#/components/parameters/cursorId'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClientsList' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/products:
    get:
      tags: [products]
      summary: List published shop products owned by org-linked sellers
      description: |
        `shop.products` is platform-scope; we expose only published products
        whose vendor has a `shop.vendor_users` row mapped to a member of the
        calling org. Orgs without a marketplace seller relationship will see
        an empty list.
      parameters:
        - $ref: '#/components/parameters/cursorAt'
        - $ref: '#/components/parameters/cursorId'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductsList' }

  /v1/products/{id}:
    get:
      tags: [products]
      summary: Get one product
      parameters:
        - $ref: '#/components/parameters/pathId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductDetail' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/webhooks/test/{endpoint_id}:
    post:
      tags: [webhooks]
      summary: Fire a synthetic webhook.test event to a specific endpoint
      description: |
        Requires `read_write` scope. Inserts a `webhook.test` delivery row;
        the `dispatch-webhook` cron will POST it within a minute.
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Delivery enqueued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookTestResponse' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
