{
 "openapi": "3.0.3",
 "info": {
  "title": "QR Maker AI — Business API v1",
  "version": "1.0.0",
  "description": "Server-to-server API for creating trackable dynamic QR codes from your own system, keeping them in sync, and pulling scan analytics into your own reports.\n\n**Auth** — `Authorization: Bearer qrs_…` (Business plan; create tokens under Settings → API Tokens). Call it from your server only: browsers are blocked by CORS, and a token must never ship in client code.\n\n**Conventions** — snake_case fields; timestamps are unix seconds; dates are `YYYY-MM-DD`. Success: `{ \"success\": true, \"data\": … }` (lists add `next_cursor`). Error: `{ \"success\": false, \"error\": \"…\", \"code\": \"…\", \"params\"?: {…} }` — branch on `code` only.\n\n**Limits** — 120 requests/minute per account (`X-RateLimit-*`, `Retry-After` on 429) and 50,000 calls per UTC month (`X-Quota-*`). Only successful (non-4xx/5xx) responses count; batch create counts successful items; an empty `/v1/scans` response and `/v1/usage` are free.\n\n**The QR image** — always encode `short_link`, never `target_url`. Re-fetch the SVG only when `image_version` changes. We only render SVG; convert to PNG on your side.\n\n**Scans** — there are no webhooks: poll `GET /v1/scans?since_id=` every ~30 s, persist `next_since_id`, and de-duplicate by event `id` (delivery is at-least-once)."
 },
 "servers": [
  {
   "url": "https://qrmaker.ai/api",
   "description": "Production"
  }
 ],
 "security": [
  {
   "bearerAuth": []
  }
 ],
 "tags": [
  {
   "name": "QR codes",
   "description": "Create, sync, retarget and delete dynamic QR codes. Codes created in the dashboard are included."
  },
  {
   "name": "Images",
   "description": "Server-rendered SVG (no PNG)."
  },
  {
   "name": "Analytics",
   "description": "Per-code and multi-code reports over custom local-date ranges."
  },
  {
   "name": "Scans",
   "description": "Incremental scan event feed (replaces webhooks)."
  },
  {
   "name": "Account",
   "description": "Usage and quota."
  }
 ],
 "paths": {
  "/v1/qrcodes": {
   "get": {
    "tags": [
     "QR codes"
    ],
    "summary": "List QR codes",
    "operationId": "listQrCodes",
    "description": "All codes on the account (API- and dashboard-created), newest first `(created_at DESC, id DESC)`. With `updated_since` the order becomes `(updated_at ASC, id ASC)` for incremental sync (inclusive: `updated_at >= updated_since`). Deleted codes are never returned.",
    "parameters": [
     {
      "$ref": "#/components/parameters/limit"
     },
     {
      "$ref": "#/components/parameters/cursor"
     },
     {
      "name": "status",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "active",
        "disabled"
       ]
      }
     },
     {
      "name": "folder",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "external_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "updated_since",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "description": "Unix seconds."
     },
     {
      "name": "include_deleted",
      "in": "query",
      "required": false,
      "schema": {
       "type": "boolean",
       "default": false
      },
      "description": "With `updated_since`, also return deleted codes as tombstones `{ id, external_id: null, status: \"deleted\", deleted_at, updated_at }` (no target_url or style; match on id). Requires `updated_since`; cannot be combined with `status` or `external_id` (400 otherwise)."
     }
    ],
    "responses": {
     "200": {
      "description": "A page of codes",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/QrCodePage"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     }
    }
   },
   "post": {
    "tags": [
     "QR codes"
    ],
    "summary": "Create a QR code (idempotent with external_id)",
    "operationId": "createQrCode",
    "description": "Only `target_url` is required; its query string is stored and redirected to byte-for-byte.\n\nWith `external_id`: if a code with that `external_id` already exists and has the **same** `target_url`, nothing is created and you get **200** with the existing code and `X-Idempotent-Replay: true` — retries are always safe. A **different** `target_url` returns **409 `EXTERNAL_ID_CONFLICT`** (`params.id` = existing code): POST never retargets; use PATCH. Without `external_id` there is no de-duplication.",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/QrCodeCreate"
       },
       "example": {
        "target_url": "https://app.example.com/signup?ref=AB12CD",
        "external_id": "AB12CD",
        "name": "Referral AB12CD",
        "folder": "spring-campaign",
        "style": {
         "fg_color": "#111827",
         "dot_style": "square"
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Created",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "200": {
      "description": "Idempotent replay — the existing code",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       },
       "X-Idempotent-Replay": {
        "description": "Always `true` on a replay.",
        "schema": {
         "type": "string",
         "enum": [
          "true"
         ]
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   }
  },
  "/v1/qrcodes/batch": {
   "post": {
    "tags": [
     "QR codes"
    ],
    "summary": "Create up to 100 QR codes",
    "operationId": "batchCreateQrCodes",
    "description": "Each item is processed independently with the same rules (and idempotency) as single create. The HTTP status is 200; each result has its own `status` and either `data` or `error`. Quota counts successful items (200/201); the whole batch is rejected with 402 if the remaining quota is smaller than the number of items. Rate limit counts the request once.",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "items"
        ],
        "additionalProperties": false,
        "properties": {
         "items": {
          "type": "array",
          "minItems": 1,
          "maxItems": 100,
          "items": {
           "$ref": "#/components/schemas/QrCodeCreate"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Per-item results, same order as the input",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/BatchItemResult"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     }
    }
   }
  },
  "/v1/qrcodes/{id}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "tags": [
     "QR codes"
    ],
    "summary": "Get a QR code",
    "operationId": "getQrCode",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "responses": {
     "200": {
      "description": "The code",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   },
   "patch": {
    "tags": [
     "QR codes"
    ],
    "summary": "Update a QR code",
    "operationId": "updateQrCode",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "description": "Send only the fields to change (at least one). Changing `target_url` re-runs the destination policy and does not change the image. Changing `short_domain` breaks already-printed codes on the old domain. `style` is merged field by field; `style: null` resets it. Deleted codes return 404.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/QrCodeUpdate"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated code",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   },
   "delete": {
    "tags": [
     "QR codes"
    ],
    "summary": "Delete a QR code",
    "operationId": "deleteQrCode",
    "description": "Permanent: the short link stops redirecting and the code cannot be restored. Its `external_id` is released for reuse.",
    "responses": {
     "200": {
      "description": "Deleted",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/Deleted"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/qrcodes/by-external-id/{external_id}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/external_id_path"
    }
   ],
   "get": {
    "tags": [
     "QR codes"
    ],
    "summary": "Get a QR code by external_id",
    "operationId": "getQrCodeByExternalId",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "responses": {
     "200": {
      "description": "The code",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   },
   "patch": {
    "tags": [
     "QR codes"
    ],
    "summary": "Update a QR code by external_id",
    "operationId": "updateQrCodeByExternalId",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/QrCodeUpdate"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated code",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "allOf": [
            {
             "$ref": "#/components/schemas/QrCode"
            },
            {
             "type": "object",
             "properties": {
              "svg": {
               "type": "string",
               "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
              }
             }
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   },
   "delete": {
    "tags": [
     "QR codes"
    ],
    "summary": "Delete a QR code by external_id",
    "operationId": "deleteQrCodeByExternalId",
    "responses": {
     "200": {
      "description": "Deleted",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/Deleted"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/qrcodes/{id}/svg": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "tags": [
     "Images"
    ],
    "summary": "Render the QR code as SVG",
    "operationId": "getQrCodeSvg",
    "description": "Encodes `short_link`. `ETag` is `\"{image_version}.{size}.{margin}\"`; send it back in `If-None-Match` to get 304. A logo from your QR Maker uploads is inlined as a data URI (no external references). If the logo could not be inlined, the image is rendered without it, `X-QR-Logo: omitted` is set and the ETag gets a `.nologo` suffix. A 304 counts as a successful call.",
    "parameters": [
     {
      "name": "size",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 64,
       "maximum": 4096,
       "default": 1000
      },
      "description": "width/height in px (viewBox is fixed)."
     },
     {
      "name": "margin",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 0,
       "maximum": 10,
       "default": 4
      },
      "description": "Quiet zone in modules (ISO/IEC 18004 recommends 4)."
     },
     {
      "name": "download",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "1",
        "true",
        "0",
        "false"
       ]
      },
      "description": "`1` adds `Content-Disposition: attachment`."
     },
     {
      "name": "If-None-Match",
      "in": "header",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "SVG document",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       },
       "ETag": {
        "schema": {
         "type": "string"
        }
       },
       "X-QR-Logo": {
        "description": "none | inlined | omitted",
        "schema": {
         "type": "string",
         "enum": [
          "none",
          "inlined",
          "omitted"
         ]
        }
       }
      },
      "content": {
       "image/svg+xml": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "304": {
      "description": "Not modified (If-None-Match matched)",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       },
       "ETag": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/qrcodes/{id}/analytics": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "tags": [
     "Analytics"
    ],
    "summary": "Analytics for one QR code",
    "operationId": "getQrCodeAnalytics",
    "description": "`from`/`to` are local dates in `tz`, both inclusive (default: the last 30 days including today; max 366 days). Day boundaries follow the zone's real midnight, including DST changes. Bots/link previews are excluded from `scans` and reported in `bot_scans`. Distributions list the top 20 values plus `{ \"key\": \"other\" }` so they sum to `scans`; empty values are `\"unknown\"` (for referers, no referer = `\"direct\"`).\n\nEvery distribution holds the top 20 plus an `other` row, so each sums to `scans`; empty values are `unknown` (`referers`: `direct`, `apps`: `browser`, `sources`: `none`).",
    "parameters": [
     {
      "$ref": "#/components/parameters/from"
     },
     {
      "$ref": "#/components/parameters/to"
     },
     {
      "$ref": "#/components/parameters/tz"
     }
    ],
    "responses": {
     "200": {
      "description": "Report",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/QrCodeAnalytics"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/analytics/summary": {
   "get": {
    "tags": [
     "Analytics"
    ],
    "summary": "Report across many codes",
    "operationId": "getAnalyticsSummary",
    "description": "One call for a whole sales report. `group_by=code` (default) includes codes with zero scans, ordered `scans DESC, id ASC`; `group_by=folder` rows are `{ folder, codes, scans, unique_visitors }`; `group_by=day` rows are `{ date, scans, unique_visitors }` with zero-filled days. `totals.unique_visitors` is de-duplicated across all codes (not the sum of rows). Results are cached for 60 seconds per account and parameters. Scan filters (`country`, `app`, `src`) can be combined; they restrict which scans are counted (including `totals.bot_scans`) — `group_by=code` still lists codes with zero matching scans.",
    "parameters": [
     {
      "$ref": "#/components/parameters/from"
     },
     {
      "$ref": "#/components/parameters/to"
     },
     {
      "$ref": "#/components/parameters/tz"
     },
     {
      "name": "group_by",
      "in": "query",
      "schema": {
       "type": "string",
       "enum": [
        "code",
        "folder",
        "day"
       ],
       "default": "code"
      }
     },
     {
      "name": "folder",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "external_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "country",
      "in": "query",
      "description": "Only count scans from this country (ISO 3166-1 alpha-2, case-insensitive).",
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z]{2}$"
      }
     },
     {
      "name": "app",
      "in": "query",
      "description": "Only count scans opened in this app (a key from `apps`); `browser` = not inside any app.",
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9_-]{1,32}$"
      }
     },
     {
      "name": "src",
      "in": "query",
      "description": "Only count scans with this placement tag (`?s=`); `none` = untagged.",
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9_-]{1,32}$"
      }
     },
     {
      "$ref": "#/components/parameters/limit"
     },
     {
      "$ref": "#/components/parameters/cursor"
     }
    ],
    "responses": {
     "200": {
      "description": "Summary",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AnalyticsSummary"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     }
    }
   }
  },
  "/v1/scans/export": {
   "get": {
    "tags": [
     "Scans"
    ],
    "summary": "Export scan events as CSV",
    "operationId": "exportScansCsv",
    "description": "All non-bot scans in the date range (`from`/`to` are local dates in `tz`, both inclusive; default last 30 days, max 366 days), oldest first, as a UTF-8 CSV (with BOM, opens correctly in Excel). Columns: `scanned_at_utc, local_time, qrcode_id, external_id, code_name, country, region, timezone, device, os, os_version, browser, browser_version, app, language, referer, placement, variant, visitor` — new columns are only ever added at the end. Cells that start with `=`, `+`, `-` or `@` get a leading `'` so spreadsheets don't run them as formulas. At most 50,000 rows per call (`X-Export-Truncated: true` when cut). Billed as 1 call per 1,000 rows (at least 1). For continuous syncing use `GET /v1/scans` instead.",
    "parameters": [
     {
      "$ref": "#/components/parameters/from"
     },
     {
      "$ref": "#/components/parameters/to"
     },
     {
      "$ref": "#/components/parameters/tz"
     },
     {
      "name": "qrcode_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "external_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "CSV file",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       },
       "X-Export-Rows": {
        "description": "Number of scan rows in the file.",
        "schema": {
         "type": "integer"
        }
       },
       "X-Export-Truncated": {
        "description": "`true` when the range had more than the row limit and only the first rows were exported; narrow the date range (or use `GET /v1/scans`).",
        "schema": {
         "type": "string"
        }
       },
       "X-Export-Max-Rows": {
        "description": "The row limit (only when truncated).",
        "schema": {
         "type": "integer"
        }
       }
      },
      "content": {
       "text/csv": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/scans": {
   "get": {
    "tags": [
     "Scans"
    ],
    "summary": "Incremental scan feed",
    "operationId": "listScans",
    "description": "Scans of your codes with `id > since_id`, ascending. Store `next_since_id` and pass it next time; if `has_more` is true, call again immediately, otherwise wait `poll_after_seconds`. Delivery is at-least-once — de-duplicate by `id`. Scans are written asynchronously after the redirect and usually appear within seconds. A response with no events does not count toward the monthly quota.",
    "parameters": [
     {
      "name": "since_id",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 0,
       "default": 0
      }
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 1000,
       "default": 100
      }
     },
     {
      "name": "qrcode_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "external_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "include_bots",
      "in": "query",
      "schema": {
       "type": "boolean",
       "default": false
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Events",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ScanFeed"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "403": {
      "$ref": "#/components/responses/Forbidden"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     },
     "500": {
      "$ref": "#/components/responses/InternalError"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/v1/usage": {
   "get": {
    "tags": [
     "Account"
    ],
    "summary": "Current month's API usage",
    "operationId": "getUsage",
    "description": "Free: does not count toward the quota (still rate-limited).",
    "responses": {
     "200": {
      "description": "Usage",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/Usage"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/v1/style": {
   "get": {
    "tags": [
     "Account"
    ],
    "summary": "Read the default style for new codes",
    "operationId": "getDefaultStyle",
    "description": "The brand style that codes created through the API inherit. `configured` lists the fields you set; the rest are system defaults. With `include=svg` the response also has a preview image of a sample link (not a real code).",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "responses": {
     "200": {
      "description": "Effective style",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/BrandStyle"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   },
   "put": {
    "tags": [
     "Account"
    ],
    "summary": "Set (replace) or clear the default style",
    "operationId": "putDefaultStyle",
    "description": "Replaces the whole default style (it is not merged); `{ \"style\": null }` or `{ \"style\": {} }` clears it. Affects only codes created afterwards: the merged style (default, then the request's own `style` field by field) is stored on each code at creation, so existing codes and their `image_version` never change. Read-only tokens get 403 `TOKEN_READ_ONLY`.",
    "parameters": [
     {
      "$ref": "#/components/parameters/include"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "style"
        ],
        "additionalProperties": false,
        "properties": {
         "style": {
          "nullable": true,
          "allOf": [
           {
            "$ref": "#/components/schemas/Style"
           }
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Effective style",
      "headers": {
       "X-RateLimit-Limit": {
        "$ref": "#/components/headers/RateLimitLimit"
       },
       "X-RateLimit-Remaining": {
        "$ref": "#/components/headers/RateLimitRemaining"
       },
       "X-RateLimit-Reset": {
        "$ref": "#/components/headers/RateLimitReset"
       },
       "X-Quota-Limit": {
        "$ref": "#/components/headers/QuotaLimit"
       },
       "X-Quota-Remaining": {
        "$ref": "#/components/headers/QuotaRemaining"
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "required": [
          "success",
          "data"
         ],
         "properties": {
          "success": {
           "type": "boolean",
           "enum": [
            true
           ]
          },
          "data": {
           "$ref": "#/components/schemas/BrandStyle"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/PaymentRequired"
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "description": "Business API token `qrs_…`. Tokens can only call `/api/v1/*` (plus the legacy `/api/qrcodes*` and `GET /api/auth/me`); anything else returns 403 `TOKEN_SCOPE`. A token can be read-only (GET/HEAD only); any other method with it returns 403 `TOKEN_READ_ONLY`."
   }
  },
  "parameters": {
   "id": {
    "name": "id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "pattern": "^[a-zA-Z0-9]{4,12}$"
    },
    "description": "QR code id (the path of short_link)."
   },
   "external_id_path": {
    "name": "external_id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "pattern": "^[A-Za-z0-9._:-]{1,128}$"
    },
    "description": "Your reference. URL-encode it (e.g. `:` as `%3A`)."
   },
   "include": {
    "name": "include",
    "in": "query",
    "schema": {
     "type": "string",
     "enum": [
      "svg"
     ]
    },
    "description": "`svg` adds the full SVG document as `data.svg`."
   },
   "limit": {
    "name": "limit",
    "in": "query",
    "schema": {
     "type": "integer",
     "minimum": 1,
     "maximum": 200,
     "default": 50
    }
   },
   "cursor": {
    "name": "cursor",
    "in": "query",
    "schema": {
     "type": "string"
    },
    "description": "Opaque `next_cursor` from the previous page of the same query."
   },
   "from": {
    "name": "from",
    "in": "query",
    "schema": {
     "type": "string",
     "format": "date"
    },
    "description": "Local start date (inclusive), YYYY-MM-DD."
   },
   "to": {
    "name": "to",
    "in": "query",
    "schema": {
     "type": "string",
     "format": "date"
    },
    "description": "Local end date (inclusive), YYYY-MM-DD."
   },
   "tz": {
    "name": "tz",
    "in": "query",
    "schema": {
     "type": "string",
     "default": "UTC"
    },
    "description": "IANA zone (`America/Toronto`, `Asia/Shanghai`) or fixed offset (`+08:00`; encode `+` as `%2B` — an unencoded `+` is tolerated). Invalid → 400 `INVALID_TIMEZONE`.",
    "examples": {
     "iana": {
      "value": "America/Toronto"
     },
     "offset": {
      "value": "+08:00"
     }
    }
   }
  },
  "headers": {
   "RateLimitLimit": {
    "description": "Requests allowed per minute (120).",
    "schema": {
     "type": "integer"
    }
   },
   "RateLimitRemaining": {
    "description": "Requests left in the current window.",
    "schema": {
     "type": "integer"
    }
   },
   "RateLimitReset": {
    "description": "Unix seconds when the window resets.",
    "schema": {
     "type": "integer"
    }
   },
   "QuotaLimit": {
    "description": "Monthly call quota (50,000).",
    "schema": {
     "type": "integer"
    }
   },
   "QuotaRemaining": {
    "description": "Calls left this UTC month, after this request is accounted for.",
    "schema": {
     "type": "integer"
    }
   },
   "RetryAfter": {
    "description": "Seconds to wait before retrying.",
    "schema": {
     "type": "integer"
    }
   }
  },
  "responses": {
   "BadRequest": {
    "description": "`VALIDATION_ERROR` (params.field), `INVALID_TARGET_URL`, `STYLE_INVALID` (params.field), `INVALID_TIMEZONE`, `INVALID_DATE_RANGE`, `RANGE_TOO_LARGE` (params.max_days), `DOMAIN_NOT_ACTIVE`",
    "headers": {
     "X-RateLimit-Limit": {
      "$ref": "#/components/headers/RateLimitLimit"
     },
     "X-RateLimit-Remaining": {
      "$ref": "#/components/headers/RateLimitRemaining"
     },
     "X-RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimitReset"
     },
     "X-Quota-Limit": {
      "$ref": "#/components/headers/QuotaLimit"
     },
     "X-Quota-Remaining": {
      "$ref": "#/components/headers/QuotaRemaining"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Unauthorized": {
    "description": "`UNAUTHORIZED` — missing, unknown, expired or revoked token",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "PaymentRequired": {
    "description": "`BUSINESS_API_TOKENS` (not on Business), `BUSINESS_API_QUOTA_EXCEEDED` (monthly quota used up), `BUSINESS_CUSTOM_DOMAIN`",
    "headers": {
     "X-RateLimit-Limit": {
      "$ref": "#/components/headers/RateLimitLimit"
     },
     "X-RateLimit-Remaining": {
      "$ref": "#/components/headers/RateLimitRemaining"
     },
     "X-RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimitReset"
     },
     "X-Quota-Limit": {
      "$ref": "#/components/headers/QuotaLimit"
     },
     "X-Quota-Remaining": {
      "$ref": "#/components/headers/QuotaRemaining"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Forbidden": {
    "description": "`TOKEN_SCOPE`, `TOKEN_READ_ONLY`, `EMAIL_NOT_VERIFIED`, `URL_BLOCKED_*` (destination rejected by policy; params.detail)",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "NotFound": {
    "description": "`NOT_FOUND` — no such code on this account (or it was deleted)",
    "headers": {
     "X-RateLimit-Limit": {
      "$ref": "#/components/headers/RateLimitLimit"
     },
     "X-RateLimit-Remaining": {
      "$ref": "#/components/headers/RateLimitRemaining"
     },
     "X-RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimitReset"
     },
     "X-Quota-Limit": {
      "$ref": "#/components/headers/QuotaLimit"
     },
     "X-Quota-Remaining": {
      "$ref": "#/components/headers/QuotaRemaining"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "MethodNotAllowed": {
    "description": "`METHOD_NOT_ALLOWED`",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Conflict": {
    "description": "`EXTERNAL_ID_CONFLICT` — params.id is the code that owns the external_id",
    "headers": {
     "X-RateLimit-Limit": {
      "$ref": "#/components/headers/RateLimitLimit"
     },
     "X-RateLimit-Remaining": {
      "$ref": "#/components/headers/RateLimitRemaining"
     },
     "X-RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimitReset"
     },
     "X-Quota-Limit": {
      "$ref": "#/components/headers/QuotaLimit"
     },
     "X-Quota-Remaining": {
      "$ref": "#/components/headers/QuotaRemaining"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "RateLimited": {
    "description": "`RATE_LIMITED` (params.retry_after)",
    "headers": {
     "Retry-After": {
      "$ref": "#/components/headers/RetryAfter"
     },
     "X-RateLimit-Limit": {
      "$ref": "#/components/headers/RateLimitLimit"
     },
     "X-RateLimit-Remaining": {
      "$ref": "#/components/headers/RateLimitRemaining"
     },
     "X-RateLimit-Reset": {
      "$ref": "#/components/headers/RateLimitReset"
     }
    },
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "InternalError": {
    "description": "`INTERNAL_ERROR` — retry with backoff",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "required": [
     "success",
     "error",
     "code"
    ],
    "properties": {
     "success": {
      "type": "boolean",
      "enum": [
       false
      ]
     },
     "error": {
      "type": "string",
      "description": "Human-readable English message. Do not parse it."
     },
     "code": {
      "type": "string",
      "description": "Stable machine-readable code — branch on this.",
      "enum": [
       "UNAUTHORIZED",
       "TOKEN_SCOPE",
       "TOKEN_READ_ONLY",
       "RATE_LIMITED",
       "NOT_FOUND",
       "METHOD_NOT_ALLOWED",
       "VALIDATION_ERROR",
       "INVALID_TARGET_URL",
       "STYLE_INVALID",
       "INVALID_TIMEZONE",
       "INVALID_DATE_RANGE",
       "RANGE_TOO_LARGE",
       "EXTERNAL_ID_CONFLICT",
       "EMAIL_NOT_VERIFIED",
       "BUSINESS_API_TOKENS",
       "BUSINESS_API_QUOTA_EXCEEDED",
       "BUSINESS_CUSTOM_DOMAIN",
       "DOMAIN_NOT_ACTIVE",
       "URL_BLOCKED_PROTOCOL",
       "URL_BLOCKED_USERINFO",
       "URL_BLOCKED_IP_HOST",
       "URL_BLOCKED_PRIVATE_HOST",
       "URL_BLOCKED_ABUSE_TLD",
       "URL_BLOCKED_SELF_REFERENCE",
       "URL_BLOCKED_URL_SHORTENER",
       "URL_BLOCKED_BLOCKED_DOMAIN",
       "INTERNAL_ERROR"
      ]
     },
     "params": {
      "type": "object",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "integer"
        }
       ]
      },
      "description": "Details, e.g. `field`, `id`, `max_days`, `retry_after`, `detail`, `index` (batch)."
     }
    },
    "example": {
     "success": false,
     "error": "style.fg_color must be a hex color (#rrggbb) or rgb()/rgba().",
     "code": "STYLE_INVALID",
     "params": {
      "field": "style.fg_color"
     }
    }
   },
   "Style": {
    "type": "object",
    "additionalProperties": false,
    "description": "All fields optional on input (camelCase keys are also accepted; unknown keys → 400 STYLE_INVALID). Responses always contain every field with its effective value.",
    "properties": {
     "dot_style": {
      "type": "string",
      "enum": [
       "square",
       "round",
       "diamond",
       "rounded-square",
       "fluid"
      ],
      "default": "square",
      "description": "`fluid` joins neighbouring modules into rounded strokes."
     },
     "color_mode": {
      "type": "string",
      "enum": [
       "solid",
       "linear",
       "radial"
      ],
      "default": "solid"
     },
     "fg_color": {
      "type": "string",
      "description": "#rgb/#rrggbb(aa) or rgb()/rgba()",
      "default": "#000000"
     },
     "bg_color": {
      "type": "string",
      "default": "#ffffff"
     },
     "bg_transparent": {
      "type": "boolean",
      "default": false
     },
     "gradient_stops": {
      "type": "array",
      "minItems": 2,
      "maxItems": 2,
      "items": {
       "type": "object",
       "required": [
        "color",
        "position"
       ],
       "properties": {
        "color": {
         "type": "string"
        },
        "position": {
         "type": "number",
         "minimum": 0,
         "maximum": 1
        }
       }
      }
     },
     "gradient_angle": {
      "type": "number",
      "minimum": 0,
      "maximum": 360,
      "default": 135
     },
     "finder_outer_color": {
      "type": "string",
      "default": "#000000"
     },
     "finder_inner_color": {
      "type": "string",
      "default": "#000000"
     },
     "finder_round": {
      "type": "boolean",
      "default": false,
      "description": "Older switch, kept for compatibility: true = `rounded`, false = `square`. If both are sent, `finder_shape` wins."
     },
     "finder_shape": {
      "type": "string",
      "enum": [
       "square",
       "rounded",
       "smooth",
       "circle"
      ],
      "default": "square",
      "description": "Shape of the three corner markers. `smooth` and `circle` also round the alignment patterns."
     },
     "logo_url": {
      "type": "string",
      "nullable": true,
      "description": "Must be a logo uploaded to your QR Maker account (`https://qrmaker.ai/api/uploads/…`). Third-party URLs are rejected."
     },
     "logo_size_ratio": {
      "type": "number",
      "minimum": 0.05,
      "maximum": 0.22,
      "default": 0.2
     },
     "error_level": {
      "type": "string",
      "enum": [
       "L",
       "M",
       "Q",
       "H"
      ],
      "default": "M",
      "description": "Raised automatically to Q/H when a logo is present."
     }
    }
   },
   "QrCode": {
    "type": "object",
    "required": [
     "id",
     "external_id",
     "name",
     "target_url",
     "short_link",
     "short_domain",
     "folder",
     "status",
     "style",
     "scan_count",
     "image_version",
     "created_at",
     "updated_at"
    ],
    "properties": {
     "id": {
      "type": "string",
      "example": "hzu23X"
     },
     "external_id": {
      "type": "string",
      "nullable": true,
      "example": "AB12CD"
     },
     "name": {
      "type": "string",
      "example": "Referral AB12CD"
     },
     "target_url": {
      "type": "string",
      "format": "uri",
      "example": "https://app.example.com/signup?ref=AB12CD"
     },
     "short_link": {
      "type": "string",
      "format": "uri",
      "description": "The only thing to encode into the QR / share.",
      "example": "https://qrmk.cc/hzu23X"
     },
     "short_domain": {
      "type": "string",
      "nullable": true,
      "description": "Your own short-link domain, or null for the default."
     },
     "folder": {
      "type": "string",
      "nullable": true
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "disabled"
      ]
     },
     "style": {
      "$ref": "#/components/schemas/Style"
     },
     "scan_count": {
      "type": "integer",
      "description": "Lifetime non-bot scans."
     },
     "image_version": {
      "type": "string",
      "description": "Changes only when the image changes (short_link or style). Not affected by target_url.",
      "example": "a1b2c3d4e5f60718"
     },
     "created_at": {
      "type": "integer"
     },
     "updated_at": {
      "type": "integer"
     }
    }
   },
   "QrCodeCreate": {
    "type": "object",
    "required": [
     "target_url"
    ],
    "additionalProperties": false,
    "properties": {
     "target_url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048,
      "description": "http/https. Subject to the destination policy (403 URL_BLOCKED_*)."
     },
     "external_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9._:-]{1,128}$",
      "nullable": true,
      "description": "Your unique reference (referral code, order id, SKU…). Makes create idempotent."
     },
     "name": {
      "type": "string",
      "maxLength": 100
     },
     "folder": {
      "type": "string",
      "maxLength": 50,
      "nullable": true
     },
     "short_domain": {
      "type": "string",
      "nullable": true,
      "description": "One of your active short-link domains; null = default."
     },
     "style": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Style"
       },
       {
        "type": "string",
        "description": "JSON string of a Style object"
       }
      ]
     }
    }
   },
   "QrCodeUpdate": {
    "type": "object",
    "minProperties": 1,
    "additionalProperties": false,
    "properties": {
     "target_url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048
     },
     "external_id": {
      "type": "string",
      "nullable": true,
      "pattern": "^[A-Za-z0-9._:-]{1,128}$",
      "description": "null clears it."
     },
     "name": {
      "type": "string",
      "maxLength": 100
     },
     "folder": {
      "type": "string",
      "maxLength": 50,
      "nullable": true
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "disabled"
      ]
     },
     "short_domain": {
      "type": "string",
      "nullable": true,
      "description": "⚠️ Changing it breaks already-printed codes on the old domain."
     },
     "style": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/Style"
       },
       {
        "type": "string",
        "description": "JSON string of a Style object"
       }
      ],
      "description": "Merged into the current style field by field. Send `null` to reset to defaults."
     }
    }
   },
   "QrCodePage": {
    "type": "object",
    "required": [
     "success",
     "data",
     "next_cursor"
    ],
    "properties": {
     "success": {
      "type": "boolean",
      "enum": [
       true
      ]
     },
     "data": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/QrCode"
      }
     },
     "next_cursor": {
      "type": "string",
      "nullable": true,
      "description": "null = no more pages."
     }
    }
   },
   "BatchItemResult": {
    "type": "object",
    "required": [
     "status"
    ],
    "properties": {
     "status": {
      "type": "integer",
      "description": "201 created, 200 idempotent replay, or the error's HTTP status."
     },
     "data": {
      "allOf": [
       {
        "$ref": "#/components/schemas/QrCode"
       },
       {
        "type": "object",
        "properties": {
         "svg": {
          "type": "string",
          "description": "Full SVG document. Present only with ?include=svg (1000 px, 4-module quiet zone)."
         }
        }
       }
      ]
     },
     "error": {
      "$ref": "#/components/schemas/Error"
     }
    }
   },
   "Deleted": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "deleted": {
      "type": "boolean",
      "enum": [
       true
      ]
     }
    }
   },
   "KeyCount": {
    "type": "object",
    "required": [
     "key",
     "count"
    ],
    "properties": {
     "key": {
      "type": "string"
     },
     "count": {
      "type": "integer"
     }
    }
   },
   "DayRow": {
    "type": "object",
    "required": [
     "date",
     "scans",
     "unique_visitors"
    ],
    "properties": {
     "date": {
      "type": "string",
      "format": "date"
     },
     "scans": {
      "type": "integer"
     },
     "unique_visitors": {
      "type": "integer"
     }
    }
   },
   "RangeEcho": {
    "type": "object",
    "properties": {
     "from": {
      "type": "string",
      "format": "date"
     },
     "to": {
      "type": "string",
      "format": "date"
     },
     "tz": {
      "type": "string",
      "description": "Normalized: `UTC`, `+08:00` or canonical IANA name."
     },
     "start_utc": {
      "type": "integer",
      "description": "Unix seconds, inclusive."
     },
     "end_utc": {
      "type": "integer",
      "description": "Unix seconds, exclusive."
     }
    }
   },
   "QrCodeAnalytics": {
    "allOf": [
     {
      "$ref": "#/components/schemas/RangeEcho"
     },
     {
      "type": "object",
      "properties": {
       "id": {
        "type": "string"
       },
       "external_id": {
        "type": "string",
        "nullable": true
       },
       "scans": {
        "type": "integer",
        "description": "Non-bot scans in range."
       },
       "unique_visitors": {
        "type": "integer",
        "description": "Distinct visitors per day (privacy design: the same person on two days counts twice)."
       },
       "bot_scans": {
        "type": "integer"
       },
       "lifetime_scans": {
        "type": "integer"
       },
       "daily": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/DayRow"
        }
       },
       "countries": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        }
       },
       "regions": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "Keys are full ISO 3166-2 codes with the country prefix, e.g. `CA-ON`, `CN-SH`, `GB-ENG`; `unknown` when not known."
       },
       "timezones": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "Scanner time zones (IANA names such as `America/Toronto`), worked out from country + region. A coarse stand-in for location — not a city. `unknown` for scans without a region."
       },
       "devices": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        }
       },
       "os": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        }
       },
       "browsers": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        }
       },
       "referers": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        }
       },
       "apps": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "The app whose built-in browser opened the link: stable lowercase keys (`wechat`, `wecom`, `alipay`, `dingtalk`, `feishu`, `lark`, `qq`, `weibo`, `douyin`, `tiktok`, `xiaohongshu`, `baidu`, `messenger`, `instagram`, `threads`, `facebook`, `line`, `kakaotalk`, `naver`, `zalo`, `snapchat`, `x`, `linkedin`, `pinterest`, `telegram`, `whatsapp`, `google`; more may be added). `browser` = a normal browser (most phone-camera scans)."
       },
       "languages": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "Preferred browser language as a BCP 47 tag (`en-CA`, `zh-CN`, `zh-Hans-CN`)."
       },
       "sources": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "Placement tag from the short link's `?s=` parameter (e.g. `qrmk.cc/hzu23X?s=poster` → `poster`); `none` when absent. The tag never changes the redirect target."
       },
       "os_versions": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/KeyCount"
        },
        "description": "`<OS> <major version>`, e.g. `iOS 18`, `Android 10`. Browser-reported: some browsers freeze the version they report, so treat as approximate."
       },
       "hourly": {
        "type": "array",
        "minItems": 7,
        "maxItems": 7,
        "items": {
         "type": "array",
         "minItems": 24,
         "maxItems": 24,
         "items": {
          "type": "integer"
         }
        },
        "description": "Local-time heatmap `hourly[weekday][hour]`: weekday 0 = Monday … 6 = Sunday (ISO 8601), hour 0–23, in the **scanner's** local time (derived from their country + region, DST-aware — independent of the `tz` parameter)."
       },
       "hourly_unknown": {
        "type": "integer",
        "description": "Scans whose local time could not be determined (not in `hourly`). Sum of `hourly` + `hourly_unknown` = `scans`."
       }
      }
     }
    ]
   },
   "AnalyticsSummary": {
    "allOf": [
     {
      "$ref": "#/components/schemas/RangeEcho"
     },
     {
      "type": "object",
      "required": [
       "success",
       "group_by",
       "totals",
       "data",
       "next_cursor"
      ],
      "properties": {
       "success": {
        "type": "boolean",
        "enum": [
         true
        ]
       },
       "group_by": {
        "type": "string",
        "enum": [
         "code",
         "folder",
         "day"
        ]
       },
       "totals": {
        "type": "object",
        "properties": {
         "scans": {
          "type": "integer"
         },
         "unique_visitors": {
          "type": "integer"
         },
         "bot_scans": {
          "type": "integer"
         }
        }
       },
       "data": {
        "type": "array",
        "items": {
         "oneOf": [
          {
           "type": "object",
           "title": "group_by=code",
           "properties": {
            "id": {
             "type": "string"
            },
            "external_id": {
             "type": "string",
             "nullable": true
            },
            "name": {
             "type": "string"
            },
            "folder": {
             "type": "string",
             "nullable": true
            },
            "scans": {
             "type": "integer"
            },
            "unique_visitors": {
             "type": "integer"
            }
           }
          },
          {
           "type": "object",
           "title": "group_by=folder",
           "properties": {
            "folder": {
             "type": "string",
             "nullable": true
            },
            "codes": {
             "type": "integer"
            },
            "scans": {
             "type": "integer"
            },
            "unique_visitors": {
             "type": "integer"
            }
           }
          },
          {
           "$ref": "#/components/schemas/DayRow"
          }
         ]
        }
       },
       "next_cursor": {
        "type": "string",
        "nullable": true
       }
      }
     }
    ]
   },
   "ScanEvent": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "Monotonically increasing. De-duplicate by it."
     },
     "qrcode_id": {
      "type": "string"
     },
     "external_id": {
      "type": "string",
      "nullable": true
     },
     "scanned_at": {
      "type": "integer"
     },
     "country": {
      "type": "string",
      "nullable": true
     },
     "region": {
      "type": "string",
      "nullable": true,
      "description": "ISO 3166-2 subdivision code without the country prefix (e.g. `ON`); the country is in `country`."
     },
     "device": {
      "type": "string",
      "nullable": true,
      "enum": [
       "Mobile",
       "Desktop",
       "Tablet",
       "Unknown",
       null
      ]
     },
     "os": {
      "type": "string",
      "nullable": true
     },
     "browser": {
      "type": "string",
      "nullable": true
     },
     "referer": {
      "type": "string",
      "nullable": true
     },
     "variant": {
      "type": "string",
      "nullable": true,
      "description": "A/B routing variant, if any."
     },
     "visitor": {
      "type": "string",
      "nullable": true,
      "description": "Opaque per-day visitor id for de-duplication. Changes every day, differs across accounts, cannot be reversed to an IP."
     },
     "is_bot": {
      "type": "boolean"
     },
     "app": {
      "type": "string",
      "nullable": true,
      "description": "In-app browser key (see analytics `apps`); null = normal browser."
     },
     "lang": {
      "type": "string",
      "nullable": true,
      "description": "Preferred browser language (BCP 47)."
     },
     "os_version": {
      "type": "string",
      "nullable": true,
      "description": "Browser-reported OS major version (approximate)."
     },
     "browser_version": {
      "type": "string",
      "nullable": true,
      "description": "Browser major version."
     },
     "tz": {
      "type": "string",
      "nullable": true,
      "description": "The scanner's IANA time zone, derived from country + region."
     },
     "local_time": {
      "type": "string",
      "nullable": true,
      "description": "The scanner's local time as ISO 8601 with offset (DST-aware), e.g. `2026-09-10T13:30:00-04:00`; null when unknown.",
      "example": "2026-09-10T13:30:00-04:00"
     },
     "src": {
      "type": "string",
      "nullable": true,
      "description": "Placement tag from the short link's `?s=`; null when absent."
     }
    }
   },
   "ScanFeed": {
    "type": "object",
    "required": [
     "success",
     "data",
     "next_since_id",
     "has_more",
     "poll_after_seconds"
    ],
    "properties": {
     "success": {
      "type": "boolean",
      "enum": [
       true
      ]
     },
     "data": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ScanEvent"
      }
     },
     "next_since_id": {
      "type": "integer",
      "description": "Pass as since_id next time."
     },
     "has_more": {
      "type": "boolean"
     },
     "poll_after_seconds": {
      "type": "integer",
      "description": "30 normally, 0 when has_more."
     }
    }
   },
   "Usage": {
    "type": "object",
    "properties": {
     "month": {
      "type": "string",
      "example": "2026-09"
     },
     "api_calls": {
      "type": "integer"
     },
     "api_quota": {
      "type": "integer",
      "example": 50000
     },
     "rate_limit_per_minute": {
      "type": "integer",
      "example": 120
     },
     "plan": {
      "type": "string"
     },
     "plan_expires_at": {
      "type": "integer",
      "nullable": true
     }
    }
   },
   "BrandStyle": {
    "type": "object",
    "required": [
     "style",
     "configured"
    ],
    "properties": {
     "style": {
      "$ref": "#/components/schemas/Style"
     },
     "configured": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Style fields explicitly set on the account (the others are system defaults)."
     },
     "svg": {
      "type": "string",
      "description": "Only with `include=svg`: a preview of a sample link (not a real code)."
     }
    }
   }
  }
 }
}
