{
  "openapi": "3.1.0",
  "info": {
    "title": "queue_arr API",
    "version": "1.0.0",
    "description": "Render animated QR codes as self-contained SVG. Every frame is a valid encoding of the same payload. Responses are valid image/svg+xml so the URL embeds directly as an HTML <img src=\"...\"> or Markdown ![](...) image.",
    "contact": {
      "name": "queue_arr",
      "url": "https://github.com/havenwood/queue_arr"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://queue-arr.shannonskipper.com"
    }
  ],
  "tags": [
    {
      "name": "Render",
      "description": "Render an animated QR code from a payload and preset."
    },
    {
      "name": "Discovery",
      "description": "API metadata for humans and machines."
    },
    {
      "name": "Monitoring",
      "description": "Health and liveness signals."
    }
  ],
  "paths": {
    "/api/v1/render": {
      "get": {
        "summary": "Render an animated QR code (GET)",
        "description": "Renders an animated SVG QR code from a payload and an optional preset. Idempotent and cache-friendly.",
        "operationId": "renderGet",
        "tags": [
          "Render"
        ],
        "parameters": [
          {
            "name": "payload",
            "in": "query",
            "required": true,
            "description": "Text or URL to encode",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "preset",
            "in": "query",
            "required": false,
            "description": "Named animation preset (default: bloom)",
            "schema": {
              "type": "string",
              "enum": [
                "bloom",
                "pulse",
                "chroma",
                "disco",
                "comet",
                "strobe",
                "snake",
                "aurora",
                "mosaic",
                "garden",
                "duotone",
                "ripple",
                "breathe",
                "starlight",
                "tide"
              ],
              "default": "bloom"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Animated SVG QR code",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (the ETag matched the If-None-Match request header)",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "missingPayload": {
                    "summary": "Missing payload",
                    "value": {
                      "type": "/api/v1/errors/missing-payload",
                      "title": "Payload Required",
                      "status": 400,
                      "detail": "The payload query parameter is required."
                    }
                  },
                  "unknownPreset": {
                    "summary": "Unknown preset",
                    "value": {
                      "type": "/api/v1/errors/unknown-preset",
                      "title": "Unknown Preset",
                      "status": 400,
                      "detail": "The requested preset is not recognized."
                    }
                  },
                  "payloadTooLarge": {
                    "summary": "Payload exceeds QR capacity",
                    "value": {
                      "type": "/api/v1/errors/payload-too-large",
                      "title": "Payload Too Large",
                      "status": 400,
                      "detail": "The payload does not fit the QR capacity."
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Render failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Render an animated QR code (POST)",
        "description": "Same as GET /render but accepts the payload and preset in a JSON body so the payload can carry characters not URL-safe.",
        "operationId": "renderPost",
        "tags": [
          "Render"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Animated SVG QR code",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Render failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "summary": "Health check",
        "description": "Returns the literal string \"ok\\n\" when the worker is reachable.",
        "operationId": "health",
        "tags": [
          "Monitoring"
        ],
        "responses": {
          "200": {
            "description": "Service is up",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "ok\n"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/": {
      "get": {
        "summary": "API index",
        "description": "Developer-facing HTML landing page that lists endpoints and shows curl examples.",
        "operationId": "index",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "HTML landing page listing available endpoints",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "summary": "OpenAPI 3.1 specification",
        "description": "Machine-readable specification of this API in OpenAPI 3.1 JSON format.",
        "operationId": "openapi",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "Machine-readable API spec",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/harvest": {
      "get": {
        "summary": "Harvest brand colors and logo from a URL",
        "description": "Fetches the given page URL, parses brand metadata (theme-color, manifest, icons, og:image), downloads the best logo image, and returns the result as JSON. Suitable for seeding a branded QR palette without client-side cross-origin fetches.",
        "operationId": "harvest",
        "tags": [
          "Discovery"
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Public https/http URL to harvest (ports 80 and 443 only). A scheme-less host like \"www.google.com\" is treated as https.",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Brand metadata and logo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HarvestResult"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (the ETag matched the If-None-Match request header)",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or blocked URL",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "invalidUrl": {
                    "summary": "Unparseable or non-http(s) URL",
                    "value": {
                      "type": "/api/v1/errors/invalid-url",
                      "title": "Invalid URL",
                      "status": 400,
                      "detail": "The url parameter is missing or not a valid http/https URL."
                    }
                  },
                  "blockedHost": {
                    "summary": "Private or internal host",
                    "value": {
                      "type": "/api/v1/errors/blocked-host",
                      "title": "Blocked Host",
                      "status": 400,
                      "detail": "The URL resolves to a private or reserved address."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (best-effort ~30 requests per 60s window per client IP)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Upstream fetch failed",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "RenderRequest": {
        "type": "object",
        "required": [
          "payload"
        ],
        "properties": {
          "payload": {
            "type": "string",
            "description": "Text or URL to encode"
          },
          "preset": {
            "type": "string",
            "enum": [
              "bloom",
              "pulse",
              "chroma",
              "disco",
              "comet",
              "strobe",
              "snake",
              "aurora",
              "mosaic",
              "garden",
              "duotone",
              "ripple",
              "breathe",
              "starlight",
              "tide"
            ],
            "default": "bloom",
            "description": "Named animation preset"
          }
        }
      },
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri-reference",
            "description": "URI identifying the problem type"
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code"
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation of this occurrence"
          }
        }
      },
      "HarvestResult": {
        "type": "object",
        "required": [
          "source",
          "colors"
        ],
        "properties": {
          "source": {
            "type": "string",
            "format": "uri",
            "description": "Final URL after redirects"
          },
          "colors": {
            "type": "object",
            "required": [
              "dark",
              "light",
              "accent"
            ],
            "properties": {
              "dark": {
                "type": "string",
                "description": "Suggested dark color (#RRGGBB or fallback)"
              },
              "light": {
                "type": "string",
                "description": "Suggested light color (#RRGGBB or fallback)"
              },
              "accent": {
                "type": "string",
                "description": "Suggested accent color (#RRGGBB or fallback)"
              },
              "themeColor": {
                "type": "string",
                "description": "Raw theme-color meta value if found"
              },
              "manifest": {
                "type": "object",
                "description": "Parsed web app manifest color fields if a manifest was found",
                "properties": {
                  "theme_color": {
                    "type": "string"
                  },
                  "background_color": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "logo": {
            "type": "object",
            "description": "Best logo image found (omitted when none found)",
            "required": [
              "mediaType",
              "dataBase64"
            ],
            "properties": {
              "mediaType": {
                "type": "string",
                "description": "MIME type of the image"
              },
              "dataBase64": {
                "type": "string",
                "description": "Base64-encoded image bytes"
              }
            }
          },
          "logoAlt": {
            "type": "object",
            "description": "Square-source alternate logo (favicon/apple-touch) when the primary is wide; omitted otherwise",
            "required": [
              "mediaType",
              "dataBase64"
            ],
            "properties": {
              "mediaType": {
                "type": "string",
                "description": "MIME type of the image"
              },
              "dataBase64": {
                "type": "string",
                "description": "Base64-encoded image bytes"
              }
            }
          }
        }
      }
    }
  }
}