{
  "openapi": "3.1.0",
  "info": {
    "title": "Откуцај (Otkucaj) ESIR API",
    "version": "1.2.0",
    "summary": "REST API v1 of the Откуцај ESIR web cash register.",
    "description": "Откуцај (Otkucaj) is a Serbian ESIR (electronic fiscal cash register) web application. This API issues and reads fiscal invoices, manages the item catalogue, and returns turnover reports.\n\nAuthentication: every route requires `Authorization: Bearer <api key>`. Keys are created and revoked in the application under Подешавања, only their SHA-256 hash is stored, and the full key is shown once at creation. Keys are NOT scoped individually: every active key may call every route.\n\nRate limit: 240 requests per minute per key, enforced on every route including unknown ones. `POST /invoices/{uuid}/email` has an additional limit of 30 messages per hour per key. No `Retry-After` header is sent.\n\nFiscalization status: Откуцај is NOT yet approved by the Poreska uprava (Serbian Tax Administration). Unless an installation is switched to L-PFR or V-PFR in its settings, it runs against the built-in demo PFR simulator: invoices are marked ДЕМО, `pfr.demo` is `true`, and such receipts have no fiscal validity. Check `GET /status` to see which PFR mode an installation runs in.\n\nAmounts are in RSD and include VAT. Tax rates are never computed by the client: they come from the PFR. Dates and times are Europe/Belgrade.",
    "contact": {
      "name": "Orvelo",
      "email": "filip@tefis.io",
      "url": "https://otkucaj.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://otkucaj.com/uslovi"
    }
  },
  "externalDocs": {
    "description": "Human readable API documentation (Serbian Cyrillic)",
    "url": "https://otkucaj.com/api-docs"
  },
  "servers": [
    {
      "url": "https://otkucaj.com/api/v1",
      "description": "Production installation at otkucaj.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "status", "description": "Installation and key information" },
    { "name": "invoices", "description": "Issuing and reading fiscal invoices" },
    { "name": "reports", "description": "Turnover reports" },
    { "name": "items", "description": "Item catalogue" },
    { "name": "reference", "description": "Tax labels and item categories" }
  ],
  "paths": {
    "/status": {
      "get": {
        "tags": ["status"],
        "operationId": "getStatus",
        "summary": "ESIR and PFR status of this installation",
        "description": "Returns the application version, the ESIR number as `esir_number/esir_version`, the live PFR status and the server time. Use `pfr.mode` to tell whether the installation fiscalizes through the demo simulator, an L-PFR or a V-PFR.",
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Status" },
                "example": {
                  "app": "Otkucaj",
                  "version": "1.2.0",
                  "esirNumber": "ДЕМО/1.0",
                  "pfr": {
                    "ok": true,
                    "mode": "demo",
                    "uid": "DM9EWQ0Z",
                    "message": "Демо ПФР симулатор: активан"
                  },
                  "time": "2026-08-20T12:34:56.789+02:00"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/me": {
      "get": {
        "tags": ["status"],
        "operationId": "getMe",
        "summary": "Information about the calling API key",
        "description": "Returns the label, the stored prefix, the created and last used timestamps, the enforced rate limit and what the key may do.\n\nOnly the stored prefix is echoed, never the key itself. `lastUsedAt` is the timestamp of the PREVIOUS call, because the current call is stamped after the key row is read. `scope` is always `full` and `permissions` lists the capabilities every active key really has: the key store carries no per-key scoping.",
        "responses": {
          "200": {
            "description": "The calling key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Me" },
                "example": {
                  "key": {
                    "id": 3,
                    "label": "Веб продавница",
                    "prefix": "kasir_9f21a…",
                    "active": true,
                    "createdAt": "2026-08-14 09:12:03",
                    "lastUsedAt": "2026-08-20 11:58:41"
                  },
                  "rateLimit": { "requests": 240, "windowSeconds": 60 },
                  "scope": "full",
                  "permissions": [
                    "invoices:read",
                    "invoices:write",
                    "items:read",
                    "items:write",
                    "reports:read",
                    "taxLabels:read",
                    "categories:read"
                  ],
                  "note": "API keys are not scoped individually: every active key may call every route."
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/tax-labels": {
      "get": {
        "tags": ["reference"],
        "operationId": "getTaxLabels",
        "summary": "Tax labels and rates currently in force",
        "description": "Active tax labels only, read from the same source the invoice validator uses. A label returned here is a label `POST /invoices` will accept. Rates are informational: the tax actually printed on a receipt is always computed by the PFR.",
        "responses": {
          "200": {
            "description": "Tax labels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["taxLabels"],
                  "properties": {
                    "taxLabels": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/TaxLabel" }
                    }
                  }
                },
                "example": {
                  "taxLabels": [
                    { "label": "Ђ", "name": "О-ПДВ", "rate": 20.0 },
                    { "label": "Е", "name": "П-ПДВ", "rate": 10.0 },
                    { "label": "Г", "name": "Без ПДВ", "rate": 0.0 },
                    { "label": "А", "name": "Није у ПДВ", "rate": 0.0 }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/categories": {
      "get": {
        "tags": ["reference"],
        "operationId": "getCategories",
        "summary": "Distinct categories of active items",
        "description": "Distinct non-empty `category` values of active items, sorted alphabetically. The same list the register screen uses as filters. Categories are set on items through the application or the CSV import; the item endpoints of this API do not read or write the category field.",
        "responses": {
          "200": {
            "description": "Categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["categories"],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": { "type": "string" }
                    }
                  }
                },
                "example": { "categories": ["Пиће", "Слаткиши", "Храна"] }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices": {
      "post": {
        "tags": ["invoices"],
        "operationId": "createInvoice",
        "summary": "Issue an invoice",
        "description": "Validates the request, sends it to the configured PFR and stores the result. On success the invoice is issued and fiscalized (or demo-issued, when the installation runs in demo mode).\n\nRules enforced by the server:\n- at least one item, at most 500 items, total greater than 0\n- every `label` must be an active tax label\n- when `payments` is omitted, the whole amount is booked as `Cash`\n- a single payment entry is stretched to the invoice total; two or more entries must sum to the total (tolerance 0.011)\n- a `Refund` transaction and a `Copy` invoice must carry `referentDocumentNumber`\n- a `Refund` must carry `buyerId`; to void your own receipt, enter your own PIB as `10:<PIB>`\n- when the installation runs in the simplified cash mode of article 6 paragraph 2 of the Pravilnik, the payment types `Card`, `Check` and `MobileMoney` are refused and must be entered as `Cash`\n\nA `502 pfr_error` means the PFR refused or was unreachable: no invoice was issued.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InvoiceCreate" },
              "examples": {
                "sale": {
                  "summary": "Cash sale of two items",
                  "value": {
                    "invoiceType": "Normal",
                    "transactionType": "Sale",
                    "cashier": "Касир 1",
                    "items": [
                      { "name": "Кафа еспресо", "unit": "ком", "quantity": 2, "unitPrice": 180.0, "label": "Ђ" },
                      { "name": "Негазирана вода 0,33", "unit": "ком", "quantity": 1, "unitPrice": 120.0, "label": "Е" }
                    ],
                    "payments": [{ "paymentType": "Cash", "amount": 480.0 }]
                  }
                },
                "splitPayment": {
                  "summary": "Split payment, cash and card",
                  "value": {
                    "invoiceType": "Normal",
                    "transactionType": "Sale",
                    "items": [
                      { "name": "Кроасан са путером", "unit": "ком", "quantity": 4, "unitPrice": 220.0, "label": "Ђ" }
                    ],
                    "payments": [
                      { "paymentType": "Cash", "amount": 500.0 },
                      { "paymentType": "Card", "amount": 380.0 }
                    ],
                    "adText": "Хвала на поверењу"
                  }
                },
                "partialRefund": {
                  "summary": "Partial refund of an earlier receipt",
                  "value": {
                    "invoiceType": "Normal",
                    "transactionType": "Refund",
                    "buyerId": "10:123456789",
                    "referentDocumentNumber": "DM9EWQ0Z-DM9EWQ0Z-42",
                    "referentDocumentDT": "2026-08-19 12:34:56",
                    "items": [
                      { "name": "Кафа еспресо", "unit": "ком", "quantity": 1, "unitPrice": 180.0, "label": "Ђ" }
                    ],
                    "payments": [{ "paymentType": "Cash", "amount": 180.0 }]
                  }
                },
                "advance": {
                  "summary": "Advance payment received by wire transfer",
                  "value": {
                    "invoiceType": "Advance",
                    "transactionType": "Sale",
                    "buyerId": "10:123456789",
                    "items": [
                      { "name": "10: Аванс", "unit": "ком", "quantity": 1, "unitPrice": 12000.0, "label": "Ђ" }
                    ],
                    "payments": [{ "paymentType": "WireTransfer", "amount": 12000.0 }],
                    "dateAndTimeOfIssue": "2026-08-18 09:00:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/InvoiceCreated" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/PfrError" }
        }
      },
      "get": {
        "tags": ["invoices"],
        "operationId": "listInvoices",
        "summary": "List invoices",
        "description": "Newest first, 100 per page. Every returned invoice carries the full public shape, including its journal.\n\n`from` and `to` are matched against the creation timestamp as `from 00:00:00` and `to 23:59:59`. These two parameters are not format checked on this route: a value that is not a date simply matches nothing. An unknown `type` is ignored rather than refused. There is no total count in the response: request the next page until fewer than 100 invoices come back.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Include invoices created on or after this date.",
            "schema": { "type": "string", "example": "2026-08-01" }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Include invoices created on or before this date.",
            "schema": { "type": "string", "example": "2026-08-20" }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter by invoice type. Any other value is ignored.",
            "schema": { "$ref": "#/components/schemas/InvoiceType" }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1 based page number, 100 invoices per page.",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of invoices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["page", "perPage", "invoices"],
                  "properties": {
                    "page": { "type": "integer", "example": 1 },
                    "perPage": { "type": "integer", "const": 100 },
                    "invoices": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Invoice" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices/{uuid}": {
      "parameters": [{ "$ref": "#/components/parameters/InvoiceUuid" }],
      "get": {
        "tags": ["invoices"],
        "operationId": "getInvoice",
        "summary": "One invoice",
        "description": "The full public shape of a single invoice, including the textual journal and the PFR verification link.",
        "responses": {
          "200": {
            "description": "The invoice",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Invoice" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices/{uuid}/journal": {
      "parameters": [{ "$ref": "#/components/parameters/InvoiceUuid" }],
      "get": {
        "tags": ["invoices"],
        "operationId": "getInvoiceJournal",
        "summary": "The receipt text",
        "description": "The stored journal as `text/plain; charset=utf-8`: the 40 column Cyrillic receipt exactly as it is printed. This is the only route in the API that does not answer with JSON. Errors on this route are still JSON.",
        "responses": {
          "200": {
            "description": "The journal",
            "content": {
              "text/plain": {
                "schema": { "type": "string" },
                "example": "============ ФИСКАЛНИ РАЧУН ============\n             ПИБ: 123456789\n          Откуцај · демо радња\n            Продајно место 1\n                Београд\nКасир:                           Касир 1\nЕСИР број:                      ДЕМО/1.0\n-------------ПРОМЕТ - ПРОДАЈА-----------\n========================================\nПФР број рачуна:    DM9EWQ0Z-DM9EWQ0Z-42\nБројач рачуна:                   42/97ПП\n======== КРАЈ ФИСКАЛНОГ РАЧУНА =========\n"
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices/{uuid}/refund": {
      "parameters": [{ "$ref": "#/components/parameters/InvoiceUuid" }],
      "post": {
        "tags": ["invoices"],
        "operationId": "refundInvoice",
        "summary": "Refund an invoice in full",
        "description": "Issues a new Refund invoice for the whole original, with the referent number and referent time pointing at the original. This mirrors the Рефундација flow of the web application.\n\nOnly an issued `Normal` or `Advance` invoice whose transaction type is `Sale` can be refunded. The refund keeps the invoice type of the original (`Advance` stays `Advance`, everything else becomes `Normal`), and reuses the original items and payments unless the body overrides the payments.\n\n`buyerId` falls back to the buyer of the original. When neither the body nor the original carries a buyer identification, the request fails with `validation` 422, because a refund without buyer identification cannot be issued.\n\nFor a PARTIAL refund do not use this route: post to `/invoices` with `transactionType` `Refund`, the lines you are refunding, and the referent number and time of the original.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RefundRequest" },
              "examples": {
                "empty": {
                  "summary": "Refund exactly as the original was paid",
                  "value": {}
                },
                "withBuyer": {
                  "summary": "Refund with an explicit buyer identification",
                  "value": {
                    "buyerId": "10:123456789",
                    "cashier": "Касир 1",
                    "adText": "Рефундација по рекламацији"
                  }
                },
                "cashBack": {
                  "summary": "Refund paid back in cash",
                  "value": {
                    "buyerId": "10:123456789",
                    "payments": [{ "paymentType": "Cash", "amount": 480.0 }]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/InvoiceCreated" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": {
            "description": "The original is not an issued Normal or Advance sale, or the refund itself failed validation (missing buyer identification, payments that do not match the total).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "validation",
                    "message": "Only an issued Normal or Advance sale invoice can be refunded."
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/PfrError" }
        }
      }
    },
    "/invoices/{uuid}/copy": {
      "parameters": [{ "$ref": "#/components/parameters/InvoiceUuid" }],
      "post": {
        "tags": ["invoices"],
        "operationId": "copyInvoice",
        "summary": "Issue a copy of an invoice",
        "description": "Issues a `Copy` invoice of an existing one, through the same code path as the Копија button of the web application. The copy carries the same items and payments, the referent number and time of the original, and prints ОВО НИЈЕ ФИСКАЛНИ РАЧУН.\n\nOnly an issued invoice that is not itself a copy can be copied. This route takes no request body.",
        "responses": {
          "201": { "$ref": "#/components/responses/InvoiceCreated" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": {
            "description": "The invoice is already a copy, or it is not in status issued.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "validation",
                    "message": "Only an issued invoice that is not itself a copy can be copied."
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/PfrError" }
        }
      }
    },
    "/invoices/{uuid}/email": {
      "parameters": [{ "$ref": "#/components/parameters/InvoiceUuid" }],
      "post": {
        "tags": ["invoices"],
        "operationId": "emailInvoice",
        "summary": "E-mail the receipt to a buyer",
        "description": "Sends the receipt (the journal plus the verification link) to one address, through the same code the web application uses, and writes the same `invoice.email` audit entry. A receipt issued in demo mode carries a plain note that it was issued in a demo environment and is not fiscalized.\n\nThis route has its own limit of 30 messages per hour per key, separate from the 240 requests per minute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to"],
                "properties": {
                  "to": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 190,
                    "description": "Recipient address.",
                    "example": "kupac@example.com"
                  }
                }
              },
              "example": { "to": "kupac@example.com" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message was handed to the mail system",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "uuid", "to"],
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "uuid": { "type": "string" },
                    "to": { "type": "string", "format": "email" }
                  }
                },
                "example": {
                  "ok": true,
                  "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
                  "to": "kupac@example.com"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": {
            "description": "`to` is missing, is not a valid e-mail address, or is longer than 190 characters.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "validation", "message": "to must be a valid e-mail address." }
                }
              }
            }
          },
          "429": {
            "description": "Either the 240 requests per minute limit or the 30 messages per hour mail limit was exceeded.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "rate_limited", "message": "E-mail limit exceeded (30 messages/hour)." }
                }
              }
            }
          },
          "502": {
            "description": "The message could not be sent.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "mail_error", "message": "The e-mail could not be sent." }
                }
              }
            }
          }
        }
      }
    },
    "/reports/summary": {
      "get": {
        "tags": ["reports"],
        "operationId": "getReportSummary",
        "summary": "Turnover for a period, with breakdowns",
        "description": "Turnover for a period with breakdowns by tax label, payment type and cashier, computed exactly as the Извештаји screen computes it.\n\nAccounting rule: only issued `Normal` and `Advance` invoices count, refunds subtract from turnover, and `ProForma`, `Copy` and `Training` never enter a total. The same sentence is returned in the `rule` field of every report response.\n\n`totals.refunds` is stated as a positive amount and counts refund invoices in the period; it is informational, the refund amounts are already subtracted from `totals.turnover`.",
        "parameters": [
          { "$ref": "#/components/parameters/ReportFrom" },
          { "$ref": "#/components/parameters/ReportTo" }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReportSummary" },
                "example": {
                  "from": "2026-08-01",
                  "to": "2026-08-20",
                  "totals": {
                    "invoices": 214,
                    "turnover": 486320.5,
                    "refunds": { "count": 3, "amount": 2140.0 }
                  },
                  "byTaxLabel": [
                    { "label": "Ђ", "name": "О-ПДВ", "rate": 20.0, "turnover": 401220.5 },
                    { "label": "Е", "name": "П-ПДВ", "rate": 10.0, "turnover": 85100.0 }
                  ],
                  "byPaymentType": [
                    { "paymentType": "Cash", "amount": 302150.5 },
                    { "paymentType": "Card", "amount": 184170.0 }
                  ],
                  "byCashier": [
                    { "cashier": "Касир 1", "invoices": 141, "turnover": 320410.5 },
                    { "cashier": "Касир 2", "invoices": 73, "turnover": 165910.0 }
                  ],
                  "rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded."
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/PeriodValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/reports/daily": {
      "get": {
        "tags": ["reports"],
        "operationId": "getReportDaily",
        "summary": "Per-day turnover and invoice count",
        "description": "Per-day turnover and invoice count for a period, using the same accounting rule and the same grouping query as the daily table of the Извештаји screen. Days with no traffic are omitted from `days` rather than returned as zero rows.",
        "parameters": [
          { "$ref": "#/components/parameters/ReportFrom" },
          { "$ref": "#/components/parameters/ReportTo" }
        ],
        "responses": {
          "200": {
            "description": "The report",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReportDaily" },
                "example": {
                  "from": "2026-08-18",
                  "to": "2026-08-20",
                  "days": [
                    { "date": "2026-08-18", "invoices": 41, "turnover": 92310.0 },
                    { "date": "2026-08-20", "invoices": 12, "turnover": 24880.5 }
                  ],
                  "rule": "Turnover counts issued Normal and Advance receipts only; refunds subtract; ProForma, Copy and Training are excluded."
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/PeriodValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/items": {
      "get": {
        "tags": ["items"],
        "operationId": "listItems",
        "summary": "List items",
        "description": "The item catalogue sorted by name, at most 1000 rows. Inactive items are included and carry `active: false`. This route has no filters and no pagination. The `category` column of an item is not exposed here; the distinct list of categories is available at `GET /categories`.",
        "responses": {
          "200": {
            "description": "The catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["items"],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Item" }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 12,
                      "plu": "1001",
                      "name": "Кафа еспресо",
                      "unit": "ком",
                      "price": 180.0,
                      "label": "Ђ",
                      "gtin": "8600123456789",
                      "active": true
                    },
                    {
                      "id": 15,
                      "plu": null,
                      "name": "Негазирана вода 0,33",
                      "unit": "ком",
                      "price": 120.0,
                      "label": "Е",
                      "gtin": null,
                      "active": true
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["items"],
        "operationId": "createItem",
        "summary": "Create an item",
        "description": "Creates an active item. `name` and `label` are required; `label` must be an active tax label. `unit` defaults to `ком`, `price` defaults to 0 and is rounded to 2 decimals, `gtin` must be 8 to 14 digits when given. A `plu` is stored as sent and is not checked for uniqueness by this route.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ItemCreate" },
              "example": {
                "plu": "1001",
                "name": "Кафа еспресо",
                "unit": "ком",
                "price": 180.0,
                "label": "Ђ",
                "gtin": "8600123456789"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "ok"],
                  "properties": {
                    "id": { "type": "integer", "example": 12 },
                    "ok": { "type": "boolean", "const": true }
                  }
                },
                "example": { "id": 12, "ok": true }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": {
            "description": "Missing or too long name, unknown tax label, negative price, or a GTIN that is not 8 to 14 digits.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "validation", "message": "label must be one of: Ђ, Е, Г, А" }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/items/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/ItemId" }],
      "put": {
        "tags": ["items"],
        "operationId": "updateItem",
        "summary": "Update an item",
        "description": "Partial update: every field that is absent from the body keeps its stored value. `PUT` and `PATCH` behave identically on this route.\n\nUnlike `POST /items`, this route does not re-validate the tax label, the price or the GTIN: what you send is what gets stored. Send `label` values that `GET /tax-labels` returns.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ItemUpdate" },
              "example": { "price": 195.0, "active": true }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No item with that id.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "not_found", "message": "Item not found." } }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["items"],
        "operationId": "patchItem",
        "summary": "Update an item (identical to PUT)",
        "description": "The same handler as `PUT /items/{id}`: a partial update where absent fields keep their stored value.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ItemUpdate" },
              "example": { "name": "Кафа еспресо велика", "price": 210.0 }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No item with that id.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "not_found", "message": "Item not found." } }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["items"],
        "operationId": "deactivateItem",
        "summary": "Deactivate an item",
        "description": "Sets `active` to false. Items are never physically deleted, because issued invoices reference them. The response is `{\"ok\": true}` even when no item with that id exists: this route does not check first.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key created in the application under Подешавања. Send it as `Authorization: Bearer kasir_...`. Keys begin with the prefix `kasir_`, are shown once at creation and stored only as a SHA-256 hash."
      }
    },
    "parameters": {
      "InvoiceUuid": {
        "name": "uuid",
        "in": "path",
        "required": true,
        "description": "The invoice UUID returned when it was issued.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f-]{36}$",
          "example": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30"
        }
      },
      "ItemId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The numeric item id.",
        "schema": { "type": "integer", "minimum": 1, "example": 12 }
      },
      "ReportFrom": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "First day of the period, YYYY-MM-DD. Defaults to today.",
        "schema": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
          "example": "2026-08-01"
        }
      },
      "ReportTo": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "Last day of the period, inclusive, YYYY-MM-DD. Defaults to today and must not be earlier than `from`.",
        "schema": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
          "example": "2026-08-20"
        }
      }
    },
    "responses": {
      "Ok": {
        "description": "Done",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["ok"],
              "properties": { "ok": { "type": "boolean", "const": true } }
            },
            "example": { "ok": true }
          }
        }
      },
      "InvoiceCreated": {
        "description": "The invoice was issued",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Invoice" },
            "example": {
              "uuid": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30",
              "invoiceType": "Normal",
              "transactionType": "Sale",
              "cashier": "Касир 1",
              "total": 480.0,
              "payments": [{ "paymentType": "Cash", "amount": 480.0 }],
              "items": [
                {
                  "name": "Кафа еспресо",
                  "unit": "ком",
                  "quantity": 2.0,
                  "unitPrice": 180.0,
                  "totalAmount": 360.0,
                  "label": "Ђ",
                  "gtin": null
                },
                {
                  "name": "Негазирана вода 0,33",
                  "unit": "ком",
                  "quantity": 1.0,
                  "unitPrice": 120.0,
                  "totalAmount": 120.0,
                  "label": "Е",
                  "gtin": null
                }
              ],
              "taxItems": [
                { "label": "Ђ", "categoryName": "О-ПДВ", "rate": 20.0, "amount": 60.0, "total": 360.0 },
                { "label": "Е", "categoryName": "П-ПДВ", "rate": 10.0, "amount": 10.91, "total": 120.0 }
              ],
              "buyerId": null,
              "referentDocumentNumber": null,
              "referentDocumentDT": null,
              "pfr": {
                "mode": "demo",
                "demo": true,
                "requestedBy": "DM9EWQ0Z",
                "signedBy": "DM9EWQ0Z",
                "invoiceNumber": "DM9EWQ0Z-DM9EWQ0Z-42",
                "invoiceCounter": "42/97ПП",
                "sdcDateTime": "2026-08-20 12:34:56",
                "verificationUrl": "https://otkucaj.com/verify?id=9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30"
              },
              "journal": "============ ФИСКАЛНИ РАЧУН ============\n...\n======== КРАЈ ФИСКАЛНОГ РАЧУНА =========\n",
              "createdAt": "2026-08-20 12:34:56"
            }
          }
        }
      },
      "BadRequest": {
        "description": "The request body is not valid JSON.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "bad_request", "message": "Request body must be valid JSON." }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The Authorization header is missing or malformed, or the key is invalid or revoked.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "unauthorized", "message": "Invalid or revoked API key." }
            }
          }
        }
      },
      "NotFound": {
        "description": "No invoice with that UUID.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "not_found", "message": "Invoice not found." } }
          }
        }
      },
      "ValidationError": {
        "description": "The request failed validation. The message is the Serbian Cyrillic text the application itself shows.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "noItems": {
                "value": {
                  "error": {
                    "code": "validation",
                    "message": "Рачун мора имати бар један артикал (items)."
                  }
                }
              },
              "unknownLabel": {
                "value": {
                  "error": {
                    "code": "validation",
                    "message": "Ставка #0: непозната пореска ознака „Ж“."
                  }
                }
              },
              "paymentsMismatch": {
                "value": {
                  "error": {
                    "code": "validation",
                    "message": "Збир плаћања (400,00) се не поклапа са укупним износом (480,00)."
                  }
                }
              },
              "refundNeedsBuyer": {
                "value": {
                  "error": {
                    "code": "validation",
                    "message": "Рефундација мора имати идентификацију купца (buyerId), нпр. 10:123456789. За поништавање сопственог рачуна унесите свој ПИБ."
                  }
                }
              }
            }
          }
        }
      },
      "PeriodValidationError": {
        "description": "A date is not in YYYY-MM-DD form or is not a real calendar date, or `from` is later than `to`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "badDate": {
                "value": {
                  "error": { "code": "validation", "message": "Dates must be in YYYY-MM-DD format." }
                }
              },
              "reversedPeriod": {
                "value": {
                  "error": { "code": "validation", "message": "from must not be later than to." }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 240 requests in one minute with this key. No Retry-After header is sent; the window is 60 seconds.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "rate_limited", "message": "Rate limit exceeded (240 requests/minute)." }
            }
          }
        }
      },
      "PfrError": {
        "description": "The PFR refused the request or was unreachable. No invoice was issued.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "pfr_error", "message": "ПФР је одбио захтев: ..." }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error in this API has this shape.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "rate_limited",
                  "bad_request",
                  "validation",
                  "not_found",
                  "pfr_error",
                  "mail_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human readable. Messages coming from the invoice validator are in Serbian Cyrillic; messages raised by the API layer are in English."
              }
            }
          }
        }
      },
      "InvoiceType": {
        "type": "string",
        "enum": ["Normal", "ProForma", "Copy", "Training", "Advance"],
        "description": "Normal (промет) and Advance (аванс) are fiscal receipts. ProForma (предрачун), Copy (копија) and Training (обука) print ОВО НИЈЕ ФИСКАЛНИ РАЧУН and never enter turnover."
      },
      "TransactionType": {
        "type": "string",
        "enum": ["Sale", "Refund"]
      },
      "PaymentType": {
        "type": "string",
        "enum": ["Cash", "Card", "Check", "WireTransfer", "Voucher", "MobileMoney", "Other"],
        "description": "Готовина, платна картица, чек, пренос на рачун, ваучер, инстант плаћање, друго безготовинско."
      },
      "Payment": {
        "type": "object",
        "required": ["paymentType", "amount"],
        "properties": {
          "paymentType": { "$ref": "#/components/schemas/PaymentType" },
          "amount": {
            "type": "number",
            "description": "Amount in RSD, VAT included. Entries of 0 or less are dropped.",
            "example": 480.0
          }
        }
      },
      "InvoiceItemIn": {
        "type": "object",
        "required": ["name", "quantity", "unitPrice"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Item name as it is printed on the receipt.",
            "example": "Кафа еспресо"
          },
          "unit": {
            "type": "string",
            "default": "ком",
            "description": "Unit of measure.",
            "example": "ком"
          },
          "quantity": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 999999,
            "description": "Rounded to 3 decimals.",
            "example": 2
          },
          "unitPrice": {
            "type": "number",
            "minimum": 0,
            "maximum": 999999999,
            "description": "Price per unit in RSD, VAT included. Rounded to 2 decimals.",
            "example": 180.0
          },
          "label": {
            "type": "string",
            "description": "Tax label, one of the labels returned by GET /tax-labels. Required in practice: an item without a known label is refused.",
            "example": "Ђ"
          },
          "labels": {
            "type": "array",
            "items": { "type": "string" },
            "maxItems": 1,
            "description": "Accepted alternative to `label`: the first entry is used. Present for compatibility with the PFR request shape.",
            "example": ["Ђ"]
          },
          "gtin": {
            "type": ["string", "null"],
            "pattern": "^\\d{8,14}$",
            "description": "Barcode, 8 to 14 digits.",
            "example": "8600123456789"
          },
          "itemId": {
            "type": ["integer", "null"],
            "description": "Optional link to a catalogue item. Stored on the invoice line, not validated."
          }
        }
      },
      "InvoiceItemOut": {
        "type": "object",
        "required": ["name", "unit", "quantity", "unitPrice", "totalAmount", "label"],
        "properties": {
          "name": { "type": "string", "example": "Кафа еспресо" },
          "unit": { "type": "string", "example": "ком" },
          "quantity": { "type": "number", "example": 2 },
          "unitPrice": { "type": "number", "example": 180.0 },
          "totalAmount": {
            "type": "number",
            "description": "quantity times unitPrice, rounded to 2 decimals.",
            "example": 360.0
          },
          "label": { "type": "string", "example": "Ђ" },
          "gtin": { "type": ["string", "null"], "example": null }
        }
      },
      "TaxItem": {
        "type": "object",
        "description": "One tax line as the PFR computed it. Rates are never computed by the client.",
        "properties": {
          "label": { "type": "string", "example": "Ђ" },
          "categoryName": { "type": "string", "example": "О-ПДВ" },
          "rate": { "type": "number", "example": 20.0 },
          "amount": {
            "type": "number",
            "description": "Tax contained in the taxed amount.",
            "example": 60.0
          },
          "total": {
            "type": "number",
            "description": "Taxed amount, VAT included.",
            "example": 360.0
          }
        },
        "additionalProperties": true
      },
      "TaxLabel": {
        "type": "object",
        "required": ["label", "name", "rate"],
        "properties": {
          "label": {
            "type": "string",
            "description": "The single Cyrillic letter printed on the receipt.",
            "example": "Ђ"
          },
          "name": { "type": "string", "example": "О-ПДВ" },
          "rate": {
            "type": "number",
            "description": "Percentage.",
            "example": 20.0
          }
        }
      },
      "InvoiceCreate": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "invoiceType": {
            "allOf": [{ "$ref": "#/components/schemas/InvoiceType" }],
            "default": "Normal"
          },
          "transactionType": {
            "allOf": [{ "$ref": "#/components/schemas/TransactionType" }],
            "default": "Sale"
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": { "$ref": "#/components/schemas/InvoiceItemIn" }
          },
          "payments": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Payment" },
            "description": "Omit to book the whole amount as Cash. One entry is stretched to the invoice total; two or more must sum to the total."
          },
          "cashier": {
            "type": "string",
            "description": "Name printed as Касир. Defaults to API when the key does not send one.",
            "example": "Касир 1"
          },
          "buyerId": {
            "type": "string",
            "maxLength": 64,
            "description": "Buyer identification, prefix plus value, for example 10:123456789 for a PIB. Mandatory on refunds.",
            "example": "10:123456789"
          },
          "buyerCostCenterId": {
            "type": "string",
            "description": "Optional buyer field printed under the buyer identification.",
            "example": "МТ-2026-114"
          },
          "referentDocumentNumber": {
            "type": "string",
            "description": "PFR number of the original invoice. Mandatory for a Refund and for a Copy.",
            "example": "DM9EWQ0Z-DM9EWQ0Z-42"
          },
          "referentDocumentDT": {
            "type": "string",
            "description": "Time of the original invoice. Anything PHP can parse is accepted and stored as YYYY-MM-DD HH:MM:SS.",
            "example": "2026-08-19 12:34:56"
          },
          "dateAndTimeOfIssue": {
            "type": "string",
            "description": "ESIR time, for a payment that was received earlier than this receipt is issued, for example a wire transfer booked the next day.",
            "example": "2026-08-18 09:00:00"
          },
          "adText": {
            "type": "string",
            "maxLength": 500,
            "description": "Advertising text printed under the receipt.",
            "example": "Хвала на поверењу"
          }
        }
      },
      "RefundRequest": {
        "type": "object",
        "description": "Every field is optional. Items are always taken from the original.",
        "properties": {
          "buyerId": {
            "type": "string",
            "maxLength": 64,
            "description": "Defaults to the buyer of the original invoice.",
            "example": "10:123456789"
          },
          "buyerCostCenterId": {
            "type": "string",
            "description": "Defaults to the value on the original invoice."
          },
          "payments": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Payment" },
            "description": "Defaults to the payments of the original. When given, they must sum to the total of the original."
          },
          "cashier": { "type": "string", "example": "Касир 1" },
          "adText": { "type": "string", "maxLength": 500 }
        }
      },
      "Invoice": {
        "type": "object",
        "description": "The public shape of a stored invoice. The same shape is returned by POST /invoices, the refund and copy routes, GET /invoices/{uuid} and every entry of GET /invoices.",
        "required": ["uuid", "invoiceType", "transactionType", "total", "items", "pfr"],
        "properties": {
          "uuid": {
            "type": "string",
            "example": "9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30"
          },
          "invoiceType": { "$ref": "#/components/schemas/InvoiceType" },
          "transactionType": { "$ref": "#/components/schemas/TransactionType" },
          "cashier": { "type": ["string", "null"], "example": "Касир 1" },
          "total": { "type": "number", "example": 480.0 },
          "payments": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Payment" }
          },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/InvoiceItemOut" }
          },
          "taxItems": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/TaxItem" }
          },
          "buyerId": { "type": ["string", "null"], "example": null },
          "referentDocumentNumber": { "type": ["string", "null"], "example": null },
          "referentDocumentDT": { "type": ["string", "null"], "example": null },
          "pfr": {
            "type": "object",
            "properties": {
              "mode": {
                "type": "string",
                "enum": ["demo", "lpfr", "vpfr"],
                "description": "The PFR that issued this invoice."
              },
              "demo": {
                "type": "boolean",
                "description": "True when the invoice was issued by the demo simulator. Such a receipt is marked ДЕМО and has no fiscal validity."
              },
              "requestedBy": { "type": ["string", "null"], "example": "DM9EWQ0Z" },
              "signedBy": { "type": ["string", "null"], "example": "DM9EWQ0Z" },
              "invoiceNumber": {
                "type": ["string", "null"],
                "description": "The PFR invoice number printed on the receipt.",
                "example": "DM9EWQ0Z-DM9EWQ0Z-42"
              },
              "invoiceCounter": {
                "type": ["string", "null"],
                "description": "Counter in the form combo/total plus the two letter abbreviation of invoice and transaction type.",
                "example": "42/97ПП"
              },
              "sdcDateTime": { "type": ["string", "null"], "example": "2026-08-20 12:34:56" },
              "verificationUrl": {
                "type": ["string", "null"],
                "description": "Link encoded in the QR code. In demo mode it points at the verification page of this installation; with a real PFR it points at the official service.",
                "example": "https://otkucaj.com/verify?id=9b2f6c1e-4d3a-4f0b-9a77-2c8e5d1f6a30"
              }
            }
          },
          "journal": {
            "type": ["string", "null"],
            "description": "The full textual receipt, 40 columns, Serbian Cyrillic."
          },
          "createdAt": { "type": "string", "example": "2026-08-20 12:34:56" }
        }
      },
      "Item": {
        "type": "object",
        "required": ["id", "name", "price", "active"],
        "properties": {
          "id": { "type": "integer", "example": 12 },
          "plu": { "type": ["string", "null"], "example": "1001" },
          "name": { "type": "string", "example": "Кафа еспресо" },
          "unit": { "type": ["string", "null"], "example": "ком" },
          "price": {
            "type": "number",
            "description": "Price in RSD, VAT included.",
            "example": 180.0
          },
          "label": {
            "type": ["string", "null"],
            "description": "Tax label. Named `tax_label` in the database, `label` here.",
            "example": "Ђ"
          },
          "gtin": { "type": ["string", "null"], "example": "8600123456789" },
          "active": { "type": "boolean", "example": true }
        }
      },
      "ItemCreate": {
        "type": "object",
        "required": ["name", "label"],
        "properties": {
          "plu": { "type": "string", "example": "1001" },
          "name": { "type": "string", "maxLength": 200, "example": "Кафа еспресо" },
          "unit": { "type": "string", "default": "ком", "example": "ком" },
          "price": { "type": "number", "minimum": 0, "default": 0, "example": 180.0 },
          "label": {
            "type": "string",
            "description": "One of the labels returned by GET /tax-labels.",
            "example": "Ђ"
          },
          "gtin": { "type": "string", "pattern": "^\\d{8,14}$", "example": "8600123456789" }
        }
      },
      "ItemUpdate": {
        "type": "object",
        "description": "Send only the fields you are changing.",
        "properties": {
          "plu": { "type": ["string", "null"], "example": "1001" },
          "name": { "type": "string", "example": "Кафа еспресо велика" },
          "unit": { "type": "string", "example": "ком" },
          "price": { "type": "number", "example": 210.0 },
          "label": { "type": "string", "example": "Ђ" },
          "gtin": { "type": ["string", "null"], "example": "8600123456789" },
          "active": { "type": "boolean", "example": true }
        }
      },
      "Status": {
        "type": "object",
        "required": ["app", "version", "esirNumber", "pfr", "time"],
        "properties": {
          "app": { "type": "string", "const": "Otkucaj" },
          "version": { "type": "string", "example": "1.2.0" },
          "esirNumber": {
            "type": "string",
            "description": "ESIR number and version as configured, joined by a slash.",
            "example": "ДЕМО/1.0"
          },
          "pfr": {
            "type": "object",
            "description": "Live PFR status. In demo mode it is always reachable. For L-PFR and V-PFR the fields of the processor status response are merged in, and `ok` is false with a `message` when it could not be reached.",
            "required": ["ok", "mode"],
            "properties": {
              "ok": { "type": "boolean" },
              "mode": { "type": "string", "enum": ["demo", "lpfr", "vpfr"] },
              "message": { "type": "string" }
            },
            "additionalProperties": true
          },
          "time": {
            "type": "string",
            "description": "Server time, Europe/Belgrade.",
            "example": "2026-08-20T12:34:56.789+02:00"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": ["key", "rateLimit", "scope", "permissions", "note"],
        "properties": {
          "key": {
            "type": "object",
            "required": ["id", "label", "prefix", "active", "createdAt", "lastUsedAt"],
            "properties": {
              "id": { "type": "integer", "example": 3 },
              "label": { "type": ["string", "null"], "example": "Веб продавница" },
              "prefix": {
                "type": ["string", "null"],
                "description": "The first characters of the key, as shown in the settings screen. The key itself is never returned.",
                "example": "kasir_9f21a…"
              },
              "active": { "type": "boolean", "example": true },
              "createdAt": { "type": ["string", "null"], "example": "2026-08-14 09:12:03" },
              "lastUsedAt": {
                "type": ["string", "null"],
                "description": "The PREVIOUS call, not this one: the current call is stamped after this row is read.",
                "example": "2026-08-20 11:58:41"
              }
            }
          },
          "rateLimit": {
            "type": "object",
            "required": ["requests", "windowSeconds"],
            "properties": {
              "requests": { "type": "integer", "const": 240 },
              "windowSeconds": { "type": "integer", "const": 60 }
            }
          },
          "scope": { "type": "string", "const": "full" },
          "permissions": {
            "type": "array",
            "items": { "type": "string" },
            "description": "What every active key may do. There is no per-key scoping."
          },
          "note": { "type": "string" }
        }
      },
      "ReportSummary": {
        "type": "object",
        "required": ["from", "to", "totals", "byTaxLabel", "byPaymentType", "byCashier", "rule"],
        "properties": {
          "from": { "type": "string", "example": "2026-08-01" },
          "to": { "type": "string", "example": "2026-08-20" },
          "totals": {
            "type": "object",
            "required": ["invoices", "turnover", "refunds"],
            "properties": {
              "invoices": {
                "type": "integer",
                "description": "Number of issued Normal and Advance invoices in the period, sales and refunds together."
              },
              "turnover": {
                "type": "number",
                "description": "Sales minus refunds, in RSD."
              },
              "refunds": {
                "type": "object",
                "required": ["count", "amount"],
                "properties": {
                  "count": { "type": "integer" },
                  "amount": {
                    "type": "number",
                    "description": "Positive sum of the refund invoices. Already subtracted from turnover."
                  }
                }
              }
            }
          },
          "byTaxLabel": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["label", "turnover"],
              "properties": {
                "label": { "type": "string", "example": "Ђ" },
                "name": {
                  "type": ["string", "null"],
                  "description": "Null when the label is no longer defined.",
                  "example": "О-ПДВ"
                },
                "rate": { "type": ["number", "null"], "example": 20.0 },
                "turnover": { "type": "number", "example": 401220.5 }
              }
            }
          },
          "byPaymentType": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["paymentType", "amount"],
              "properties": {
                "paymentType": { "type": "string", "example": "Cash" },
                "amount": { "type": "number", "example": 302150.5 }
              }
            }
          },
          "byCashier": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["cashier", "invoices", "turnover"],
              "properties": {
                "cashier": { "type": ["string", "null"], "example": "Касир 1" },
                "invoices": { "type": "integer", "example": 141 },
                "turnover": { "type": "number", "example": 320410.5 }
              }
            }
          },
          "rule": {
            "type": "string",
            "description": "The accounting rule applied, stated in the response so a report can never be read under the wrong assumption."
          }
        }
      },
      "ReportDaily": {
        "type": "object",
        "required": ["from", "to", "days", "rule"],
        "properties": {
          "from": { "type": "string", "example": "2026-08-18" },
          "to": { "type": "string", "example": "2026-08-20" },
          "days": {
            "type": "array",
            "description": "Days with no traffic are omitted.",
            "items": {
              "type": "object",
              "required": ["date", "invoices", "turnover"],
              "properties": {
                "date": { "type": "string", "example": "2026-08-18" },
                "invoices": { "type": "integer", "example": 41 },
                "turnover": { "type": "number", "example": 92310.0 }
              }
            }
          },
          "rule": { "type": "string" }
        }
      }
    }
  }
}
