{
  "openapi": "3.1.0",
  "info": {
    "title": "YumScan API",
    "version": "1.0.0",
    "description": "Send a menu photo, get back every dish with a name, English description, ingredient list, and a representative image URL.\n\nThe server (1) sends the image to Claude Haiku to extract items, (2) runs a Google image search per item via Serper, (3) downloads the first usable image into R2, and (4) returns the public URLs.",
    "contact": {
      "name": "YumScan",
      "url": "https://getyumscan.com"
    }
  },
  "servers": [
    {
      "url": "https://getyumscan.com",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/scan": {
      "post": {
        "summary": "Scan a menu image",
        "description": "POST raw image bytes in the request body. Set Content-Type to the actual MIME type of the image (image/jpeg, image/png, image/webp, or image/gif). Any other value is coerced to image/jpeg.",
        "operationId": "scanMenu",
        "requestBody": {
          "required": true,
          "description": "Raw image bytes — not multipart, not base64.",
          "content": {
            "image/jpeg": {
              "schema": { "type": "string", "format": "binary" }
            },
            "image/png": {
              "schema": { "type": "string", "format": "binary" }
            },
            "image/webp": {
              "schema": { "type": "string", "format": "binary" }
            },
            "image/gif": {
              "schema": { "type": "string", "format": "binary" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Menu parsed successfully.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanResponse" },
                "examples": {
                  "twoItems": {
                    "summary": "Greek mezze menu (excerpt)",
                    "value": {
                      "items": [
                        {
                          "name": "Tzatziki",
                          "description": "Cool Greek yogurt dip with cucumber, garlic, and dill.",
                          "ingredients": ["yogurt", "cucumber", "garlic", "dill", "olive oil"],
                          "imageUrl": "https://images.getyumscan.com/dish/3a1f...c9"
                        },
                        {
                          "name": "Pantzaria",
                          "description": "Tender marinated baby beets served with garlic and potato spread.",
                          "ingredients": ["baby beets", "skordalia", "vinegar"],
                          "imageUrl": null,
                          "imageError": "all 5 candidate(s) failed: 403 Forbidden | not an image (text/html) | 404 Not Found | threw: TypeError | 403 Forbidden"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty request body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Empty body — send the image bytes" }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method (only POST is accepted).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "POST an image to this endpoint" }
              }
            }
          },
          "502": {
            "description": "Upstream menu-analysis call failed (typically an Anthropic API error).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": "Menu analysis failed",
                  "detail": "Anthropic 529: overloaded_error"
                }
              }
            }
          }
        }
      },
      "options": {
        "summary": "CORS preflight",
        "description": "Returns CORS headers permitting POST from any origin with Content-Type.",
        "operationId": "scanMenuPreflight",
        "responses": {
          "200": {
            "description": "CORS headers.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": { "type": "string", "example": "*" }
              },
              "Access-Control-Allow-Methods": {
                "schema": { "type": "string", "example": "POST, OPTIONS" }
              },
              "Access-Control-Allow-Headers": {
                "schema": { "type": "string", "example": "Content-Type" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ScanResponse": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "description": "One entry per distinct food or drink item detected on the menu. Order matches the model's reading order; expect 0–50 items for a typical menu page.",
            "items": { "$ref": "#/components/schemas/MenuItem" }
          }
        }
      },
      "MenuItem": {
        "type": "object",
        "required": ["name", "description", "ingredients", "imageUrl"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Dish name in English. Translated from the source language if necessary.",
            "example": "Spaghetti Carbonara"
          },
          "description": {
            "type": "string",
            "description": "One appetizing English sentence describing the dish.",
            "example": "Roman pasta with cured pork, eggs, pecorino, and black pepper."
          },
          "ingredients": {
            "type": "array",
            "description": "Main ingredients in English.",
            "items": { "type": "string" },
            "example": ["spaghetti", "guanciale", "egg yolk", "pecorino romano", "black pepper"]
          },
          "imageUrl": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "Public URL of a representative image stored in R2. `null` if every candidate image returned by the search failed to download (see `imageError`).",
            "example": "https://images.getyumscan.com/dish/3a1f...c9"
          },
          "imageError": {
            "type": "string",
            "description": "Present only when `imageUrl` is `null`. Human-readable explanation of why no image could be stored (e.g. 4xx/5xx from upstream CDN, non-image Content-Type, network error, or Serper returning no results).",
            "example": "all 5 candidate(s) failed: 403 Forbidden | not an image (text/html) | 404 Not Found | threw: TypeError | 403 Forbidden"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Short human-readable error message."
          },
          "detail": {
            "type": "string",
            "description": "Optional extra context (upstream status, exception text)."
          }
        }
      }
    }
  }
}
