{
  "openapi": "3.1.0",
  "info": {
    "title": "BananaSplit QR Code API",
    "version": "1.6.0",
    "description": "Public BananaSplit API for private-by-design, stateless QR code generation. Agents that can only fetch URLs should use GET with format=json and Accept: application/json, then show the QR in chat (decode png_base64, attach svg, or emit markdown which points at the dedicated PNG URL). format=json always returns JSON. The PNG asset is GET /api/qr-code.png. Do not fetch image/png from an agent fetcher that rejects that type. Default logo=none; pass logo=banana for BananaSplit branding. Use POST JSON for Wi-Fi, vCard, email and SMS. No authentication is required. GET /healthz returns {\"ok\":true}.",
    "contact": {
      "name": "BananaSplit",
      "url": "https://bananasplit.net/contact/",
      "email": "hello@bananasplit.net"
    },
    "license": {
      "name": "Proprietary \u2014 all rights reserved",
      "url": "https://bananasplit.net/en/terms/"
    }
  },
  "security": [],
  "servers": [
    {
      "url": "https://bananasplit.net",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "QR codes",
      "description": "Private-by-design, stateless QR code generation for agents and workflows."
    }
  ],
  "paths": {
    "/api/qr-code": {
      "get": {
        "operationId": "createQrCode",
        "summary": "Create a print-ready QR code with GET",
        "description": "Returns a deterministic QR code in one request. Fetch-only agents should use format=json with Accept: application/json. format=json always returns JSON, including when Accept contains text/html. Then render the QR: decode png_base64, write svg to a file, or emit markdown (it points at /api/qr-code.png, which returns image/png only). Canonical encoded example: https://bananasplit.net/api/qr-code?type=url&url=https%3A%2F%2Fferlar.pt&format=json . If the client cannot retrieve the response, give the user https://bananasplit.net/api/qr-code.png?type=url&url=... . Do not silently substitute a locally generated QR code.",
        "tags": [
          "QR codes"
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Public structured payload type. Infer url from url= and text from text= when omitted.",
            "schema": {
              "type": "string",
              "enum": [
                "url",
                "text"
              ]
            },
            "example": "url"
          },
          {
            "name": "url",
            "in": "query",
            "description": "Website to encode when type=url. HTTPS is added when the value has no scheme. Percent-encode the value, e.g. url=https%3A%2F%2Fferlar.pt.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "https://ferlar.pt"
          },
          {
            "name": "text",
            "in": "query",
            "description": "Plain text to encode when type=text.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048
            }
          },
          {
            "name": "data",
            "in": "query",
            "description": "Raw value to encode. When present, it takes precedence over type-specific fields.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048
            },
            "example": "https://bananasplit.net"
          },
          {
            "name": "color",
            "in": "query",
            "description": "Six-digit foreground hex colour, with or without #.",
            "schema": {
              "type": "string",
              "pattern": "^#?[0-9A-Fa-f]{6}$",
              "default": "7B260F"
            }
          },
          {
            "name": "background",
            "in": "query",
            "description": "Six-digit background hex colour, with or without #.",
            "schema": {
              "type": "string",
              "pattern": "^#?[0-9A-Fa-f]{6}$",
              "default": "FFFFFF"
            }
          },
          {
            "name": "logo",
            "in": "query",
            "description": "No centre logo by default. Pass banana for the BananaSplit mark.",
            "schema": {
              "type": "string",
              "enum": [
                "banana",
                "none"
              ],
              "default": "none"
            }
          },
          {
            "name": "corners",
            "in": "query",
            "description": "Outer Image corner style.",
            "schema": {
              "type": "string",
              "enum": [
                "square",
                "rounded"
              ],
              "default": "square"
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Image width and height in pixels.",
            "schema": {
              "type": "integer",
              "minimum": 128,
              "maximum": 2048,
              "default": 1024
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "json for fetch-only agents, svg for print, png for binary preview.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "svg",
                "png"
              ],
              "default": "svg"
            }
          },
          {
            "name": "download",
            "in": "query",
            "description": "Set to 1 to return an attachment filename instead of inline.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ],
              "default": "0"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A stateless QR code as JSON, SVG, or PNG.",
            "headers": {
              "X-QR-Payload-Type": {
                "schema": {
                  "type": "string"
                }
              },
              "X-QR-Payload": {
                "schema": {
                  "type": "string"
                },
                "description": "Encoded public URL or text. Omitted for secrets."
              },
              "X-QR-Logo": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "banana",
                    "none"
                  ]
                }
              },
              "X-QR-Size": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-QR-Format": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "json",
                    "svg",
                    "png"
                  ]
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/svg+xml"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrJsonResponse"
                }
              }
            }
          },
          "400": {
            "description": "A required parameter is missing or invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The value exceeds 2048 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The value could not be represented as a QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "The caller exceeded 60 generated codes per minute at the serving edge location.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createStructuredQrCode",
        "summary": "Create a QR code from structured JSON",
        "description": "Agent-friendly JSON operation that returns a deterministic SVG. Pass raw data or choose URL, Wi-Fi, vCard, email, SMS, or text and provide the corresponding fields. BananaSplit builds and escapes the standards-compatible payload, processes it in memory, and does not store it.",
        "tags": [
          "QR codes"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QrCodeRequest"
              },
              "examples": {
                "url": {
                  "summary": "Print-ready QR for a URL",
                  "value": {
                    "type": "url",
                    "url": "https://bananasplit.net",
                    "color": "1288C8",
                    "background": "FFF9E8",
                    "logo": "banana",
                    "corners": "rounded",
                    "size": 1024
                  }
                },
                "wifi": {
                  "summary": "Hidden WPA Wi-Fi network",
                  "value": {
                    "type": "wifi",
                    "ssid": "Banana Guest",
                    "password": "split-with-friends",
                    "security": "WPA",
                    "hidden": true,
                    "logo": "none"
                  }
                },
                "vcard": {
                  "summary": "Contact card",
                  "value": {
                    "type": "vcard",
                    "name": "Ana Split",
                    "organization": "BananaSplit",
                    "phone": "+351910000000",
                    "email": "ana@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A stateless QR code as JSON, SVG, or PNG.",
            "headers": {
              "X-QR-Payload-Type": {
                "schema": {
                  "type": "string"
                }
              },
              "X-QR-Payload": {
                "schema": {
                  "type": "string"
                },
                "description": "Encoded public URL or text. Omitted for secrets."
              },
              "X-QR-Logo": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "banana",
                    "none"
                  ]
                }
              },
              "X-QR-Size": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-QR-Format": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "json",
                    "svg",
                    "png"
                  ]
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/svg+xml"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrJsonResponse"
                }
              }
            }
          },
          "400": {
            "description": "The JSON body or selected payload is invalid or incomplete.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The generated payload exceeds 2048 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "The request does not use application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The value could not be represented as a QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "The caller exceeded 60 generated codes per minute at the serving edge location.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/qr-code/url": {
      "get": {
        "operationId": "createUrlQrCode",
        "summary": "Create a URL QR code with GET",
        "description": "Shortcut for environments that can only fetch URLs. Equivalent to GET /api/qr-code?type=url&url=... Canonical encoded example: https://bananasplit.net/api/qr-code/url?url=https%3A%2F%2Fferlar.pt&logo=none&format=png . Set logo=none for third-party destinations.",
        "tags": [
          "QR codes"
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Website to encode. HTTPS is added when the value has no scheme. Percent-encode the value, e.g. url=https%3A%2F%2Fferlar.pt.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "https://ferlar.pt"
          },
          {
            "name": "color",
            "in": "query",
            "description": "Six-digit foreground hex colour, with or without #.",
            "schema": {
              "type": "string",
              "pattern": "^#?[0-9A-Fa-f]{6}$",
              "default": "7B260F"
            }
          },
          {
            "name": "background",
            "in": "query",
            "description": "Six-digit background hex colour, with or without #.",
            "schema": {
              "type": "string",
              "pattern": "^#?[0-9A-Fa-f]{6}$",
              "default": "FFFFFF"
            }
          },
          {
            "name": "logo",
            "in": "query",
            "description": "No centre logo by default. Pass banana for the BananaSplit mark.",
            "schema": {
              "type": "string",
              "enum": [
                "banana",
                "none"
              ],
              "default": "none"
            }
          },
          {
            "name": "corners",
            "in": "query",
            "description": "Outer SVG corner style.",
            "schema": {
              "type": "string",
              "enum": [
                "square",
                "rounded"
              ],
              "default": "square"
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "SVG width and height in pixels.",
            "schema": {
              "type": "integer",
              "minimum": 128,
              "maximum": 2048,
              "default": 1024
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "json for fetch-only agents, svg for print, png for binary preview.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "svg",
                "png"
              ],
              "default": "svg"
            }
          },
          {
            "name": "download",
            "in": "query",
            "description": "Set to 1 to return an attachment filename instead of inline.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ],
              "default": "0"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A stateless QR code as JSON, SVG, or PNG.",
            "headers": {
              "X-QR-Payload-Type": {
                "schema": {
                  "type": "string"
                }
              },
              "X-QR-Payload": {
                "schema": {
                  "type": "string"
                },
                "description": "Encoded public URL or text. Omitted for secrets."
              },
              "X-QR-Logo": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "banana",
                    "none"
                  ]
                }
              },
              "X-QR-Size": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-QR-Format": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "json",
                    "svg",
                    "png"
                  ]
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/svg+xml"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrJsonResponse"
                }
              }
            }
          },
          "400": {
            "description": "A required parameter is missing or invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The value exceeds 2048 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The value could not be represented as a QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "The caller exceeded 60 generated codes per minute at the serving edge location.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/qr-code.png": {
      "get": {
        "operationId": "getQrCodePng",
        "summary": "Return a PNG QR code",
        "description": "Same query parameters as GET /api/qr-code. Always returns image/png. This is image_url. It is not a stored id; the PNG is generated from the query string.",
        "tags": ["QR codes"],
        "responses": {
          "200": {
            "description": "PNG QR code.",
            "content": {
              "image/png": {
                "schema": { "type": "string", "format": "binary" }
              }
            }
          }
        }
      }
    },
    "/api/qr-code.svg": {
      "get": {
        "operationId": "getQrCodeSvg",
        "summary": "Return an SVG QR code",
        "description": "Same query parameters as GET /api/qr-code. Always returns image/svg+xml.",
        "tags": ["QR codes"],
        "responses": {
          "200": {
            "description": "SVG QR code.",
            "content": {
              "image/svg+xml": {
                "schema": { "type": "string", "contentMediaType": "image/svg+xml" }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "healthz",
        "summary": "Liveness check",
        "description": "Unauthenticated JSON used to distinguish API errors from DNS, TLS, or egress failures. Also available at GET /api/qr-code/healthz.",
        "tags": ["QR codes"],
        "responses": {
          "200": {
            "description": "Service reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean", "const": true }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "QrCodeRequest": {
        "type": "object",
        "description": "A raw or structured QR request. Prefer a structured type when an agent has separate fields, especially for Wi-Fi and vCard, so BananaSplit can escape the payload correctly.",
        "anyOf": [
          {
            "required": [
              "data"
            ]
          },
          {
            "required": [
              "type"
            ]
          }
        ],
        "properties": {
          "data": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "Raw value to encode. When present, it takes precedence over type-specific fields."
          },
          "type": {
            "type": "string",
            "enum": [
              "url",
              "wifi",
              "vcard",
              "email",
              "sms",
              "text"
            ],
            "description": "Structured payload type."
          },
          "url": {
            "type": "string",
            "description": "Required for type=url. HTTPS is added when the value has no scheme."
          },
          "text": {
            "type": "string",
            "description": "Required for type=text."
          },
          "ssid": {
            "type": "string",
            "description": "Required network name for type=wifi."
          },
          "password": {
            "type": "string",
            "description": "Wi-Fi password. Ignored when security=nopass."
          },
          "security": {
            "type": "string",
            "enum": [
              "WPA",
              "WEP",
              "nopass"
            ],
            "default": "WPA",
            "description": "Use WPA for WPA, WPA2, and WPA3 networks."
          },
          "hidden": {
            "type": "boolean",
            "default": false,
            "description": "Whether the Wi-Fi SSID is hidden."
          },
          "name": {
            "type": "string",
            "description": "Required display name for type=vcard."
          },
          "organization": {
            "type": "string",
            "description": "Optional vCard organisation."
          },
          "jobTitle": {
            "type": "string",
            "description": "Optional vCard job title."
          },
          "phone": {
            "type": "string",
            "description": "vCard phone, or required destination for type=sms."
          },
          "email": {
            "type": "string",
            "description": "Optional vCard email address."
          },
          "website": {
            "type": "string",
            "description": "Optional vCard website."
          },
          "to": {
            "type": "string",
            "description": "Required recipient for type=email."
          },
          "subject": {
            "type": "string",
            "description": "Optional email subject."
          },
          "message": {
            "type": "string",
            "description": "Optional email or SMS message."
          },
          "color": {
            "type": "string",
            "pattern": "^#?[0-9A-Fa-f]{6}$",
            "default": "7B260F",
            "description": "Foreground hex colour."
          },
          "background": {
            "type": "string",
            "pattern": "^#?[0-9A-Fa-f]{6}$",
            "default": "FFFFFF",
            "description": "Background hex colour."
          },
          "logo": {
            "type": "string",
            "enum": [
              "banana",
              "none"
            ],
            "default": "none",
            "description": "No centre logo by default. Pass banana for the BananaSplit mark. Arbitrary uploaded logos and remote logo URLs are not supported."
          },
          "corners": {
            "type": "string",
            "enum": [
              "square",
              "rounded"
            ],
            "default": "square"
          },
          "size": {
            "type": "integer",
            "minimum": 128,
            "maximum": 2048,
            "default": 1024
          },
          "download": {
            "type": "boolean",
            "default": false,
            "description": "Return an SVG attachment filename."
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "svg",
              "png"
            ],
            "default": "svg",
            "description": "json for fetch-only agents, svg for print, png for binary preview."
          }
        },
        "additionalProperties": false
      },
      "ApiError": {
        "type": "object",
        "description": "A machine-readable API error with a stable code and recovery hint.",
        "required": [
          "error",
          "code",
          "message",
          "hint"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Backward-compatible human-readable error text."
          },
          "code": {
            "type": "string",
            "description": "Stable snake_case error identifier."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of the error."
          },
          "hint": {
            "type": "string",
            "description": "A concrete next step for resolving the error."
          }
        },
        "additionalProperties": false
      },
      "QrJsonResponse": {
        "type": "object",
        "required": [
          "ok",
          "type",
          "payload_type",
          "payload",
          "format",
          "mime_type",
          "size",
          "svg",
          "png_base64",
          "png_data_url",
          "image_url",
          "svg_url",
          "markdown"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "type": {
            "type": "string",
            "description": "Same as payload_type."
          },
          "payload_type": {
            "type": "string",
            "examples": [
              "url",
              "text",
              "wifi"
            ]
          },
          "payload": {
            "type": [
              "string",
              "null"
            ],
            "description": "Encoded public URL or text. Null for secrets."
          },
          "format": {
            "type": "string",
            "const": "json"
          },
          "mime_type": {
            "type": "string",
            "const": "image/png",
            "description": "MIME type of image_url and png_base64."
          },
          "size": {
            "type": "integer"
          },
          "logo": {
            "type": "string",
            "enum": [
              "banana",
              "none"
            ]
          },
          "color": {
            "type": "string"
          },
          "background": {
            "type": "string"
          },
          "svg": {
            "type": "string",
            "description": "Full SVG markup. Write this to a file and attach it when the chat cannot render markdown images."
          },
          "png_base64": {
            "type": "string",
            "description": "PNG bytes as standard Base64, without a data: prefix. Decode and attach this to show the QR in chat."
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Dedicated PNG URL (/api/qr-code.png?...) for public payloads. Always image/png. Never the JSON endpoint. Null for secrets."
          },
          "svg_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Dedicated SVG URL (/api/qr-code.svg?...) for public payloads. Always image/svg+xml. Null for secrets."
          },
          "png_data_url": {
            "type": "string",
            "description": "Same PNG as a data:image/png;base64,... URL. Many chat clients strip data URIs."
          },
          "markdown": {
            "type": "string",
            "description": "Markdown image pointing at image_url for public payloads, or a data URI for secrets. Not all chat clients render markdown images."
          }
        }
      }
    }
  }
}
