openapi: 3.1.0
info:
  title: Fakturo API
  description: |
    The Fakturo API enables third-party applications to securely access business data through OAuth 2.0 authentication.
    
    ## Authentication
    
    Every API endpoint takes `Authorization: Bearer <token>`, where the token is either:

    - **a personal API key** (`fk_live_…`), created in Fakturo under Postavke > API i MCP for one business,
      with chosen scopes. The simplest way for your own scripts, your accountant or an AI assistant; or
    - **an OAuth 2.0 access token** (authorization code flow, PKCE supported):
      1. **Register your application** in Fakturo Settings > OAuth Applications, or dynamically
         (`POST /api/oauth/register`, RFC 7591; discovery at `/.well-known/oauth-authorization-server`)
      2. **Direct users** to the authorization endpoint
      3. **Exchange the authorization code** for an access token
      4. **Make API requests** using the Bearer token

    ## MCP

    AI assistants (Claude, ChatGPT, …) connect to the MCP server at `/api/mcp` (Streamable HTTP,
    stateless JSON-RPC). It uses the same tokens and scopes; an unauthenticated request gets a 401 with
    `WWW-Authenticate: Bearer resource_metadata=…` pointing at `/.well-known/oauth-protected-resource`.

    ## Errors

    Endpoints added in 2026-10 answer errors as `{ "error": "...", "details"?: ... }`.

    ## Rate Limits
    
    Access tokens are valid for 1 year. Please implement proper error handling for expired or revoked tokens.
    
    ## Base URL
    
    Replace `{instance}` with your Fakturo instance URL:
    ```
    https://{instance}.fakturo.app
    ```
  version: 1.0.0
  contact:
    name: Fakturo Support
    url: https://fakturo.app
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://{instance}.fakturo.app
    description: Production server
    variables:
      instance:
        default: your-instance
        description: Your Fakturo instance subdomain
  - url: http://localhost:5173
    description: Local development server

tags:
  - name: OAuth
    description: OAuth 2.0 authentication endpoints
  - name: Units
    description: Units of measurement
  - name: VAT
    description: VAT (Value Added Tax) categories
  - name: KPD
    description: KPD (Classification of Products by Activity) codes
  - name: Inventory
    description: Inventory and product management
  - name: Businesses
    description: Business settings, fiscalization and the eRačun address in Porezna's AMS
  - name: Sales Locations
    description: Sales locations and points of sale management
  - name: Invoices
    description: Invoice creation and management with fiscalization support
  - name: Contacts
    description: Customer and supplier contact management
  - name: Templates
    description: Invoice (PDF) templates and the business default
  - name: Email
    description: Automatic email templates and the log of sent emails
  - name: History
    description: Change history (who changed what, when, through what)
  - name: Reports
    description: Revenue in the business base currency

