openapi: 3.1.1

info:
  title: NetLicensing REST API
  version: 2.x
  summary: NetLicensing REST API for vendors and API-key clients (REST v2).
  description: |
    The NetLicensing REST API lets you manage products, licensing models, licensees and licenses,
    validate licenses from your software, and run the NetLicensing shop and webhooks.

    ## Wire format
    * Requests that carry data use `application/x-www-form-urlencoded`. When a `POST` has no
      `Content-Type`, NetLicensing assumes form encoding.
    * Responses use a generic envelope (`netlicensing-context.xsd`), not typed objects. Each business
      entity is one `item` with a `type` and a flat list of `property` name/value pairs (all values are
      strings) plus optional nested `list`s. The property names each entity emits are documented in the
      component schema descriptions and in the `x-netlicensing-properties` extension.
    * Response media type: **XML is the default.** JSON is returned only when the `Accept` header contains
      `application/json`. If `Accept` contains both, XML wins. Signed responses (XML-DSig) are produced only
      for XML.
    * Custom properties: on create/update, every form field that is not a reserved property of the
      entity is stored as a custom property and returned in the item's `property` list.

    ## Authentication
    HTTP Basic, in one of two ways:
    * **Vendor credentials**: username and password of the vendor account (role `ROLE_VENDOR`).
    * **API key**: username `apiKey` (literal) and the API key token number as the password. The role
      comes from the key's `apiKeyRole` (`ROLE_APIKEY_LICENSEE` < `ROLE_APIKEY_ANALYTICS` <
      `ROLE_APIKEY_OPERATION` < `ROLE_APIKEY_MAINTENANCE` < `ROLE_APIKEY_ADMIN`).

    Each operation lists the roles allowed to call it in `x-netlicensing-roles`. A role that is not
    listed gets `403`.

    ## Paging, sorting and filtering
    List endpoints take one `filter` query parameter: `key=value` pairs separated by `;`.
    Generic keys: `page` (0-based, default 0), `items` (page size, default and max 100),
    `sortorder` (property, default `number`), `sortdirection` (`ASC` default, or `DESC`). When `page`
    or `items` is missing, an `INFO` entry is added to `infos`. Entity-specific filter keys are listed
    where the code shows them.

    ## Errors
    Errors return the envelope with an `infos.info[]` entry of type `ERROR`. Its `id` names the error
    (for example `NotFoundException`). Status codes:
    * `400`: invalid request or business rule violation. **A missing entity also returns 400, not 404.**
    * `401`: missing or wrong credentials.
    * `402`: payment required for the operation.
    * `403`: the credentials are valid, but the role may not call the operation.
    * `404`: unknown endpoint, a feature that is not available, or an unsupported HTTP method.
    * `429`: too many requests. Retry after the number of seconds in `Retry-After` (300).
    * `500`: internal error.
  contact:
    name: Labs64 NetLicensing
    url: https://netlicensing.io
  license:
    name: Labs64 NetLicensing Terms of Service
    url: https://www.labs64.com/legal/terms-of-service/netlicensing/
  termsOfService: https://www.labs64.com/legal/terms-of-service/netlicensing/

externalDocs:
  description: RESTful API reference in Wiki
  url: https://netlicensing.io/wiki/restful-api

servers:
  - url: https://go.netlicensing.io/core/v2/rest
    description: NetLicensing

security:
  - vendorBasic: []
  - apiKeyBasic: []

tags:
  - name: Product
    description: Products are the top-level entities that hold your product modules, licensees and discounts.
  - name: ProductModule
    description: Product modules are the licensable parts of a product. Each module uses one licensing model.
  - name: LicenseTemplate
    description: License templates define what a license grants (type, price, currency) within a product module.
  - name: Licensee
    description: Licensees are your customers' installations or users. Includes license validation and transfer.
  - name: License
    description: Licenses are issued to a licensee from a license template.
  - name: Transaction
    description: Transactions record purchases and automatic license changes.
  - name: Token
    description: Tokens grant temporary or API-key access, and create shop links for licensees.
  - name: PaymentMethod
    description: Payment methods available in the NetLicensing shop.
  - name: Bundle
    description: Bundles group several license templates so they can be sold and issued together.
  - name: Notification
    description: Notifications send webhooks to your endpoint when selected events happen.
  - name: Utility
    description: Reference data such as licensing models, license types, countries and currencies.
  - name: LicensingModel
    description: Extra operations offered by specific licensing models.
  - name: Reporting
    description: Chart data for usage and revenue reports.

