{
  "openapi": "3.1.0",
  "info": {
    "title": "JustQRs API",
    "version": "1.0.0",
    "description": "Create, update and track QR codes. Dynamic codes encode a short URL whose destination can change after printing and record scan analytics; static codes encode the content directly and never change. The /v1/qrcodes endpoints need an API key on a paid JustQRs plan (or an active trial) — create one at https://justqrs.com/app/keys. /v1/render needs no key: it renders static QR images for free (no logos, 30 per minute per IP). The response's `payload` is the exact string to encode in the QR image. MCP clients can use the same API as tools at https://api.justqrs.com/mcp (OAuth sign-in or API key) or https://api.justqrs.com/mcp/free (no account, free images only).",
    "contact": {
      "name": "JustQRs",
      "url": "https://justqrs.com"
    }
  },
  "externalDocs": {
    "url": "https://justqrs.com/docs"
  },
  "servers": [
    {
      "url": "https://api.justqrs.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/qrcodes": {
      "get": {
        "operationId": "listQrCodes",
        "summary": "List your QR codes",
        "responses": {
          "200": {
            "description": "All QR codes on the account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createQrCode",
        "summary": "Create a QR code",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateQrRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed; the message names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Dynamic QR code limit reached (500 on the base plan; add-on packs raise it). Code \"qr_limit_reached\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/qrcodes/{qrId}": {
      "parameters": [
        {
          "name": "qrId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getQrCode",
        "summary": "Get one QR code",
        "responses": {
          "200": {
            "description": "The QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateQrCode",
        "summary": "Update a QR code",
        "description": "Changing `content` on a dynamic code changes where every printed copy goes. A static code's printed image encodes its content directly, so only reprinting changes it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateQrRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, or nothing to update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteQrCode",
        "summary": "Delete a QR code",
        "description": "Printed copies of a deleted dynamic code stop working.",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/qrcodes/{qrId}/stats": {
      "parameters": [
        {
          "name": "qrId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getQrCodeStats",
        "summary": "Scan analytics for a QR code",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "7",
                "30",
                "90"
              ],
              "default": "30"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scan totals, daily series and breakdowns.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrStats"
                }
              }
            }
          },
          "400": {
            "description": "days must be 7, 30, or 90.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/qrcodes/{qrId}/image": {
      "parameters": [
        {
          "name": "qrId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getQrCodeImage",
        "summary": "Render a QR code as an image",
        "description": "The stored code's payload rendered with its design, served inline.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "png",
                "svg",
                "pdf"
              ],
              "default": "png"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 64,
              "maximum": 4096,
              "description": "px; defaults to design.size."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Unknown format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Account is on the free plan — API access requires a paid plan (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key invalid, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/render": {
      "post": {
        "operationId": "renderQrCode",
        "summary": "Render a static QR code image (free, no API key)",
        "description": "Static content only (landing-page kinds need a dynamic code), no logos, PNG or SVG up to 2048px. Nothing is stored. Limited to 30 requests per minute per IP (MCP: 120).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The image, served inline.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (including landing-page kinds and pdf).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Logos need an uploaded image, which requires an account (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached; retry after the Retry-After seconds (code \"rate_limited\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "renderQrCodeFromLink",
        "summary": "Render a static QR code image from a link (free, no API key)",
        "description": "Static content only (landing-page kinds need a dynamic code), no logos, PNG or SVG up to 2048px. Nothing is stored. Limited to 30 requests per minute per IP (MCP: 120). The GET form takes the request as base64url JSON in `c`, so the URL itself is a shareable image link.",
        "security": [],
        "parameters": [
          {
            "name": "c",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "base64url-encoded JSON {\"content\": StaticQrContent, \"design\"?: QrDesign}."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "png",
                "svg"
              ],
              "default": "png"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 64,
              "maximum": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image, served inline.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (including landing-page kinds and pdf).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Logos need an uploaded image, which requires an account (code \"upgrade_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached; retry after the Retry-After seconds (code \"rate_limited\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from the dashboard, e.g. `Authorization: Bearer jqr_…`."
      }
    },
    "schemas": {
      "QrMode": {
        "type": "string",
        "enum": [
          "dynamic",
          "static"
        ],
        "description": "dynamic: encodes a short URL; destination editable later; scans tracked; counts toward the plan's dynamic limit. static: encodes the content itself; never changes; not tracked."
      },
      "QrContent": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/UrlContent"
          },
          {
            "$ref": "#/components/schemas/TextContent"
          },
          {
            "$ref": "#/components/schemas/PhoneContent"
          },
          {
            "$ref": "#/components/schemas/SmsContent"
          },
          {
            "$ref": "#/components/schemas/EmailContent"
          },
          {
            "$ref": "#/components/schemas/WifiContent"
          },
          {
            "$ref": "#/components/schemas/VCardContent"
          },
          {
            "$ref": "#/components/schemas/BioContent"
          },
          {
            "$ref": "#/components/schemas/BusinessContent"
          },
          {
            "$ref": "#/components/schemas/PdfContent"
          },
          {
            "$ref": "#/components/schemas/AppContent"
          }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "url": "#/components/schemas/UrlContent",
            "text": "#/components/schemas/TextContent",
            "phone": "#/components/schemas/PhoneContent",
            "sms": "#/components/schemas/SmsContent",
            "email": "#/components/schemas/EmailContent",
            "wifi": "#/components/schemas/WifiContent",
            "vcard": "#/components/schemas/VCardContent",
            "bio": "#/components/schemas/BioContent",
            "business": "#/components/schemas/BusinessContent",
            "pdf": "#/components/schemas/PdfContent",
            "app": "#/components/schemas/AppContent"
          }
        }
      },
      "StaticQrContent": {
        "description": "The kinds a static code can encode (no hosted landing pages).",
        "oneOf": [
          {
            "$ref": "#/components/schemas/UrlContent"
          },
          {
            "$ref": "#/components/schemas/TextContent"
          },
          {
            "$ref": "#/components/schemas/PhoneContent"
          },
          {
            "$ref": "#/components/schemas/SmsContent"
          },
          {
            "$ref": "#/components/schemas/EmailContent"
          },
          {
            "$ref": "#/components/schemas/WifiContent"
          },
          {
            "$ref": "#/components/schemas/VCardContent"
          }
        ],
        "discriminator": {
          "propertyName": "kind"
        }
      },
      "UrlContent": {
        "type": "object",
        "description": "Opens a web page.",
        "required": [
          "kind",
          "url"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "url"
          },
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "format": "uri",
            "description": "http:// or https:// URL."
          }
        },
        "example": {
          "kind": "url",
          "url": "https://example.com"
        }
      },
      "TextContent": {
        "type": "object",
        "description": "Shows plain text.",
        "required": [
          "kind",
          "text"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "text"
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          }
        },
        "example": {
          "kind": "text",
          "text": "Table 12 — ask for the specials"
        }
      },
      "PhoneContent": {
        "type": "object",
        "description": "Starts a phone call.",
        "required": [
          "kind",
          "phone"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "phone"
          },
          "phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32,
            "pattern": "^[+0-9() .-]+$"
          }
        },
        "example": {
          "kind": "phone",
          "phone": "+1 555 010 0100"
        }
      },
      "SmsContent": {
        "type": "object",
        "description": "Opens a pre-filled text message.",
        "required": [
          "kind",
          "phone"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "sms"
          },
          "phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32,
            "pattern": "^[+0-9() .-]+$"
          },
          "message": {
            "type": "string",
            "maxLength": 320
          }
        },
        "example": {
          "kind": "sms",
          "phone": "+15550100100",
          "message": "JOIN"
        }
      },
      "EmailContent": {
        "type": "object",
        "description": "Opens a pre-filled email.",
        "required": [
          "kind",
          "to"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "email"
          },
          "to": {
            "type": "string",
            "minLength": 1,
            "maxLength": 254,
            "format": "email"
          },
          "subject": {
            "type": "string",
            "maxLength": 200
          },
          "body": {
            "type": "string",
            "maxLength": 1000
          }
        },
        "example": {
          "kind": "email",
          "to": "hello@example.com",
          "subject": "Quote request"
        }
      },
      "WifiContent": {
        "type": "object",
        "description": "Joins a Wi-Fi network.",
        "required": [
          "kind",
          "ssid",
          "security"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "wifi"
          },
          "ssid": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32
          },
          "security": {
            "type": "string",
            "minLength": 1,
            "enum": [
              "WPA",
              "WEP",
              "nopass"
            ]
          },
          "password": {
            "type": "string",
            "maxLength": 63,
            "description": "Ignored when security is nopass."
          },
          "hidden": {
            "type": "boolean"
          }
        },
        "example": {
          "kind": "wifi",
          "ssid": "Cafe Guest",
          "security": "WPA",
          "password": "espresso42"
        }
      },
      "VCardContent": {
        "type": "object",
        "description": "Saves a contact card.",
        "required": [
          "kind",
          "firstName"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "vcard"
          },
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "lastName": {
            "type": "string",
            "maxLength": 64
          },
          "org": {
            "type": "string",
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "maxLength": 64
          },
          "phone": {
            "type": "string",
            "maxLength": 32
          },
          "email": {
            "type": "string",
            "maxLength": 254
          },
          "url": {
            "type": "string",
            "maxLength": 2048
          },
          "note": {
            "type": "string",
            "maxLength": 500
          }
        },
        "example": {
          "kind": "vcard",
          "firstName": "Ada",
          "lastName": "Lovelace",
          "org": "Analytical Engines"
        }
      },
      "BioContent": {
        "type": "object",
        "description": "Hosted link-in-bio page. Dynamic codes only.",
        "required": [
          "kind",
          "headline"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "bio"
          },
          "headline": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "bio": {
            "type": "string",
            "maxLength": 500
          },
          "avatarUrl": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "http:// or https:// URL. Must be a file uploaded to JustQRs from the dashboard; the API cannot upload files."
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LandingLink"
            },
            "maxItems": 20
          },
          "theme": {
            "$ref": "#/components/schemas/LandingTheme"
          }
        },
        "example": {
          "kind": "bio",
          "headline": "Ada's links",
          "links": [
            {
              "label": "Portfolio",
              "url": "https://example.com"
            }
          ]
        }
      },
      "BusinessContent": {
        "type": "object",
        "description": "Hosted business page with contact details and hours. Dynamic codes only.",
        "required": [
          "kind",
          "businessName"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "business"
          },
          "businessName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "phone": {
            "type": "string",
            "maxLength": 32
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email"
          },
          "website": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "http:// or https:// URL."
          },
          "address": {
            "type": "string",
            "maxLength": 200
          },
          "hoursLines": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            },
            "maxItems": 14,
            "description": "Each line at most 80 chars."
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LandingLink"
            },
            "maxItems": 10
          },
          "theme": {
            "$ref": "#/components/schemas/LandingTheme"
          }
        },
        "example": {
          "kind": "business",
          "businessName": "Corner Bakery",
          "hoursLines": [
            "Mon–Fri 7am–6pm",
            "Sat 8am–2pm"
          ]
        }
      },
      "PdfContent": {
        "type": "object",
        "description": "Hosted page that opens a PDF. Dynamic codes only.",
        "required": [
          "kind",
          "pdfUrl"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "pdf"
          },
          "pdfUrl": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "format": "uri",
            "description": "http:// or https:// URL. Must be a file uploaded to JustQRs from the dashboard; the API cannot upload files."
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "theme": {
            "$ref": "#/components/schemas/LandingTheme"
          }
        },
        "example": {
          "kind": "pdf",
          "pdfUrl": "https://justqrs.com/u/pdfs/acc/menu.pdf",
          "title": "Menu"
        }
      },
      "AppContent": {
        "type": "object",
        "description": "Hosted app-download page that sends each phone to its store. Dynamic codes only.",
        "required": [
          "kind",
          "appName"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "app"
          },
          "appName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "appStoreUrl": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "https:// URL on apps.apple.com or itunes.apple.com. At least one of appStoreUrl or playStoreUrl is required."
          },
          "playStoreUrl": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "https:// URL on play.google.com. At least one of appStoreUrl or playStoreUrl is required."
          },
          "fallbackUrl": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "http:// or https:// URL."
          },
          "tagline": {
            "type": "string",
            "maxLength": 120
          },
          "theme": {
            "$ref": "#/components/schemas/LandingTheme"
          }
        },
        "example": {
          "kind": "app",
          "appName": "Cafe Rewards",
          "appStoreUrl": "https://apps.apple.com/app/id000000000"
        }
      },
      "RenderRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "content": {
            "$ref": "#/components/schemas/StaticQrContent"
          },
          "design": {
            "$ref": "#/components/schemas/QrDesign",
            "description": "Any design except logo."
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "svg"
            ],
            "default": "png"
          },
          "size": {
            "type": "integer",
            "minimum": 64,
            "maximum": 2048,
            "description": "px; defaults to design.size."
          }
        },
        "example": {
          "content": {
            "kind": "wifi",
            "ssid": "Cafe Guest",
            "security": "WPA",
            "password": "espresso42"
          },
          "design": {
            "dots": {
              "style": "rounded",
              "color": "#1f2a44"
            }
          }
        }
      },
      "LandingLink": {
        "type": "object",
        "required": [
          "label",
          "url"
        ],
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "http:// or https:// URL."
          }
        }
      },
      "LandingTheme": {
        "type": "object",
        "required": [
          "preset"
        ],
        "properties": {
          "preset": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "ocean",
              "sunset",
              "forest"
            ]
          },
          "primaryColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$"
          }
        }
      },
      "Gradient": {
        "type": "object",
        "required": [
          "type",
          "stops"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "linear",
              "radial"
            ]
          },
          "rotation": {
            "type": "number",
            "minimum": 0,
            "maximum": 6.2832,
            "description": "Radians."
          },
          "stops": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "items": {
              "type": "object",
              "properties": {
                "offset": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "color": {
                  "type": "string",
                  "pattern": "^#[0-9a-fA-F]{6}$"
                }
              }
            }
          }
        }
      },
      "QrDesign": {
        "type": "object",
        "description": "Every field is optional; omitted fields take the defaults shown. Out-of-range numbers are clamped.",
        "properties": {
          "size": {
            "type": "integer",
            "minimum": 64,
            "maximum": 4096,
            "default": 512,
            "description": "Export size in px."
          },
          "margin": {
            "type": "integer",
            "minimum": 0,
            "maximum": 128,
            "default": 16,
            "description": "Quiet zone in px."
          },
          "ecLevel": {
            "type": "string",
            "enum": [
              "L",
              "M",
              "Q",
              "H"
            ],
            "default": "Q",
            "description": "Error correction; use H with a logo."
          },
          "transparentBackground": {
            "type": "boolean",
            "default": false
          },
          "background": {
            "type": "object",
            "properties": {
              "color": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$",
                "default": "#ffffff"
              },
              "gradient": {
                "$ref": "#/components/schemas/Gradient"
              }
            }
          },
          "dots": {
            "type": "object",
            "properties": {
              "color": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$",
                "default": "#000000"
              },
              "gradient": {
                "$ref": "#/components/schemas/Gradient"
              },
              "style": {
                "type": "string",
                "enum": [
                  "square",
                  "dots",
                  "rounded",
                  "extra-rounded",
                  "classy",
                  "classy-rounded"
                ],
                "default": "square"
              }
            }
          },
          "cornerSquares": {
            "type": "object",
            "properties": {
              "color": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$",
                "default": "#000000"
              },
              "gradient": {
                "$ref": "#/components/schemas/Gradient"
              },
              "style": {
                "type": "string",
                "enum": [
                  "square",
                  "dot",
                  "extra-rounded",
                  "rounded",
                  "dots",
                  "classy",
                  "classy-rounded"
                ],
                "default": "square"
              }
            }
          },
          "cornerDots": {
            "type": "object",
            "properties": {
              "color": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$",
                "default": "#000000"
              },
              "gradient": {
                "$ref": "#/components/schemas/Gradient"
              },
              "style": {
                "type": "string",
                "enum": [
                  "square",
                  "dot",
                  "rounded",
                  "dots",
                  "classy",
                  "classy-rounded",
                  "extra-rounded"
                ],
                "default": "square"
              }
            }
          },
          "logo": {
            "type": "object",
            "required": [
              "src"
            ],
            "description": "Center logo. Must be a file uploaded to JustQRs from the dashboard; the API cannot upload files.",
            "properties": {
              "src": {
                "type": "string",
                "maxLength": 2048
              },
              "sizeRatio": {
                "type": "number",
                "minimum": 0.1,
                "maximum": 0.5,
                "default": 0.3
              },
              "margin": {
                "type": "integer",
                "minimum": 0,
                "maximum": 40,
                "default": 4
              },
              "hideDots": {
                "type": "boolean",
                "default": true
              }
            }
          }
        }
      },
      "CreateQrRequest": {
        "type": "object",
        "required": [
          "name",
          "mode",
          "content"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "mode": {
            "$ref": "#/components/schemas/QrMode"
          },
          "content": {
            "$ref": "#/components/schemas/QrContent"
          },
          "design": {
            "$ref": "#/components/schemas/QrDesign"
          }
        },
        "example": {
          "name": "Front door menu",
          "mode": "dynamic",
          "content": {
            "kind": "url",
            "url": "https://example.com/menu"
          },
          "design": {
            "dots": {
              "style": "rounded",
              "color": "#1f2a44"
            },
            "ecLevel": "Q"
          }
        }
      },
      "UpdateQrRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "content": {
            "$ref": "#/components/schemas/QrContent"
          },
          "design": {
            "$ref": "#/components/schemas/QrDesign"
          }
        }
      },
      "QrResponse": {
        "type": "object",
        "required": [
          "qrId",
          "name",
          "mode",
          "type",
          "content",
          "design",
          "payload",
          "scanCount",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "qrId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "mode": {
            "$ref": "#/components/schemas/QrMode"
          },
          "type": {
            "type": "string",
            "enum": [
              "url",
              "text",
              "phone",
              "sms",
              "email",
              "wifi",
              "vcard",
              "bio",
              "business",
              "pdf",
              "app"
            ],
            "description": "Same as content.kind."
          },
          "shortCode": {
            "type": "string",
            "description": "Dynamic codes only."
          },
          "content": {
            "$ref": "#/components/schemas/QrContent"
          },
          "design": {
            "$ref": "#/components/schemas/QrDesign"
          },
          "payload": {
            "type": "string",
            "description": "Exactly what the QR image must encode — the short URL for dynamic codes, the encoded content for static ones. Render it with design to get the image."
          },
          "scanCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "QrListResponse": {
        "type": "object",
        "required": [
          "qrcodes"
        ],
        "properties": {
          "qrcodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QrResponse"
            }
          }
        }
      },
      "QrStats": {
        "type": "object",
        "required": [
          "totalScans",
          "rangeScans",
          "rangeUniques",
          "series",
          "breakdowns"
        ],
        "properties": {
          "totalScans": {
            "type": "integer",
            "description": "All-time scans."
          },
          "rangeScans": {
            "type": "integer",
            "description": "Scans in the requested window."
          },
          "rangeUniques": {
            "type": "integer",
            "description": "Unique scanners per day, summed over the window."
          },
          "series": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "date",
                "scans",
                "uniques"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date",
                  "description": "UTC day."
                },
                "scans": {
                  "type": "integer"
                },
                "uniques": {
                  "type": "integer"
                }
              }
            }
          },
          "breakdowns": {
            "type": "object",
            "description": "Scan counts in the window keyed by value.",
            "properties": {
              "country": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "city": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "device": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "os": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "browser": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      }
    }
  }
}
