{
  "openapi": "3.0.3",
  "info": {
    "title": "QR Code Harbor API",
    "version": "1.2.0",
    "description": "Developer API for creating and managing QR codes and barcodes programmatically. Available on Business and Founder plans. Barcodes support svg and png output; static QR codes support optional server-rendered styling (rounded/dot module and eye shapes, gradients) — see api-docs.html §8 and §10 for exact capabilities.",
    "contact": { "name": "QR Code Harbor Support", "url": "https://qrcodeharbor.com/contact.html" }
  },
  "servers": [
    { "url": "https://qrcodeharbor.com/api/v1", "description": "Production" }
  ],
  "security": [
    { "BearerAuth": [] },
    { "ApiKeyAuth": [] }
  ],
  "tags": [
    { "name": "QR Codes", "description": "Create and retrieve static and dynamic QR codes" },
    { "name": "Bulk", "description": "Bulk QR code generation" },
    { "name": "Barcodes", "description": "Generate 1D/2D/postal barcodes" },
    { "name": "Webhooks", "description": "Register scan-event webhooks for dynamic QR codes" },
    { "name": "API Keys", "description": "Manage API keys (session auth, not API-key auth)" }
  ],
  "paths": {
    "/qr/static": {
      "post": {
        "tags": ["QR Codes"],
        "summary": "Create a static QR code",
        "description": "Content is baked into the QR code at creation time and cannot be edited afterward. Supports all 37 content types from the web generator (see the CreateStaticQRRequest.type enum and api-docs.html §7 for the full per-type field reference).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateStaticQRRequest" } } }
        },
        "responses": {
          "201": {
            "description": "QR code created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StaticQRCode" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/qr/dynamic": {
      "post": {
        "tags": ["QR Codes"],
        "summary": "Create a dynamic QR code",
        "description": "Creates a short redirect URL (/r/:id) whose destination can be updated later via PATCH /qr/dynamic/{id}, without reprinting the code.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["destination"],
                "properties": { "destination": { "type": "string", "format": "uri", "example": "https://example.com" } }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dynamic QR code created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DynamicQRCode" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/qr/dynamic/{id}": {
      "get": {
        "tags": ["QR Codes"],
        "summary": "Retrieve dynamic QR code metadata",
        "parameters": [ { "$ref": "#/components/parameters/QRId" } ],
        "responses": {
          "200": {
            "description": "Dynamic QR code metadata",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "id": { "type": "string" },
              "mode": { "type": "string", "enum": ["dynamic"] },
              "short_url": { "type": "string", "format": "uri" },
              "destination": { "type": "string" },
              "scan_count": { "type": "integer" },
              "created_at": { "type": "integer" }
            } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["QR Codes"],
        "summary": "Update a dynamic QR code's destination",
        "parameters": [ { "$ref": "#/components/parameters/QRId" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["destination"],
                "properties": { "destination": { "type": "string", "format": "uri" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "mode": { "type": "string", "enum": ["dynamic"] }, "destination": { "type": "string" } } } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/qr/dynamic/{id}/analytics": {
      "get": {
        "tags": ["QR Codes"],
        "summary": "Get scan analytics for a dynamic QR code",
        "parameters": [ { "$ref": "#/components/parameters/QRId" } ],
        "responses": {
          "200": {
            "description": "Scan analytics breakdown",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "id": { "type": "string" },
              "mode": { "type": "string", "enum": ["dynamic"] },
              "destination": { "type": "string" },
              "created_at": { "type": "integer" },
              "scan_count": { "type": "integer" },
              "unique_visitors": { "type": "integer" },
              "byCountry": { "type": "array", "items": { "type": "object" } },
              "byCity": { "type": "array", "items": { "type": "object" } },
              "byRegion": { "type": "array", "items": { "type": "object" } },
              "byDevice": { "type": "array", "items": { "type": "object" } },
              "byOS": { "type": "array", "items": { "type": "object" } },
              "byBrowser": { "type": "array", "items": { "type": "object" } },
              "byReferer": { "type": "array", "items": { "type": "object" } },
              "byLanguage": { "type": "array", "items": { "type": "object" } },
              "daily": { "type": "array", "items": { "type": "object" } },
              "hourly": { "type": "array", "items": { "type": "object" } },
              "recent": { "type": "array", "items": { "type": "object" } },
              "points": { "type": "array", "items": { "type": "object" } }
            } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/qr/{id}": {
      "get": {
        "tags": ["QR Codes"],
        "summary": "Retrieve static QR code metadata",
        "parameters": [ { "$ref": "#/components/parameters/QRId" } ],
        "responses": {
          "200": { "description": "QR code metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StaticQRCode" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/qr/{id}/download": {
      "get": {
        "tags": ["QR Codes"],
        "summary": "Download the QR code image",
        "description": "svg is served directly from storage; png and pdf are rendered on demand from the stored payload.",
        "parameters": [
          { "$ref": "#/components/parameters/QRId" },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["svg", "png", "pdf"], "default": "svg" },
            "description": "Image format to return."
          }
        ],
        "responses": {
          "200": { "description": "image/svg+xml, image/png, or application/pdf body depending on format" },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/qr/bulk": {
      "post": {
        "tags": ["Bulk"],
        "summary": "Create multiple static QR codes in one call",
        "description": "Accepts a JSON array of QR row objects (up to the plan's bulkRows limit, default 1000). The top-level format applies to the download_url returned for every item in the job; each item only needs type + data. Any of the 37 content types supported by POST /qr/static can be mixed within one bulk job.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["items"],
                "properties": {
                  "format": { "type": "string", "enum": ["svg", "png", "pdf"], "default": "svg" },
                  "items": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "type": "object",
                      "required": ["type", "data"],
                      "properties": {
                        "name": { "type": "string" },
                        "type": { "$ref": "#/components/schemas/QRContentType" },
                        "data": { "type": "object" }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Bulk job completed synchronously",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": { "type": "string" },
                    "status": { "type": "string", "enum": ["completed"] },
                    "count": { "type": "integer" },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "id": { "type": "string" },
                          "download_url": { "type": "string", "format": "uri" }
                        }
                      }
                    },
                    "created_at": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/barcode": {
      "post": {
        "tags": ["Barcodes"],
        "summary": "Generate a barcode",
        "description": "Renders a 1D, 2D, or postal barcode using the same bwip-js engine as /barcode.html. format=\"svg\" (default) returns the JSON envelope in BarcodeResponse; format=\"png\" rasterizes that SVG server-side with @resvg/resvg-wasm (a WASM SVG renderer that runs in the Cloudflare Worker with no DOM/canvas) and returns raw image/png bytes instead of JSON. Available symbologies are gated by plan (functions/_plans.js barcodeSymbologies); Business/Founder get 13 of the 38 symbologies bwip-js supports (see api-docs.html §Barcodes for the exact list and how to request more).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateBarcodeRequest" } } }
        },
        "responses": {
          "201": {
            "description": "Barcode generated. application/json (BarcodeResponse) for format=svg; image/png bytes for format=png.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/BarcodeResponse" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "List your scan-event webhooks",
        "responses": {
          "200": {
            "description": "List of webhooks",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "webhooks": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookSummary" } } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PlanRequired" }
        }
      },
      "post": {
        "tags": ["Webhooks"],
        "summary": "Register a scan-event webhook for a dynamic QR code",
        "description": "One webhook per dynamic QR code (code_id). Fires a fire-and-forget POST to url on every scan of that code — see api-docs.html §Webhooks for the payload shape and the X-Webhook-Signature HMAC scheme.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["code_id", "url"],
                "properties": {
                  "code_id": { "type": "string", "description": "id of a dynamic QR code you own (from POST /qr/dynamic)." },
                  "url": { "type": "string", "format": "uri", "description": "https:// endpoint to receive scan events." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook registered",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookCreated" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/webhooks/{id}": {
      "delete": {
        "tags": ["Webhooks"],
        "summary": "Delete a webhook",
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "example": "whk_8f2c1a9d" } ],
        "responses": {
          "200": {
            "description": "Webhook deleted",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string", "enum": ["deleted"] } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/keys": {
      "get": {
        "tags": ["API Keys"],
        "summary": "List API keys for the signed-in account",
        "security": [{ "SessionAuth": [] }],
        "description": "Requires a signed-in session (Firebase ID token as Authorization: Bearer), not an API key.",
        "responses": {
          "200": {
            "description": "List of API keys (never includes the full secret)",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "keys": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKeySummary" } } } } } }
          },
          "401": { "$ref": "#/components/responses/SessionUnauthorized" }
        }
      },
      "post": {
        "tags": ["API Keys"],
        "summary": "Create a new API key",
        "security": [{ "SessionAuth": [] }],
        "description": "Requires a signed-in session on a Business or Founder plan. The full key is returned exactly once and cannot be retrieved again.",
        "requestBody": {
          "required": false,
          "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "example": "Production server" } } } } }
        },
        "responses": {
          "201": {
            "description": "Newly created key",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyCreated" } } }
          },
          "401": { "$ref": "#/components/responses/SessionUnauthorized" },
          "403": { "description": "Account is not on a Business or Founder plan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionError" } } } }
        }
      }
    },
    "/keys/{id}": {
      "delete": {
        "tags": ["API Keys"],
        "summary": "Revoke an API key",
        "security": [{ "SessionAuth": [] }],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": {
          "200": {
            "description": "Key revoked",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "id": { "type": "string" },
              "status": { "type": "string", "enum": ["revoked"] },
              "revoked_at": { "type": "integer" }
            } } } }
          },
          "401": { "$ref": "#/components/responses/SessionUnauthorized" },
          "404": { "description": "Key not found or not owned by the signed-in account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionError" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "qch_live_xxxx",
        "description": "API key sent as: Authorization: Bearer qch_live_xxxxxxxx"
      },
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key sent as: X-API-Key: qch_live_xxxxxxxx"
      },
      "SessionAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Firebase ID token",
        "description": "Used only by /keys endpoints — a signed-in user session, not an API key."
      }
    },
    "parameters": {
      "QRId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "example": "qr_8f2c1a9d"
      }
    },
    "schemas": {
      "QRContentType": {
        "type": "string",
        "description": "Full parity with the web generator's 37 content types. See api-docs.html §7 for the data object fields required by each.",
        "enum": [
          "url", "text", "email", "phone", "sms", "wifi", "vcard", "whatsapp",
          "facebook", "instagram", "youtube", "twitter", "linkedin", "tiktok", "snapchat", "pinterest",
          "event", "location", "booking", "payment", "crypto", "googleReviews", "coupon",
          "pdf", "video", "image", "appstore", "deeplink", "mecard", "zoom", "spotify",
          "discord", "telegram", "audio", "bookmark", "calendar", "document"
        ]
      },
      "CreateStaticQRRequest": {
        "type": "object",
        "required": ["type", "data"],
        "properties": {
          "type": { "$ref": "#/components/schemas/QRContentType" },
          "data": {
            "type": "object",
            "description": "Fields depend on type. Common ones: url.url; text.text; wifi.ssid+encryption(WPA|WEP|nopass)+password+hidden; vcard.firstName+lastName (or name)+company(+title,phone,email,website,address,address2,extraFields[]); email.to(+subject,body); sms.phone(+message); phone.phone; whatsapp.phone(+message); location.lat+lng OR address (+label,format); event.title+start (+location,end,description); payment.app+id(+amount,note); crypto.address(+coinId,symbol,network,amount,memo); booking.service+handle; mecard.name(+phone,email,url,address,birthday,nickname,note); zoom.meetingId(+password); coupon.code+description OR url. See api-docs.html §7 for the complete per-type reference."
          },
          "format": { "type": "string", "enum": ["svg", "png", "pdf"], "default": "svg" },
          "errorCorrectionLevel": { "type": "string", "enum": ["L", "M", "Q", "H"], "default": "M" },
          "size": { "type": "integer", "minimum": 64, "maximum": 4096, "default": 512 },
          "margin": { "type": "integer", "minimum": 0, "maximum": 40, "default": 4 },
          "fg": { "type": "string", "description": "Hex color for the foreground (dark modules). Ignored if style.gradient is set.", "default": "#000000" },
          "bg": { "type": "string", "description": "Hex color for the background.", "default": "#ffffff" },
          "style": {
            "type": "object",
            "description": "Optional server-rendered styling — a limited subset of the web generator's qr-code-styling look, built directly from the QR module matrix (no DOM/canvas). See api-docs.html §10 for exactly what is/isn't supported (no classy/extra-rounded connected-blob styles, no logo embedding).",
            "properties": {
              "dotShape": { "type": "string", "enum": ["square", "dots", "rounded"], "default": "square", "description": "Shape of each dark data module (outside the 3 corner eyes)." },
              "eyeShape": { "type": "string", "enum": ["square", "dot"], "default": "square", "description": "Shape of the 3 corner finder-pattern eyes." },
              "gradient": {
                "type": "object",
                "description": "Overrides fg with a gradient fill.",
                "properties": {
                  "type": { "type": "string", "enum": ["linear", "radial"], "default": "linear" },
                  "rotation": { "type": "number", "description": "Degrees, linear gradients only.", "default": 0 },
                  "colors": {
                    "type": "array",
                    "minItems": 2,
                    "description": "Each entry is a 2-element [offset, hexColor] tuple, e.g. [0, \"#F59E0B\"]: offset is a number 0-1, hexColor is a string.",
                    "items": { "type": "array", "items": {}, "minItems": 2, "maxItems": 2, "example": [0, "#F59E0B"] }
                  }
                }
              }
            }
          }
        }
      },
      "StaticQRCode": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "qr_8f2c1a9d" },
          "mode": { "type": "string", "enum": ["static"] },
          "type": { "type": "string" },
          "format": { "type": "string", "enum": ["svg", "png", "pdf"] },
          "styled": { "type": "boolean", "description": "True if the request's style object produced non-default styling (see CreateStaticQRRequest.style)." },
          "download_url": { "type": "string", "format": "uri" },
          "created_at": { "type": "integer", "description": "Unix ms timestamp" }
        }
      },
      "DynamicQRCode": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "mode": { "type": "string", "enum": ["dynamic"] },
          "short_url": { "type": "string", "format": "uri" },
          "download_url": { "type": "string", "format": "uri", "nullable": true },
          "destination": { "type": "string" },
          "edit_token": { "type": "string", "description": "Store securely — grants edit/delete access via the non-API dashboard endpoints." },
          "created_at": { "type": "integer" }
        }
      },
      "CreateBarcodeRequest": {
        "type": "object",
        "required": ["symbology", "data"],
        "properties": {
          "symbology": {
            "type": "string",
            "description": "bwip-js barcode identifier. Full engine list (38): ean13, ean8, upca, upce, isbn, issn, gs1-128, gs1datamatrix, gs1qrcode, databaromni, databarexpanded, databarlimited, code128, code39, code93, rationalizedCodabar, msi, interleaved2of5, itf14, code11, pharmacode, telepen, qrcode, microqrcode, datamatrix, pdf417, micropdf417, azteccode, maxicode, codablockf, dotcode, hanxin, postnet, onecode, royalmail, auspost, japanpost, kix. Which of these your plan allows is capped by functions/_plans.js barcodeSymbologies (see api-docs.html §Barcodes)."
          },
          "data": { "type": "string", "description": "Raw content to encode. Passed straight to bwip-js — some symbologies (e.g. isbn, issn, postnet, onecode) require an exact digit count or a dashed format; see api-docs.html §Barcodes for known format quirks." },
          "format": { "type": "string", "enum": ["svg", "png"], "default": "svg", "description": "svg returns the BarcodeResponse JSON envelope; png rasterizes that SVG with @resvg/resvg-wasm and returns raw image/png bytes." },
          "options": {
            "type": "object",
            "properties": {
              "scale": { "type": "integer", "minimum": 1, "maximum": 10, "default": 3 },
              "height": { "type": "number", "description": "Bar height in mm (ignored for 2D/postal symbologies).", "default": 10 },
              "margin": { "type": "integer", "minimum": 0, "maximum": 40, "default": 4 },
              "fg": { "type": "string", "default": "#000000" },
              "bg": { "type": "string", "default": "#ffffff" },
              "includetext": { "type": "boolean", "default": true, "description": "Show the human-readable text under the bars (linear symbologies only)." },
              "pngScale": { "type": "number", "minimum": 1, "maximum": 4, "default": 2, "description": "format=png only. Multiplies the barcode's native pixel size for a crisper raster; independent of scale (bwip-js module density)." }
            }
          }
        }
      },
      "BarcodeResponse": {
        "type": "object",
        "properties": {
          "symbology": { "type": "string" },
          "format": { "type": "string", "enum": ["svg"] },
          "svg": { "type": "string", "description": "Inline SVG markup." },
          "created_at": { "type": "integer" }
        }
      },
      "WebhookSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "whk_8f2c1a9d" },
          "code_id": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "status": { "type": "string", "enum": ["active", "disabled"] },
          "fail_count": { "type": "integer" },
          "created_at": { "type": "integer" },
          "last_fired_at": { "type": "integer", "nullable": true },
          "last_status": { "type": "integer", "nullable": true }
        }
      },
      "WebhookCreated": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookSummary" },
          {
            "type": "object",
            "properties": {
              "secret": { "type": "string", "description": "Shown only once, at creation. Used to verify the X-Webhook-Signature header (HMAC-SHA256 hex of the raw request body)." }
            }
          }
        ]
      },
      "ApiKeySummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "key_prefix": { "type": "string", "example": "qch_live_ab12cd34" },
          "status": { "type": "string", "enum": ["active", "revoked"] },
          "created_at": { "type": "integer" },
          "last_used_at": { "type": "integer", "nullable": true }
        }
      },
      "ApiKeyCreated": {
        "allOf": [
          { "$ref": "#/components/schemas/ApiKeySummary" },
          {
            "type": "object",
            "properties": {
              "key": { "type": "string", "description": "Full secret key — shown only once, never retrievable again." }
            }
          }
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "plan_required",
                  "forbidden",
                  "not_found",
                  "validation_error",
                  "rate_limit_exceeded"
                ]
              },
              "message": { "type": "string" }
            },
            "required": ["code", "message"]
          }
        },
        "required": ["error"]
      },
      "SessionError": {
        "type": "object",
        "description": "Error shape used by session-authed endpoints (/api/keys*), distinct from the {error:{code,message}} shape used by API-key-authed /api/v1/* endpoints.",
        "properties": {
          "error": { "type": "string", "example": "Please sign in." }
        },
        "required": ["error"]
      }
    },
    "responses": {
      "Unauthorized": { "description": "Missing or invalid API key (missing_api_key / invalid_api_key)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PlanRequired": { "description": "Account is not on a Business or Founder plan (plan_required)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Authenticated but not the owner of this resource (forbidden)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Resource not found (not_found)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "ValidationError": { "description": "Invalid request body (validation_error)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Monthly or burst request limit exceeded (rate_limit_exceeded)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "SessionUnauthorized": { "description": "Not signed in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionError" } } } }
    }
  }
}