paths:
  /api/oauth/authorize:
    get:
      tags:
        - OAuth
      summary: Initiate OAuth authorization
      description: |
        Redirects the user to log in and authorize your application. 
        After authorization, the user will be redirected to your `redirect_uri` with an authorization code.
      operationId: authorize
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: string
          description: Your application's client ID
          example: abc123def456
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
            format: uri
          description: Must match one of your registered redirect URIs
          example: https://yourapp.com/callback
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            enum: [code]
          description: Must be "code"
          example: code
        - name: scope
          in: query
          required: false
          schema:
            type: string
          description: Space-separated list of scopes
          example: read:inventory write:inventory read:units read:vat_categories read:kpd_codes read:contacts write:contacts read:sales_locations write:sales_locations read:invoices write:invoices
        - name: state
          in: query
          required: false
          schema:
            type: string
          description: Random string for CSRF protection
          example: random_string_12345
      responses:
        '303':
          description: Redirect to login or back to redirect_uri with code
          headers:
            Location:
              schema:
                type: string
                format: uri
              description: Redirect URL
        '400':
          description: Invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: OAuth application not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/oauth/token:
    post:
      tags:
        - OAuth
      summary: Exchange authorization code for access token
      description: |
        Exchange an authorization code received from the authorization endpoint for an access token.
        The access token can then be used to make authenticated API requests.
      operationId: getToken
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - grant_type
                - code
                - redirect_uri
                - client_id
                - client_secret
              properties:
                grant_type:
                  type: string
                  enum: [authorization_code]
                  description: Must be "authorization_code"
                code:
                  type: string
                  description: Authorization code from the authorization endpoint
                redirect_uri:
                  type: string
                  format: uri
                  description: Must match the redirect_uri from authorization request
                client_id:
                  type: string
                  description: Your application's client ID
                client_secret:
                  type: string
                  description: Your application's client secret
      responses:
        '200':
          description: Access token successfully issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          description: Invalid request or authorization code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid client credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/units-of-measurement:
    get:
      tags:
        - Units
      summary: List units of measurement
      description: Get a paginated list of all available units of measurement with optional search filtering.
      operationId: listUnits
      security:
        - BearerAuth: [read:units]
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UnitOfMeasurement'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/vat-categories:
    get:
      tags:
        - VAT
      summary: List VAT categories
      description: Get a paginated list of all available VAT categories with optional search filtering.
      operationId: listVatCategories
      security:
        - BearerAuth: [read:vat_categories]
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/VatCategory'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/kpd-codes:
    get:
      tags:
        - KPD
      summary: List KPD codes
      description: Get a paginated list of all available KPD (Classification of Products by Activity) codes with optional search filtering.
      operationId: listKpdCodes
      security:
        - BearerAuth: [read:kpd_codes]
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: string
          description: Filter by exact KPD code ID
        - name: name
          in: query
          required: false
          schema:
            type: string
          description: Search by name (case-insensitive partial match)
        - name: area
          in: query
          required: false
          schema:
            type: string
          description: Search by area (case-insensitive partial match)
        - name: compartment
          in: query
          required: false
          schema:
            type: string
          description: Search by compartment (case-insensitive partial match)
        - name: group
          in: query
          required: false
          schema:
            type: string
          description: Search by group (case-insensitive partial match)
        - name: class
          in: query
          required: false
          schema:
            type: string
          description: Search by class (case-insensitive partial match)
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: Search by category (case-insensitive partial match)
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/KpdCode'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/kpd-codes/{id}:
    get:
      tags:
        - KPD
      summary: Get a single KPD code
      description: Retrieve details of a specific KPD code by its ID.
      operationId: getKpdCode
      security:
        - BearerAuth: [read:kpd_codes]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: KPD code ID
          example: '01.11.10'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/KpdCode'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: KPD code not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/contacts:
    get:
      tags:
        - Contacts
      summary: List contacts
      description: |
        Get a paginated list of contacts for the authenticated business.
        Results can be filtered by name, email, or tax ID.
      operationId: listContacts
      security:
        - BearerAuth: [read:contacts]
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search term to filter contacts by name, email, tax ID or partner ID (partner_code)
          example: john
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  total:
                    type: integer
                    description: Total number of contacts
                    example: 42
                  limit:
                    type: integer
                    example: 100
                  offset:
                    type: integer
                    example: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Contacts
      summary: Create a contact
      description: |
        Create a new contact for the authenticated business.
        The contact will automatically be associated with the business linked to the access token.
      operationId: createContact
      security:
        - BearerAuth: [write:contacts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '201':
          description: Contact created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/contacts/{id}:
    get:
      tags:
        - Contacts
      summary: Get a single contact
      description: Get details of a specific contact
      operationId: getContact
      security:
        - BearerAuth: [read:contacts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Contact ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Contacts
      summary: Update a contact
      description: Update an existing contact
      operationId: updateContact
      security:
        - BearerAuth: [write:contacts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Contact ID
          example: 123e4567-e89b-12d3-a456-426614174000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '200':
          description: Contact updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Contacts
      summary: Delete a contact
      description: Delete a contact from the business
      operationId: deleteContact
      security:
        - BearerAuth: [write:contacts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Contact ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Contact deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/inventory:
    get:
      tags:
        - Inventory
      summary: List inventory items
      description: |
        Get a paginated list of inventory items for the authenticated business, ordered by name.
        Results are automatically filtered to only show items belonging to the business associated with the access token.
        `search` matches the name, description or item code (case-insensitive substring).
      operationId: listInventory
      security:
        - BearerAuth: [read:inventory]
      parameters:
        - $ref: '#/components/parameters/Search'
        - name: code
          in: query
          required: false
          schema:
            type: string
          description: Exact item code (šifra artikla), case-insensitive
          example: '00042'
        - name: item_type
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/InventoryItemType'
          description: Only items of this type
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/InventoryItem'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Inventory
      summary: Create an inventory item
      description: |
        Create a new inventory item for the authenticated business.
        The item will automatically be associated with the business linked to the access token.
      operationId: createInventory
      security:
        - BearerAuth: [write:inventory]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryItemInput'
            examples:
              basic:
                summary: Basic inventory item
                value:
                  name: Premium Widget
                  price: 99.99
              complete:
                summary: Complete inventory item
                value:
                  code: '00042'
                  item_type: goods_stock
                  name: Premium Widget
                  description: High-quality widget for professional use
                  price: 99.99
                  unit_of_measurement_id: piece
                  vat_category_id: S
                  vat_rate: 25
                  kpd_code_id: '01.11.10'
      responses:
        '201':
          description: Inventory item created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryItem'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Another item of the business already has this code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/inventory/{id}:
    get:
      tags:
        - Inventory
      summary: Get a single inventory item
      description: Retrieve details of a specific inventory item by its ID. The item must belong to the authenticated business.
      operationId: getInventory
      security:
        - BearerAuth: [read:inventory]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Inventory item ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Inventory item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Inventory
      summary: Update an inventory item
      description: Update an existing inventory item. The item must belong to the authenticated business.
      operationId: updateInventory
      security:
        - BearerAuth: [write:inventory]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Inventory item ID
          example: 123e4567-e89b-12d3-a456-426614174000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryItemInput'
      responses:
        '200':
          description: Inventory item updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryItem'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Another item of the business already has this code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Inventory item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Inventory
      summary: Delete an inventory item
      description: Delete an existing inventory item. The item must belong to the authenticated business.
      operationId: deleteInventory
      security:
        - BearerAuth: [write:inventory]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Inventory item ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Inventory item deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Inventory item deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Inventory item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/units-of-measurement/{id}:
    get:
      tags:
        - Units
      summary: Get a single unit of measurement
      description: Retrieve details of a specific unit of measurement by its ID.
      operationId: getUnit
      security:
        - BearerAuth: [read:units]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Unit of measurement ID
          example: kg
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnitOfMeasurement'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Unit of measurement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/vat-categories/{id}:
    get:
      tags:
        - VAT
      summary: Get a single VAT category
      description: Retrieve details of a specific VAT category by its ID.
      operationId: getVatCategory
      security:
        - BearerAuth: [read:vat_categories]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: VAT category ID
          example: S
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatCategory'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: VAT category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/businesses/{id}:
    get:
      tags:
        - Businesses
      summary: Get a business
      description: A business the caller is an active member of. An API key reaches only the business it was created for.
      operationId: getBusiness
      security:
        - BearerAuth: [read:businesses]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Business ID
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Business'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Business not found, or the caller is not a member (`{ error }`)
    put:
      tags:
        - Businesses
      summary: Update a business
      description: |
        Replaces the business's settings; owners and admins only. A field left out is cleared
        (null, or false for the flags), except `f1_vat_status_source`, `eracun_environment` and
        `vat_exemption_reason`, which keep their values, and the F1 certificate, which is kept while
        B2C stays on.
        Send back what `GET` returned, with your changes.

        The business's eRačun address in the AMS (Porezna's register through which other access
        points deliver eRačuni to it) follows the saved settings, as on the business page in Fakturo:

        - B2B on: the OIB (`vat_number`) is registered in the AMS of `eracun_environment`.
        - B2B off, also when `b2b_invoicing_enabled` is left out: it is deleted from the AMS.
        - A new `eracun_environment` or `vat_number` moves it. A production registration is deleted
          first. If that fails, a move to another environment does not happen: the business stays
          registered in production. A new `vat_number` that stays in production is registered anyway,
          and the previous OIB stays in the production AMS. A conformance registration is left in place.

        The settings are saved even when the AMS step fails: `mps_registration` reports it, and
        `POST /api/v1/businesses/{id}/mps/retry` tries again. 'registered' means the AMS accepted the
        registration: a new one waits until the taxpayer confirms it in ePorezna (FiskAplikacija >
        Fiskalizacija 2.0 > Administracija > Adrese za zaprimanje eRačuna).
      operationId: updateBusiness
      security:
        - BearerAuth: [write:businesses]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Business ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BusinessInput'
      responses:
        '200':
          description: Business updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Business'
                  - type: object
                    properties:
                      mps_registration:
                        $ref: '#/components/schemas/BusinessMpsRegistration'
        '400':
          description: Invalid field (`{ error, details }`, the field in `details`)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing scope, an API key of another business, or a member who is neither the owner nor an admin
        '404':
          description: Business not found, or the caller is not a member (`{ error }`)

  /api/v1/businesses/{id}/mps/retry:
    post:
      tags:
        - Businesses
      summary: Retry the AMS registration
      description: |
        Finishes what a save could not: registers a business with B2B on in the AMS of its
        `eracun_environment` (after a failed or pending attempt, or when it is registered in another
        environment), or deletes a registration left behind with B2B off. Owners and admins only.
      operationId: retryBusinessMpsRegistration
      security:
        - BearerAuth: [write:businesses]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Business ID
      responses:
        '200':
          description: 'Done, or nothing to do: `{ success: true, message, business: { id, mps_registration_status, mps_registered_at } }`'
        '400':
          description: 'The AMS step failed, or B2B is off with nothing to delete: `{ success: false, error, message }`'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing scope, an API key of another business, or a member who is neither the owner nor an admin
        '404':
          description: Business not found, or the caller is not a member (`{ error }`)

  /api/v1/sales-locations:
    get:
      tags:
        - Sales Locations
      summary: List sales locations
      description: |
        Get a paginated list of sales locations for the authenticated business.
        Results are automatically filtered to show only locations belonging to the business associated with the access token.
      operationId: listSalesLocations
      security:
        - BearerAuth: [read:sales_locations]
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SalesLocation'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Sales Locations
      summary: Create a sales location
      description: |
        Create a new sales location for the authenticated business.
        The location will automatically be associated with the business linked to the access token.
      operationId: createSalesLocation
      security:
        - BearerAuth: [write:sales_locations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SalesLocationInput'
      responses:
        '201':
          description: Sales location created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesLocation'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/sales-locations/{id}:
    get:
      tags:
        - Sales Locations
      summary: Get a single sales location
      description: Retrieve details of a specific sales location by its ID. The location must belong to the authenticated business.
      operationId: getSalesLocation
      security:
        - BearerAuth: [read:sales_locations]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Sales location ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesLocation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Sales location not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Sales Locations
      summary: Update a sales location
      description: Update an existing sales location. The location must belong to the authenticated business.
      operationId: updateSalesLocation
      security:
        - BearerAuth: [write:sales_locations]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Sales location ID
          example: 123e4567-e89b-12d3-a456-426614174000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SalesLocationInput'
      responses:
        '200':
          description: Sales location updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesLocation'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Sales location not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Sales Locations
      summary: Delete a sales location
      description: Delete an existing sales location. The location must belong to the authenticated business.
      operationId: deleteSalesLocation
      security:
        - BearerAuth: [write:sales_locations]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Sales location ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Sales location deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Sales location deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Sales location not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/invoices:
    get:
      tags:
        - Invoices
      summary: List invoices
      description: |
        Get a paginated list of invoices for the authenticated business with optional filtering.
        Results are automatically filtered to show only invoices belonging to the business associated with the access token.
        Response includes related contact, sales location, line items, and tags.
      operationId: listInvoices
      security:
        - BearerAuth: [read:invoices]
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [draft, sent, paid, overdue, cancelled]
          description: Filter by invoice status
          example: paid
        - name: contact_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by contact ID
        - name: invoice_type
          in: query
          required: false
          schema:
            type: string
          description: Filter by invoice type
          example: '1-bill'
        - name: customer_type
          in: query
          required: false
          schema:
            type: string
            enum: [b2b, b2c]
          description: Filter by customer type (B2B or B2C)
          example: b2b
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: |
            Only invoices whose invoice number contains this text, ignoring case; `%` and `_` match only themselves.
            Finds an invoice you created with your own number, however old, e.g. after a duplicate-number error (`code` 23505).
          example: PD_2026_41
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
                  total:
                    type: integer
                    description: Total number of invoices
                    example: 42
                  limit:
                    type: integer
                    example: 100
                  offset:
                    type: integer
                    example: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Invoices
      summary: Create an invoice
      description: |
        Create a new invoice for the authenticated business with line items.
        The invoice will automatically be associated with the business linked to the access token.
        Invoice number is auto-generated. Totals are calculated automatically based on items and VAT rates.
        
        **Tax Calculation:**
        Tax is applied based on the following rules:
        - Business must be VAT registered
        - AND contact must be either B2C, OR B2B in Croatia (`country_code` HR, or none; whether or not VAT registered), OR B2B outside Croatia AND VAT registered
        
        If tax should not be applied, `tax_amount` will be 0 regardless of item VAT rates.
        
        **Discounts:**
        Invoice-level discounts can be applied using either `discount_percentage` or `discount_amount`.
        Only one discount type is applied (percentage takes precedence).
        
        **Delivery Fees:**
        Delivery/shipping charges can be added using `delivery_fee_amount`.
        The fee can have its own VAT rate (`delivery_fee_vat_rate`), which will be applied if tax applies.
        Use UN/CEFACT charge reason codes (FC=Freight, DL=Delivery) in `delivery_fee_reason_code`.
      operationId: createInvoice
      security:
        - BearerAuth: [write:invoices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceInput'
            examples:
              basic:
                summary: Basic invoice
                value:
                  contact_id: 123e4567-e89b-12d3-a456-426614174001
                  sales_location_id: 123e4567-e89b-12d3-a456-426614174002
                  date_issued: '2025-11-11'
                  due_date: '2025-11-25'
                  currency: EUR
                  items:
                    - inventory_id: 123e4567-e89b-12d3-a456-426614174003
                      quantity: 2
                      unit_price: 99.99
              with_discount:
                summary: Invoice with 10% discount
                value:
                  contact_id: 123e4567-e89b-12d3-a456-426614174001
                  sales_location_id: 123e4567-e89b-12d3-a456-426614174002
                  date_issued: '2025-11-11'
                  due_date: '2025-11-25'
                  currency: EUR
                  discount_percentage: 10
                  discount_reason_code: '95'
                  discount_reason_text: Volume discount
                  items:
                    - inventory_id: 123e4567-e89b-12d3-a456-426614174003
                      quantity: 5
                      unit_price: 99.99
                      vat_rate: 25
              with_delivery_fee:
                summary: Invoice with delivery fee
                value:
                  contact_id: 123e4567-e89b-12d3-a456-426614174001
                  sales_location_id: 123e4567-e89b-12d3-a456-426614174002
                  date_issued: '2025-11-11'
                  due_date: '2025-11-25'
                  currency: EUR
                  delivery_fee_amount: 15.00
                  delivery_fee_vat_rate: 25
                  delivery_fee_reason_code: 'FC'
                  delivery_fee_reason_text: Standard shipping
                  items:
                    - inventory_id: 123e4567-e89b-12d3-a456-426614174003
                      quantity: 2
                      unit_price: 99.99
                      vat_rate: 25
      responses:
        '201':
          description: Invoice created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Invoice'
                  - type: object
                    properties:
                      fiscalization:
                        $ref: '#/components/schemas/InvoiceFiscalizationResult'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Referenced contact, sales location, or inventory item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/invoices/{id}:
    get:
      tags:
        - Invoices
      summary: Get a single invoice
      description: |
        Retrieve details of a specific invoice by its ID, including all line items, contact details, sales location, and tags.
        The invoice must belong to the authenticated business.
      operationId: getInvoice
      security:
        - BearerAuth: [read:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Invoice ID
          example: 123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/invoices/{id}/mark-paid:
    post:
      tags:
        - Invoices
      summary: Mark invoice as paid
      description: |
        Mark an invoice as paid and handle fiscalization based on customer type:
        
        **B2B Invoices:**
        - If already fiscalized, reports payment to the fiscalization system
        - If not fiscalized but fiscalization is enabled, attempts to fiscalize via UBL/e-Račun
          and sends the eRačun to the buyer. Its UBL is checked against the EN 16931 and HR CIUS
          rules first: an eRačun that fails them is refused with 422 and nothing changes.

        **B2C Invoices:**
        - Simply marked as paid locally (no payment reporting required)
        - B2C fiscalization (FINA) happens at invoice creation time, not at payment
        
        Bills (1-bill) and proformas (2-advance_payment_bill) can be marked as paid. A proforma
        is never fiscalized or reported: it is only marked paid.
      operationId: markInvoicePaid
      security:
        - BearerAuth: [write:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Invoice ID
          example: 123e4567-e89b-12d3-a456-426614174000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - paid_at
              properties:
                paid_at:
                  type: string
                  format: date-time
                  description: Payment date and time
                  example: '2025-11-11T10:30:00Z'
                payment_method:
                  type: string
                  enum: [cash, card, other]
                  description: Payment method (used for B2B payment reporting)
                  example: card
      responses:
        '200':
          description: Invoice marked as paid successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  fiscalized:
                    type: boolean
                    description: Whether fiscalization was attempted and succeeded
                    example: true
                  jir:
                    type: string
                    nullable: true
                    description: Unique invoice identifier from tax authority (if fiscalized)
                    example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
                  zki:
                    type: string
                    nullable: true
                    description: Security code of the invoice (if fiscalized)
                    example: '1a2b3c4d5e6f7890'
                  payment_reported:
                    type: boolean
                    description: Whether payment was reported to fiscalization system (B2B only)
                    example: true
                  fiscalization_error:
                    type: string
                    nullable: true
                    description: Error message if fiscalization failed (invoice still marked as paid)
                    example: 'Connection to fiscalization service failed'
                  eracun_sent:
                    type: boolean
                    description: |
                      Set when this call fiscalized the invoice: whether the eRačun was sent to the
                      buyer (AS4). The outcome is also kept on the invoice (`as4_delivery_status`).
                    example: true
                  eracun_error:
                    type: string
                    description: Why the eRačun was not sent; also kept in `as4_error_description`
                    example: 'HTTP 503: Service Unavailable'
        '400':
          description: Invalid request (e.g., wrong invoice type, already paid, or invalid payment_method)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            The invoice would be fiscalized here, but its eRačun fails the EN 16931 / HR CIUS rules.
            Nothing changes: it is not marked as paid, fiscalized or sent. The rules are kept in
            the invoice's `fiscalization_error`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: 'eRačun ne prolazi pravila EN 16931 i HR CIUS, pa nije fiskaliziran ni poslan kupcu'
                  validation_errors:
                    type: array
                    items:
                      type: string
                    description: One message per broken rule
                    example:
                      - '[BR-E-10]-A VAT breakdown (BG-23) with VAT Category code (BT-118) "Exempt from VAT" shall have a VAT exemption reason code (BT-121) or a VAT exemption reason text (BT-120).'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Invoice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/invoices/{id}/send:
    post:
      tags:
        - Invoices
      summary: Send a draft (fiscalize or email)
      description: |
        Sends a draft exactly like the Send button: a business that fiscalizes fiscalizes it
        (F1 for a B2C bill, eRačun + AS4 for B2B) and emails the PDF when the invoice has a template;
        otherwise the PDF is emailed. Only bills (1-bill, 3-cancellation, 4-approval) are fiscalized.
        502 when fiscalization fails (the invoice stays a draft). 409 when a bill without a number would be
        numbered on the register the API numbers its own bills on at its sales location (`api_device_number`):
        nothing is numbered or sent until the location's `device_number` differs.
      operationId: sendInvoice
      security:
        - BearerAuth: [send:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_method:
                  type: string
                  enum: [cash, card, transfer, other]
                  description: For a B2C bill (NacinPlac); default other
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/invoices/{id}/duplicate:
    post:
      tags:
        - Invoices
      summary: Duplicate an invoice (new draft)
      operationId: duplicateInvoice
      security:
        - BearerAuth: [write:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/invoices/{id}/convert:
    post:
      tags:
        - Invoices
      summary: Turn an offer or proforma into a bill (new draft)
      operationId: convertInvoice
      security:
        - BearerAuth: [write:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/invoices/{id}/storno:
    post:
      tags:
        - Invoices
      summary: Storno a sent bill (new draft, lines negated)
      description: |
        The storno is linked to the original (its eRačun names it in BillingReference). A bill has at most one storno (409). Bills with a document discount or delivery fee are refused (400).
      operationId: stornoInvoice
      security:
        - BearerAuth: [write:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/invoices/{id}/pdf:
    get:
      tags:
        - Invoices
      summary: Invoice as PDF
      operationId: invoicePdf
      security:
        - BearerAuth: [read:invoices]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
        - name: template
          in: query
          schema:
            type: string
          description: 'custom:<id> or global:<id> (default: the invoice''s own template)'
      responses:
        '200':
          description: The PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '404':
          description: Invoice not found, or it has no template and none was given
  /api/v1/invoices/{id}/history:
    get:
      tags:
        - History
      summary: Change history of an invoice
      operationId: invoiceHistory
      security:
        - BearerAuth: [read:history]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Invoice id (UUID)
        - name: limit
          in: query
          schema:
            type: integer
          description: 'At most 200'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/history:
    get:
      tags:
        - History
      summary: Change history of the business
      operationId: history
      security:
        - BearerAuth: [read:history]
      parameters:
        - name: table
          in: query
          schema:
            type: string
          description: 'invoices, invoice_items, contacts, inventory, businesses, …'
        - name: row_id
          in: query
          schema:
            type: string
          description: 'One record (UUID)'
        - name: limit
          in: query
          schema:
            type: integer
          description: 'At most 200'
        - name: offset
          in: query
          schema:
            type: integer
          description: 'Paging'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/email-templates:
    get:
      tags:
        - Email
      summary: Automatic emails, their placeholders and the business versions
      operationId: listEmailTemplates
      security:
        - BearerAuth: [read:email_templates]
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/email-templates/{key}:
    put:
      tags:
        - Email
      summary: Set the business's version of an email
      operationId: putEmailTemplate
      security:
        - BearerAuth: [write:email_templates]
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
            enum: [invoice_send, payment_reminder, accountant_export, received_deadline]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                language:
                  type: string
                  example: hr
                subject:
                  type: string
                body_html:
                  type: string
                  description: HTML with {{placeholder}} fields only (values are HTML-escaped; nothing is executed)
                enabled:
                  type: boolean
                  description: false = this email is not sent
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
    delete:
      tags:
        - Email
      summary: Back to the built-in default
      operationId: resetEmailTemplate
      security:
        - BearerAuth: [write:email_templates]
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
        - name: language
          in: query
          schema:
            type: string
          description: 'Default hr'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/email-templates/{key}/test:
    post:
      tags:
        - Email
      summary: Send a test of an email to yourself
      operationId: testEmailTemplate
      security:
        - BearerAuth: [write:email_templates]
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/email-log:
    get:
      tags:
        - Email
      summary: Emails sent for the business
      operationId: emailLog
      security:
        - BearerAuth: [read:email_log]
      parameters:
        - name: invoice_id
          in: query
          schema:
            type: string
          description: 'One invoice'
        - name: status
          in: query
          schema:
            type: string
          description: 'sent or failed'
        - name: limit
          in: query
          schema:
            type: integer
          description: 'At most 200'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/templates:
    get:
      tags:
        - Templates
      summary: Invoice templates and the business default
      operationId: listTemplates
      security:
        - BearerAuth: [read:templates]
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
    post:
      tags:
        - Templates
      summary: Create a template
      operationId: createTemplate
      security:
        - BearerAuth: [write:templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, template]
              properties:
                name:
                  type: string
                template:
                  type: string
                  description: HTML with EJS (`<%= it.invoice_number %>`), rendered in a sandboxed browser
                language:
                  type: string
                  example: en
                description:
                  type: string
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/templates/default:
    put:
      tags:
        - Templates
      summary: Set the business default template
      operationId: setDefaultTemplate
      security:
        - BearerAuth: [write:templates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                template:
                  type: [string, 'null']
                  example: custom:123e4567-e89b-12d3-a456-426614174000
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/templates/{ref}:
    get:
      tags:
        - Templates
      summary: A template with its HTML
      operationId: getTemplate
      security:
        - BearerAuth: [read:templates]
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
          description: custom:<id> or global:<id>
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
    patch:
      tags:
        - Templates
      summary: Change one of the business's templates
      operationId: updateTemplate
      security:
        - BearerAuth: [write:templates]
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
          description: custom:<id> or global:<id>
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
    delete:
      tags:
        - Templates
      summary: Delete one of the business's templates
      operationId: deleteTemplate
      security:
        - BearerAuth: [write:templates]
      parameters:
        - name: ref
          in: path
          required: true
          schema:
            type: string
          description: custom:<id> or global:<id>
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/inventory/{id}/translations:
    get:
      tags:
        - Inventory
      summary: A product's translations
      operationId: getTranslations
      security:
        - BearerAuth: [read:inventory]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Product id (UUID)
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
    put:
      tags:
        - Inventory
      summary: Replace a product's translations
      description: |
        A PDF in a template of that language prints the translated name and description.
      operationId: putTranslations
      security:
        - BearerAuth: [write:inventory]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Product id (UUID)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                translations:
                  type: array
                  items:
                    type: object
                    required: [language, name]
                    properties:
                      language:
                        type: string
                        example: en
                      name:
                        type: string
                      description:
                        type: string
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/reports/revenue:
    get:
      tags:
        - Reports
      summary: Revenue summary in the base currency
      operationId: revenue
      security:
        - BearerAuth: [read:reports]
      parameters:
        - name: from
          in: query
          schema:
            type: string
          description: 'YYYY-MM-DD (default 1 January)'
        - name: to
          in: query
          schema:
            type: string
          description: 'YYYY-MM-DD (default today)'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/v1/reports/revenue-by-month:
    get:
      tags:
        - Reports
      summary: Issued and paid revenue per month
      operationId: revenueByMonth
      security:
        - BearerAuth: [read:reports]
      parameters:
        - name: year
          in: query
          schema:
            type: integer
          description: 'Default this year'
      responses:
        '200':
          description: OK
        '400':
          description: Invalid request (`{ error, details? }`)
        '401':
          description: Missing or invalid token / API key
        '403':
          description: Missing scope
        '404':
          description: Not found
  /api/oauth/register:
    post:
      tags:
        - OAuth
      summary: Dynamic client registration (RFC 7591)
      description: |
        For MCP clients and other apps that register themselves. Public clients
        (token_endpoint_auth_method none) must use PKCE (S256). A client can do nothing
        until a user approves it for a business on the consent screen.
      operationId: registerClient
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [redirect_uris]
              properties:
                client_name:
                  type: string
                redirect_uris:
                  type: array
                  items:
                    type: string
                token_endpoint_auth_method:
                  type: string
                  enum: [none, client_secret_post, client_secret_basic]
      responses:
        '201':
          description: Registered (client_secret only for confidential clients)
        '400':
          description: Invalid client metadata
  /api/mcp:
    post:
      tags:
        - OAuth
      summary: MCP server (JSON-RPC 2.0, Streamable HTTP, stateless)
      description: |
        Methods: initialize, ping, tools/list, tools/call. The tools offered depend on the
        token's scopes (e.g. list_invoices, create_invoice, send_invoice, revenue_summary,
        update_email_template, get_history).
      operationId: mcp
      security:
        - BearerAuth: []
      responses:
        '200':
          description: JSON-RPC response
        '202':
          description: Notification accepted
        '401':
          description: 'Unauthenticated; WWW-Authenticate: Bearer resource_metadata="…"'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: An OAuth 2.0 access token from /api/oauth/token, or a personal API key (fk_live_…) from Postavke > API i MCP

  parameters:
    Search:
      name: search
      in: query
      required: false
      schema:
        type: string
      description: Search term to filter results by name or description
      example: product

    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 100
      description: Maximum number of results to return
      example: 10

    Offset:
      name: offset
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
        default: 0
      description: Number of results to skip for pagination
      example: 0

  schemas:
    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: The access token to use for API requests
          example: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
        token_type:
          type: string
          description: Token type (always "Bearer")
          example: Bearer
        expires_in:
          type: integer
          description: Token lifetime in seconds
          example: 31536000
        scope:
          type: string
          description: Space-separated list of granted scopes
          example: read:inventory write:inventory read:units read:vat_categories read:kpd_codes read:contacts write:contacts read:sales_locations write:sales_locations read:invoices write:invoices

    UnitOfMeasurement:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the unit
          example: kg
        name:
          type: string
          description: Display name of the unit
          example: Kilogram
        created_at:
          type: string
          format: date-time
          description: Timestamp when the unit was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the unit was last updated
          example: '2025-11-08T12:00:00Z'

    VatCategory:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the VAT category
          example: standard
        name:
          type: string
          description: Display name of the VAT category
          example: Standardna stopa (25%)
        created_at:
          type: string
          format: date-time
          description: Timestamp when the category was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the category was last updated
          example: '2025-11-08T12:00:00Z'

    KpdCode:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the KPD code
          example: '01.11.10'
        name:
          type: string
          description: Name/description of the KPD classification
          example: Wheat and meslin
        area:
          type: string
          nullable: true
          description: KPD area classification
          example: Agriculture, forestry and fishing
        compartment:
          type: string
          description: KPD compartment classification
          example: A
        group:
          type: string
          description: KPD group classification
          example: '01'
        class:
          type: string
          description: KPD class classification
          example: '01.1'
        category:
          type: string
          description: KPD category classification
          example: '01.11'
        created_at:
          type: string
          format: date-time
          description: Timestamp when the code was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the code was last updated
          example: '2025-11-08T12:00:00Z'

    InventoryItemType:
      type: string
      description: |
        Vrsta artikla (as in Fina e-Račun): services (prodaja usluga), goods_no_stock (prodaja robe – zalihe se ne vode),
        goods_stock (prodaja robe – evidencija zaliha), material (materijal), semi_product (poluproizvod), product (proizvod),
        license (prodaja licencije), electronic_services (prodaja elektroničkih usluga), motor_vehicle (motorno vozilo),
        food_preparation (priprema hrane), mixed_drink (sastavljeno piće), package (paket)
      enum:
        - services
        - goods_no_stock
        - goods_stock
        - material
        - semi_product
        - product
        - license
        - electronic_services
        - motor_vehicle
        - food_preparation
        - mixed_drink
        - package
      example: goods_stock

    VatExemptionReasonCode:
      type: string
      description: |
        VATEX code of a VAT exemption reason (eRačun BT-121), from Porezna's list (Tehnička specifikacija
        Fiskalizacija eRačuna i eIzvještavanje, Dodatak 15). VATEX-EU-AE, VATEX-EU-IC, VATEX-EU-G and VATEX-EU-O
        go only with VAT categories AE, K, G and O; the other codes are exemptions (category E).
      enum:
        - VATEX-EU-79-C
        - VATEX-EU-132
        - VATEX-EU-132-1A
        - VATEX-EU-132-1B
        - VATEX-EU-132-1C
        - VATEX-EU-132-1D
        - VATEX-EU-132-1E
        - VATEX-EU-132-1F
        - VATEX-EU-132-1G
        - VATEX-EU-132-1H
        - VATEX-EU-132-1I
        - VATEX-EU-132-1J
        - VATEX-EU-132-1K
        - VATEX-EU-132-1L
        - VATEX-EU-132-1M
        - VATEX-EU-132-1N
        - VATEX-EU-132-1O
        - VATEX-EU-132-1P
        - VATEX-EU-132-1Q
        - VATEX-EU-143
        - VATEX-EU-143-1A
        - VATEX-EU-143-1B
        - VATEX-EU-143-1C
        - VATEX-EU-143-1D
        - VATEX-EU-143-1E
        - VATEX-EU-143-1F
        - VATEX-EU-143-1FA
        - VATEX-EU-143-1G
        - VATEX-EU-143-1H
        - VATEX-EU-143-1I
        - VATEX-EU-143-1J
        - VATEX-EU-143-1K
        - VATEX-EU-143-1L
        - VATEX-EU-148
        - VATEX-EU-148-A
        - VATEX-EU-148-B
        - VATEX-EU-148-C
        - VATEX-EU-148-D
        - VATEX-EU-148-E
        - VATEX-EU-148-F
        - VATEX-EU-148-G
        - VATEX-EU-151
        - VATEX-EU-151-1A
        - VATEX-EU-151-1AA
        - VATEX-EU-151-1B
        - VATEX-EU-151-1C
        - VATEX-EU-151-1D
        - VATEX-EU-151-1E
        - VATEX-EU-309
        - VATEX-EU-AE
        - VATEX-EU-D
        - VATEX-EU-F
        - VATEX-EU-G
        - VATEX-EU-I
        - VATEX-EU-IC
        - VATEX-EU-J
        - VATEX-EU-O
        - VATEX-FR-FRANCHISE
        - VATEX-FR-CNWVAT
      example: VATEX-EU-132-1C

    InventoryItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the inventory item
          example: 123e4567-e89b-12d3-a456-426614174000
        code:
          type: string
          nullable: true
          maxLength: 50
          description: Item code (šifra artikla), unique per business (case-insensitive); the seller's item id (UBL BT-155) on eRačuni
          example: '00042'
        item_type:
          allOf:
            - $ref: '#/components/schemas/InventoryItemType'
          nullable: true
        name:
          type: string
          description: Name of the inventory item
          example: Premium Widget
        description:
          type: string
          nullable: true
          description: Description of the inventory item
          example: High-quality widget for professional use
        price:
          type: number
          format: decimal
          description: Price of the item without VAT (net). The price with VAT is price × (1 + vat_rate / 100), rounded to cents.
          example: 99.99
        unit_of_measurement_id:
          type: string
          nullable: true
          description: ID of the associated unit of measurement
          example: piece
        vat_category_id:
          type: string
          nullable: true
          description: ID of the associated VAT category
          example: S
        vat_rate:
          type: number
          format: decimal
          description: VAT rate percentage
          example: 25
        vat_exemption_reason:
          type: string
          nullable: true
          maxLength: 1024
          description: |
            VAT exemption reason the eRačun gives for this item (BT-120), for VAT categories E, AE, K, G and O;
            null for the others. A B2B eRačun with an exempt (E) line whose item has no reason is refused
            before fiscalization (BR-E-10, HR-BR-26, HR-BR-36).
          example: Oslobođeno PDV-a prema čl. 39. st. 1. t. c) Zakona o PDV-u
        vat_exemption_reason_code:
          allOf:
            - $ref: '#/components/schemas/VatExemptionReasonCode'
          nullable: true
        kpd_code_id:
          type: string
          nullable: true
          description: ID of the associated KPD code
          example: '01.11.10'
        created_at:
          type: string
          format: date-time
          description: Timestamp when the item was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the item was last updated
          example: '2025-11-08T12:00:00Z'
        units_of_measurement:
          allOf:
            - $ref: '#/components/schemas/UnitOfMeasurement'
            - nullable: true
          description: Associated unit of measurement details
        vat_categories:
          allOf:
            - $ref: '#/components/schemas/VatCategory'
            - nullable: true
          description: Associated VAT category details
        kpd_codes:
          allOf:
            - $ref: '#/components/schemas/KpdCode'
            - nullable: true
          description: Associated KPD code details

    InventoryItemInput:
      type: object
      required:
        - name
        - price
      properties:
        code:
          type: string
          nullable: true
          maxLength: 50
          description: |
            Item code (šifra artikla), optional; trimmed, empty or null clears it. Must be unique within the business
            (case-insensitive), otherwise 409. On update, leaving it out keeps the stored code.
          example: '00042'
        item_type:
          allOf:
            - $ref: '#/components/schemas/InventoryItemType'
          nullable: true
          description: Vrsta artikla, optional. On update, leaving it out keeps the stored type.
        name:
          type: string
          description: Name of the inventory item
          example: Premium Widget
          minLength: 1
        description:
          type: string
          nullable: true
          description: Description of the inventory item
          example: High-quality widget for professional use
        price:
          type: number
          format: decimal
          description: Price of the item without VAT (net)
          example: 99.99
          minimum: 0
        unit_of_measurement_id:
          type: string
          nullable: true
          description: ID of the unit of measurement
          example: piece
        vat_category_id:
          type: string
          nullable: true
          description: ID of the VAT category (S, Z, E, AE, etc.)
          example: S
        vat_rate:
          type: number
          format: decimal
          description: VAT rate percentage (automatically locked to 0 for Z, E, AE categories)
          example: 25
          minimum: 0
          maximum: 100
        vat_exemption_reason:
          type: string
          nullable: true
          maxLength: 1024
          description: |
            VAT exemption reason on the eRačun (BT-120), trimmed; null or "" clears it. Only for VAT categories
            E, AE, K, G and O: sending one with another category is a 400, and an item moved to such a category
            loses it. On update, leaving it out keeps the stored reason.
          example: Oslobođeno PDV-a prema čl. 39. st. 1. t. c) Zakona o PDV-u
        vat_exemption_reason_code:
          allOf:
            - $ref: '#/components/schemas/VatExemptionReasonCode'
          nullable: true
          description: |
            VATEX code of the reason (BT-121), any case; null or "" clears it. It must fit the VAT category
            (400 otherwise). Without vat_exemption_reason, the eRačun gives the code's description as the
            reason. On update, leaving it out keeps the stored code, unless the new category does not take it.
        kpd_code_id:
          type: string
          nullable: true
          description: ID of the KPD code
          example: '01.11.10'

    Business:
      type: object
      properties:
        id:
          type: string
          format: uuid
          example: 123e4567-e89b-12d3-a456-426614174001
        name:
          type: string
          example: Obrt Primjer
        logo_url:
          type: string
          nullable: true
        default_currency:
          type: string
          example: EUR
        vat_registered:
          type: boolean
          description: In the VAT system
        vat_exemption_reason:
          type: string
          nullable: true
          maxLength: 1024
          description: >-
            The reason a business outside the VAT system gives on its eRačuni for lines whose item has
            no reason of its own. `null` is the small-business exemption (čl. 90. st. 1. Zakona o PDV-u).
        f1_vat_status_source:
          type: string
          enum: [invoice, business]
          description: >-
            Where an F1 (B2C) bill's "U sustavu PDV-a" comes from: the invoice's `vat_included`
            (default) or the business's `vat_registered`
        vat_number:
          type: string
          nullable: true
          description: The OIB, without the HR prefix; registered in the AMS while B2B is on
          example: '94577403194'
        address:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        postal_code:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        bank_name:
          type: string
          nullable: true
        bank_code:
          type: string
          nullable: true
        account_number:
          type: string
          nullable: true
        fiscalization_enabled:
          type: boolean
          description: B2B or B2C is on
        b2b_invoicing_enabled:
          type: boolean
          description: eRačuni (Fiskalizacija 2.0) to and from other businesses
        b2c_invoicing_enabled:
          type: boolean
          description: Fiscalized bills to consumers (Fiskalizacija 1.0)
        eracun_environment:
          type: string
          enum: [conformance, production, sandbox]
          description: >-
            Where the business's eRačuni are fiscalized and sent, and whose AMS its OIB is registered in:
            Porezna's conformance testing portal (default) or production. `sandbox`, the APIS-IT test
            service, has no AMS; only Fakturo puts a business there.
          example: conformance
        subscription_status:
          type: string
          enum: [active, canceled, past_due, trialing]
        mps_registration_status:
          type: string
          enum: [not_required, pending, registered, failed]
          description: >-
            The eRačun address in the AMS: not_required (B2B off), registered (the AMS accepted it),
            failed (see `mps_registration_error`), or pending (not tried yet)
        mps_registration_error:
          type: string
          nullable: true
          description: >-
            Why the last AMS step failed. With status 'registered', a deletion from the production AMS
            failed and the registration is still there.
        mps_registered_at:
          type: string
          format: date-time
          nullable: true
        mps_last_attempt_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        role:
          type: string
          enum: [owner, admin]
          description: The caller's role in the business
        is_current:
          type: boolean
          description: Whether this is the business the token or API key belongs to

    BusinessInput:
      type: object
      description: The business's full settings. A field left out is cleared (null, or false for the flags) unless noted.
      required:
        - name
        - default_currency
      properties:
        name:
          type: string
          example: Obrt Primjer
        default_currency:
          type: string
          example: EUR
        logo_url:
          type: string
          nullable: true
        vat_registered:
          type: boolean
          description: Needs `vat_number`
        vat_exemption_reason:
          type: string
          nullable: true
          maxLength: 1024
          description: >-
            The eRačun exemption reason outside the VAT system (see `Business`). Left out, it is not
            changed; `null` clears it.
        vat_number:
          type: string
          nullable: true
          minLength: 8
          maxLength: 15
          description: >-
            The OIB, without the HR prefix. Required with `vat_registered`, `b2b_invoicing_enabled`
            or `b2c_invoicing_enabled`. A new one moves the AMS registration.
          example: '94577403194'
        address:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        postal_code:
          type: string
          nullable: true
        email:
          type: string
          format: email
          nullable: true
        phone:
          type: string
          nullable: true
        bank_name:
          type: string
          nullable: true
        bank_code:
          type: string
          nullable: true
        account_number:
          type: string
          nullable: true
        b2b_invoicing_enabled:
          type: boolean
          description: Off, or left out, deletes the business's registration from the AMS
        b2c_invoicing_enabled:
          type: boolean
          description: Off, or left out, also deletes the F1 certificate
        eracun_environment:
          type: string
          enum: [conformance, production]
          description: >-
            The eRačun (Fiskalizacija 2.0) environment; left out, it is not changed. With B2B on, the
            AMS registration moves to its AMS. A business in `sandbox` may send that back unchanged.
          example: production
        f1_vat_status_source:
          type: string
          enum: [invoice, business]
          description: Left out, it is not changed
        fiscalization_certificate:
          type: string
          format: byte
          description: The F1 certificate, a base64 PKCS#12 file (B2C only). Left out, the saved one is kept.
        fiscalization_certificate_password:
          type: string
          description: Its password (B2C only). Left out, the saved one is kept.

    BusinessMpsRegistration:
      type: object
      description: What an update changed in the AMS; left out when the registration already matched the settings
      properties:
        success:
          type: boolean
          description: false when the AMS refused or could not be reached. The settings are saved either way.
        action:
          type: string
          enum: [registered, unregistered, moved]
          description: >-
            registered (B2B on, or a missing or failed registration), unregistered (B2B off),
            moved (a new `eracun_environment` or `vat_number`)
        message:
          type: string
          description: The AMS outcome, or the error when `success` is false
          example: Successfully registered 94577403194 with MPS

    SalesLocation:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the sales location
          example: 123e4567-e89b-12d3-a456-426614174000
        business_id:
          type: string
          format: uuid
          description: ID of the business this location belongs to
        name:
          type: string
          description: Name of the sales location
          example: Main Store
        location_number:
          type: string
          description: Location number for fiscalization
          example: '001'
        is_default:
          type: boolean
          description: Whether this is the default sales location
          example: true
        device_number:
          type: integer
          description: >-
            Cash register (naplatni uređaj, OznNapUr) of the bills Fakturo numbers at this location
            (web app, MCP, `POST /api/v1/invoices/{id}/send`, invoices created without `invoice_number`).
          example: 2
        api_device_number:
          type: integer
          nullable: true
          description: >-
            Cash register of the bills an API caller numbers itself (`invoice_number` given) at this location;
            null when the API numbers no bills here (they would go to 1). Where the API numbers bills, Fakturo
            numbers none on this register: `device_number` must differ, or sending a bill Fakturo numbers
            answers 409 with `code` FKR01.
          example: 1
        number_format:
          type: string
          description: How bill numbers Fakturo assigns print (#N# sequence, #BU# location_number, #NU# device_number, #YYYY# year)
          example: '#N#/#BU#/#NU#'
        created_at:
          type: string
          format: date-time
          description: Timestamp when the location was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the location was last updated
          example: '2025-11-08T12:00:00Z'

    SalesLocationInput:
      type: object
      required:
        - name
        - location_number
      properties:
        name:
          type: string
          description: Name of the sales location
          example: Main Store
          minLength: 1
        location_number:
          type: string
          description: Location number for fiscalization
          example: '001'
          minLength: 1
        is_default:
          type: boolean
          description: Whether this is the default sales location
          example: true
          default: false
        device_number:
          type: integer
          minimum: 1
          maximum: 9999
          description: Register of the bills Fakturo numbers here. Left out on update, it is not changed; on create, 1.
          example: 2
        api_device_number:
          type: integer
          nullable: true
          minimum: 1
          maximum: 9999
          description: >-
            Register of the bills the API caller numbers itself here, or null. Left out on update, it is not
            changed; on create, 1 (a location created through the API is one an API caller files its bills at).
            Given together with `device_number`, the two must differ (400).
          example: 1

    Invoice:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the invoice
          example: 123e4567-e89b-12d3-a456-426614174000
        business_id:
          type: string
          format: uuid
          description: ID of the business this invoice belongs to
        contact_id:
          type: string
          format: uuid
          description: ID of the contact/customer
        sales_location_id:
          type: string
          format: uuid
          description: ID of the sales location
        invoice_number:
          type: string
          nullable: true
          description: >-
            The invoice number. A bill (1-bill, 3-cancellation, 4-approval) without a caller-supplied number
            is numbered per sales location when it is sent or fiscalized (e.g. 117/5/1, from the location's
            number format); until then it is null. Other documents get INV-YYYY-NNNNN when created.
          example: 117/5/1
        invoice_type:
          type: string
          description: Type of invoice
          example: '1-bill'
        customer_type:
          type: string
          enum: [b2b, b2c]
          nullable: true
          description: Customer type derived from contact (B2B or B2C)
          example: b2b
        date_issued:
          type: string
          format: date
          description: Date when the invoice was issued
          example: '2025-11-11'
        due_date:
          type: string
          format: date
          description: Payment due date
          example: '2025-11-25'
        status:
          type: string
          enum: [draft, sent, paid, overdue, cancelled]
          description: Current status of the invoice
          example: paid
        currency:
          type: string
          description: Currency code (ISO 4217)
          example: EUR
        subtotal:
          type: number
          format: decimal
          description: Sum of the items' net amounts, before the document discount and delivery fee
          example: 100.00
        tax_amount:
          type: number
          format: decimal
          description: Total VAT, on the base after the document discount and delivery fee
          example: 22.50
        discount_amount:
          type: number
          format: decimal
          nullable: true
          description: Fixed document discount, in the basis of the item prices (net, or gross when vat_included). It lowers the VAT base.
          example: 10.00
        discount_percentage:
          type: number
          format: decimal
          nullable: true
          description: Document discount as a percentage of the items (a povratna naknada is not discounted)
          example: 10
        discount_reason_code:
          type: string
          nullable: true
          description: UN/CEFACT discount reason code (e.g., 95 for volume discount)
          example: '95'
        discount_reason_text:
          type: string
          nullable: true
          description: Human-readable discount reason
          example: Volume discount
        discount_vat_category_id:
          type: string
          nullable: true
          description: VAT category ID for the discount
          example: S
        discount_vat_rate:
          type: number
          format: decimal
          nullable: true
          description: VAT rate for the discount. When no item has this rate, or none is set, the discount is spread over the items' VAT rates in proportion to their amounts.
          example: 25
        delivery_fee_amount:
          type: number
          format: decimal
          nullable: true
          description: Delivery/shipping fee, in the basis of the item prices (net, or gross when vat_included)
          example: 10.00
        delivery_fee_vat_category_id:
          type: string
          nullable: true
          description: VAT category ID for the delivery fee
          example: S
        delivery_fee_vat_rate:
          type: number
          format: decimal
          nullable: true
          description: VAT rate applied to the delivery fee. When none is set, the fee is spread over the items' VAT rates in proportion to their amounts.
          example: 25
        delivery_fee_reason_code:
          type: string
          nullable: true
          description: UN/CEFACT charge reason code (e.g., FC for Freight, DL for Delivery)
          example: 'FC'
        delivery_fee_reason_text:
          type: string
          nullable: true
          description: Human-readable delivery fee reason
          example: Standard shipping
        total_amount:
          type: number
          format: decimal
          description: Amount due - subtotal minus discount plus delivery fee, plus VAT on that base
          example: 112.50
        notes:
          type: string
          nullable: true
          description: Additional notes or comments (printed at the end of the document)
          example: Thank you for your business
        order_reference:
          type: string
          nullable: true
          description: The buyer's order number (eRačun BT-13)
          example: PO-2025-118
        period_start:
          type: string
          format: date
          nullable: true
          description: Start of the billing / delivery period (eRačun BG-14)
          example: '2025-10-01'
        period_end:
          type: string
          format: date
          nullable: true
          description: End of the billing / delivery period (eRačun BG-14)
          example: '2025-10-31'
        payment_method:
          type: string
          enum: [transfer, cash, card, check, other]
          nullable: true
          description: How the buyer pays
          example: transfer
        fiscalized:
          type: boolean
          description: Whether the invoice has been fiscalized
          example: true
        jir:
          type: string
          nullable: true
          description: Unique invoice identifier from tax authority
          example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
        zki:
          type: string
          nullable: true
          description: Security code of the invoice
          example: '1a2b3c4d5e6f7890'
        fiscalization_error:
          type: string
          nullable: true
          description: |
            Why the invoice is not fiscalized. For a B2B eRačun that fails the EN 16931 / HR CIUS
            rules: a first line, then one line per broken rule.
        as4_delivery_status:
          type: string
          enum: [pending, delivered, failed]
          description: |
            B2B eRačun sent over AS4: `failed` when the send failed (reason in
            `as4_error_description`), `delivered` once the buyer's receipt arrives.
          example: pending
        as4_error_description:
          type: string
          nullable: true
          description: Why the eRačun was not delivered
        paid_at:
          type: string
          format: date-time
          nullable: true
          description: Payment timestamp
          example: '2025-11-11T10:30:00Z'
        created_at:
          type: string
          format: date-time
          description: Timestamp when the invoice was created
          example: '2025-11-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Timestamp when the invoice was last updated
          example: '2025-11-08T12:00:00Z'
        contact:
          $ref: '#/components/schemas/Contact'
        sales_location:
          $ref: '#/components/schemas/SalesLocation'
        items:
          type: array
          items:
            $ref: '#/components/schemas/InvoiceItem'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'

    InvoiceInput:
      type: object
      required:
        - contact_id
        - sales_location_id
        - date_issued
        - due_date
        - currency
        - items
      properties:
        contact_id:
          type: string
          format: uuid
          description: ID of the contact/customer
          example: 123e4567-e89b-12d3-a456-426614174001
        sales_location_id:
          type: string
          format: uuid
          description: ID of the sales location
          example: 123e4567-e89b-12d3-a456-426614174002
        invoice_type:
          type: string
          description: Type of invoice
          example: '1-bill'
          default: '1-bill'
        invoice_number:
          type: string
          nullable: true
          description: >-
            Custom invoice number, stored exactly as given. If not provided: a bill (1-bill, 3-cancellation,
            4-approval) created as a draft has no number until it is sent; one created with status 'sent' or
            fiscalize true gets the next number of its sales location (e.g. 117/5/1). Other documents get
            INV-YYYY-NNNNN. A number you supply is unique within the business for the year it belongs to:
            the year written in it (PD_2026_41 belongs to 2026, whenever it is sent), else the year of
            `date_issued`, so numbering without a year can start again each January. Sending a number again
            within its year answers 409 with `code` 23505, and `GET /api/v1/invoices?search=` finds the
            invoice that has it.
          example: 'INV-2025-00042'
        date_issued:
          type: string
          format: date
          description: Date when the invoice was issued
          example: '2025-11-11'
        due_date:
          type: string
          format: date
          description: Payment due date
          example: '2025-11-25'
        status:
          type: string
          enum: [draft, sent, paid, overdue, cancelled]
          description: Initial status of the invoice
          example: draft
          default: draft
        currency:
          type: string
          description: Currency code (ISO 4217)
          example: EUR
        notes:
          type: string
          nullable: true
          description: Additional notes or comments
          example: Thank you for your business
        vat_included:
          type: boolean
          description: |
            Item unit prices, discount_amount and delivery_fee_amount include VAT.
            The eRačun reports them net (BT-146, BT-92, BT-99).
          default: false
          example: false
        discount_amount:
          type: number
          format: decimal
          nullable: true
          description: |
            Fixed document discount (mutually exclusive with discount_percentage, percentage takes precedence),
            in the basis of the item prices: net, or gross when vat_included. It lowers the VAT base,
            so tax_amount is computed after it.
          example: 50.00
          minimum: 0
        discount_percentage:
          type: number
          format: decimal
          nullable: true
          description: |
            Discount as a percentage of the items (takes precedence over discount_amount if both provided).
            A povratna naknada (fee_name) is not discounted. It lowers the VAT base.
          example: 10
          minimum: 0
          maximum: 100
        discount_reason_code:
          type: string
          nullable: true
          description: |
            UN/CEFACT discount reason code. Common codes:
            - 41: Bonus for works ahead of schedule
            - 42: Other bonus
            - 60: Manufacturer's consumer discount
            - 62: Due to military status
            - 63: Due to work accident
            - 64: Special agreement
            - 65: Production error discount
            - 66: New outlet discount
            - 67: Sample discount
            - 68: End-of-range discount
            - 70: Incoterm discount
            - 71: Point of sales threshold allowance
            - 88: Material surcharge/deduction
            - 95: Discount
            - 100: Special rebate
            - 102: Fixed long term
            - 103: Temporary
            - 104: Standard
            - 105: Yearly turnover
            The fiscalisation report (EvidentirajERacun) accepts only 64, 95 and 100-105 of these;
            with another code it carries only discount_reason_text.
          example: '95'
        discount_reason_text:
          type: string
          nullable: true
          description: Human-readable discount reason
          example: Volume discount for large order
        discount_vat_category_id:
          type: string
          nullable: true
          description: VAT category ID for the discount (e.g., S, Z, E, AE)
          example: S
        discount_vat_rate:
          type: number
          format: decimal
          nullable: true
          description: |
            VAT rate the discount is taken from. Used when an item has this rate (and category, if
            discount_vat_category_id is set); otherwise the discount is spread over the items' VAT
            rates in proportion to their amounts.
          example: 25
          minimum: 0
          maximum: 100
        delivery_fee_amount:
          type: number
          format: decimal
          nullable: true
          description: |
            Delivery/shipping fee, in the basis of the item prices: net, or gross when vat_included.
            VAT is calculated on it if tax applies (see delivery_fee_vat_rate).
          example: 10.00
          minimum: 0
        delivery_fee_vat_category_id:
          type: string
          nullable: true
          description: VAT category ID for the delivery fee (e.g., S, Z, E, AE)
          example: S
        delivery_fee_vat_rate:
          type: number
          format: decimal
          nullable: true
          description: |
            VAT rate to apply to the delivery fee. When omitted, the fee takes the items' VAT rates,
            spread in proportion to their amounts. 0 means no VAT (category Z unless
            delivery_fee_vat_category_id says otherwise).
          example: 25
          minimum: 0
          maximum: 100
        delivery_fee_reason_code:
          type: string
          nullable: true
          description: |
            UN/CEFACT charge reason code. Common codes:
            - FC: Freight service
            - FI: Financing
            - DL: Delivery
            - EAB: Early payment allowance charge
            - IN: Insurance
            - MAC: Minimum order/minimum billing charge
            - PC: Packing
            - SH: Special handling
            - TX: Tax
          example: 'FC'
        delivery_fee_reason_text:
          type: string
          nullable: true
          description: Human-readable delivery fee reason
          example: Standard shipping
        items:
          type: array
          description: Line items for the invoice (at least one required)
          minItems: 1
          items:
            $ref: '#/components/schemas/InvoiceItemInput'
        tag_ids:
          type: array
          description: Array of tag IDs to associate with the invoice
          items:
            type: string
            format: uuid
        invoice_template_id:
          type: string
          format: uuid
          nullable: true
          description: ID of business-specific invoice template
        default_invoice_template_id:
          type: string
          format: uuid
          nullable: true
          description: ID of default invoice template
        fiscalize:
          type: boolean
          nullable: true
          description: |
            Whether to fiscalize the invoice immediately after creation.
            If true (or if status is 'sent'), the invoice will be fiscalized based on contact type:
            - B2C: Uses RacunZahtjev (Croatian fiscalization)
            - B2B: Uses EvidentirajERacunZahtjev and sends to e-Račun AS4. The eRačun's UBL is
              checked against the EN 16931 and HR CIUS rules first; one that fails them is neither
              fiscalized nor sent and stays a draft (`fiscalization.validationErrors`).
            Fiscalization requires the business to have fiscalization enabled.
          example: true
          default: false
        payment_method:
          type: string
          enum: [transfer, cash, card, check, other]
          nullable: true
          description: |
            How the buyer pays; stored on the invoice. For a B2C bill fiscalized in this request it is
            also the F1 payment method (NacinPlac), defaulting to 'other' if not specified. A value
            outside the list is still used for F1 as before, but not stored.
          example: card
        order_reference:
          type: string
          nullable: true
          maxLength: 200
          description: The buyer's order (purchase order) number. On the eRačun as cac:OrderReference (BT-13).
          example: PO-2025-118
        period_start:
          type: string
          format: date
          nullable: true
          description: Start of the billing / delivery period. On the eRačun as cac:InvoicePeriod (BG-14).
          example: '2025-10-01'
        period_end:
          type: string
          format: date
          nullable: true
          description: End of the billing / delivery period; not before period_start.
          example: '2025-10-31'

    InvoiceItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the invoice item
        invoice_id:
          type: string
          format: uuid
          description: ID of the parent invoice
        inventory_id:
          type: string
          format: uuid
          description: ID of the inventory item
        name:
          type: string
          description: Item name
          example: Premium Widget
        description:
          type: string
          nullable: true
          description: Item description
          example: High-quality widget
        quantity:
          type: number
          description: Quantity
          example: 2
        unit_price:
          type: number
          format: decimal
          description: Unit price
          example: 99.99
        vat_rate:
          type: number
          format: decimal
          description: VAT rate percentage applied to this item
          example: 25
        discount_percent:
          type: number
          format: decimal
          description: Line discount (rabat) in percent; 0 without one
          example: 0
        line_total:
          type: number
          format: decimal
          description: Line total after the line discount, including tax (if applicable based on business/contact tax rules)
          example: 249.98
        created_at:
          type: string
          format: date-time
          description: Timestamp when the item was created

    InvoiceItemInput:
      type: object
      required:
        - inventory_id
        - quantity
        - unit_price
      properties:
        inventory_id:
          type: string
          format: uuid
          description: ID of the inventory item (must belong to the same business)
          example: 123e4567-e89b-12d3-a456-426614174003
        name:
          type: string
          description: Optional override for item name (defaults to inventory item name)
          example: Premium Widget
        description:
          type: string
          nullable: true
          description: Optional item description
          example: High-quality widget
        quantity:
          type: number
          description: Quantity
          example: 2
          minimum: 0.01
        unit_price:
          type: number
          format: decimal
          description: Unit price
          example: 99.99
          minimum: 0
        vat_rate:
          type: number
          format: decimal
          nullable: true
          description: VAT rate percentage override (defaults to inventory item's vat_rate if not provided)
          example: 25
          minimum: 0
          maximum: 100
        discount_percent:
          type: number
          format: decimal
          nullable: true
          description: |
            Line discount (rabat) in percent of the line amount, two decimals, default 0. Not allowed on a
            povratna naknada. The line amount before it is quantity × unit_price rounded to cents; the
            discount is that × discount_percent / 100 rounded to cents; line_total is after it, and the
            document discount applies on top. On the eRačun: a line-level cac:AllowanceCharge (BG-27,
            reason code 95), with LineExtensionAmount net of it.
          example: 10
          minimum: 0
          exclusiveMaximum: true
          maximum: 100

    Contact:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the contact
        name:
          type: string
          description: Contact name
          example: John Doe
        email:
          type: string
          format: email
          nullable: true
          description: Contact email
          example: john@example.com
        phone:
          type: string
          nullable: true
          description: Contact phone
          example: '+385 1 234 5678'
        street_name:
          type: string
          description: Street name and number
          example: 'Ilica 123'
        city_name:
          type: string
          description: City name
          example: 'Zagreb'
        postal_zone:
          type: string
          description: Postal/ZIP code
          example: '10000'
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: 'HR'
        vat_number:
          type: string
          nullable: true
          description: Tax ID or VAT number
          example: '12345678901'
        partner_code:
          type: string
          nullable: true
          maxLength: 50
          description: Partner ID - the buyer's id in the business's own records
          example: 'K-0042'
        contact_type:
          type: string
          enum: [b2b, b2c]
          description: Contact type (B2B for businesses, B2C for consumers)
          example: b2b
        vat_registered:
          type: boolean
          description: Whether the contact is VAT registered (decides whether VAT is charged only for B2B contacts outside Croatia)
          example: true

    ContactInput:
      type: object
      required:
        - name
        - street_name
        - city_name
        - postal_zone
        - country_code
      properties:
        name:
          type: string
          description: Contact name (required)
          example: John Doe
        email:
          type: string
          format: email
          nullable: true
          description: Contact email
          example: john@example.com
        phone:
          type: string
          nullable: true
          description: Contact phone
          example: '+385 1 234 5678'
        street_name:
          type: string
          description: Street name and number (required)
          example: 'Ilica 123'
        city_name:
          type: string
          description: City name (required)
          example: 'Zagreb'
        postal_zone:
          type: string
          description: Postal/ZIP code (required)
          example: '10000'
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code (required, will be uppercased)
          example: 'HR'
        vat_number:
          type: string
          nullable: true
          description: Tax ID or VAT number
          example: '12345678901'
        partner_code:
          type: string
          nullable: true
          maxLength: 50
          description: >-
            Partner ID - the buyer's id in the business's own records (1-50 characters,
            trimmed; null or "" clears it). On update (PUT), leaving it out keeps the stored value.
          example: 'K-0042'
        contact_type:
          type: string
          enum: [b2b, b2c]
          description: Contact type (B2B for businesses, B2C for consumers)
          example: b2b
          default: b2b
        vat_registered:
          type: boolean
          description: Whether the contact is VAT registered (affects tax calculation only for B2B contacts outside Croatia)
          example: true
          default: false

    Tag:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the tag
        name:
          type: string
          description: Tag name
          example: Urgent
        color:
          type: string
          nullable: true
          description: Tag color (hex code)
          example: '#FF5733'

    Pagination:
      type: object
      properties:
        total:
          type: integer
          description: Total number of items available
          example: 150
        limit:
          type: integer
          description: Maximum number of items per page
          example: 10
        offset:
          type: integer
          description: Number of items skipped
          example: 0
        hasMore:
          type: boolean
          description: Whether there are more items available
          example: true

    InvoiceFiscalizationResult:
      type: object
      description: Returned on creation when fiscalization was requested (`fiscalize`, or status 'sent').
      properties:
        fiscalized:
          type: boolean
          example: true
        jir:
          type: string
          description: Unique invoice identifier from the tax authority
        zki:
          type: string
          description: Security code of the invoice
        qrCodeLink:
          type: string
          description: B2C only
        fiscalizationError:
          type: string
          description: Why the invoice was not fiscalized
        validationErrors:
          type: array
          items:
            type: string
          description: |
            B2B only: the EN 16931 / HR CIUS rules the eRačun fails, one message per rule. Such an
            eRačun is neither fiscalized nor sent, the invoice stays a draft (also when created as
            'sent'), and the messages are kept in its `fiscalization_error`.
          example:
            - '[BR-E-10]-A VAT breakdown (BG-23) with VAT Category code (BT-118) "Exempt from VAT" shall have a VAT exemption reason code (BT-121) or a VAT exemption reason text (BT-120).'
        eracunSent:
          type: boolean
          description: |
            B2B only: whether the fiscalized eRačun was sent to the buyer (AS4). The outcome is also
            kept on the invoice (`as4_message_id`, or `as4_delivery_status` 'failed').
        eracunError:
          type: string
          description: B2B only. Why the eRačun was not sent; also kept in `as4_error_description`

    Error:
      type: object
      properties:
        message:
          type: string
          description: Human-readable error message
          example: Invalid client credentials

  responses:
    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Missing Authorization header

    Forbidden:
      description: Insufficient permissions (missing required scope)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: 'Insufficient scope. Required: read:inventory'