paths:

  # ------------------------------------------------------------------ Product
  /product:
    get:
      tags: [Product]
      operationId: listProducts
      summary: List products
      description: |
        For `ROLE_APIKEY_LICENSEE` the filter is forced to the licensee bound to the API key, so the
        result holds only that licensee's products.
        Extra filter keys: `exportSettings=true` also returns the products' ProductModule and
        LicenseTemplate items. `exportAssets=true` also returns License and Licensee items plus one
        `VendorDataExport` item, and marks the response for signing. `licenseeNumber=<n1>,<n2>`
        restricts the exported licenses and licensees.
      x-netlicensing-roles: &rolesReadInclLicensee [ROLE_VENDOR, ROLE_APIKEY_ADMIN, ROLE_APIKEY_MAINTENANCE, ROLE_APIKEY_OPERATION, ROLE_APIKEY_ANALYTICS, ROLE_APIKEY_LICENSEE]
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Product]
      operationId: createProduct
      summary: Create product
      x-netlicensing-roles: &rolesMaintenance [ROLE_VENDOR, ROLE_APIKEY_ADMIN, ROLE_APIKEY_MAINTENANCE]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/ProductForm'
                - required: [name]
            encoding:
              discount: { style: form, explode: true }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /product/{productNumber}:
    parameters:
      - $ref: '#/components/parameters/productNumber'
    get:
      tags: [Product]
      operationId: getProduct
      summary: Get product
      description: For `ROLE_APIKEY_LICENSEE` the product must belong to the key's licensee, otherwise the call fails.
      x-netlicensing-roles: *rolesReadInclLicensee
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Product]
      operationId: updateProduct
      summary: Update product
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/ProductForm' }
            encoding:
              discount: { style: form, explode: true }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Product]
      operationId: deleteProduct
      summary: Delete product
      x-netlicensing-roles: *rolesMaintenance
      parameters:
        - $ref: '#/components/parameters/forceCascade'
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /product/{productNumber}/discount/{promoCode}/resolve:
    parameters:
      - $ref: '#/components/parameters/productNumber'
      - name: promoCode
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Product]
      operationId: resolveProductDiscount
      summary: Resolve a promo code to its discount license template
      description: Returns one LicenseTemplate item (the discount template matching the promo code).
      x-netlicensing-roles: *rolesReadInclLicensee
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                cartTotal:
                  type: string
                  description: Decimal cart total. A non-decimal value returns 400 (`MalformedRequestException`).
                  examples: ['49.90']
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------ ProductModule
  /productmodule:
    get:
      tags: [ProductModule]
      operationId: listProductModules
      summary: List product modules
      x-netlicensing-roles: &rolesRead [ROLE_VENDOR, ROLE_APIKEY_ADMIN, ROLE_APIKEY_MAINTENANCE, ROLE_APIKEY_OPERATION, ROLE_APIKEY_ANALYTICS]
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [ProductModule]
      operationId: createProductModule
      summary: Create product module
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/ProductModuleForm'
                - required: [productNumber, name, licensingModel]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /productmodule/{productModuleNumber}:
    parameters:
      - $ref: '#/components/parameters/productModuleNumber'
    get:
      tags: [ProductModule]
      operationId: getProductModule
      summary: Get product module
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [ProductModule]
      operationId: updateProductModule
      summary: Update product module
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/ProductModuleForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [ProductModule]
      operationId: deleteProductModule
      summary: Delete product module
      x-netlicensing-roles: *rolesMaintenance
      parameters:
        - $ref: '#/components/parameters/forceCascade'
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ---------------------------------------------------------- LicenseTemplate
  /licensetemplate:
    get:
      tags: [LicenseTemplate]
      operationId: listLicenseTemplates
      summary: List license templates
      x-netlicensing-roles: *rolesRead
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [LicenseTemplate]
      operationId: createLicenseTemplate
      summary: Create license template
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/LicenseTemplateForm'
                - required: [productModuleNumber, name, licenseType]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /licensetemplate/{licenseTemplateNumber}:
    parameters:
      - $ref: '#/components/parameters/licenseTemplateNumber'
    get:
      tags: [LicenseTemplate]
      operationId: getLicenseTemplate
      summary: Get license template
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [LicenseTemplate]
      operationId: updateLicenseTemplate
      summary: Update license template
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/LicenseTemplateForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [LicenseTemplate]
      operationId: deleteLicenseTemplate
      summary: Delete license template
      x-netlicensing-roles: *rolesMaintenance
      parameters:
        - $ref: '#/components/parameters/forceCascade'
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ----------------------------------------------------------------- Licensee
  /licensee:
    get:
      tags: [Licensee]
      operationId: listLicensees
      summary: List licensees
      x-netlicensing-roles: *rolesRead
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Licensee]
      operationId: createLicensee
      summary: Create licensee
      x-netlicensing-roles: &rolesOperation [ROLE_VENDOR, ROLE_APIKEY_ADMIN, ROLE_APIKEY_MAINTENANCE, ROLE_APIKEY_OPERATION]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/LicenseeForm'
                - required: [productNumber]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /licensee/{licenseeNumber}:
    parameters:
      - $ref: '#/components/parameters/licenseeNumber'
    get:
      tags: [Licensee]
      operationId: getLicensee
      summary: Get licensee
      x-netlicensing-roles: *rolesReadInclLicensee
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Licensee]
      operationId: updateLicensee
      summary: Update licensee
      x-netlicensing-roles: *rolesOperation
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/LicenseeForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Licensee]
      operationId: deleteLicensee
      summary: Delete licensee
      x-netlicensing-roles: *rolesOperation
      parameters:
        - $ref: '#/components/parameters/forceCascade'
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /licensee/{licenseeNumber}/validate:
    parameters:
      - $ref: '#/components/parameters/licenseeNumber'
    get:
      tags: [Licensee]
      operationId: validateLicenseeGet
      deprecated: true
      summary: Validate licensee (deprecated GET form)
      description: |
        Same as the POST form, with the validation parameters passed as query parameters. If
        `licenseeName` is missing, `name` is used. The response always contains a `WARNING` info with
        id `methodIsDeprecated`.
      x-netlicensing-roles: *rolesReadInclLicensee
      parameters:
        - name: params
          in: query
          required: false
          style: form
          explode: true
          schema: { $ref: '#/components/schemas/ValidationForm' }
      responses:
        '200': { $ref: '#/components/responses/ValidationResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Licensee]
      operationId: validateLicensee
      summary: Validate licensee
      description: |
        Runs the licensing models of the licensee's product modules. If the API key used has a
        `privateKey` property, the XML response is signed with that key.
      x-netlicensing-roles: *rolesReadInclLicensee
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/ValidationForm' }
      responses:
        '200': { $ref: '#/components/responses/ValidationResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /licensee/{licenseeNumber}/transfer:
    parameters:
      - name: licenseeNumber
        in: path
        required: true
        description: Target licensee (receives the licenses).
        schema: { type: string }
    post:
      tags: [Licensee]
      operationId: transferLicenses
      summary: Transfer licenses from a source licensee to this licensee
      x-netlicensing-roles: *rolesReadInclLicensee
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sourceLicenseeNumber]
              properties:
                sourceLicenseeNumber:
                  type: string
                  description: Licensee whose licenses are moved. It must be marked for transfer (`markedForTransfer=true`), and both licensees must be active.
      responses:
        '204': { description: Transferred. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------------ License
  /license:
    get:
      tags: [License]
      operationId: listLicenses
      summary: List licenses
      x-netlicensing-roles: *rolesReadInclLicensee
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [License]
      operationId: createLicense
      summary: Create license
      x-netlicensing-roles: *rolesOperation
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/LicenseForm'
                - required: [licenseeNumber, licenseTemplateNumber]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /license/{licenseNumber}:
    parameters:
      - $ref: '#/components/parameters/licenseNumber'
    get:
      tags: [License]
      operationId: getLicense
      summary: Get license
      x-netlicensing-roles: *rolesReadInclLicensee
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [License]
      operationId: updateLicense
      summary: Update license
      x-netlicensing-roles: *rolesOperation
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/LicenseForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [License]
      operationId: deleteLicense
      summary: Delete license
      x-netlicensing-roles: *rolesOperation
      parameters:
        - $ref: '#/components/parameters/forceCascade'
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # -------------------------------------------------------------- Transaction
  /transaction:
    get:
      tags: [Transaction]
      operationId: listTransactions
      summary: List transactions
      description: |
        Extra filter keys: `licenseeNumber=<number>` returns only that licensee's transactions.
        `shopOnly=true` returns only transactions with source `SHOP`. Results are always sorted newest
        first: `sortorder` and `sortdirection` are ignored.
      x-netlicensing-roles: *rolesRead
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Transaction]
      operationId: createTransaction
      summary: Create transaction
      x-netlicensing-roles: &rolesVendorAdmin [ROLE_VENDOR, ROLE_APIKEY_ADMIN]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/TransactionForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /transaction/{transactionNumber}:
    parameters:
      - $ref: '#/components/parameters/transactionNumber'
    get:
      tags: [Transaction]
      operationId: getTransaction
      summary: Get transaction
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Transaction]
      operationId: updateTransaction
      summary: Update transaction
      x-netlicensing-roles: *rolesVendorAdmin
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/TransactionForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /transaction/{transactionNumber}/sendorderconfirmation:
    parameters:
      - $ref: '#/components/parameters/transactionNumber'
    get:
      tags: [Transaction]
      operationId: sendOrderConfirmation
      summary: Send the order confirmation e-mail for a transaction
      description: This is a `GET` that has a side effect (it sends an e-mail).
      x-netlicensing-roles: *rolesRead
      responses:
        '204': { description: Confirmation sent. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # -------------------------------------------------------------------- Token
  /token:
    get:
      tags: [Token]
      operationId: listTokens
      summary: List active tokens
      description: 'Extra filter key: `tokenType=<TokenType>`. An unknown value adds a WARNING info.'
      x-netlicensing-roles: *rolesVendorAdmin
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Token]
      operationId: createToken
      summary: Create token
      description: |
        Every role may call this endpoint, but an API key that is not `ROLE_APIKEY_ADMIN` may only
        create `SHOP` tokens and `ACTION` tokens with `action=licenseeLogin`. Vendor credentials may
        create any type. Otherwise 403.
        Lifetimes: DEFAULT 1800 s, APIKEY unlimited, SHOP 1800 s (unlimited without `licenseeNumber`),
        ACTION 604800 s.
      x-netlicensing-roles: *rolesReadInclLicensee
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/TokenForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /token/{tokenNumber}:
    parameters:
      - $ref: '#/components/parameters/tokenNumber'
    get:
      tags: [Token]
      operationId: getToken
      summary: Get token
      description: |
        Can be called without credentials. An expired or inactive token fails with `NotFoundException`
        ("Requested token is expired."), returned as 400.
      x-netlicensing-roles: [ROLE_VENDOR, ROLE_APIKEY_ADMIN, ROLE_ANONYMOUS]
      security:
        - {}
        - vendorBasic: []
        - apiKeyBasic: []
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Token]
      operationId: deleteToken
      summary: Delete token
      x-netlicensing-roles: *rolesVendorAdmin
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------ PaymentMethod
  /paymentmethod:
    get:
      tags: [PaymentMethod]
      operationId: listPaymentMethods
      summary: List payment methods
      description: Paging attributes (`pagenumber` and the others) are **not** set on this list response.
      x-netlicensing-roles: *rolesVendorAdmin
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /paymentmethod/{paymentMethodNumber}:
    parameters:
      - name: paymentMethodNumber
        in: path
        required: true
        schema: { $ref: '#/components/schemas/PaymentMethodEnum' }
    get:
      tags: [PaymentMethod]
      operationId: getPaymentMethod
      summary: Get payment method
      x-netlicensing-roles: *rolesVendorAdmin
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [PaymentMethod]
      operationId: updatePaymentMethod
      summary: Update payment method
      x-netlicensing-roles: *rolesVendorAdmin
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              description: '`active` plus custom properties (for example `paypal.subject`).'
              properties:
                active: { $ref: '#/components/schemas/BooleanString' }
              additionalProperties: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------------- Bundle
  /bundle:
    get:
      tags: [Bundle]
      operationId: listBundles
      summary: List bundles
      x-netlicensing-roles: *rolesRead
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Bundle]
      operationId: createBundle
      summary: Create bundle
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/BundleForm'
                - required: [name]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /bundle/{bundleNumber}:
    parameters:
      - $ref: '#/components/parameters/bundleNumber'
    get:
      tags: [Bundle]
      operationId: getBundle
      summary: Get bundle
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Bundle]
      operationId: updateBundle
      summary: Update bundle
      x-netlicensing-roles: *rolesMaintenance
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/BundleForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Bundle]
      operationId: deleteBundle
      summary: Delete bundle
      description: No `forceCascade` parameter.
      x-netlicensing-roles: *rolesOperation
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /bundle/{bundleNumber}/obtain:
    parameters:
      - $ref: '#/components/parameters/bundleNumber'
    post:
      tags: [Bundle]
      operationId: obtainBundle
      summary: Create the bundle's licenses for a licensee
      description: Returns one License item for each license created.
      x-netlicensing-roles: *rolesOperation
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [licenseeNumber]
              properties:
                licenseeNumber:
                  type: string
                  description: Licensee that receives the licenses.
                transactionNumber:
                  type: string
                  description: Optional. Links the created licenses to an existing transaction.
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------- Notification
  /notification:
    get:
      tags: [Notification]
      operationId: listNotifications
      summary: List notifications (webhooks)
      x-netlicensing-roles: *rolesVendorAdmin
      parameters:
        - $ref: '#/components/parameters/filter'
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Notification]
      operationId: createNotification
      summary: Create notification
      x-netlicensing-roles: *rolesVendorAdmin
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/NotificationForm'
                - required: [name, events, protocol, endpoint]
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /notification/{notificationNumber}:
    parameters:
      - name: notificationNumber
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Notification]
      operationId: getNotification
      summary: Get notification
      x-netlicensing-roles: *rolesVendorAdmin
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Notification]
      operationId: updateNotification
      summary: Update notification
      x-netlicensing-roles: *rolesVendorAdmin
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/NotificationForm' }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Notification]
      operationId: deleteNotification
      summary: Delete notification
      x-netlicensing-roles: *rolesVendorAdmin
      responses:
        '204': { description: Deleted. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ------------------------------------------------------------------ Utility
  /utility/licensingModels:
    get:
      tags: [Utility]
      operationId: listLicensingModels
      summary: List licensing models
      description: One `LicensingModelProperties` item (property `name`) per registered model.
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /utility/licenseTypes:
    get:
      tags: [Utility]
      operationId: listLicenseTypes
      summary: List license types
      description: One `LicenseType` item (property `name`) per value of `LicenseType`.
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /utility/countries:
    get:
      tags: [Utility]
      operationId: listCountries
      summary: List countries
      description: '`Country` items with properties `code`, `name`, `vatPercent`, `isEu`.'
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  /utility/currencies:
    get:
      tags: [Utility]
      operationId: listCurrencies
      summary: List currencies
      description: '`Currency` items (property `name`), one for each currency except `NONE`.'
      x-netlicensing-roles: *rolesRead
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ----------------------------------------------------------- LicensingModel
  /licensingmodel/PricingTable/render:
    get:
      tags: [LicensingModel]
      operationId: renderPricingTable
      summary: Render the HTML pricing table of a PricingTable product module
      description: |
        Only the `PricingTable` licensing model offers extra operations. The product module must use the
        `PricingTable` model, otherwise 400 (`IllegalOperationException`). Send `Accept: text/html`.
      x-netlicensing-roles: *rolesRead
      parameters:
        - name: productModuleNumber
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Rendered HTML.
          content:
            text/html:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

  # ---------------------------------------------------------------- Reporting
  /reporting/{reportName}:
    get:
      tags: [Reporting]
      operationId: getReport
      summary: Get a chart report
      description: |
        Returns one `Chart` item. Its properties include `data`, a JSON-encoded array of points, plus
        the chart's own properties. A nested list `Properties` may be present.
        An unknown report name returns 400 (`NotFoundException`).
      x-netlicensing-roles: *rolesRead
      parameters:
        - name: reportName
          in: path
          required: true
          schema:
            type: string
            enum: [requests-day, licenses-revenue-day, revenue-license-type]
        - name: filter
          in: query
          required: false
          description: '`;`-separated `key=value` pairs, for example the reporting period (`start`, `end`) and `unit`.'
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Envelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        default: { $ref: '#/components/responses/Error' }

components:

  securitySchemes:
    vendorBasic:
      type: http
      scheme: basic
      description: Vendor account username and password (role `ROLE_VENDOR`).
    apiKeyBasic:
      type: http
      scheme: basic
      description: Username must be the literal `apiKey`; password is the API key (APIKEY token number). Role comes from the key's `apiKeyRole`.

  parameters:
    filter:
      name: filter
      in: query
      required: false
      description: '`;`-separated `key=value` pairs, e.g. `page=0;items=50;sortorder=number;sortdirection=DESC`.'
      schema: { type: string }
    forceCascade:
      name: forceCascade
      in: query
      required: false
      description: '`true` also deletes dependent entities. Any other value, or no value, means `false`.'
      schema: { type: string, enum: ['true', 'false'] }
    productNumber:
      name: productNumber
      in: path
      required: true
      schema: { type: string }
    productModuleNumber:
      name: productModuleNumber
      in: path
      required: true
      schema: { type: string }
    licenseTemplateNumber:
      name: licenseTemplateNumber
      in: path
      required: true
      schema: { type: string }
    licenseeNumber:
      name: licenseeNumber
      in: path
      required: true
      schema: { type: string }
    licenseNumber:
      name: licenseNumber
      in: path
      required: true
      schema: { type: string }
    transactionNumber:
      name: transactionNumber
      in: path
      required: true
      schema: { type: string }
    tokenNumber:
      name: tokenNumber
      in: path
      required: true
      schema: { type: string }
    bundleNumber:
      name: bundleNumber
      in: path
      required: true
      schema: { type: string }

  responses:
    Envelope:
      description: NetLicensing envelope. The item types depend on the operation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Netlicensing' }
        application/xml:
          schema:
            type: string
            description: XML document valid against `netlicensing-context.xsd` (namespace `http://netlicensing.labs64.com/schema/context`). May carry an XML-DSig `Signature`.
    ValidationResult:
      description: |
        Validation result. `items` holds one `Licensee` item (its `number` plus non-hidden properties)
        and one `ProductModuleValidation` item per validated module (`productModuleNumber` and
        model-specific properties and lists). `ttl` is set.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Netlicensing' }
        application/xml:
          schema:
            type: string
            description: XML document valid against `netlicensing-context.xsd`. Signed when the API key has a `privateKey`.
    BadRequest:
      description: Invalid request, business rule violation, or entity not found.
      content: &errorContent
        application/json:
          schema: { $ref: '#/components/schemas/Netlicensing' }
        application/xml:
          schema: { type: string }
    Unauthorized:
      description: |
        Missing or wrong credentials. The response carries `WWW-Authenticate: Basic realm="Labs64 NetLicensing"`
        (not sent when the request has `X-Requested-With: XMLHttpRequest`). The body may not be an envelope.
      headers:
        WWW-Authenticate:
          schema: { type: string }
    Forbidden:
      description: The credentials are valid, but the role may not call this operation.
      content: *errorContent
    Error:
      description: |
        Other errors (`402`, `404`, `429`, `500`). Body is the envelope with `infos.info[]` (type `ERROR`).
      headers:
        Retry-After:
          description: Sent with 429 (value `300`).
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Netlicensing' }
        application/xml:
          schema: { type: string }

  schemas:

    # --- Envelope
    Netlicensing:
      type: object
      properties:
        signature:
          description: Always `null` in JSON. Signatures are produced only for XML.
          type: ['null', object]
        infos:
          type: ['null', object]
          properties:
            info:
              type: array
              items: { $ref: '#/components/schemas/Info' }
        items:
          type: ['null', object]
          properties:
            item:
              type: array
              items:
                description: One entity. The documented entity types are listed; other item types use the generic `Item` shape.
                anyOf:
                  - $ref: '#/components/schemas/ProductItem'
                  - $ref: '#/components/schemas/ProductModuleItem'
                  - $ref: '#/components/schemas/LicenseTemplateItem'
                  - $ref: '#/components/schemas/LicenseeItem'
                  - $ref: '#/components/schemas/LicenseItem'
                  - $ref: '#/components/schemas/TransactionItem'
                  - $ref: '#/components/schemas/TokenItem'
                  - $ref: '#/components/schemas/BundleItem'
                  - $ref: '#/components/schemas/NotificationItem'
                  - $ref: '#/components/schemas/PaymentMethodItem'
                  - $ref: '#/components/schemas/Item'
            pagenumber: { type: ['null', string], description: '0-based page index (string).' }
            itemsnumber: { type: ['null', string], description: Items on this page (string). }
            totalpages: { type: ['null', string] }
            totalitems: { type: ['null', string] }
            hasnext: { type: ['null', string], description: "'true' or 'false'." }
        id:
          type: ['null', string]
          maxLength: 32
        ttl:
          type: ['null', string]
          description: Validity timestamp of the response (set on validation results).
    Info:
      type: object
      properties:
        value: { type: string }
        id: { type: ['null', string], description: 'Message or error code; often the exception class simple name.' }
        type:
          type: string
          description: Upper-case in JSON (Java enum name). Lower-case in XML (`error|warning|info`).
          enum: [ERROR, WARNING, INFO]
    Item:
      type: object
      required: [type]
      properties:
        property:
          type: array
          items: { $ref: '#/components/schemas/Property' }
        list:
          type: array
          items: { $ref: '#/components/schemas/List' }
        type:
          type: string
          description: |
            Entity type. Values used by the endpoints in this spec: Product, ProductModule,
            LicenseTemplate, Licensee, License, Transaction, Token, PaymentMethod, Bundle, Notification,
            ProductModuleValidation, VendorDataExport, LicensingModelProperties, LicenseType, Country,
            Currency, Chart.
    Property:
      type: object
      required: [name]
      properties:
        value: { type: ['null', string] }
        name: { type: string }
    List:
      type: object
      required: [name]
      properties:
        property:
          type: array
          items: { $ref: '#/components/schemas/Property' }
        list:
          type: array
          items: { $ref: '#/components/schemas/List' }
        name: { type: string }

    # --- Entity property catalogues (response items). Documentation only; the wire format is Item.
    ProductItem:
      description: 'Item `type=Product`. Plus custom properties. Repeated nested list `discount` with properties `totalPrice`, `currency`, and `amountFix` or `amountPercent`.'
      x-netlicensing-properties: [number, active, name, version, licenseeAutoCreate, description, licensingInfo, inUse]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Product } }
    ProductModuleItem:
      description: Item `type=ProductModule`. Plus custom properties (licensing-model settings).
      x-netlicensing-properties: [number, active, name, productNumber, licensingModel, inUse]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: ProductModule } }
    LicenseTemplateItem:
      description: Item `type=LicenseTemplate`. Plus custom properties.
      x-netlicensing-properties: [number, active, name, licenseType, price, currency, automatic, hidden, hideLicenses, productModuleNumber, productModuleName, inUse]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: LicenseTemplate } }
    LicenseeItem:
      description: Item `type=Licensee`. Plus custom properties.
      x-netlicensing-properties: [number, active, productNumber, inUse, aliases]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Licensee } }
    LicenseItem:
      description: Item `type=License`. Plus custom properties.
      x-netlicensing-properties: [number, active, name, price, currency, hidden, licenseeNumber, licenseTemplateNumber, productModuleNumber]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: License } }
    TransactionItem:
      description: Item `type=Transaction`. Plus custom properties. Repeated nested list `licenseTransactionJoin` with `licenseNumber` and `transactionNumber`.
      x-netlicensing-properties: [number, active, status, source, grandTotal, discount, currency, datecreated, dateclosed, inUse]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Transaction } }
    TokenItem:
      description: Item `type=Token`. Plus custom properties. `shopURL` only for SHOP tokens, `privateKey` (encrypted) only when present.
      x-netlicensing-properties: [number, active, expirationTime, tokenType, shopURL, vendorNumber, oAuth, privateKey]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Token } }
    BundleItem:
      description: Item `type=Bundle`. Plus custom properties.
      x-netlicensing-properties: [number, active, name, price, currency, description, productNumber, licenseTemplateNumbers, staleLicenseTemplateNumbers]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Bundle } }
    NotificationItem:
      description: Item `type=Notification`. Plus custom properties.
      x-netlicensing-properties: [number, active, name, events, protocol, payload, endpoint]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: Notification } }
    PaymentMethodItem:
      description: Item `type=PaymentMethod`. `number`, `active` plus custom properties.
      x-netlicensing-properties: [number, active]
      allOf:
        - $ref: '#/components/schemas/Item'
        - properties: { type: { const: PaymentMethod } }

    # --- Request forms
    BooleanString:
      type: string
      description: Parsed with Java `Boolean.parseBoolean`. Only `true` (case-insensitive) is true.
      enum: ['true', 'false']
    Currency:
      type: string
      enum: [EUR, USD]
    LicenseType:
      type: string
      enum: [FEATURE, TIMEVOLUME, FLOATING, QUANTITY]
    LicensingModel:
      type: string
      enum: [TryAndBuy, Rental, Subscription, Floating, MultiFeature, PayPerUse, PricingTable, Quota, NodeLocked, Discount]
    TokenType:
      type: string
      description: Case-insensitive.
      enum: [DEFAULT, APIKEY, SHOP, ACTION]
    ApiKeyRole:
      type: string
      enum: [ROLE_APIKEY_LICENSEE, ROLE_APIKEY_ANALYTICS, ROLE_APIKEY_OPERATION, ROLE_APIKEY_MAINTENANCE, ROLE_APIKEY_ADMIN]
    TransactionStatus:
      type: string
      enum: [PENDING, CLOSED]
    TransactionSource:
      type: string
      enum: [SHOP, AUTO_LICENSE_CREATE, AUTO_LICENSE_UPDATE, AUTO_LICENSE_DELETE, AUTO_LICENSEE_CREATE, AUTO_LICENSEE_DELETE, AUTO_LICENSEE_VALIDATE, AUTO_LICENSETEMPLATE_DELETE, AUTO_PRODUCTMODULE_DELETE, AUTO_PRODUCT_DELETE, AUTO_LICENSES_TRANSFER, SUBSCRIPTION_UPDATE, RECURRING_PAYMENT, CANCEL_RECURRING_PAYMENT, OBTAIN_BUNDLE]
    PaymentMethodEnum:
      type: string
      enum: ['NULL', PAYPAL, PAYPAL_SANDBOX, STRIPE, STRIPE_TESTING]

    TimeVolumePeriod:
      type: string
      description: Unit of `timeVolume`. Defaults to `DAY` when omitted.
      enum: [DAY, WEEK, MONTH, YEAR]

    EntityBaseForm:
      type: object
      properties:
        number:
          type: string
          maxLength: 1000
          pattern: '^[a-zA-Z0-9-_.@+&$:]+$'
          description: |
            Unique number that identifies the entity. Optional on create: when omitted, NetLicensing
            generates one. On update, a new value renames the entity.
        active:
          $ref: '#/components/schemas/BooleanString'
          description: |
            `false` disables the entity. Defaults to `true` when omitted on create. On update, an omitted
            value leaves the current state unchanged.
      additionalProperties:
        type: string
        description: Custom property. Any field that is not documented for the entity is stored as a custom property and returned with it.

    ProductForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            name:
              type: string
              description: Product name. Together with the version it identifies the product for the end customer. Required on create.
            version:
              type: string
              description: Product version. Optional, shown next to the product name.
            licenseeAutoCreate:
              $ref: '#/components/schemas/BooleanString'
              description: If `true`, a licensee that does not exist is created at its first validation.
            description:
              type: string
              description: Product description.
            licensingInfo:
              type: string
              description: Licensing information shown to the end customer.
            discount:
              type: array
              description: |
                Repeatable. Volume discount in the format `totalPrice;currency;amount`. Append `%` to `amount`
                for a percentage, otherwise it is a fixed amount in `currency`.
              items: { type: string, examples: ['100.00;EUR;10%', '50.00;USD;5.00'] }
            licenseeSecretMode:
              type: string
              enum: [PREDEFINED, CLIENT]
              description: How licensee secrets are handled.
            vatMode:
              type: string
              enum: [GROSS, NET]
              description: Whether prices include VAT (`GROSS`) or exclude it (`NET`). `NET` is allowed only for company accounts.

    ProductModuleForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            productNumber:
              type: string
              description: Product the module belongs to. Required on create.
            name:
              type: string
              description: Module name shown to end customers in the NetLicensing Shop. Required on create.
            licensingModel:
              $ref: '#/components/schemas/LicensingModel'
              description: |
                Licensing model of the module. It defines which license templates the module can have and how
                its licenses are validated. Required on create.
            maxCheckoutValidity:
              type: string
              description: '`Floating` (required): maximum time a session stays checked out, in seconds. Non-negative integer.'
              examples: ['3600']
            yellowThreshold:
              type: string
              description: |
                Warning level threshold.
                `Rental`: remaining time volume, in days, below which the warning level turns yellow.
                `Floating`, `NodeLocked`, `PayPerUse`, `Subscription`, `PricingTable`: percentage (0-100).
            redThreshold:
              type: string
              description: '`Rental`: remaining time volume, in days, below which the warning level turns red. Non-negative integer.'
            nodeSecretMode:
              type: string
              enum: [PREDEFINED, CLIENT]
              description: |
                `NodeLocked` (required): `CLIENT` takes the node secret from the validation call.
                `PREDEFINED` uses secrets set up in advance and is not allowed when the product has
                `licenseeAutoCreate=true`.

    LicenseTemplateForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            productModuleNumber:
              type: string
              description: Product module the template belongs to. Required on create.
            name:
              type: string
              description: Template name. Required on create.
            licenseType:
              $ref: '#/components/schemas/LicenseType'
              description: |
                Type of licenses created from this template. Required: each licensing model accepts only
                certain types (`TryAndBuy`, `Rental`: FEATURE or TIMEVOLUME; `Subscription`: TIMEVOLUME;
                `Floating`: FLOATING; `MultiFeature`, `Discount`: FEATURE; `PayPerUse`, `Quota`, `NodeLocked`:
                QUANTITY). Any other type returns 400. `PricingTable` licenses must be FEATURE or TIMEVOLUME.
            price:
              type: string
              description: Decimal price, 0 or more. A price above 0 needs `currency`.
            currency: { $ref: '#/components/schemas/Currency' }
            automatic:
              $ref: '#/components/schemas/BooleanString'
              description: If `true`, every new licensee gets one license from this template automatically.
            hidden:
              $ref: '#/components/schemas/BooleanString'
              description: If `true`, the template is not offered in the NetLicensing Shop.
            hideLicenses:
              $ref: '#/components/schemas/BooleanString'
              description: If `true`, licenses from this template are not shown to the end customer but still count in validation.
            timeVolume:
              type: string
              description: 'TIMEVOLUME templates (required): positive integer, in units of `timeVolumePeriod`.'
            timeVolumePeriod: { $ref: '#/components/schemas/TimeVolumePeriod' }
            maxSessions:
              type: string
              description: 'FLOATING templates (required): positive integer, number of concurrent sessions.'
            quantity:
              type: string
              description: 'QUANTITY templates (required): positive integer. `PayPerUse`: number of uses; `Quota`: quota; `NodeLocked`: number of nodes.'
            gracePeriod:
              $ref: '#/components/schemas/BooleanString'
              description: |
                `Rental`, `Subscription` TIMEVOLUME templates: marks the template as the grace period template.
                It must be hidden and, for `Subscription`, not automatic. Only one active grace period
                template is allowed per product module.
            promoCode:
              type: string
              description: '`Discount` (required): promo code. Letters, digits and `- _ . @ + & $ :`. Must be unique among active discount templates.'
            discountType:
              type: string
              enum: [PERCENT, FIX]
              description: '`Discount` (required).'
            discountAmount:
              type: string
              description: '`Discount` (required): percentage (0-100) for `PERCENT`, decimal amount for `FIX`.'
            discountCurrency:
              allOf: [{ $ref: '#/components/schemas/Currency' }]
              description: '`Discount`: required when `discountType=FIX`.'
            minCartTotal:
              type: string
              description: '`Discount`: minimum cart total (decimal) for the promo code to apply.'
            startDate:
              type: string
              format: date-time
              description: '`Discount`: start of the promo code validity.'
            endDate:
              type: string
              format: date-time
              description: '`Discount`: end of the promo code validity. Must not be before `startDate`.'

    LicenseeForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            productNumber:
              type: string
              description: Product the licensee belongs to. Required on create.
            name:
              type: string
              description: Human-readable licensee name.
            aliases:
              type: string
              description: Alternative identifiers of the licensee.
            markedForTransfer:
              $ref: '#/components/schemas/BooleanString'
              description: Marks the licensee as a source for a license transfer. Shop tokens cannot be created for such a licensee.

    LicenseForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            licenseeNumber:
              type: string
              description: Licensee that receives the license. Required on create.
            licenseTemplateNumber:
              type: string
              description: Template the license is created from. Required on create.
            transactionNumber:
              type: string
              description: Optional. Links the license to an existing transaction.
            name:
              type: string
              description: Name of the licensed item. Taken from the license template when omitted.
            hidden:
              $ref: '#/components/schemas/BooleanString'
              description: If `true`, the license is not shown to the end customer. Taken from the license template when omitted.
            startDate:
              type: string
              format: date-time
              description: 'TIMEVOLUME licenses: start of the time volume. Required for `Rental` and for non-automatic `Subscription` licenses.'
            parentFeature:
              type: string
              description: '`Rental` TIMEVOLUME licenses (required): number of the FEATURE license that this time volume extends.'
            timeVolume:
              type: string
              description: TIMEVOLUME licenses. Taken from the license template when omitted.
            timeVolumePeriod: { $ref: '#/components/schemas/TimeVolumePeriod' }
            maxSessions:
              type: string
              description: FLOATING licenses. Taken from the license template when omitted.
            quantity:
              type: string
              description: QUANTITY licenses (`PayPerUse`, `Quota`, `NodeLocked`). Taken from the license template when omitted.
            usedQuantity:
              type: string
              description: '`PayPerUse`: quantity already used. Must not exceed `quantity`.'
            gracePeriod:
              $ref: '#/components/schemas/BooleanString'
              description: '`Rental`, `Subscription`: marks the license as a grace period license.'
      description: '`price` and `currency` are read-only on licenses: they are copied from the license template.'

    TransactionForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            status: { $ref: '#/components/schemas/TransactionStatus' }
            source:
              allOf: [{ $ref: '#/components/schemas/TransactionSource' }]
              description: Use `SHOP` for transactions you create. The other values are set by NetLicensing for automatic transactions.
            grandTotal:
              type: string
              description: Decimal grand total.
            discount:
              type: string
              description: Decimal discount.
            currency: { $ref: '#/components/schemas/Currency' }

    TokenForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            tokenType:
              allOf: [{ $ref: '#/components/schemas/TokenType' }]
              description: Defaults to `DEFAULT`.
            expirationTime:
              type: string
              format: date-time
              description: Expiration time. When omitted, the default lifetime of the token type applies.
            apiKeyRole:
              allOf: [{ $ref: '#/components/schemas/ApiKeyRole' }]
              description: '`APIKEY` tokens: role of the API key. Defaults to `ROLE_APIKEY_LICENSEE`.'
            privateKey:
              type: string
              description: '`APIKEY` tokens: private key (on one line, without spaces) used to sign the XML responses of licensee validation made with this key.'
            licenseeNumber:
              type: string
              description: '`SHOP` and `ACTION` tokens: target licensee. Must exist and must not be marked for transfer.'
            productNumber:
              type: string
              description: '`SHOP` tokens: required when it cannot be taken from `licenseeNumber`.'
            successURL:
              type: string
              description: '`SHOP` tokens: page the customer returns to after checkout.'
            successURLTitle:
              type: string
              description: '`SHOP` tokens: title of the return link after checkout.'
            cancelURL:
              type: string
              description: '`SHOP` tokens: page the customer returns to when checkout is cancelled.'
            cancelURLTitle:
              type: string
              description: '`SHOP` tokens: title of the return link when checkout is cancelled.'
            bundleNumber:
              type: string
              description: '`SHOP` tokens: sell this bundle. Must exist.'
            bundlePrice:
              type: string
              description: "`SHOP` tokens: positive bundle price in '0.00' format."
            predefinedShoppingItem:
              type: string
              description: '`SHOP` tokens: name of the shopping item shown to the customer.'
            action:
              type: string
              enum: [licenseeLogin]
              description: '`ACTION` tokens: `licenseeLogin`. The created token has a `customerPortalURL` that opens the customer portal for the licensee.'

    BundleForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            name:
              type: string
              description: Bundle name shown to the end customer. Required.
            price:
              type: string
              description: Decimal price. A price above 0 needs `currency`.
            currency: { $ref: '#/components/schemas/Currency' }
            description:
              type: string
              description: Bundle description.
            licenseTemplateNumbers:
              type: string
              description: Comma-separated license template numbers.

    NotificationForm:
      allOf:
        - $ref: '#/components/schemas/EntityBaseForm'
        - type: object
          properties:
            name:
              type: string
              description: Notification name. Required.
            events:
              type: string
              description: 'Comma-separated events. Required. Values: LICENSEE_CREATED, LICENSE_CREATED, WARNING_LEVEL_CHANGED, PAYMENT_TRANSACTION_PROCESSED.'
            protocol:
              type: string
              enum: [WEBHOOK]
              description: Required.
            endpoint:
              type: string
              format: uri
              description: Webhook URL. Required for `WEBHOOK` and must be a valid URL.
            payload:
              type: string
              description: Custom payload sent with the webhook.

    ValidationForm:
      type: object
      description: |
        Validation parameters. Product module parameters can be sent in two forms:
        * **One module:** `productModuleNumber=M001&sessionId=abc&action=checkOut`.
        * **Several modules:** add the same numeric suffix to a module and its parameters, for example
          `productModuleNumber0=M001&sessionId0=abc&productModuleNumber1=M002&nodeSecret1=xyz`.

        Module parameters by licensing model:
        * `Floating`: `sessionId` (required), `action` (`checkOut` or `checkIn`, required),
          `maxCheckoutValidity` (seconds; overrides the module setting).
        * `NodeLocked`: `nodeSecret` (required for `CLIENT` node secret mode).
        * `PayPerUse`: `usedQuantity` (uses to record) or `reserveQuantity` (uses to reserve). Do not
          send both.

        Any other key is stored as a licensee property. Keys starting with `productNumber` or
        `licenseeName` are matched by prefix.
      properties:
        productNumber:
          type: string
          description: Product of the licensee. Required when the licensee does not exist yet and the product has `licenseeAutoCreate=true`.
        licenseeName:
          type: string
          description: Name set on a licensee that is created automatically.
        licenseeSecret:
          type: string
          deprecated: true
          description: Deprecated. Stored as a licensee property.
        dryRun:
          $ref: '#/components/schemas/BooleanString'
          description: If `true`, the validation runs without saving changes (dry run).
        forOfflineUse:
          $ref: '#/components/schemas/BooleanString'
          description: If `true`, the response is prepared for offline validation.
        productModuleNumber:
          type: string
          description: Product module the following module parameters apply to (single-module form).
        sessionId:
          type: string
          description: '`Floating`: session identifier.'
        action:
          type: string
          enum: [checkOut, checkIn]
          description: '`Floating`: take a session from the pool or return it.'
        maxCheckoutValidity:
          type: string
          description: '`Floating`: checkout validity in seconds for this call.'
        nodeSecret:
          type: string
          description: '`NodeLocked`: unique secret of the node.'
        usedQuantity:
          type: string
          description: '`PayPerUse`: number of uses to record.'
        reserveQuantity:
          type: string
          description: '`PayPerUse`: number of uses to reserve.'
      additionalProperties: { type: string }
