openapi: 3.1.0
info:
  title: Mufmuf Compatibility API
  version: "1.0.0"
  description: >
    Public, agent-friendly visual compatibility API. Send two images and get a
    1-10 compatibility score with sub-scores and a short explanation in a single
    JSON request. AI-generated and directional only, not a factual judgment.
    Photos are processed for the comparison and not retained by this endpoint.
  contact:
    name: Mufmuf
    url: https://mufmuf.app
  license:
    name: Proprietary
servers:
  - url: https://mufmuf.app
    description: Production
security:
  - apiKey: []
paths:
  /api/v1/compare:
    post:
      operationId: compareCompatibility
      summary: Score how visually compatible two photos are
      description: >
        Returns a compatibility score (1-10), sub-scores (style, energy,
        presentation, aesthetic harmony), and a short explanation for two
        images. Each image is sent inline as a base64 string or data URI
        (jpeg, png, or webp). No cookies, browser, or multi-step upload.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompareRequest"
            examples:
              dataUris:
                summary: Two data URIs
                value:
                  userImage: "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
                  targetImage: "data:image/png;base64,iVBORw0KGgo..."
              bareBase64:
                summary: Bare base64 with an explicit type
                value:
                  userImage: "/9j/4AAQSkZJRg..."
                  targetImage: "/9j/4AAQSkZJRg..."
                  imageType: "image/jpeg"
      responses:
        "200":
          description: Comparison result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CompareResult"
              example:
                score: 8
                subScores:
                  style: 8
                  energy: 7
                  presentation: 8
                  aestheticHarmony: 8
                summary: "Strong visual harmony with complementary styles."
                disclaimer: "AI-generated and directional only, not a factual judgment."
        "400":
          description: Invalid request (bad JSON, missing/unsupported images, or an image too large)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidRequest:
                  value: { error: "invalid_request" }
                imageTooLarge:
                  value: { error: "image_too_large" }
                invalidImage:
                  value: { error: "invalid_image" }
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "unauthorized" }
        "413":
          description: Request body exceeds the size ceiling (24 MB)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "payload_too_large" }
        "422":
          description: The image content was filtered
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "content_filtered" }
        "429":
          description: Per-key daily request cap reached, or the model is briefly rate-limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RateLimitError" }
              example: { error: "rate_limited", limit: 100, resetsAt: "2026-06-26T00:00:00.000Z" }
        "503":
          description: Service temporarily unavailable (not configured or a transient backend error)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "unavailable" }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >
        An API key minted at https://mufmuf.app/settings/api-keys, sent as
        `Authorization: Bearer mk_live_...`. Each key carries a per-key daily
        request limit.
  schemas:
    CompareRequest:
      type: object
      required: [userImage, targetImage]
      properties:
        userImage:
          type: string
          description: First photo as a data URI or bare base64 (jpeg/png/webp). Max 8 MB decoded.
        targetImage:
          type: string
          description: Second photo as a data URI or bare base64 (jpeg/png/webp). Max 8 MB decoded.
        imageType:
          type: string
          enum: [image/jpeg, image/png, image/webp]
          description: Required only when the images are bare base64 (not data URIs).
    CompareResult:
      type: object
      required: [score, subScores, summary, disclaimer]
      properties:
        score:
          type: number
          minimum: 1
          maximum: 10
          description: Overall visual compatibility, 1-10.
        subScores:
          type: object
          properties:
            style: { type: number, minimum: 1, maximum: 10 }
            energy: { type: number, minimum: 1, maximum: 10 }
            presentation: { type: number, minimum: 1, maximum: 10 }
            aestheticHarmony: { type: number, minimum: 1, maximum: 10 }
        summary:
          type: string
          description: A short plain-text explanation of the score.
        disclaimer:
          type: string
          description: Fixed notice that the result is AI-generated and directional only.
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        detail: { type: string }
    RateLimitError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            limit: { type: integer, description: The per-key daily request cap. }
            resetsAt:
              type: string
              format: date-time
              description: When the daily counter resets (UTC).
