{
  "openapi": "3.1.0",
  "info": {
    "title": "QRTap REST API",
    "version": "1.0.0",
    "summary": "Managed QR codes and short links, public pages, guest reviews, booking requests, smart redirect rules, anonymous scan analytics, branded artifacts and real-time scan webhooks.",
    "description": "QRTap (https://app.qrtap.kg) is a managed QR code and short link platform. Every code encodes a stable short URL (`https://app.qrtap.kg/{slug}`); the destination behind it can be changed at any time via the API without reprinting. Scan analytics are aggregated and anonymous by design: visitor IP addresses and user-agent strings are never stored.\n\nAuthentication: send your organization's API key as `Authorization: Bearer <key>`. Keys are issued per organization, shown once at creation, and stored only as a SHA-256 hash. To get a key, contact the QRTap team via https://app.qrtap.kg/support.\n\nIdempotency: code creation is idempotent by `external_ref` — re-sending the same `external_ref` returns the existing code instead of creating a duplicate, so bulk calls are safe to retry.\n\nHuman documentation: https://app.qrtap.kg/developers\n\nRate limiting: every `/api/v1` endpoint allows **120 requests per minute per API key** (fixed one-minute window). Over the limit the API answers `429` with `{\"error\":\"rate_limited\"}` and a `Retry-After` header holding the number of seconds until the window resets; `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (epoch seconds) accompany it. Webhook management routes (`/api/v1/webhooks*`) are exempt. The counter lives in the memory of the process that serves the request: with several web instances behind a load balancer the effective ceiling is 120 x instances, so treat the documented number as the guaranteed floor and always honour `Retry-After` rather than assuming an exact global budget.",
    "contact": {
      "name": "QRTap support",
      "url": "https://app.qrtap.kg/support",
      "email": "ceo@qrtap.kg"
    },
    "termsOfService": "https://app.qrtap.kg/oferta"
  },
  "servers": [
    {
      "url": "https://app.qrtap.kg",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "codes",
      "description": "Managed QR codes / short links: create, list, retarget, history."
    },
    {
      "name": "stats",
      "description": "Aggregated anonymous scan analytics (bots excluded; no IP / user-agent stored)."
    },
    {
      "name": "artifacts",
      "description": "Rendered QR artifacts: PNG (decode-verified), SVG (true vector), PDF (print)."
    },
    {
      "name": "design-templates",
      "description": "Reusable branded looks (module colour + centre logo) applied to codes."
    },
    {
      "name": "webhooks",
      "description": "Real-time scan webhook endpoints (HMAC-signed deliveries)."
    },
    {
      "name": "pages",
      "description": "Public pages (mobile business cards served at /a/{slug}): create, read, update, publish."
    },
    {
      "name": "reviews",
      "description": "Guest reviews left on public pages: moderation queue, publish / hide."
    },
    {
      "name": "bookings",
      "description": "Booking requests left on public pages: inbox and the owner's decision."
    },
    {
      "name": "smart-links",
      "description": "Conditional redirect rules evaluated on every scan (device, language, local hours)."
    }
  ],
  "paths": {
    "/api/v1/codes": {
      "post": {
        "tags": [
          "codes"
        ],
        "operationId": "createCode",
        "summary": "Create a managed code",
        "description": "Creates one managed QR code / short link. The response contains `short_url` — the stable printed link encoded in the QR; it never changes when you retarget. Creation is idempotent by `external_ref`: re-sending the same `external_ref` returns the existing (non-archived) code without touching its destination.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCodeRequest"
              },
              "example": {
                "target_url": "https://example.com/menu",
                "label": "Menu table 5",
                "external_ref": "SMK-P-001",
                "group": "partner"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Code created (or an existing code returned for a repeated `external_ref`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code"
                },
                "example": {
                  "id": "b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10",
                  "label": "Menu table 5",
                  "target_url": "https://example.com/menu",
                  "external_ref": "SMK-P-001",
                  "group": "partner",
                  "state": "deployed",
                  "design_template_id": null,
                  "slug": "k3v9tqzx",
                  "short_url": "https://app.qrtap.kg/k3v9tqzx",
                  "png_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=png",
                  "svg_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=svg",
                  "created_at": "2026-07-01T08:00:00.000Z",
                  "updated_at": "2026-07-20T10:15:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed. `error` is one of `invalid_request` (schema violation, see `detail`), `invalid_url` (target_url is not a valid http(s) URL), `unknown_design_template` (design_template_id does not belong to your organization).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_url"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "codes"
        ],
        "operationId": "listCodes",
        "summary": "List / filter codes",
        "description": "Lists non-archived codes of your organization, newest first. Filter by `group` and/or `external_ref`. Add `include=stats` to get each code's all-time non-bot scan total in the same response — one call to sync all counters instead of N per-code `/stats` requests.",
        "parameters": [
          {
            "name": "group",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Return only codes with this group (exact match)."
          },
          {
            "name": "external_ref",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Return only codes with this external_ref (exact match)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Page size. Values are clamped to 1..500."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Pagination offset."
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "stats"
              ]
            },
            "description": "`include=stats` adds an all-time `scans` counter to every code."
          }
        ],
        "responses": {
          "200": {
            "description": "Codes, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Code"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/bulk": {
      "post": {
        "tags": [
          "codes"
        ],
        "operationId": "createCodesBulk",
        "summary": "Create a batch of codes (max 500)",
        "description": "Creates up to 500 codes in one call. Results are per-item: one bad item does not fail the batch. Idempotent by `external_ref`, so the whole call is safe to retry.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "minItems": 1,
                "maxItems": 500,
                "items": {
                  "$ref": "#/components/schemas/CreateCodeRequest"
                }
              },
              "example": [
                {
                  "target_url": "https://example.com/p/1",
                  "external_ref": "P-001",
                  "group": "partner"
                },
                {
                  "target_url": "https://example.com/p/2",
                  "external_ref": "P-002",
                  "group": "partner"
                }
              ]
            }
          }
        },
        "responses": {
          "201": {
            "description": "Per-item results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "created",
                    "failed",
                    "data"
                  ],
                  "properties": {
                    "created": {
                      "type": "integer",
                      "description": "Number of items with ok=true."
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Number of items with ok=false."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "$ref": "#/components/schemas/BulkItemOk"
                          },
                          {
                            "$ref": "#/components/schemas/BulkItemFailed"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The whole payload failed validation (not an array, empty, or over 500 items).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/{id}": {
      "get": {
        "tags": [
          "codes"
        ],
        "operationId": "getCode",
        "summary": "Get one code",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          }
        ],
        "responses": {
          "200": {
            "description": "The code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code"
                },
                "example": {
                  "id": "b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10",
                  "label": "Menu table 5",
                  "target_url": "https://example.com/menu",
                  "external_ref": "SMK-P-001",
                  "group": "partner",
                  "state": "deployed",
                  "design_template_id": null,
                  "slug": "k3v9tqzx",
                  "short_url": "https://app.qrtap.kg/k3v9tqzx",
                  "png_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=png",
                  "svg_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=svg",
                  "created_at": "2026-07-01T08:00:00.000Z",
                  "updated_at": "2026-07-20T10:15:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "codes"
        ],
        "operationId": "changeDestination",
        "summary": "Change destination (retarget)",
        "description": "Points the code at a new URL. The printed QR and `short_url` are unchanged — a new destination version becomes active immediately; the previous one is kept in history.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_url"
                ],
                "properties": {
                  "target_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "New destination. http(s) only."
                  }
                }
              },
              "example": {
                "target_url": "https://example.com/new-landing"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated code (with the new target_url).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_id`, `invalid_request` (see `detail`) or `invalid_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/{id}/history": {
      "get": {
        "tags": [
          "codes"
        ],
        "operationId": "getCodeHistory",
        "summary": "Destination history (audit trail)",
        "description": "Every destination the code has pointed at, newest first. The printed code never changed — only the target did.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          }
        ],
        "responses": {
          "200": {
            "description": "Destination versions, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/HistoryEntry"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "version": 2,
                      "target_url": "https://example.com/new-landing",
                      "reason": "Смена назначения через API",
                      "status": "active",
                      "activated_at": "2026-07-20T10:15:00.000Z",
                      "created_at": "2026-07-20T10:15:00.000Z"
                    },
                    {
                      "version": 1,
                      "target_url": "https://example.com/menu",
                      "reason": "Создан через API",
                      "status": "superseded",
                      "activated_at": "2026-07-01T08:00:00.000Z",
                      "created_at": "2026-07-01T08:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/{id}/stats": {
      "get": {
        "tags": [
          "stats"
        ],
        "operationId": "getCodeStats",
        "summary": "Scan stats for one code",
        "description": "Aggregated, anonymous scan analytics for one code: total, per-day series and device split. Bots are excluded. No visitor IP or user-agent is ever stored.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregates for the requested range (all time when from/to omitted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodeStats"
                },
                "example": {
                  "total": 128,
                  "by_day": [
                    {
                      "day": "2026-07-19",
                      "count": 60
                    },
                    {
                      "day": "2026-07-20",
                      "count": 68
                    }
                  ],
                  "by_device": [
                    {
                      "device": "mobile",
                      "count": 110
                    },
                    {
                      "device": "desktop",
                      "count": 18
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/{id}/qr": {
      "get": {
        "tags": [
          "artifacts"
        ],
        "operationId": "renderCodeArtifact",
        "summary": "Render the QR artifact (PNG / SVG / PDF)",
        "description": "Renders the code's QR artifact. The QR always encodes the stable short URL, never the destination. PNG output is decode-verified before return; when the code has a design template, the branded render is rasterised and re-decoded, and the logo is scaled back or dropped if it hurts scannability. The `X-QRTap-QA` response header reports the outcome: `branded` (logo at requested size), `downgraded` (logo shrunk/dropped to keep the code scannable) or `plain` (no template). Captions are drawn into SVG output only. Caption text is screened against Kyrgyz advertising law (Article 17, Law No. 121 'On advertising'): promo wording (discounts, bonuses, gifts, promo codes) is refused with 422 before any render.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "png",
                "svg",
                "pdf"
              ],
              "default": "png"
            },
            "description": "png — raster for sharing/screens (decode-verified); svg — true vector, styled when the code has a design template; pdf — single-page vector for print shops."
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 256,
              "maximum": 3000,
              "default": 1024
            },
            "description": "PNG pixel width (ignored for svg/pdf). ~1080 for social sharing, ~2000 for print."
          },
          {
            "name": "caption",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 48
            },
            "description": "Short label drawn under the code (SVG output). Screened for prohibited promo wording (422 on match)."
          },
          {
            "name": "errorCorrection",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "M",
                "Q",
                "H"
              ],
              "default": "Q"
            },
            "description": "QR error-correction level for plain (non-templated) renders."
          }
        ],
        "responses": {
          "200": {
            "description": "The rendered artifact. `Cache-Control: public, max-age=86400`.",
            "headers": {
              "X-QRTap-QA": {
                "description": "Decode-QA outcome: branded | downgraded | plain.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "branded",
                    "downgraded",
                    "plain"
                  ]
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_id` or `invalid_request` (bad format/size/errorCorrection).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The caption contains promo wording prohibited by Article 17 of the Kyrgyz advertising law (stocks, discounts, bonuses, gifts, promo codes). The artifact is not rendered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "prohibited_content",
                  "detail": "Подпись содержит запрещённое стимулирование (ст. 17 Закона КР «О рекламе»): уберите упоминания акций, скидок, бонусов, подарков и промокодов.",
                  "matched": "скидка"
                }
              }
            }
          },
          "500": {
            "description": "Decode verification of the rendered PNG failed (`qa_failed`). Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/groups/{group}/stats": {
      "get": {
        "tags": [
          "stats"
        ],
        "operationId": "getGroupStats",
        "summary": "Aggregate scan stats for a whole group",
        "description": "Aggregates scans across every non-archived code with the given group, plus the number of codes in the group. Canonical groups: partner, equipment, event_menu, bag_insert, training.",
        "parameters": [
          {
            "name": "group",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Group name (codes are created with one of: partner, equipment, event_menu, bag_insert, training)."
          },
          {
            "$ref": "#/components/parameters/StatsFrom"
          },
          {
            "$ref": "#/components/parameters/StatsTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregates across the group (empty aggregates when the group has no codes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupStats"
                },
                "example": {
                  "group": "partner",
                  "codes": 50,
                  "total": 1240,
                  "by_day": [
                    {
                      "day": "2026-07-20",
                      "count": 82
                    }
                  ],
                  "by_device": [
                    {
                      "device": "mobile",
                      "count": 1100
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_group` — empty group segment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/design-templates": {
      "post": {
        "tags": [
          "design-templates"
        ],
        "operationId": "createDesignTemplate",
        "summary": "Create a branded design template",
        "description": "A reusable look (brand module colour + centre logo + default caption) referenced by codes via `design_template_id`. The logo is accepted only as a data URI (never a URL — no server-side fetching), then sanitised and re-encoded server-side to a bounded clean PNG (metadata/EXIF stripped). A decode-QA gate protects readability at render time: if the logo or colour breaks the read, the logo is shrunk or dropped.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDesignTemplateRequest"
              },
              "example": {
                "name": "Partner brand",
                "module_color": "#16613F",
                "logo": "data:image/png;base64,iVBORw0KGgo...",
                "caption": "Наведите камеру"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created. Logo bytes are never echoed back — only `has_logo`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DesignTemplate"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (schema violation, see `detail`) or `invalid_image` (the logo failed sanitisation).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "design-templates"
        ],
        "operationId": "listDesignTemplates",
        "summary": "List design templates",
        "description": "All templates of your organization, newest first. Logo bytes are not included.",
        "responses": {
          "200": {
            "description": "Templates.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DesignTemplate"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "operationId": "registerWebhook",
        "summary": "Register a scan webhook endpoint",
        "description": "Registers an HTTPS endpoint that receives a POST for every scan of your organization's codes (see the `scan` webhook definition). The response contains `signing_secret` exactly once — store it to verify the `X-QRTap-Signature` header on deliveries.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Delivery URL. Must be https://."
                  }
                }
              },
              "example": {
                "url": "https://example.com/qrtap/webhook"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Endpoint registered. `signing_secret` is shown only in this response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointCreated"
                },
                "example": {
                  "id": "3f0e8a52-6f3e-4b7f-9d2a-1c6f6a2e9b10",
                  "url": "https://example.com/qrtap/webhook",
                  "active": true,
                  "signing_secret": "whsec_0000000000000000000000000000000000000000000000fake",
                  "created_at": "2026-07-20T10:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (see `detail`) or `invalid_url` (not https).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "get": {
        "tags": [
          "webhooks"
        ],
        "operationId": "listWebhooks",
        "summary": "List webhook endpoints",
        "description": "All endpoints of your organization, newest first. Signing secrets are never returned.",
        "responses": {
          "200": {
            "description": "Endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "operationId": "deactivateWebhook",
        "summary": "Deactivate a webhook endpoint",
        "description": "Stops deliveries to this endpoint. Returns `{\"deactivated\": true}` on success; 404 with `{\"deactivated\": false}` when the endpoint does not exist or is already inactive.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Webhook endpoint id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeactivateResult"
                },
                "example": {
                  "deactivated": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Endpoint not found or already inactive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeactivateResult"
                },
                "example": {
                  "deactivated": false
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pages": {
      "get": {
        "tags": [
          "pages"
        ],
        "operationId": "listPages",
        "summary": "List public pages",
        "description": "Lists the public pages of your organization, newest version first, without the content blob (fetch one page to read it). Today a workspace has at most one page; the list shape is the API contract, so more page kinds will not be a breaking change.",
        "responses": {
          "200": {
            "description": "Pages of your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PageSummary"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "9d1c1f0a-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
                      "slug": "cozy-cafe-x1",
                      "title": "Кофейня «Тандем»",
                      "segment": "otzyvy",
                      "theme": "emerald",
                      "status": "published",
                      "published": true,
                      "version": 3,
                      "public_url": "https://app.qrtap.kg/a/cozy-cafe-x1",
                      "created_at": "2026-07-01T08:00:00.000Z",
                      "updated_at": "2026-07-20T10:15:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "pages"
        ],
        "operationId": "createPage",
        "summary": "Create and publish a page",
        "description": "Creates the organization's public page and publishes it immediately. The response carries `slug` and `public_url` — point a QR code at that `public_url` (POST /api/v1/codes) to get a printable card.\n\nRepeating the call updates the SAME page with a new version instead of creating a second one, and never changes the slug, so an agent may retry safely and printed codes keep working. Content is sanitised server-side; a payload without a name or without any contact is refused with 422.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageWriteRequest"
              },
              "example": {
                "content": {
                  "segment": "otzyvy",
                  "theme": "emerald",
                  "name": "Кофейня «Тандем»",
                  "specialty": "Спешелти-кофе в центре Бишкека",
                  "phone": "0555 123 456",
                  "hours": "ежедневно 8:00–22:00",
                  "address": "Бишкек, ул. Киевская 100",
                  "services": [
                    {
                      "name": "Фильтр-кофе",
                      "price": "180 сом"
                    }
                  ],
                  "reviewLinks": {
                    "twogis": "https://2gis.kg/bishkek/firm/70000001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Page created (or a new version of the existing page) and published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "id": "9d1c1f0a-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
                  "slug": "cozy-cafe-x1",
                  "title": "Кофейня «Тандем»",
                  "segment": "otzyvy",
                  "theme": "emerald",
                  "status": "published",
                  "published": true,
                  "version": 3,
                  "public_url": "https://app.qrtap.kg/a/cozy-cafe-x1",
                  "created_at": "2026-07-01T08:00:00.000Z",
                  "updated_at": "2026-07-20T10:15:00.000Z",
                  "content": {
                    "kind": "profile",
                    "segment": "otzyvy",
                    "theme": "emerald",
                    "name": "Кофейня «Тандем»",
                    "specialty": "Спешелти-кофе в центре Бишкека",
                    "bio": "",
                    "phone": "+996555123456",
                    "whatsapp": "https://wa.me/996555123456",
                    "instagram": "https://instagram.com/tandem.coffee",
                    "twogis": "",
                    "bookingUrl": "",
                    "services": [
                      {
                        "name": "Фильтр-кофе",
                        "price": "180 сом"
                      }
                    ],
                    "hours": "ежедневно 8:00–22:00",
                    "address": "Бишкек, ул. Киевская 100",
                    "reviewLinks": {
                      "twogis": "https://2gis.kg/bishkek/firm/70000001"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not `{\"content\": {…}}` (see `detail`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/InvalidContent"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/pages/{slug}": {
      "get": {
        "tags": [
          "pages"
        ],
        "operationId": "getPage",
        "summary": "Get one page with its content",
        "parameters": [
          {
            "$ref": "#/components/parameters/PageSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "The page, including its current content.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "id": "9d1c1f0a-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
                  "slug": "cozy-cafe-x1",
                  "title": "Кофейня «Тандем»",
                  "segment": "otzyvy",
                  "theme": "emerald",
                  "status": "published",
                  "published": true,
                  "version": 3,
                  "public_url": "https://app.qrtap.kg/a/cozy-cafe-x1",
                  "created_at": "2026-07-01T08:00:00.000Z",
                  "updated_at": "2026-07-20T10:15:00.000Z",
                  "content": {
                    "kind": "profile",
                    "segment": "otzyvy",
                    "theme": "emerald",
                    "name": "Кофейня «Тандем»",
                    "specialty": "Спешелти-кофе в центре Бишкека",
                    "bio": "",
                    "phone": "+996555123456",
                    "whatsapp": "https://wa.me/996555123456",
                    "instagram": "https://instagram.com/tandem.coffee",
                    "twogis": "",
                    "bookingUrl": "",
                    "services": [
                      {
                        "name": "Фильтр-кофе",
                        "price": "180 сом"
                      }
                    ],
                    "hours": "ежедневно 8:00–22:00",
                    "address": "Бишкек, ул. Киевская 100",
                    "reviewLinks": {
                      "twogis": "https://2gis.kg/bishkek/firm/70000001"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidPageSlug"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "pages"
        ],
        "operationId": "updatePage",
        "summary": "Update page content",
        "description": "Replaces the page content with a new published version. The body replaces the content wholesale — GET the page first if you only mean to change one field. The slug and `public_url` never change, so printed QR codes stay valid. A slug that belongs to another organization answers 404, never 403.",
        "parameters": [
          {
            "$ref": "#/components/parameters/PageSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageWriteRequest"
              },
              "example": {
                "content": {
                  "kind": "profile",
                  "segment": "otzyvy",
                  "theme": "emerald",
                  "name": "Кофейня «Тандем»",
                  "specialty": "Спешелти-кофе в центре Бишкека",
                  "bio": "",
                  "phone": "+996555123456",
                  "whatsapp": "https://wa.me/996555123456",
                  "instagram": "https://instagram.com/tandem.coffee",
                  "twogis": "",
                  "bookingUrl": "",
                  "services": [
                    {
                      "name": "Фильтр-кофе",
                      "price": "180 сом"
                    }
                  ],
                  "hours": "ежедневно 8:00–22:00",
                  "address": "Бишкек, ул. Киевская 100",
                  "reviewLinks": {
                    "twogis": "https://2gis.kg/bishkek/firm/70000001"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated page (version incremented).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "id": "9d1c1f0a-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
                  "slug": "cozy-cafe-x1",
                  "title": "Кофейня «Тандем»",
                  "segment": "otzyvy",
                  "theme": "emerald",
                  "status": "published",
                  "published": true,
                  "version": 4,
                  "public_url": "https://app.qrtap.kg/a/cozy-cafe-x1",
                  "created_at": "2026-07-01T08:00:00.000Z",
                  "updated_at": "2026-07-20T10:15:00.000Z",
                  "content": {
                    "kind": "profile",
                    "segment": "otzyvy",
                    "theme": "emerald",
                    "name": "Кофейня «Тандем»",
                    "specialty": "Спешелти-кофе в центре Бишкека",
                    "bio": "",
                    "phone": "+996555123456",
                    "whatsapp": "https://wa.me/996555123456",
                    "instagram": "https://instagram.com/tandem.coffee",
                    "twogis": "",
                    "bookingUrl": "",
                    "services": [
                      {
                        "name": "Фильтр-кофе",
                        "price": "180 сом"
                      }
                    ],
                    "hours": "ежедневно 8:00–22:00",
                    "address": "Бишкек, ул. Киевская 100",
                    "reviewLinks": {
                      "twogis": "https://2gis.kg/bishkek/firm/70000001"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidPageSlug"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/InvalidContent"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/reviews": {
      "get": {
        "tags": [
          "reviews"
        ],
        "operationId": "listReviews",
        "summary": "List guest reviews",
        "description": "Guest reviews left on your public pages, newest first (max 200). Nothing a guest submits is visible until it is published, so `pending` — the default — is the moderation queue. Only the name and text the guest typed are stored: no IP, no user-agent, no identifier of any kind.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ReviewStatusFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "Reviews in the requested bucket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Review"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "55555555-5555-4555-8555-555555555555",
                      "author_name": "Айгуль",
                      "body": "Очень уютно, спасибо!",
                      "status": "pending",
                      "created_at": "2026-07-20T10:15:00.000Z",
                      "moderated_at": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`status` is not one of pending | published | hidden.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "detail": "status must be pending, published or hidden."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/reviews/{id}/moderate": {
      "post": {
        "tags": [
          "reviews"
        ],
        "operationId": "moderateReview",
        "summary": "Publish or hide a review",
        "description": "The only way a guest review becomes visible on the public page. Publishing takes effect immediately. A review id from another organization answers 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Review id (UUID) from GET /api/v1/reviews."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModerateReviewRequest"
              },
              "example": {
                "action": "publish"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The review in its new state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Review"
                },
                "example": {
                  "id": "55555555-5555-4555-8555-555555555555",
                  "author_name": "Айгуль",
                  "body": "Очень уютно, спасибо!",
                  "status": "published",
                  "created_at": "2026-07-20T10:15:00.000Z",
                  "moderated_at": "2026-07-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "`action` missing or not publish | hide, or the id is not a UUID (`invalid_id`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/bookings": {
      "get": {
        "tags": [
          "bookings"
        ],
        "operationId": "listBookings",
        "summary": "List booking requests",
        "description": "Booking requests left by guests on your public pages, newest first (max 200). `status=new` is the inbox that still needs a decision. `preferred_at` is free text as the guest typed it and is deliberately never parsed — read it and call the guest back. Only what the guest typed is stored.",
        "parameters": [
          {
            "$ref": "#/components/parameters/BookingStatusFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "Booking requests.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Booking"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "66666666-6666-4666-8666-666666666666",
                      "service": "Маникюр",
                      "preferred_at": "завтра после 14:00",
                      "guest_name": "Айгуль",
                      "guest_phone": "0555 123 456",
                      "status": "new",
                      "created_at": "2026-07-20T10:15:00.000Z",
                      "updated_at": "2026-07-20T10:15:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`status` is not one of new | confirmed | declined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "detail": "status must be new, confirmed or declined."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/bookings/{id}/status": {
      "post": {
        "tags": [
          "bookings"
        ],
        "operationId": "setBookingStatus",
        "summary": "Confirm or decline a booking request",
        "description": "Records the owner's decision. QRTap does not notify the guest — the phone number on the request is how the business follows up. `new` is not accepted: a decided request is never reopened through the API. A request from another organization answers 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Booking request id (UUID) from GET /api/v1/bookings."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingDecisionRequest"
              },
              "example": {
                "status": "confirmed"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request in its new state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                },
                "example": {
                  "id": "66666666-6666-4666-8666-666666666666",
                  "service": "Маникюр",
                  "preferred_at": "завтра после 14:00",
                  "guest_name": "Айгуль",
                  "guest_phone": "0555 123 456",
                  "status": "confirmed",
                  "created_at": "2026-07-20T10:15:00.000Z",
                  "updated_at": "2026-07-21T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "`status` missing or not confirmed | declined, or the id is not a UUID (`invalid_id`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/codes/{id}/smart-rules": {
      "get": {
        "tags": [
          "smart-links"
        ],
        "operationId": "getSmartRules",
        "summary": "Get a code's smart rules",
        "description": "Returns the code's current conditional redirect rules; an empty array means the code always uses its primary destination.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          }
        ],
        "responses": {
          "200": {
            "description": "Current rule set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SmartRulesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "put": {
        "tags": [
          "smart-links"
        ],
        "operationId": "setSmartRules",
        "summary": "Replace a code's smart rules",
        "description": "Replaces the code's rule set (max 5 rules). On every scan the rules are evaluated in order and the FIRST match wins; if nothing matches — or the stored set is ever unreadable — the redirect falls back to the code's primary destination, so a bad rule can never break a printed code. `rules: []` or `rules: null` clears the set.\n\nConditions: `{\"kind\":\"device\",\"in\":[\"ios\",\"android\",\"desktop\",\"other\"]}`, `{\"kind\":\"lang\",\"in\":[\"ru\",\"ky\"]}`, `{\"kind\":\"hours\",\"tz\":\"Asia/Bishkek\",\"from\":\"09:00\",\"to\":\"18:00\",\"days\":[1,2,3,4,5]}` (`to` exclusive; from > to crosses midnight; days are ISO 1=Mon..7=Sun). Evaluation uses coarse classified signals only — raw IP, user-agent and Accept-Language are never stored. Smart rules require a paid plan (402 otherwise).",
        "parameters": [
          {
            "$ref": "#/components/parameters/CodeId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmartRulesRequest"
              },
              "example": {
                "rules": [
                  {
                    "if": {
                      "kind": "device",
                      "in": [
                        "ios"
                      ]
                    },
                    "to": "https://apps.apple.com/app/id1"
                  },
                  {
                    "if": {
                      "kind": "device",
                      "in": [
                        "android"
                      ]
                    },
                    "to": "https://play.google.com/store/apps/details?id=kg.qrtap"
                  },
                  {
                    "if": {
                      "kind": "hours",
                      "tz": "Asia/Bishkek",
                      "from": "22:00",
                      "to": "09:00",
                      "days": [
                        1,
                        2,
                        3,
                        4,
                        5
                      ]
                    },
                    "to": "https://example.com/night-menu"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored (normalised) rule set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SmartRulesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Body is not `{\"rules\": […] | null}` (`invalid_request`), the id is not a UUID (`invalid_id`), or a rule target is not an http(s) URL (`invalid_url`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Smart rules require a paid plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "plan_required",
                  "detail": "Smart rules require a paid plan."
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "A rule is structurally invalid (unknown condition kind, bad time, unknown timezone, more than 5 rules …).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_rules",
                  "detail": "Правило 1: неизвестный тип условия."
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "scan": {
      "post": {
        "operationId": "scanEventDelivery",
        "summary": "Scan event delivered to your registered endpoint",
        "description": "Sent asynchronously for every scan of your organization's codes while at least one endpoint is active. Delivery is at-least-once: design your handler to be idempotent. Reply with any 2xx within 8 seconds to acknowledge; any other status or a timeout is retried with exponential backoff (roughly 2^attempt minutes, capped at 12 hours) for up to 8 attempts, after which the event is dropped.\n\nVerify authenticity: compute HMAC-SHA256 of the raw request body using your endpoint's `signing_secret` and compare the hex digest with the value after `sha256=` in the `X-QRTap-Signature` header.\n\nPrivacy: the payload carries no personal data — only the code identity, your references, a timestamp and a coarse device class.",
        "parameters": [
          {
            "name": "X-QRTap-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "scan"
            }
          },
          {
            "name": "X-QRTap-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`sha256=<hex>` where <hex> = HMAC-SHA256(raw_body, signing_secret)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanEvent"
              },
              "example": {
                "code_id": "b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10",
                "external_ref": "SMK-P-001",
                "group": "partner",
                "ts": "2026-07-20T12:34:56.000Z",
                "device_type": "mobile"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx acknowledges the delivery."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Organization API key: `Authorization: Bearer qrtap_key_XXXX`. Issued per organization by the QRTap team (https://app.qrtap.kg/support); shown once, stored as SHA-256 hash, revocable."
      }
    },
    "parameters": {
      "CodeId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Code id (UUID) as returned at creation."
      },
      "StatsFrom": {
        "name": "from",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Range start (inclusive), ISO 8601 timestamp. Omit for all time."
      },
      "StatsTo": {
        "name": "to",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Range end (exclusive), ISO 8601 timestamp."
      },
      "PageSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9-]{3,64}$"
        },
        "description": "Page slug as minted by the platform (the /a/{slug} path segment)."
      },
      "ReviewStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "published",
            "hidden"
          ],
          "default": "pending"
        },
        "description": "Which bucket to list. Defaults to `pending` — the moderation queue."
      },
      "BookingStatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "new",
            "confirmed",
            "declined"
          ]
        },
        "description": "Optional filter. Omit to list every request."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized",
              "detail": "Invalid or revoked API key."
            }
          }
        }
      },
      "NotFound": {
        "description": "No such object in your organization.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found"
            }
          }
        }
      },
      "InvalidId": {
        "description": "The id path segment is not a UUID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_id"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded: more than 120 requests in one minute with this API key. Wait `Retry-After` seconds and repeat — the request had no effect.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the current window resets.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          "X-RateLimit-Limit": {
            "description": "Requests allowed per window per API key.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window (0 on a 429).",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Epoch seconds at which the current window ends.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "rate_limited",
              "detail": "Too many requests: at most 120 per minute per API key. Retry in 37s."
            }
          }
        }
      },
      "InvalidContent": {
        "description": "The page content was refused by the server-side sanitizer (see `detail`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_content",
              "detail": "Укажите имя — это заголовок страницы."
            }
          }
        }
      },
      "InvalidPageSlug": {
        "description": "The slug path segment is malformed (lowercase a-z, 0-9 and dashes, 3..64 chars), or the body is not `{\"content\": {…}}`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_id"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code, e.g. unauthorized, invalid_request, invalid_url, invalid_id, not_found, unknown_design_template, invalid_image, invalid_group, prohibited_content, qa_failed."
          },
          "detail": {
            "description": "Human-readable detail or an array of validation issues (for invalid_request)."
          },
          "matched": {
            "type": "string",
            "description": "For prohibited_content: the promo wording that triggered the refusal."
          }
        },
        "additionalProperties": true
      },
      "CreateCodeRequest": {
        "type": "object",
        "required": [
          "target_url"
        ],
        "properties": {
          "target_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Destination URL the code redirects to. http(s) only. Can be changed later without reprinting."
          },
          "label": {
            "type": "string",
            "maxLength": 160,
            "description": "Human-readable title shown in the cabinet. Defaults to \"Код\"."
          },
          "external_ref": {
            "type": "string",
            "maxLength": 200,
            "description": "Your identifier for this code (partner id, SKU, event id). Unique per organization among non-archived codes; creation is idempotent by this field."
          },
          "group": {
            "type": "string",
            "enum": [
              "partner",
              "equipment",
              "event_menu",
              "bag_insert",
              "training"
            ],
            "description": "Code type used to slice analytics."
          },
          "design_template_id": {
            "type": "string",
            "format": "uuid",
            "description": "Apply a branded design template (must belong to your organization)."
          }
        }
      },
      "Code": {
        "type": "object",
        "required": [
          "id",
          "label",
          "target_url",
          "external_ref",
          "group",
          "state",
          "design_template_id",
          "slug",
          "short_url",
          "png_url",
          "svg_url",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string"
          },
          "target_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current destination URL."
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "group": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": "string",
            "description": "Lifecycle state; API-created codes are `deployed`."
          },
          "design_template_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "Short-link slug. The QR encodes short_url = base + / + slug."
          },
          "short_url": {
            "type": "string",
            "format": "uri",
            "description": "Stable printed link encoded in the QR. Never changes on retarget."
          },
          "png_url": {
            "type": "string",
            "format": "uri",
            "description": "Artifact URL (PNG). Requires the same Bearer auth."
          },
          "svg_url": {
            "type": "string",
            "format": "uri",
            "description": "Artifact URL (SVG). Requires the same Bearer auth."
          },
          "scans": {
            "type": "integer",
            "description": "All-time non-bot scan count. Present only when listing with include=stats."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BulkItemOk": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "ok"
            ],
            "properties": {
              "ok": {
                "const": true
              }
            }
          },
          {
            "$ref": "#/components/schemas/Code"
          }
        ]
      },
      "BulkItemFailed": {
        "type": "object",
        "required": [
          "ok",
          "external_ref",
          "error"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {
            "type": "string",
            "description": "Lower-cased error code, e.g. invalid_url, unknown_design_template."
          }
        }
      },
      "HistoryEntry": {
        "type": "object",
        "required": [
          "version",
          "target_url",
          "reason",
          "status",
          "activated_at",
          "created_at"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "1-based destination version."
          },
          "target_url": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Why the version was created."
          },
          "status": {
            "type": "string",
            "description": "active | superseded."
          },
          "activated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DayCount": {
        "type": "object",
        "required": [
          "day",
          "count"
        ],
        "properties": {
          "day": {
            "type": "string",
            "description": "YYYY-MM-DD."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "DeviceCount": {
        "type": "object",
        "required": [
          "device",
          "count"
        ],
        "properties": {
          "device": {
            "type": "string",
            "description": "Coarse device class: mobile | tablet | desktop | other | unknown."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "CodeStats": {
        "type": "object",
        "required": [
          "total",
          "by_day",
          "by_device"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Non-bot scans in range."
          },
          "by_day": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DayCount"
            }
          },
          "by_device": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeviceCount"
            }
          }
        }
      },
      "GroupStats": {
        "type": "object",
        "required": [
          "group",
          "codes",
          "total",
          "by_day",
          "by_device"
        ],
        "properties": {
          "group": {
            "type": "string"
          },
          "codes": {
            "type": "integer",
            "description": "Non-archived codes in the group."
          },
          "total": {
            "type": "integer"
          },
          "by_day": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DayCount"
            }
          },
          "by_device": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeviceCount"
            }
          }
        }
      },
      "CreateDesignTemplateRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "module_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Brand module colour (dark hex, e.g. #16613F). Applied only when it keeps contrast; QA has the final say."
          },
          "logo": {
            "type": "string",
            "maxLength": 2800000,
            "description": "Centre logo/image as a data URI: data:image/(png|jpeg|webp);base64,... Never a URL. Sanitised and re-encoded server-side."
          },
          "caption": {
            "type": "string",
            "maxLength": 80
          }
        }
      },
      "DesignTemplate": {
        "type": "object",
        "required": [
          "id",
          "name",
          "module_color",
          "caption",
          "has_logo",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "module_color": {
            "type": [
              "string",
              "null"
            ]
          },
          "caption": {
            "type": [
              "string",
              "null"
            ]
          },
          "has_logo": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "url",
          "active",
          "created_at",
          "last_delivery_at",
          "failure_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_delivery_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "failure_count": {
            "type": "integer"
          }
        }
      },
      "WebhookEndpointCreated": {
        "type": "object",
        "required": [
          "id",
          "url",
          "active",
          "signing_secret",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "active": {
            "type": "boolean"
          },
          "signing_secret": {
            "type": "string",
            "description": "whsec_-prefixed secret for verifying X-QRTap-Signature. Shown only once."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDeactivateResult": {
        "type": "object",
        "required": [
          "deactivated"
        ],
        "properties": {
          "deactivated": {
            "type": "boolean"
          }
        }
      },
      "ScanEvent": {
        "type": "object",
        "required": [
          "code_id",
          "external_ref",
          "group",
          "ts",
          "device_type"
        ],
        "properties": {
          "code_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "group": {
            "type": [
              "string",
              "null"
            ]
          },
          "ts": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "device_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "mobile | tablet | desktop | other."
          }
        }
      },
      "ProfileService": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Service name."
          },
          "price": {
            "type": "string",
            "maxLength": 40,
            "description": "Price as free text, e.g. \"1500 сом\"."
          }
        }
      },
      "PageContent": {
        "type": "object",
        "description": "Content of a public page. Sanitised server-side on every write by the same authority the workspace builder uses: unknown keys are dropped, phone numbers normalised to +digits, WhatsApp/Instagram rewritten to canonical links, and only http/https URLs survive. `name` is required, and at least one contact (`phone`, `whatsapp`, `instagram` or `bookingUrl`) must be present — otherwise the write is refused with 422 `invalid_content`.",
        "required": [
          "name"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "profile"
            ],
            "description": "Always \"profile\" (set server-side)."
          },
          "segment": {
            "type": "string",
            "pattern": "^[a-z0-9-]{1,32}$",
            "default": "master",
            "description": "Page kind: master | vrach | psiholog | otzyvy | … Use `otzyvy` for a review-funnel page."
          },
          "theme": {
            "type": "string",
            "enum": [
              "emerald",
              "ocean",
              "plum",
              "terracotta",
              "graphite"
            ],
            "default": "emerald",
            "description": "Accent colour preset."
          },
          "name": {
            "type": "string",
            "maxLength": 80,
            "description": "Business or person name — the page heading."
          },
          "specialty": {
            "type": "string",
            "maxLength": 120
          },
          "bio": {
            "type": "string",
            "maxLength": 1200
          },
          "phone": {
            "type": "string",
            "description": "Any format on input; stored as +digits (9..15 digits)."
          },
          "whatsapp": {
            "type": "string",
            "description": "Number on input; stored as https://wa.me/<digits>."
          },
          "instagram": {
            "type": "string",
            "description": "@handle or profile URL; stored as https://instagram.com/<handle>."
          },
          "twogis": {
            "type": "string",
            "format": "uri"
          },
          "bookingUrl": {
            "type": "string",
            "format": "uri"
          },
          "hours": {
            "type": "string",
            "maxLength": 160
          },
          "address": {
            "type": "string",
            "maxLength": 200
          },
          "licenseNumber": {
            "type": "string",
            "maxLength": 80
          },
          "services": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "$ref": "#/components/schemas/ProfileService"
            }
          },
          "reviewLinks": {
            "type": "object",
            "description": "Review-platform links (segment `otzyvy`). http/https only.",
            "properties": {
              "twogis": {
                "type": "string",
                "format": "uri"
              },
              "google": {
                "type": "string",
                "format": "uri"
              },
              "yandex": {
                "type": "string",
                "format": "uri"
              },
              "flamp": {
                "type": "string",
                "format": "uri"
              }
            },
            "additionalProperties": false
          },
          "socialLinks": {
            "type": "object",
            "description": "Social icon row. http/https only; @handles are normalised.",
            "properties": {
              "tiktok": {
                "type": "string"
              },
              "telegram": {
                "type": "string"
              },
              "website": {
                "type": "string",
                "format": "uri"
              },
              "youtube": {
                "type": "string",
                "format": "uri"
              }
            },
            "additionalProperties": false
          }
        },
        "additionalProperties": true,
        "example": {
          "kind": "profile",
          "segment": "otzyvy",
          "theme": "emerald",
          "name": "Кофейня «Тандем»",
          "specialty": "Спешелти-кофе в центре Бишкека",
          "bio": "",
          "phone": "+996555123456",
          "whatsapp": "https://wa.me/996555123456",
          "instagram": "https://instagram.com/tandem.coffee",
          "twogis": "",
          "bookingUrl": "",
          "services": [
            {
              "name": "Фильтр-кофе",
              "price": "180 сом"
            }
          ],
          "hours": "ежедневно 8:00–22:00",
          "address": "Бишкек, ул. Киевская 100",
          "reviewLinks": {
            "twogis": "https://2gis.kg/bishkek/firm/70000001"
          }
        }
      },
      "PageSummary": {
        "type": "object",
        "description": "Page without its content blob — the list projection.",
        "required": [
          "id",
          "slug",
          "status",
          "published",
          "version",
          "public_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "Public slug; stable for the life of the page."
          },
          "title": {
            "type": "string",
            "description": "Page heading (mirrors content.name)."
          },
          "segment": {
            "type": "string"
          },
          "theme": {
            "type": "string",
            "enum": [
              "emerald",
              "ocean",
              "plum",
              "terracotta",
              "graphite"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "archived"
            ]
          },
          "published": {
            "type": "boolean",
            "description": "Convenience mirror of status === \"published\"."
          },
          "version": {
            "type": "integer",
            "description": "Current content version (increments on every write)."
          },
          "public_url": {
            "type": "string",
            "format": "uri",
            "description": "Where the page is served: https://app.qrtap.kg/a/{slug}."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the current version was published."
          }
        },
        "example": {
          "id": "9d1c1f0a-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
          "slug": "cozy-cafe-x1",
          "title": "Кофейня «Тандем»",
          "segment": "otzyvy",
          "theme": "emerald",
          "status": "published",
          "published": true,
          "version": 3,
          "public_url": "https://app.qrtap.kg/a/cozy-cafe-x1",
          "created_at": "2026-07-01T08:00:00.000Z",
          "updated_at": "2026-07-20T10:15:00.000Z"
        }
      },
      "Page": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PageSummary"
          },
          {
            "type": "object",
            "required": [
              "content"
            ],
            "properties": {
              "content": {
                "$ref": "#/components/schemas/PageContent"
              }
            }
          }
        ],
        "description": "Page including its current content."
      },
      "PageWriteRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "content": {
            "$ref": "#/components/schemas/PageContent"
          }
        },
        "additionalProperties": false
      },
      "Review": {
        "type": "object",
        "required": [
          "id",
          "author_name",
          "body",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "author_name": {
            "type": "string",
            "description": "What the guest typed. No IP, user-agent or any other identifier is ever stored."
          },
          "body": {
            "type": "string",
            "maxLength": 600
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "published",
              "hidden"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "moderated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "example": {
          "id": "55555555-5555-4555-8555-555555555555",
          "author_name": "Айгуль",
          "body": "Очень уютно, спасибо!",
          "status": "pending",
          "created_at": "2026-07-20T10:15:00.000Z",
          "moderated_at": null
        }
      },
      "ModerateReviewRequest": {
        "type": "object",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "publish",
              "hide"
            ],
            "description": "`publish` shows the review on the public page; `hide` removes it."
          }
        },
        "additionalProperties": false
      },
      "Booking": {
        "type": "object",
        "required": [
          "id",
          "service",
          "guest_name",
          "guest_phone",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "service": {
            "type": "string",
            "maxLength": 120
          },
          "preferred_at": {
            "type": "string",
            "maxLength": 80,
            "description": "Free-form desired time as the guest typed it (\"завтра после 14:00\"). Never parsed by the platform."
          },
          "guest_name": {
            "type": "string",
            "maxLength": 60
          },
          "guest_phone": {
            "type": "string",
            "maxLength": 30,
            "description": "As typed, validated to 9..15 digits."
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "confirmed",
              "declined"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "example": {
          "id": "66666666-6666-4666-8666-666666666666",
          "service": "Маникюр",
          "preferred_at": "завтра после 14:00",
          "guest_name": "Айгуль",
          "guest_phone": "0555 123 456",
          "status": "new",
          "created_at": "2026-07-20T10:15:00.000Z",
          "updated_at": "2026-07-20T10:15:00.000Z"
        }
      },
      "BookingDecisionRequest": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "declined"
            ],
            "description": "The decision. `new` is not accepted — a decided request is never reopened through the API."
          }
        },
        "additionalProperties": false
      },
      "SmartRuleCondition": {
        "oneOf": [
          {
            "type": "object",
            "title": "device",
            "required": [
              "kind",
              "in"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "device"
                ]
              },
              "in": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "string",
                  "enum": [
                    "ios",
                    "android",
                    "desktop",
                    "other"
                  ]
                }
              }
            }
          },
          {
            "type": "object",
            "title": "lang",
            "required": [
              "kind",
              "in"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "lang"
                ]
              },
              "in": {
                "type": "array",
                "minItems": 1,
                "maxItems": 8,
                "items": {
                  "type": "string",
                  "pattern": "^[a-zA-Z]{2,8}$"
                },
                "description": "Primary language subtags, e.g. [\"ru\",\"ky\"]."
              }
            }
          },
          {
            "type": "object",
            "title": "hours",
            "required": [
              "kind",
              "tz",
              "from",
              "to"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "hours"
                ]
              },
              "tz": {
                "type": "string",
                "maxLength": 64,
                "description": "IANA timezone, e.g. Asia/Bishkek."
              },
              "from": {
                "type": "string",
                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
              },
              "to": {
                "type": "string",
                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                "description": "Exclusive. A window where from > to crosses midnight."
              },
              "days": {
                "type": "array",
                "minItems": 1,
                "maxItems": 7,
                "items": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 7
                },
                "description": "ISO weekdays, 1 = Monday … 7 = Sunday. Omit for every day."
              }
            }
          }
        ],
        "description": "Evaluated against coarse, already-classified request signals only — never a raw IP, user-agent or Accept-Language header."
      },
      "SmartRule": {
        "type": "object",
        "required": [
          "if",
          "to"
        ],
        "properties": {
          "if": {
            "$ref": "#/components/schemas/SmartRuleCondition"
          },
          "to": {
            "type": "string",
            "format": "uri",
            "description": "Destination for this rule (http/https)."
          }
        },
        "additionalProperties": false
      },
      "SmartRulesRequest": {
        "type": "object",
        "required": [
          "rules"
        ],
        "properties": {
          "rules": {
            "type": "array",
            "maxItems": 5,
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/SmartRule"
            },
            "description": "The full rule array (replaces whatever was stored). `[]` or `null` clears the rules."
          }
        },
        "additionalProperties": false,
        "example": {
          "rules": [
            {
              "if": {
                "kind": "device",
                "in": [
                  "ios"
                ]
              },
              "to": "https://apps.apple.com/app/id1"
            },
            {
              "if": {
                "kind": "device",
                "in": [
                  "android"
                ]
              },
              "to": "https://play.google.com/store/apps/details?id=kg.qrtap"
            },
            {
              "if": {
                "kind": "hours",
                "tz": "Asia/Bishkek",
                "from": "22:00",
                "to": "09:00",
                "days": [
                  1,
                  2,
                  3,
                  4,
                  5
                ]
              },
              "to": "https://example.com/night-menu"
            }
          ]
        }
      },
      "SmartRulesResponse": {
        "type": "object",
        "required": [
          "code_id",
          "version",
          "rules"
        ],
        "properties": {
          "code_id": {
            "type": "string",
            "format": "uuid"
          },
          "version": {
            "type": "integer",
            "enum": [
              1
            ],
            "description": "Rule-set schema version (currently always 1)."
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SmartRule"
            }
          }
        },
        "example": {
          "code_id": "b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10",
          "version": 1,
          "rules": [
            {
              "if": {
                "kind": "device",
                "in": [
                  "ios"
                ]
              },
              "to": "https://apps.apple.com/app/id1"
            },
            {
              "if": {
                "kind": "device",
                "in": [
                  "android"
                ]
              },
              "to": "https://play.google.com/store/apps/details?id=kg.qrtap"
            },
            {
              "if": {
                "kind": "hours",
                "tz": "Asia/Bishkek",
                "from": "22:00",
                "to": "09:00",
                "days": [
                  1,
                  2,
                  3,
                  4,
                  5
                ]
              },
              "to": "https://example.com/night-menu"
            }
          ]
        }
      }
    },
    "examples": {
      "CodeExample": {
        "summary": "A managed code",
        "value": {
          "id": "b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10",
          "label": "Menu table 5",
          "target_url": "https://example.com/menu",
          "external_ref": "SMK-P-001",
          "group": "partner",
          "state": "deployed",
          "design_template_id": null,
          "slug": "k3v9tqzx",
          "short_url": "https://app.qrtap.kg/k3v9tqzx",
          "png_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=png",
          "svg_url": "https://app.qrtap.kg/api/v1/codes/b6a4a1de-46a5-4f0e-a2af-3f5d6a2e9b10/qr?format=svg",
          "created_at": "2026-07-01T08:00:00.000Z",
          "updated_at": "2026-07-20T10:15:00.000Z"
        }
      }
    }
  }
}
