openapi: 3.1.0
info:
  title: Raccoon ERP API
  version: 1.0.0
  description: "Multi-tenant SaaS REST API for Raccoon, the agentic cloud ERP for small and\nmedium businesses:\
    \ business partners, products, invoices, files and tax.\nAuthentication uses JWT Bearer tokens.\n\n\
    ## Conventions\n\n- **Resource paths are versioned and plural** — `/resource/v{n}/{objects}`.\n- **A\
    \ link to another object is an embedded reference, never a bare id** —\n  `{ \"_id\", \"_class\",\
    \ \"_name\" }`. Only `_id` is required on input; `_name`\n  is a read-only display label the API composes\
    \ on output.\n- **`DELETE` responses are always `204 No Content` with an empty body** —\n  including\
    \ soft-deletes, cancellations, and \"reset to default\" semantics.\n  Clients that need the post-deletion\
    \ state must re-fetch.\n"
servers:
- url: https://api.rccn.io
  description: Production
- url: http://localhost:3000
  description: SAM local / LocalStack
- url: http://localhost:{port}
  description: 'Secondary local API (e.g. a git-worktree checkout running beside the main one) — the e2e
    OpenAPI validator matches any local port.

    '
  variables:
    port:
      default: '3000'
- url: https://{apiId}.execute-api.{region}.amazonaws.com/{stage}
  description: AWS deployment
  variables:
    apiId:
      default: your-api-id
    region:
      default: eu-central-1
    stage:
      default: dev
security:
- BearerAuth: []
tags:
- name: CustomObjects
  x-displayName: Custom Objects
paths:
  /auth/register:
    post:
      tags:
      - Auth
      summary: Register a new tenant and admin user
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Tenant and admin created, tokens returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /auth/login:
    post:
      tags:
      - Auth
      summary: Authenticate and obtain tokens
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Authenticated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/refresh:
    post:
      tags:
      - Auth
      summary: Rotate refresh token and obtain new token pair
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200':
          description: New token pair issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/logout:
    post:
      tags:
      - Auth
      summary: Revoke refresh token (logout)
      responses:
        '204':
          description: Logged out successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/request-password-reset:
    post:
      tags:
      - Auth
      summary: Email a time-limited password-reset link
      description: 'Always returns `200` regardless of whether `email` matches a

        registered, active user — this prevents callers from using the

        response to enumerate registered accounts.

        '
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestPasswordResetRequest'
      responses:
        '200':
          description: Request accepted (email sent if the account exists)
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /auth/reset-password:
    post:
      tags:
      - Auth
      summary: Consume a reset-link token and set a new password
      description: 'Single-use: the token is invalidated on first use whether or not

        the password update succeeds. On success, every outstanding refresh

        token for the user is revoked.

        '
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
      responses:
        '204':
          description: Password reset; all refresh tokens revoked (any already-issued access token remains
            valid until it naturally expires — see ACCESS_TOKEN_TTL, 15 minutes by default)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/api-keys:
    get:
      tags:
      - API Keys
      summary: List the calling user's API keys
      description: 'Returns the API keys owned by the authenticated user (never another

        user''s keys). The bcrypt hash is never returned — only safe metadata.

        '
      responses:
        '200':
          description: API key list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - API Keys
      summary: Create a new API key for the calling user
      description: 'The raw key string is returned in the `key` field exactly once and

        cannot be recovered. Only a bcrypt hash of the secret portion (salt

        embedded) is persisted.


        Forbidden when the calling session was itself authenticated with an

        API key (`auth_source == "api_key"`) — preventing privilege chaining.

        An API key inherits the privileges of the owning user, so the role of

        the resulting session matches the user''s role.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
      responses:
        '201':
          description: API key created. The `key` field is the raw key, shown once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/api-keys/{key_id}:
    parameters:
    - in: path
      name: key_id
      required: true
      schema:
        type: string
      description: Short identifier embedded in the raw key string.
    get:
      tags:
      - API Keys
      summary: Get a single API key (metadata only)
      responses:
        '200':
          description: API key details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - API Keys
      summary: Revoke an API key
      responses:
        '204':
          description: API key revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/business-partners:
    get:
      tags:
      - Business Partners
      summary: List business partners (paginated)
      parameters:
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/NextTokenParam'
      - $ref: '#/components/parameters/PartnerTypeParam'
      - $ref: '#/components/parameters/StatusParam'
      - $ref: '#/components/parameters/SearchQueryParam'
      responses:
        '200':
          description: Paginated list of business partners
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedPartners'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Business Partners
      summary: Create a business partner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BusinessPartnerCreate'
      responses:
        '201':
          description: Business partner created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessPartnerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/business-partners/{business_partner_id}:
    parameters:
    - $ref: '#/components/parameters/BusinessPartnerIdParam'
    get:
      tags:
      - Business Partners
      summary: Get a business partner (includes embedded addresses)
      responses:
        '200':
          description: Business partner with addresses
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessPartnerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Business Partners
      summary: Replace a business partner (full update)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BusinessPartnerUpdate'
      responses:
        '200':
          description: Updated business partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessPartnerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Business Partners
      summary: Partially update a business partner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BusinessPartnerUpdate'
      responses:
        '200':
          description: Updated business partner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessPartnerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Business Partners
      summary: Soft-delete a business partner (sets status=inactive)
      responses:
        '204':
          description: Business partner deactivated
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/business-partners/{business_partner_id}/grant-login:
    parameters:
    - $ref: '#/components/parameters/BusinessPartnerIdParam'
    post:
      tags:
      - Business Partners
      summary: Grant login access to a person-type business partner (owner only)
      description: Creates a user account linked to the business partner and sets its `business_partner_role`.
        Restricted to the tenant owner. Only person-type partners without existing login access are eligible.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantLoginRequest'
      responses:
        '201':
          description: Login access granted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrantLoginResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Business Partners
      summary: Revoke login access from a business partner (owner only)
      description: Deactivates the linked user account and clears the partner's `business_partner_role`.
        Restricted to the tenant owner. The owner's own login cannot be revoked.
      responses:
        '204':
          description: Login access revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/business-partners/{business_partner_id}/addresses:
    parameters:
    - $ref: '#/components/parameters/BusinessPartnerIdParam'
    get:
      tags:
      - Addresses
      summary: List addresses for a business partner
      responses:
        '200':
          description: Address list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAddresses'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
      - Addresses
      summary: Add an address to a business partner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddressCreate'
      responses:
        '201':
          description: Address created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/business-partners/{business_partner_id}/addresses/{address_id}:
    parameters:
    - $ref: '#/components/parameters/BusinessPartnerIdParam'
    - $ref: '#/components/parameters/AddressIdParam'
    get:
      tags:
      - Addresses
      summary: Get a specific address
      responses:
        '200':
          description: Address details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Addresses
      summary: Replace an address (full update)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddressUpdate'
      responses:
        '200':
          description: Updated address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Addresses
      summary: Partially update an address
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddressUpdate'
      responses:
        '200':
          description: Updated address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Addresses
      summary: Delete an address
      responses:
        '204':
          description: Address deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/projects:
    get:
      tags:
      - Projects
      summary: List projects for the authenticated tenant
      parameters:
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/NextTokenParam'
      - $ref: '#/components/parameters/SearchQueryParam'
      responses:
        '200':
          description: Paginated project list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProjects'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Projects
      summary: Create a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectCreate'
      responses:
        '201':
          description: Created project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/projects/{project_id}:
    parameters:
    - name: project_id
      in: path
      required: true
      schema:
        type: string
        example: prj_a1b2c3d4e5f6
    get:
      tags:
      - Projects
      summary: Get a project
      responses:
        '200':
          description: Project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Projects
      summary: Partially update a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectUpdate'
      responses:
        '200':
          description: Updated project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Projects
      summary: Delete a project
      responses:
        '204':
          description: Project deleted
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/custom-objects:
    get:
      tags:
      - CustomObjects
      summary: List a tenant type's custom objects (type-scoped, name-sorted)
      description: 'Lists the custom objects of one tenant `custom_object`-class type. The `type` query
        parameter is required — objects are only ever viewed within their type group, and the query is
        strongly consistent.

        '
      parameters:
      - name: type
        in: query
        required: true
        schema:
          type: string
          example: series
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/NextTokenParam'
      responses:
        '200':
          description: Paginated custom-object list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCustomObjects'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    post:
      tags:
      - CustomObjects
      summary: Create a custom object
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomObjectCreate'
      responses:
        '201':
          description: Created custom object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomObjectResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/custom-objects/{custom_object_id}:
    parameters:
    - name: custom_object_id
      in: path
      required: true
      schema:
        type: string
        example: cob_a1b2c3d4e5f6
    get:
      tags:
      - CustomObjects
      summary: Get a custom object
      responses:
        '200':
          description: Custom object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomObjectResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - CustomObjects
      summary: Replace a custom object
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomObjectUpdate'
      responses:
        '200':
          description: Updated custom object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomObjectResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    patch:
      tags:
      - CustomObjects
      summary: Partially update a custom object
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomObjectUpdate'
      responses:
        '200':
          description: Updated custom object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomObjectResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - CustomObjects
      summary: Delete a custom object
      responses:
        '204':
          description: Custom object deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/changes:
    get:
      tags:
      - Changes
      summary: Poll the tenant change feed
      description: 'Returns the changes recorded at or after `since`, oldest first, so a

        client can refresh whatever it has on screen that appears in the

        result. Each entry names a subject to re-read; none carries field

        values.


        The response''s `next_token` is a durable **cursor**, not a page marker:

        it is never null, and the client sends it back as `since` on the next

        poll. Omitting `since` subscribes from now and returns an empty page —

        a client that has just loaded has no backlog to catch up on.


        Delivery is at-least-once. The cursor lags real time by a couple of

        seconds so a change written late within a second cannot be skipped,

        which means consecutive polls re-deliver a small overlap; de-duplicate

        on `_id`. Refetching is idempotent, so this is a deliberate trade.

        '
      parameters:
      - name: since
        in: query
        required: false
        description: Cursor from a previous response's `next_token`.
        schema:
          type: string
          example: '1785483570'
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 100
      responses:
        '200':
          description: Changes since the cursor, plus the cursor to poll with next
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedChanges'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '410':
          description: 'The cursor is older than the feed''s one-hour retention, so the

            changes it would have skipped are unknowable. The client drops the

            cursor and refreshes everything it is showing.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /resource/v1/changes/{change_id}:
    get:
      tags:
      - Changes
      summary: Read a single change row
      parameters:
      - name: change_id
        in: path
        required: true
        schema:
          type: string
          example: chg_a1b2c3d4e5f6
      responses:
        '200':
          description: The change row
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/companies/{company_id}/identifiers:
    parameters:
    - name: company_id
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - Identifiers
      summary: List identifiers for a company
      responses:
        '200':
          description: Identifiers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Identifiers
      summary: Add an identifier to a company
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Created identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/companies/{company_id}/identifiers/{identifier_id}:
    parameters:
    - name: company_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - Identifiers
      summary: Get a company identifier
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Identifiers
      summary: Update a company identifier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Identifiers
      summary: Delete a company identifier
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/companies/{company_id}/identifiers/{identifier_id}/validations:
    parameters:
    - name: company_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    post:
      summary: Append a validation result to an identifier (async-validator callback)
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierValidationResultRequest'
      responses:
        '200':
          description: Identifier with the validation applied (or skipped)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierWithAppliedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/products:
    get:
      tags:
      - Products
      summary: List products (optionally filtered by project)
      parameters:
      - name: project_id
        in: query
        required: false
        schema:
          type: string
          example: prj_a1b2c3d4e5f6
        description: Filter products by project ID
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/NextTokenParam'
      - $ref: '#/components/parameters/SearchQueryParam'
      responses:
        '200':
          description: Paginated product list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProducts'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Products
      summary: Create a product
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
      responses:
        '201':
          description: Created product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSaveResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/products/{product_id}:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
        example: prd_a1b2c3d4e5f6
    get:
      tags:
      - Products
      summary: Get a product
      responses:
        '200':
          description: Product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Products
      summary: Partially update a product
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpdate'
      responses:
        '200':
          description: Updated product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductSaveResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - Products
      summary: Delete a product
      responses:
        '204':
          description: Product deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/products/{product_id}/project:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
        example: prd_a1b2c3d4e5f6
    put:
      tags:
      - Products
      summary: Assign or unassign a product's project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductSetProject'
      responses:
        '200':
          description: Updated product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/products/{product_id}/prices:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
        example: prd_a1b2c3d4e5f6
    post:
      tags:
      - Products
      summary: Append a price row to a product
      description: 'Sub-resource sugar over the parent Product update. Single-price mode

        accepts at most one row across the product''s lifetime — adding a

        second one returns 422 until the relevant `pricing_config` flag is

        enabled on the product type. Scoping fields (country_codes,

        channel, …) are rejected unless their gating flag is on.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceInput'
      responses:
        '201':
          description: The created price row
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Price'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/products/{product_id}/prices/{price_id}:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
        example: prd_a1b2c3d4e5f6
    - name: price_id
      in: path
      required: true
      schema:
        type: string
        example: prc_a1b2c3d4e5f6
    put:
      tags:
      - Products
      summary: Replace one price row
      description: 'Full replace: the body is the new row contents; the `price_id`

        stays with the row across the swap so downstream snapshots

        (invoice lines, change logs) keep their back-link.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceInput'
      responses:
        '200':
          description: The replaced price row
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Price'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    patch:
      tags:
      - Products
      summary: Partially update one price row
      description: 'Body is an arbitrary subset of `Price` fields. The service merges

        the patch into the existing row, re-validates the result as a

        `PriceInput`, and re-runs the prices-array validators (scoping

        guard, currency consistency, scoping-tuple uniqueness).

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Subset of Price fields to overlay.
              additionalProperties: true
      responses:
        '200':
          description: The updated price row
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Price'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Products
      summary: Remove one price row from a product
      responses:
        '204':
          description: Removed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/invoices:
    get:
      tags:
      - Invoices
      summary: List invoices (newest first)
      parameters:
      - $ref: '#/components/parameters/LimitParam'
      - $ref: '#/components/parameters/NextTokenParam'
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - finalized
          - paid
          - overdue
          - cancelled
      - name: business_partner_id
        in: query
        required: false
        schema:
          type: string
      - name: type_id
        in: query
        required: false
        schema:
          type: string
      - $ref: '#/components/parameters/SearchQueryParam'
      responses:
        '200':
          description: Paginated invoice list
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                - next_token
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/InvoiceResponse'
                  next_token:
                    type:
                    - string
                    - 'null'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Invoices
      summary: Create a new invoice (starts as draft)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceCreate'
      responses:
        '201':
          description: Created invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
        example: inv_a1b2c3d4e5f6
    get:
      tags:
      - Invoices
      summary: Get invoice by ID
      responses:
        '200':
          description: Invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Invoices
      summary: Update invoice fields and/or status
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceUpdate'
      responses:
        '200':
          description: Updated invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    patch:
      tags:
      - Invoices
      summary: Partially update invoice fields and/or status
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceUpdate'
      responses:
        '200':
          description: Updated invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Invoices
      summary: Cancel an invoice (soft-delete; status → cancelled)
      responses:
        '204':
          description: Cancelled
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/finalize:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
        example: inv_a1b2c3d4e5f6
    post:
      tags:
      - Invoices
      summary: Finalize invoice (draft → finalized)
      responses:
        '200':
          description: Finalized invoice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/addresses/sync-from-partner:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
        example: inv_a1b2c3d4e5f6
    post:
      tags:
      - Invoices
      summary: Refresh invoice address snapshots from the partner's preferred addresses
      description: 'Replaces the invoice''s address snapshots with the current preferred

        (primary) addresses from the linked business partner, for every slot

        declared on the invoice type. The frontend calls this after the user

        confirms a "Update addresses to {new partner}''s defaults?" prompt

        triggered by a business-partner change. Only allowed while the invoice

        is in `draft`.

        '
      responses:
        '200':
          description: Invoice with refreshed address snapshots
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/documents:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
        example: inv_a1b2c3d4e5f6
    get:
      tags:
      - Documents
      summary: List documents for an invoice
      description: 'Returns all document rows attached to the invoice. A document is a derived business
        document (invoice PDF, delivery slip, offer, …) generated asynchronously by a worker.

        '
      responses:
        '200':
          description: Documents for the invoice
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                - next_token
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/DocumentResponse'
                  next_token:
                    type:
                    - string
                    - 'null'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
      - Documents
      summary: Create a document for an invoice
      description: 'Creates a document row in `pdf_pending` and emits a `document.created` event. The
        PDF generation worker subscribes and asynchronously attaches the rendered file — clients should
        poll the row until status flips to `pdf_ready`.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentCreate'
      responses:
        '201':
          description: Document created (status pdf_pending)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/documents/{document_id}:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
    - name: document_id
      in: path
      required: true
      schema:
        type: string
        example: doc_a1b2c3d4e5f67890
    get:
      tags:
      - Documents
      summary: Get a single document
      responses:
        '200':
          description: Document
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - Documents
      summary: Partial update — typically used to attach the rendered PDF
      description: 'The only mutable field today is `file`. Setting `file` on a `pdf_pending` document
        implicitly transitions it to `pdf_ready` — the status FSM is server-controlled and callers cannot
        write `status` directly. Used by the invoice-document-generator service to report a rendered PDF
        (per architecture.md § Async Workers — no internal API; a UI client could call it too). Requires
        admin/superuser role.

        Idempotent: replaying on an already-attached document returns 200 with the existing row (the originally
        attached PDF is preserved).

        Per architecture.md § Embedded References, only `_id` is required inside the `file` Reference;
        `_class` and `_name` are accepted and resolved server-side from the linked File row.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                file:
                  allOf:
                  - $ref: '#/components/schemas/Reference'
                  nullable: true
              example:
                file:
                  _id: fil_a1b2c3d4e5f6
                  _class: file
                  _name: INV-2026-00001.pdf
      responses:
        '200':
          description: Document updated (or no-op replay).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Documents
      summary: Delete a document
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/invoices/{invoice_id}/documents/{document_id}/retry:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
    - name: document_id
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - Documents
      summary: Retry a `pdf_failed` document
      description: 'Moves a `pdf_failed` document back to `pdf_pending` and re-publishes the `document.created`
        event so the worker picks it up again. Idempotent for already-pending rows.

        '
      responses:
        '200':
          description: Retry accepted (status pdf_pending)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/files:
    post:
      summary: Issue a presigned upload form for a new file
      description: 'Validates the request against the file-type registry, enforces

        cardinality (a `cardinality: one` type rejects a second active upload

        with HTTP 409), and returns a presigned POST form scoped to the

        caller''s tenant prefix in S3.


        After uploading the bytes via the returned `upload` form, an S3 event

        promotes the row from `status=pending` to `ready` (or `failed`).

        '
      tags:
      - Files
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileCreateRequest'
      responses:
        '201':
          description: File row created in `pending` and presigned upload issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileCreateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    get:
      summary: List files attached to a parent entity
      tags:
      - Files
      parameters:
      - name: parent_id
        in: query
        required: true
        schema:
          type: string
      - name: parent_class
        in: query
        required: true
        schema:
          type: string
          enum:
          - business_partner
          - product
          - project
          - invoice
          - tenant
          - chat
      - name: type
        in: query
        required: false
        description: Filter by namespaced file-type key.
        schema:
          type: string
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 50
          maximum: 200
      - name: next_token
        in: query
        required: false
        schema:
          type: string
      responses:
        '200':
          description: Files for the given parent (newest first)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedFiles'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/files/{file_id}:
    parameters:
    - name: file_id
      in: path
      required: true
      schema:
        type: string
        example: fil_a1b2c3d4e5f6
    get:
      summary: Get file metadata
      tags:
      - Files
      responses:
        '200':
          description: File metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Soft-delete a file
      description: 'Sets `status=soft_deleted` and `deleted_at`. The S3 object is kept

        for 30 days then hard-deleted by a nightly cleanup, unless the file

        carries `legal_hold=true`.

        '
      tags:
      - Files
      responses:
        '204':
          description: Soft-deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/files/{file_id}/download:
    parameters:
    - name: file_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Issue a short-lived presigned download URL
      tags:
      - Files
      responses:
        '200':
          description: Presigned GET URL valid for ~5 minutes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileDownloadResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /resource/v1/identifiers:
    get:
      summary: Cross-entity reverse lookup by identifier value
      description: 'Returns all identifier records matching the given schema_key and value within the
        authenticated tenant. Each result contains a `parent` Reference pointing to the owning entity.
        Use the nested sub-resource endpoints for CRUD operations on identifiers.

        '
      tags:
      - Identifiers
      parameters:
      - name: schema_key
        in: query
        required: true
        schema:
          type: string
          example: ean13
      - name: identifier
        in: query
        required: true
        schema:
          type: string
          example: '4006381333931'
      responses:
        '200':
          description: Matching identifier records
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                - next_token
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/IdentifierResponse'
                  next_token:
                    type:
                    - string
                    - 'null'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/identifier-schemas:
    get:
      summary: Identifier schemas selectable for an entity class
      description: 'Returns the platform identifier schemas that can be attached to the given entity class.
        Every scheme is returned (a company may hold a foreign VAT registration, so none are hidden);
        when `country` is supplied the scheme issued by that country (e.g. `vat_eu` for an EU company,
        `vat_gb` for a UK company) is sorted first and flagged `recommended`. Drives the add-identifier
        dropdown on the Company form.

        '
      tags:
      - Identifiers
      parameters:
      - name: entity_class
        in: query
        required: true
        schema:
          type: string
          example: company
      - name: country
        in: query
        required: false
        description: ISO 3166-1 alpha-2 code of the entity's home country.
        schema:
          type: string
          example: DE
      responses:
        '200':
          description: Matching identifier schemas
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                - next_token
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/IdentifierSchema'
                  next_token:
                    type:
                    - string
                    - 'null'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/products/{product_id}/identifiers:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
        example: prd_a1b2c3d4e5f6
    get:
      summary: List identifiers for a product
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Add an identifier to a product
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Identifier created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/products/{product_id}/identifiers/{identifier_id}:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Get a product identifier
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update a product identifier
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      summary: Delete a product identifier
      tags:
      - Identifiers
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/products/{product_id}/identifiers/{identifier_id}/validations:
    parameters:
    - name: product_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    post:
      summary: Append a validation result to an identifier (async-validator callback)
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierValidationResultRequest'
      responses:
        '200':
          description: Identifier with the validation applied (or skipped)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierWithAppliedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/business-partners/{business_partner_id}/identifiers:
    parameters:
    - name: business_partner_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: List identifiers for a business partner
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Add an identifier to a business partner
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Identifier created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/business-partners/{business_partner_id}/identifiers/{identifier_id}:
    parameters:
    - name: business_partner_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Get a business partner identifier
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update a business partner identifier
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      summary: Delete a business partner identifier
      tags:
      - Identifiers
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/business-partners/{business_partner_id}/identifiers/{identifier_id}/validations:
    parameters:
    - name: business_partner_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    post:
      summary: Append a validation result to an identifier (async-validator callback)
      description: 'Dedicated write path for async validators (e.g. the VIES worker) to

        report a verdict, kept off the general identifier PATCH so a worker

        write and a concurrent user value-edit can''t clobber each other.

        Applies staleness + never-downgrade rules; `applied=false` when skipped.

        '
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierValidationResultRequest'
      responses:
        '200':
          description: Identifier with the validation applied (or skipped)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierWithAppliedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/identifiers:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: List identifiers for an invoice
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Add an identifier to an invoice
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Identifier created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/invoices/{invoice_id}/identifiers/{identifier_id}:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Get an invoice identifier
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update an invoice identifier
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      summary: Delete an invoice identifier
      tags:
      - Identifiers
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/invoices/{invoice_id}/identifiers/{identifier_id}/validations:
    parameters:
    - name: invoice_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    post:
      summary: Append a validation result to an identifier (async-validator callback)
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierValidationResultRequest'
      responses:
        '200':
          description: Identifier with the validation applied (or skipped)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierWithAppliedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/projects/{project_id}/identifiers:
    parameters:
    - name: project_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: List identifiers for a project
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Add an identifier to a project
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Identifier created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/projects/{project_id}/identifiers/{identifier_id}:
    parameters:
    - name: project_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Get a project identifier
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update a project identifier
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      summary: Delete a project identifier
      tags:
      - Identifiers
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /resource/v1/projects/{project_id}/identifiers/{identifier_id}/validations:
    parameters:
    - name: project_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    post:
      summary: Append a validation result to an identifier (async-validator callback)
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierValidationResultRequest'
      responses:
        '200':
          description: Identifier with the validation applied (or skipped)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierWithAppliedResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/custom-objects/{custom_object_id}/identifiers:
    parameters:
    - name: custom_object_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: List identifiers for a custom object
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      summary: Add an identifier to a custom object
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierNestedCreate'
      responses:
        '201':
          description: Identifier created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/custom-objects/{custom_object_id}/identifiers/{identifier_id}:
    parameters:
    - name: custom_object_id
      in: path
      required: true
      schema:
        type: string
    - name: identifier_id
      in: path
      required: true
      schema:
        type: string
    get:
      summary: Get a custom-object identifier
      tags:
      - Identifiers
      responses:
        '200':
          description: Identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      summary: Update a custom-object identifier
      tags:
      - Identifiers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifierUpdate'
      responses:
        '200':
          description: Updated identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifierResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      summary: Delete a custom-object identifier
      tags:
      - Identifiers
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /codelists/v1/codelists:
    get:
      tags:
      - Codelists
      summary: List codelists in the catalog (global + tenant)
      parameters:
      - name: scope
        in: query
        schema:
          type: string
          enum:
          - global
          - tenant
      - name: namespace
        in: query
        schema:
          type: string
      - name: industry
        in: query
        schema:
          type: string
      - name: authority
        in: query
        schema:
          type: string
      - name: status
        in: query
        schema:
          type: string
          enum:
          - draft
          - active
          - deprecated
      responses:
        '200':
          description: Catalog of codelist headers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodelistCatalogResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /codelists/v1/codelists/{namespace}/{version}:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
      description: Codelist namespace; may contain slashes (e.g. iso/3166-1). Pass each segment as a separate
        path segment.
    - name: version
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - Codelists
      summary: Get a codelist header
      responses:
        '200':
          description: Codelist header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodelistHeader'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /codelists/v1/codelists/{namespace}/{version}/values:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    - name: locale
      in: query
      schema:
        type: string
        default: en
    - name: parent_code
      in: query
      schema:
        type: string
    - name: status
      in: query
      schema:
        type: string
        enum:
        - active
        - deprecated
        - retired
    - name: q
      in: query
      schema:
        type: string
    - name: limit
      in: query
      schema:
        type: integer
        default: 50
    - name: offset
      in: query
      schema:
        type: integer
        default: 0
    get:
      tags:
      - Codelists
      summary: List codes in a codelist
      responses:
        '200':
          description: Codes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodeValuesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /codelists/v1/codelists/{namespace}/{version}/values/{code}:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    - name: code
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - Codelists
      summary: Get a single code
      responses:
        '200':
          description: Code value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodeValueResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /codelists/v1/codelists/{namespace}/{version}/values/{code}/migration:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    - name: code
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - Codelists
      summary: Walk replaced_by_code chain to a terminal node
      responses:
        '200':
          description: Migration chain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MigrationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /codelists/v1/validate:
    post:
      tags:
      - Codelists
      summary: Validate a (namespace, version, code) tuple
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/codelists:
    post:
      tags:
      - Codelists
      summary: Create a tenant codelist (slug becomes part of the namespace)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodelistCreateRequest'
      responses:
        '201':
          description: Created codelist header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodelistHeader'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/codelists/{namespace}:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
      description: Tenant codelist namespace (must start with tenant:).
    put:
      tags:
      - Codelists
      summary: Publish a new immutable version of a tenant codelist
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodelistUpdateRequest'
      responses:
        '201':
          description: New version published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodelistHeader'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/codelists/{namespace}/{version}/values:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - Codelists
      summary: Add a code to a tenant codelist
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodeValueCreateRequest'
      responses:
        '201':
          description: Created code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodeValueResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /resource/v1/codelists/{namespace}/{version}/values/{code}:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    - name: code
      in: path
      required: true
      schema:
        type: string
    put:
      tags:
      - Codelists
      summary: Update a tenant codelist code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodeValueUpdateRequest'
      responses:
        '200':
          description: Updated code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodeValueResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      tags:
      - Codelists
      summary: Delete a code value (only allowed while the version is unpublished)
      responses:
        '204':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /resource/v1/codelists/{namespace}/{version}/values/{code}/deprecate:
    parameters:
    - name: namespace
      in: path
      required: true
      schema:
        type: string
    - name: version
      in: path
      required: true
      schema:
        type: string
    - name: code
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - Codelists
      summary: Mark a tenant code as deprecated
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeprecateRequest'
      responses:
        '200':
          description: Updated code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CodeValueResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    RegisterAddress:
      type: object
      description: 'Address block on the registration payload. Used by the post-

        registration seed (Phase D) to build the starter Company and sample

        customers. Tax IDs (VAT, legal/tax registration, IBAN) are NOT

        accepted on the form — they are filled with country-prefix

        placeholders (`DEXXXXXXXXX`, etc.) by the seed; the user replaces

        them in Settings → Companies before issuing real invoices.

        '
      required:
      - street_line_1
      - city
      - postal_code
      - country_code
      properties:
        street_line_1:
          type: string
          minLength: 1
          maxLength: 200
          example: Friedrichstr. 1
        street_line_2:
          type:
          - string
          - 'null'
          maxLength: 200
        city:
          type: string
          minLength: 1
          maxLength: 100
          example: Berlin
        state_province:
          type:
          - string
          - 'null'
          maxLength: 100
        postal_code:
          type: string
          minLength: 1
          maxLength: 20
          example: '10117'
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          minLength: 2
          maxLength: 2
          example: DE
    RegisterRequest:
      type: object
      required:
      - tenant_name
      - admin_email
      - admin_password
      - admin_first_name
      - admin_last_name
      - address
      - invite_code
      properties:
        tenant_name:
          type: string
          minLength: 1
          example: Acme GmbH
        admin_email:
          type: string
          format: email
          example: admin@acme.de
        admin_password:
          type: string
          format: password
          minLength: 8
        admin_first_name:
          type: string
          minLength: 1
          example: Jane
        admin_last_name:
          type: string
          minLength: 1
          example: Doe
        address:
          $ref: '#/components/schemas/RegisterAddress'
        invite_code:
          type: string
          minLength: 1
          maxLength: 40
          description: 'A registration invite, issued by a Raccoon administrator. Registration

            is closed without one. Matched case-insensitively and ignoring

            whitespace; an unknown, revoked, expired or used-up code is rejected

            with 422.

            '
          example: RCCN-7K3P-QX92
    ReferenceValue:
      type: object
      required:
      - _id
      - _class
      - _name
      description: Stored value for a field of type "reference"
      properties:
        _id:
          type: string
          example: bp_a1b2c3
        _class:
          type: string
          enum:
          - business_partner
          - channel
          - chat_session
          - codelist
          - company
          - file
          - invoice
          - invoice_numbering_schema
          - letter_paper
          - product
          - project
          - tenant
          - user
          - service
          - system
          - api_key
          example: business_partner
        _name:
          type: string
          readOnly: true
          description: Read-only, backend-composed display label of the referenced entity (a person's
            full name, a product's name, a project's title, …). A backend-meta field — never accepted
            on input.
          example: Acme Corp
    AuthResponse:
      type: object
      required:
      - tenant
      - user_id
      - access_token
      - refresh_token
      - token_type
      - expires_in
      - auth_source
      properties:
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        user_id:
          type: string
          example: usr_def456
        access_token:
          type: string
        refresh_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 900
        auth_source:
          type: string
          enum:
          - login
          - api_key
          description: 'Indicates how this session was authenticated. `login` for email +

            password, `api_key` when the caller exchanged an API key for the

            token pair. The flag is propagated through refresh; clients use it

            to disable actions that must not chain off an API-key session (e.g.

            creating another API key).

            '
    ErrorResponse:
      description: 'Standard error envelope returned by every non-2xx response. All three fields are always
        present; `details` may be an empty object.

        '
      type: object
      required:
      - error
      - message
      - details
      additionalProperties: false
      properties:
        error:
          type: string
          description: Machine-readable error code matching the exception class.
          example: not_found
        message:
          type: string
          description: Human-readable description of what went wrong.
          example: Business partner not found
        details:
          type: object
          description: 'Optional structured context. Shape depends on the error code — see `ValidationErrorDetails`
            for the `validation_error` case.

            '
          additionalProperties: true
          example: {}
    LoginRequest:
      type: object
      description: 'Accepts either an email + password pair, or an API key. The server

        enforces that at least one of the two auth methods is fully provided.

        '
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password
        api_key:
          type: string
          description: 'Raw API key string (`rccn_{key_id}_{secret}`). When present, `email`

            and `password` are ignored. The returned token pair carries

            `auth_source: api_key`.

            '
    RefreshRequest:
      type: object
      required:
      - refresh_token
      properties:
        refresh_token:
          type: string
    RequestPasswordResetRequest:
      type: object
      required:
      - email
      properties:
        email:
          type: string
          format: email
    ResetPasswordRequest:
      type: object
      required:
      - token
      - new_password
      properties:
        token:
          type: string
          description: The signed, single-use token from the reset-link email.
        new_password:
          type: string
          format: password
          minLength: 8
    Reference:
      type: object
      required:
      - _id
      - _class
      - _name
      description: 'An embedded reference to another entity.


        **In responses** all three fields are always present (the backend builds

        every reference from the live entity), so they are marked `required`.

        `_name` is a backend-meta field (sibling of `_id` / `_class`): the

        read-only, backend-composed display label.


        **In request bodies** only `_id` is required: the backend resolves the

        entity from `_id` and derives `_class` and `_name` itself, ignoring any

        values the caller submits. A minimal reference is therefore

        `{"_id": "bp_..."}`. To clear a nullable reference, send `null`.

        '
      properties:
        _id:
          type: string
          example: prod_fd74a5ce31b6
        _class:
          type: string
          example: product
        _name:
          type: string
          readOnly: true
          description: Read-only, backend-composed display label of the referenced entity. A backend-meta
            field — never accepted on input.
          example: Widget A
    ApiKeyResponse:
      type: object
      description: 'Safe representation of a stored API key — the hash is never exposed.

        '
      required:
      - key_id
      - name
      - created_at
      properties:
        key_id:
          type: string
          description: Short identifier embedded in the raw key string.
          example: a1b2c3d4e5f6
        name:
          type: string
          example: CI deploy key
        created_at:
          type: integer
          description: Unix epoch seconds.
          example: 1748815200
        last_used_at:
          type:
          - integer
          - 'null'
          description: 'Unix epoch seconds of the most recent successful exchange of this

            API key for a JWT pair at `POST /auth/auth`. Stays unchanged for

            subsequent requests within the same JWT session.

            '
        created_by:
          description: 'Reference to the user who created the key. Carries the user display

            name denormalized at creation time. Null when the creator could not

            be resolved.

            '
          oneOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
    ApiKeyListResponse:
      type: object
      description: 'Envelope wrapping the caller''s API keys. The endpoint does not paginate

        (a single user is not expected to hold more than a handful of keys);

        ``next_token`` is always ``null`` and kept for shape stability with

        every other list endpoint.

        '
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyResponse'
        next_token:
          type:
          - string
          - 'null'
    ApiKeyCreate:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: CI deploy key
    ApiKeyCreateResponse:
      type: object
      description: 'Response from creating an API key. The `key` field carries the raw key

        string and is shown to the caller exactly once — store it immediately.

        Only a bcrypt hash of the secret portion is persisted server-side.

        '
      required:
      - key_id
      - key
      - name
      - created_at
      properties:
        key_id:
          type: string
          example: a1b2c3d4e5f6
        key:
          type: string
          description: 'The raw key string in the form `rccn_{key_id}_{secret}`. Pass this

            verbatim in `POST /auth/login` body field `api_key` to exchange it

            for a JWT pair (see `LoginRequest`).

            '
          example: rccn_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
        name:
          type: string
          example: CI deploy key
        created_at:
          type: integer
          example: 1748815200
        created_by:
          description: Reference to the user who created the key.
          oneOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
    EntityName:
      type: string
      readOnly: true
      description: Read-only, backend-composed display label of this entity (a person's full name, a product's
        name, a project's title, …). A backend-meta field (sibling of `_id` / `_class`) — never accepted
        on input.
      example: Acme Corp
    PersonPartnerResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - partner_type
      - first_name
      - last_name
      - status
      - language_code
      - tenant
      - created_at
      - updated_at
      - data
      - email_addresses
      - vat_partner_type
      properties:
        _id:
          type: string
          example: bp_a1b2c3
        _class:
          type: string
          example: business_partner
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Jane Doe
        partner_type:
          type: string
          enum:
          - person
        first_name:
          type: string
          example: Jane
        last_name:
          type: string
          example: Doe
        status:
          type: string
          enum:
          - active
          - inactive
        language_code:
          type: string
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        created_at:
          type: integer
          description: Unix epoch seconds
          example: 1746144000
        updated_at:
          type: integer
          description: Unix epoch seconds
          example: 1746144000
        data:
          type: object
          additionalProperties: true
          description: Type-specific custom-field data; `{}` when untyped
        email_addresses:
          type: array
          items:
            type: string
            format: email
          description: All e-mail addresses linked to this partner (including login users)
        vat_partner_type:
          type: string
          enum:
          - b2c
          - b2b
          - public_authority
          description: 'Buyer kind for VAT regime resolution. The VAT-ID itself is a

            `vat_eu` identifier (see the identifiers sub-resource), not a

            field here.

            '
    CompanyPartnerResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - partner_type
      - company_name
      - status
      - language_code
      - tenant
      - created_at
      - updated_at
      - data
      - email_addresses
      - vat_partner_type
      properties:
        _id:
          type: string
        _class:
          type: string
          example: business_partner
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Acme GmbH
        partner_type:
          type: string
          enum:
          - company
        company_name:
          type: string
          example: Acme GmbH
        status:
          type: string
          enum:
          - active
          - inactive
        language_code:
          type: string
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        created_at:
          type: integer
          description: Unix epoch seconds
        updated_at:
          type: integer
          description: Unix epoch seconds
        data:
          type: object
          additionalProperties: true
        email_addresses:
          type: array
          items:
            type: string
            format: email
        vat_partner_type:
          type: string
          enum:
          - b2c
          - b2b
          - public_authority
    BusinessPartnerResponse:
      oneOf:
      - $ref: '#/components/schemas/PersonPartnerResponse'
      - $ref: '#/components/schemas/CompanyPartnerResponse'
      discriminator:
        propertyName: partner_type
        mapping:
          person: '#/components/schemas/PersonPartnerResponse'
          company: '#/components/schemas/CompanyPartnerResponse'
    PaginatedPartners:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/BusinessPartnerResponse'
        next_token:
          type:
          - string
          - 'null'
    BusinessPartnerCommon:
      type: object
      required: []
      properties:
        phone:
          type: string
          example: +49 30 123456
        mobile:
          type: string
          example: +49 170 1234567
        email:
          type: string
          format: email
        website:
          type: string
          format: uri
        notes:
          type: string
        status:
          type: string
          enum:
          - active
          - inactive
          default: active
        language_code:
          type: string
          minLength: 2
          maxLength: 5
          default: en
          example: de
        discount_percent:
          type: number
          minimum: 0
          maximum: 100
          default: 0
          description: 'Per-partner discount applied to product list prices on Order-to-Cash

            documents. Defaults to 0 (no discount). 0 % is semantically the

            "no override" state.

            '
          example: 10
        payment_due_days:
          type:
          - integer
          - 'null'
          minimum: 0
          description: 'Calendar days from order date to payment due date. `null` (or absent)

            means inherit `default_payment_due_days` from the partner type.

            '
          example: 30
        preferred_invoice_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
          description: 'Reference to the BP''s preferred invoice (billing) address. Must

            point at an address belonging to this BP that has

            `is_invoice_address: true`. The server validates both invariants on

            write. `null` means no preferred address is set; the invoice form

            falls back to the first address with the matching role flag.

            '
        preferred_shipping_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
          description: 'Like `preferred_invoice_address` but for the `shipping` slot.

            '
        preferred_service_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
          description: 'Like `preferred_invoice_address` but for the `service` slot.

            '
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: 'Optional tenant-defined sales channel this partner belongs to.

            `null` means unassigned.

            '
        vat_partner_type:
          type: string
          enum:
          - b2c
          - b2b
          - public_authority
          default: b2c
          description: 'Buyer kind, and the first input to VAT regime resolution. `b2b` with a

            VIES-validated VAT-ID unlocks intra-EU reverse-charge. Distinct

            from `partner_type` (the person/company discriminator). The VAT-ID

            itself is a `vat_eu` identifier (see the identifiers sub-resource).

            '
    PersonPartnerCreate:
      allOf:
      - $ref: '#/components/schemas/BusinessPartnerCommon'
      - type: object
        required:
        - partner_type
        - first_name
        - last_name
        properties:
          partner_type:
            type: string
            enum:
            - person
          salutation:
            type: string
            example: Ms.
          salutation_code:
            type: string
            enum:
            - frau
            - herr
            - divers
            - mme
            - m
            - ms
            - mr
            - mx
            description: 'Machine-readable salutation feeding a future formal-greeting

              helper (deferred Phase 3 invoice-text epic).

              '
            example: ms
          first_name:
            type: string
            minLength: 1
            example: Jane
          last_name:
            type: string
            minLength: 1
            example: Doe
          date_of_birth:
            type: string
            format: date
            example: '1985-03-15'
          gender:
            type: string
            enum:
            - male
            - female
            - other
            - unknown
    CompanyPartnerCreate:
      allOf:
      - $ref: '#/components/schemas/BusinessPartnerCommon'
      - type: object
        required:
        - partner_type
        - company_name
        properties:
          partner_type:
            type: string
            enum:
            - company
          company_name:
            type: string
            minLength: 1
            example: Acme GmbH
          legal_form:
            type: string
            example: GmbH
            description: 'Legal form. Common values: GmbH, AG, UG, GbR, Inc., LLC, Corp., Ltd., S.A.,
              BV, NV

              '
          tax_id:
            type: string
            example: DE123456789
          commercial_register_number:
            type: string
            example: HRB 12345
    BusinessPartnerCreate:
      oneOf:
      - $ref: '#/components/schemas/PersonPartnerCreate'
      - $ref: '#/components/schemas/CompanyPartnerCreate'
      discriminator:
        propertyName: partner_type
        mapping:
          person: '#/components/schemas/PersonPartnerCreate'
          company: '#/components/schemas/CompanyPartnerCreate'
    BusinessPartnerUpdate:
      type: object
      required: []
      properties:
        phone:
          type: string
        mobile:
          type: string
        email:
          type: string
          format: email
        website:
          type: string
          format: uri
        notes:
          type: string
        status:
          type: string
          enum:
          - active
          - inactive
        language_code:
          type: string
        salutation:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        date_of_birth:
          type: string
          format: date
        gender:
          type: string
          enum:
          - male
          - female
          - other
          - unknown
        company_name:
          type: string
        legal_form:
          type: string
        tax_id:
          type: string
        vat_partner_type:
          type: string
          enum:
          - b2c
          - b2b
          - public_authority
        commercial_register_number:
          type: string
        type_id:
          type:
          - string
          - 'null'
        schema_version:
          type:
          - string
          - 'null'
        data:
          type: object
          additionalProperties: true
        discount_percent:
          type: number
          minimum: 0
          maximum: 100
        payment_due_days:
          type:
          - integer
          - 'null'
          minimum: 0
        preferred_invoice_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
        preferred_shipping_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
        preferred_service_address:
          allOf:
          - $ref: '#/components/schemas/Reference'
          nullable: true
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Tenant-defined sales channel, or null to unassign.
    GrantLoginRequest:
      type: object
      required:
      - email
      - password
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password
          minLength: 8
        role:
          type: string
          enum:
          - administrator
          default: administrator
    GrantLoginResponse:
      type: object
      required:
      - user_id
      - business_partner_id
      - role
      - email
      - business_partner_role
      properties:
        user_id:
          type: string
        business_partner_id:
          type: string
        role:
          type: string
          enum:
          - administrator
        email:
          type: string
          format: email
        business_partner_role:
          type: string
          enum:
          - administrator
    AddressResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - business_partner
      - is_invoice_address
      - is_shipping_address
      - is_service_address
      - street_line_1
      - city
      - country_code
      - tenant
      - created_at
      - updated_at
      properties:
        _id:
          type: string
          example: addr_a1b2c3
        _class:
          type: string
          example: address
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Hofbrunnstraße 12, 81479 München
        business_partner:
          $ref: '#/components/schemas/Reference'
        is_invoice_address:
          type: boolean
        is_shipping_address:
          type: boolean
        is_service_address:
          type: boolean
        street_line_1:
          type: string
        city:
          type: string
        country_code:
          type: string
          minLength: 2
          maxLength: 2
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        created_at:
          type: integer
          description: Unix epoch seconds
        updated_at:
          type: integer
          description: Unix epoch seconds
    PaginatedAddresses:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AddressResponse'
        next_token:
          type:
          - string
          - 'null'
    AddressCreate:
      type: object
      required:
      - street_line_1
      - city
      - country_code
      properties:
        label:
          type: string
          description: 'Free-text structural label (e.g. "HQ", "Munich warehouse"). Purely

            descriptive — has no behaviour.

            '
          example: HQ
        is_invoice_address:
          type: boolean
          default: false
          description: 'When true, this address is eligible to fill the `invoice` slot on

            invoices for this business partner. Auto-defaults to `true` for the

            very first address created on a BP (the freelancer-friendly default).

            '
        is_shipping_address:
          type: boolean
          default: false
          description: 'When true, this address is eligible to fill the `shipping` slot on

            invoices for this business partner.

            '
        is_service_address:
          type: boolean
          default: false
          description: 'When true, this address is eligible to fill the `service` slot on

            invoices for this business partner.

            '
        street_line_1:
          type: string
          minLength: 1
          example: Musterstraße 1
        street_line_2:
          type: string
          example: Apt 2B
        city:
          type: string
          minLength: 1
          example: Berlin
        state_province:
          type: string
          example: Berlin
        postal_code:
          type: string
          example: '10115'
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          example: DE
          description: ISO 3166-1 alpha-2 country code
    AddressUpdate:
      type: object
      required: []
      properties:
        label:
          type: string
        is_invoice_address:
          type: boolean
        is_shipping_address:
          type: boolean
        is_service_address:
          type: boolean
        street_line_1:
          type: string
        street_line_2:
          type: string
        city:
          type: string
        state_province:
          type: string
        postal_code:
          type: string
        country_code:
          type: string
          minLength: 2
          maxLength: 2
    ProjectResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - title
      - tenant
      - data
      - created_at
      - updated_at
      properties:
        _id:
          type: string
          example: prj_a1b2c3d4e5f6
        _class:
          type: string
          example: project
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Spring Catalogue
        title:
          type: string
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        data:
          type: object
          additionalProperties: true
          description: Custom field values; `{}` when untyped
        created_at:
          type: integer
          description: Unix epoch seconds
        updated_at:
          type: integer
          description: Unix epoch seconds
    PaginatedProjects:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ProjectResponse'
        next_token:
          type:
          - string
          - 'null'
    ProjectCreate:
      type: object
      required:
      - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          example: Q2 2026 Launch
        description:
          type: string
        type_id:
          type:
          - string
          - 'null'
          description: Optional global or tenant type identifier (e.g. "client_project")
        schema_version:
          type:
          - string
          - 'null'
          description: Type version. Omitted = latest.
        data:
          type: object
          additionalProperties: true
          description: Custom field values keyed by section field IDs.
    ProjectUpdate:
      type: object
      required: []
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        type_id:
          type:
          - string
          - 'null'
        schema_version:
          type:
          - string
          - 'null'
        data:
          type: object
          additionalProperties: true
    CustomObjectResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - type_id
      - name
      - schema_version
      - tenant
      - data
      - created_at
      - updated_at
      properties:
        _id:
          type: string
          example: cob_a1b2c3d4e5f6
        _class:
          type: string
          example: custom_object
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Studies in Modern Poetry
        type_id:
          type: string
          example: series
        name:
          type: string
        schema_version:
          type: string
          example: 1.0.0
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        data:
          type: object
          additionalProperties: true
        created_at:
          type: integer
          description: Unix epoch seconds
        updated_at:
          type: integer
          description: Unix epoch seconds
    PaginatedCustomObjects:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CustomObjectResponse'
        next_token:
          type:
          - string
          - 'null'
    CustomObjectCreate:
      type: object
      required:
      - type_id
      - name
      properties:
        type_id:
          type: string
          minLength: 1
          maxLength: 100
          description: The tenant `custom_object`-class type this record is shaped by (e.g. "series").
        name:
          type: string
          minLength: 1
          maxLength: 200
          example: Studies in Modern Poetry
        data:
          type: object
          additionalProperties: true
          description: Custom field values keyed by the type's section field IDs.
    CustomObjectUpdate:
      type: object
      required: []
      properties:
        type_id:
          type: string
          minLength: 1
          maxLength: 100
        name:
          type: string
          minLength: 1
          maxLength: 200
        data:
          type: object
          additionalProperties: true
    ChangeResponse:
      type: object
      description: 'One entry in the tenant change feed — "this object moved". A change carries

        no field values: the client re-reads `subject` through that entity''s own

        endpoint. Rows expire an hour after they are recorded, so the feed is a

        notification channel rather than a durable history.

        '
      required:
      - _id
      - _class
      - subject
      - resource_class
      - subclass
      - method
      - actor_kind
      - created_at
      - updated_at
      properties:
        _id:
          type: string
          example: chg_a1b2c3d4e5f6
        _class:
          type: string
          enum:
          - change
        subject:
          description: 'The aggregate the client should re-read. `_name` is always empty — the

            feed never resolves it, both to keep a 200-row page to a single query

            and because a `deleted` subject no longer exists to name.

            '
          $ref: '#/components/schemas/Reference'
        resource_class:
          type: string
          description: Aggregate root class, e.g. `product`.
          example: product
        subclass:
          type: string
          description: 'Fully-qualified path of what changed within the aggregate, e.g.

            `product.price` or `business_partner.identifier`.

            '
          example: product.price
        method:
          type: string
          enum:
          - created
          - updated
          - deleted
        actor_kind:
          type: string
          description: Who caused it — `user`, `agent`, `service`, or `system`.
          example: agent
        created_at:
          type: integer
          description: Unix epoch seconds. Also the feed's ordering position.
        updated_at:
          type: integer
          description: Unix epoch seconds.
        _links:
          type: object
          additionalProperties:
            type: string
    PaginatedChanges:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ChangeResponse'
        next_token:
          type: string
          description: 'The feed cursor, **never null** — unlike every other list endpoint,

            where `next_token` marks a page. Store it and send it back as `since`

            on the next poll. It deliberately lags real time by a couple of

            seconds, so consecutive polls re-deliver a small overlap; de-duplicate

            on `_id`.

            '
          example: '1785483570'
    IdentifierResponse:
      type: object
      required:
      - _id
      - _class
      - parent
      - schema_key
      - value
      - validations
      - tenant
      - created_at
      - updated_at
      properties:
        _id:
          type: string
        _class:
          type: string
          example: identifier
        parent:
          $ref: '#/components/schemas/Reference'
        schema_key:
          type: string
        value:
          type: string
        validations:
          type: array
          description: 'Zero or more validation results for this identifier value, one per

            validator. Open shape (each is `{type, status, result, ...}`); VIES is

            the first producer. Written only by the validations-append endpoint

            (worker callback), never by the general identifier PATCH.

            '
          items:
            type: object
            additionalProperties: true
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        created_at:
          type: integer
        updated_at:
          type: integer
    IdentifierListResponse:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IdentifierResponse'
    IdentifierNestedCreate:
      type: object
      required:
      - schema_key
      - value
      properties:
        schema_key:
          type: string
          example: ean13
        value:
          type: string
          example: '4006381333931'
        label:
          type: string
          nullable: true
    IdentifierUpdate:
      type: object
      required: []
      properties:
        value:
          type: string
          nullable: true
        label:
          type: string
          nullable: true
    IdentifierValidationResultRequest:
      type: object
      description: 'One validation outcome an async validator (e.g. the VIES worker for

        `vat_eu`, the Crossref worker for `doi`) POSTs to

        `.../identifiers/{id}/validations`. `value` echoes what was validated (the

        endpoint drops the result if the identifier''s current value changed since);

        the rest is stored verbatim as one entry in the open `validations` array,

        keyed by `type`.

        '
      required:
      - value
      - type
      - status
      - result
      - validated_at
      properties:
        value:
          type: string
          description: The identifier value the validator checked (staleness guard).
        type:
          type: string
          example: vies
        status:
          type: string
          example: completed
        result:
          type: string
          enum:
          - SUCCESS
          - INVALID
          - UNAVAILABLE
        validated_at:
          type: integer
        name_on_record:
          type:
          - string
          - 'null'
        consultation_number:
          type:
          - string
          - 'null'
        work_type:
          type:
          - string
          - 'null'
          description: Crossref work type (e.g. journal-article) for a `doi` result.
        log:
          type:
          - string
          - 'null'
    IdentifierWithAppliedResponse:
      allOf:
      - $ref: '#/components/schemas/IdentifierResponse'
      - type: object
        required:
        - applied
        properties:
          applied:
            type: boolean
            description: 'False when the result was dropped (staleness — value changed — or

              never-downgrade of a cached SUCCESS); the identifier is unchanged.

              '
    MonetaryAmount:
      type: object
      required:
      - amount
      - currency
      description: "A monetary value with an ISO 4217 currency code.\n\n`currency` may be `null` to denote\
        \ the **identity (\"zero\")\nelement** — `{\"amount\": 0, \"currency\": null}`. The identity\n\
        absorbs into arithmetic (`identity + x = x`, `x - x = identity`,\n`identity * n = identity`);\
        \ any non-zero amount must carry a\ncurrency. This is the wire form returned for an invoice draft\n\
        whose totals haven't been computed yet.\n\n## Precision contract\n\n`amount` is emitted as a **JSON\
        \ number** carrying the server's\n`Decimal` with full precision — the server writes the Decimal's\
        \ exact\ntextual form as a raw JSON number token, bypassing any IEEE-754\nround-trip. Sub-cent\
        \ values such as\n`0.0003 USD` (audiobook-streaming royalty rates, FX conversions,\nregulated\
        \ reference prices) come over the wire exactly.\n\n## Client-side parsing\n\nStandard JSON parsers\
        \ convert JSON numbers to IEEE-754 floats,\nwhich lose precision for arithmetic on decimals that\
        \ aren't\npower-of-10 fractions. Pick the right tool for the job:\n\n- **Display only** — the\
        \ default `float` is fine. Both\n  Python's `repr(0.0003)` and JavaScript's `(0.0003).toString()`\n\
        \  return `\"0.0003\"`, so user-visible numbers render correctly.\n- **Exact arithmetic** (summing\
        \ line items, computing tax) — parse\n  with a Decimal / BigDecimal type. Python: `json.loads(text,\n\
        \  parse_float=Decimal)`. JavaScript: a BigDecimal library such as\n  `big.js` or `decimal.js`.\n\
        \n## Client-side encoding for sub-cent values\n\nA client that produces a sub-cent amount with\
        \ `json.dumps` (Python)\nor `JSON.stringify` (JS) will *also* lose precision before the bytes\n\
        leave the process — both encoders convert `Decimal`/`BigDecimal` to\n`float` first. Two paths\
        \ preserve precision on the way in:\n\n1. Send the amount as a **quoted decimal string** (the\n\
        \   `MonetaryAmount.amount` input schema accepts both `number` and\n   `string` via `oneOf` on\
        \ the request side; the server's Pydantic\n   coerces the string to an exact `Decimal`).\n2. Or\
        \ use a JSON encoder on the client that supports raw number\n   tokens (same trick as our server's\
        \ encoder).\n\nFor everyday cent-precision values, sending a JSON number is fine.\n"
      properties:
        amount:
          type: number
          example: 12.5
        currency:
          type:
          - string
          - 'null'
          minLength: 3
          maxLength: 3
          example: EUR
          description: 'ISO 4217 code. `null` denotes the identity element

            (necessarily `amount: 0`).

            '
    Price:
      type: object
      required:
      - _id
      - _class
      - amount
      - currency
      - tax_mode
      - vat_category
      - country_codes
      - channel
      - valid_from
      - valid_to
      - is_campaign
      - compare_at_amount
      - min_quantity
      - unit_of_measure
      - compute
      - priority
      properties:
        _id:
          type: string
          example: prc_a1b2c3d4e5f6
        _class:
          type: string
          enum:
          - price
        amount:
          type: number
          example: 12.5
        currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
        tax_mode:
          type: string
          enum:
          - net
          - gross
        vat_category:
          type:
          - string
          - 'null'
          enum:
          - H
          - S
          - R
          - R2
          - T
          - Z
          - EXEMPT
          - null
          description: ONIX List 62 VAT category override for this row. Null inherits the Product-level
            `vat_category`. Settable only under `by_country`.
        country_codes:
          type: array
          items:
            type: string
            minLength: 2
            maxLength: 2
          description: ISO 3166-1 alpha-2 codes. Empty = any. Gated by `by_country`.
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Reference to a tenant-defined Channel. Null = applies to any channel. Gated by
            `by_channel`.
        valid_from:
          type:
          - string
          - 'null'
          format: date
          description: ISO calendar date (YYYY-MM-DD), inclusive. Gated by `time_validity`.
        valid_to:
          type:
          - string
          - 'null'
          format: date
        is_campaign:
          type: boolean
          default: false
          description: Gated by `campaigns`.
        compare_at_amount:
          anyOf:
          - $ref: '#/components/schemas/MonetaryAmount'
          - type: 'null'
        min_quantity:
          type: number
          default: 1
          description: Volume-tier breakpoint. Gated by `scale_prices`.
        unit_of_measure:
          type: string
          default: EA
        compute:
          type:
          - object
          - 'null'
          description: Reserved for `formula_prices`.
        priority:
          type: integer
          default: 0
          description: Tie-breaker; higher wins. Always available.
    ProductResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - type_id
      - schema_version
      - name
      - data
      - vat_category
      - tenant
      - created_at
      - updated_at
      properties:
        _id:
          type: string
          example: prd_a1b2c3d4e5f6
        _class:
          type: string
          enum:
          - product
        _name:
          $ref: '#/components/schemas/EntityName'
          example: Oak Dining Table
        type_id:
          type: string
          example: toy
        schema_version:
          type: string
          example: 1.0.0
        name:
          type: string
          example: My Product
        data:
          type: object
          additionalProperties: true
        vat_category:
          type: string
          enum:
          - H
          - S
          - R
          - R2
          - T
          - Z
          - EXEMPT
          description: ONIX List 62 VAT category. The per-invoice rate is resolved from it.
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        created_at:
          type: integer
          example: 1672531200
        updated_at:
          type: integer
          example: 1672531200
        project:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Project this product belongs to, null if unassigned
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Sales channel this product belongs to, null if unassigned
        prices:
          type: array
          description: 'Inline price entries. Empty array when no price has been assigned.

            Single-price mode = 0 or 1 entries, no scoping fields populated.

            Multi-dim mode (any `pricing_config` flag on) = N entries with the

            gated scoping fields.

            '
          items:
            $ref: '#/components/schemas/Price'
    PaginatedProducts:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ProductResponse'
        next_token:
          type:
          - string
          - 'null'
    PriceInput:
      type: object
      description: 'Input variant of `Price`. `_id` is optional (server generates one if

        omitted); `_class` is accepted but ignored (server always assigns

        `"price"`). `tax_mode` is optional too — when omitted the service fills it

        from the tenant_config registry (`pricing.default_tax_mode`). An optional

        `vat_category` is accepted only under `by_country`; otherwise the

        Product-level category applies. Carries the same `_id` + `_class` identity

        pair as every other embedded object.

        '
      required:
      - amount
      - currency
      properties:
        _id:
          type:
          - string
          - 'null'
          description: Optional; server generates a `prc_…` id when omitted.
        _class:
          type:
          - string
          - 'null'
          enum:
          - price
          - null
          description: Accepted on input but ignored — the server always assigns "price".
        amount:
          oneOf:
          - type: number
          - type: string
            pattern: ^-?[0-9]+(\.[0-9]+)?$
          description: 'JSON number for common cases; quoted decimal string when the value

            must be preserved with sub-cent precision (the client''s standard

            JSON encoder would otherwise round-trip through IEEE-754 float

            before sending). The server coerces both to Decimal. See

            `common.yaml#/schemas/MonetaryAmount` for the full precision

            contract and client guidance.

            '
          example: 12.5
        currency:
          type: string
          minLength: 3
          maxLength: 3
          example: EUR
        tax_mode:
          type:
          - string
          - 'null'
          enum:
          - net
          - gross
          - null
        vat_category:
          type:
          - string
          - 'null'
          enum:
          - H
          - S
          - R
          - R2
          - T
          - Z
          - EXEMPT
          - null
        country_codes:
          type: array
          items:
            type: string
            minLength: 2
            maxLength: 2
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
        valid_from:
          type:
          - string
          - 'null'
          format: date
        valid_to:
          type:
          - string
          - 'null'
          format: date
        is_campaign:
          type: boolean
          default: false
        compare_at_amount:
          anyOf:
          - $ref: '#/components/schemas/MonetaryAmount'
          - type: 'null'
        min_quantity:
          oneOf:
          - type: number
          - type: string
            pattern: ^[0-9]+(\.[0-9]+)?$
          default: 1
        unit_of_measure:
          type: string
          default: EA
        compute:
          type:
          - object
          - 'null'
        priority:
          type: integer
          default: 0
    ProductCreate:
      type: object
      required:
      - type_id
      - name
      properties:
        project:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Optional project to assign this product to
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Optional tenant-defined sales channel to assign this product to
        type_id:
          type: string
          minLength: 1
          maxLength: 100
          example: toy
        schema_version:
          type: string
          minLength: 1
          maxLength: 50
          example: 1.0.0
        name:
          type: string
          minLength: 1
          maxLength: 200
          example: My Product
        data:
          type: object
          additionalProperties: true
        vat_category:
          type:
          - string
          - 'null'
          enum:
          - H
          - S
          - R
          - R2
          - T
          - Z
          - EXEMPT
          - null
          description: ONIX List 62 VAT category. Optional on input — defaults to the product type's `recommended_vat_category`,
            else the tenant default (`vat.default_vat_category`). The VAT rate is resolved per invoice.
        prices:
          type: array
          description: 'Inline price entries. Single-price mode (`pricing_config`

            with no flags enabled on the product type) accepts at most one entry

            with no scoping fields. Multi-dimension mode accepts N entries with

            the corresponding scoping fields populated. The service layer

            rejects rows whose scoping fields aren''t gated by the relevant

            `pricing_config` flag.

            '
          items:
            $ref: '#/components/schemas/PriceInput'
    ProductSaveWarning:
      type: object
      description: A non-fatal advisory raised while saving a product. The save still succeeds; the warning
        flags something the user probably wants to fix.
      required:
      - code
      - field
      - message
      properties:
        code:
          type: string
          example: prices.by_country_missing_vat_category
        field:
          type: string
          description: The request field the warning relates to.
          example: prices
        message:
          type: string
          description: Human-readable explanation.
    ProductSaveResponse:
      description: Product create/update response — the saved product plus a non-fatal `warnings` array
        (always present, empty when there is nothing to flag).
      allOf:
      - $ref: '#/components/schemas/ProductResponse'
      - type: object
        required:
        - warnings
        properties:
          warnings:
            type: array
            items:
              $ref: '#/components/schemas/ProductSaveWarning'
    ProductUpdate:
      type: object
      required: []
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        data:
          type: object
          additionalProperties: true
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Sales channel to assign, or null to unassign
        vat_category:
          type:
          - string
          - 'null'
          enum:
          - H
          - S
          - R
          - R2
          - T
          - Z
          - EXEMPT
          - null
          description: ONIX List 62 VAT category. Omit or null to leave unchanged.
        type_id:
          type:
          - string
          - 'null'
        schema_version:
          type:
          - string
          - 'null'
        prices:
          type:
          - array
          - 'null'
          description: 'Omit (or `null`) to leave the existing prices unchanged. Empty

            array to clear. Any non-empty list replaces the prices array.

            '
          items:
            $ref: '#/components/schemas/PriceInput'
    ProductSetProject:
      type: object
      properties:
        project:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Project reference to assign, or null to unassign
    Decimal:
      description: 'Decimal value emitted as a JSON number with full precision. Accepts a

        number or a numeric string on input; always emitted as a number on

        output. See `common.yaml#/schemas/MonetaryAmount` for the full

        precision contract (server encoder, client parsing guidance, and the

        sub-cent-input recommendation to use quoted strings when the client''s

        JSON encoder would otherwise convert through float).

        '
      oneOf:
      - type: number
      - type: string
        pattern: ^-?\d+(\.\d+)?$
    LineItemResponse:
      type: object
      required:
      - line_item_id
      - description
      - quantity
      - unit_price
      - tax_rate
      - line_total
      - product
      - resolved_price_id
      properties:
        line_item_id:
          type: string
          example: li_a1b2c3d4e5f6
        description:
          type: string
          example: Consulting services
        quantity:
          $ref: '#/components/schemas/Decimal'
        unit_price:
          $ref: '#/components/schemas/Decimal'
        tax_rate:
          $ref: '#/components/schemas/Decimal'
        line_total:
          $ref: '#/components/schemas/Decimal'
        product:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Back-link to the catalog product the line was priced from, when applicable.
        resolved_price_id:
          type:
          - string
          - 'null'
          description: 'Id of the Price row the resolver chose, when the line was priced by

            the multi-dimension resolver. Always null in single-price mode and

            when the caller supplied an explicit `unit_price`.

            '
    VatBreakdownLine:
      type: object
      description: 'One row of the per-VAT-rate breakdown (UStG §14). Server-computed

        by `Invoice.recompute_totals()`; the renderer prefers the persisted

        list over recomputation so the breakdown is part of the byte-stable

        legal record (GoBD).

        '
      required:
      - rate
      - taxable_amount
      - vat_amount
      properties:
        rate:
          $ref: '#/components/schemas/Decimal'
          description: Tax rate as a percentage in [0, 100].
        taxable_amount:
          $ref: '#/components/schemas/MonetaryAmount'
          description: Net sum of line items at this rate.
        vat_amount:
          $ref: '#/components/schemas/MonetaryAmount'
          description: VAT sum at this rate.
    PaymentTerms:
      type: object
      description: 'Structured payment-terms block. Coexists with the free-text

        `payment_terms_text` (EN 16931 BT-20 — the source of truth). The

        renderer prints both blocks under the totals when set.

        '
      properties:
        skonto_percent:
          anyOf:
          - $ref: '#/components/schemas/Decimal'
          - type: 'null'
          description: 0–100. Discount percentage if paid within `skonto_days`.
        skonto_days:
          type:
          - integer
          - 'null'
          minimum: 0
        late_payment_notice:
          type:
          - string
          - 'null'
          description: Pre-rendered text. The author handles translation.
    InvoiceAddress:
      type: object
      description: 'Snapshot of an EN 16931 postal address (BG-5 / BG-8 / BG-15) stored on

        the invoice — used both for the slot-keyed buyer/shipping/service

        addresses and for the seller snapshot. Only the always-present fields

        are declared — strict response validation treats every declared property

        as required, so optional fields (`source_address_id`, `street_line_2`,

        `state_province`, `postal_code`) are intentionally omitted from the

        schema. They may still appear on the wire.

        '
      required:
      - street_line_1
      - city
      - country_code
      properties:
        street_line_1:
          type: string
        city:
          type: string
        country_code:
          type: string
          minLength: 2
          maxLength: 2
    InvoiceResponse:
      type: object
      description: 'EN 16931 aligned invoice header. Seller_* fields and

        `payment_instructions` are populated from the `invoice.seller.*`

        tenant-config keys and refreshed on every save while in draft; they

        are read-only via the API. `invoice_number` is empty (`""`) until the

        invoice leaves `draft` — the UI should render "Draft" in that case.

        '
      required:
      - _id
      - _class
      - _name
      - invoice_number
      - business_partner
      - status
      - issue_date
      - due_date
      - document_type_code
      - currency
      - payment_means_code
      - line_items
      - tax_exclusive
      - tax_total
      - tax_inclusive
      - vat_breakdown
      - notes
      - data
      - addresses
      - created_at
      - updated_at
      - created_by
      - tenant
      properties:
        _id:
          type: string
          example: inv_a1b2c3d4e5f6
        _class:
          type: string
          example: invoice
        _name:
          $ref: '#/components/schemas/EntityName'
          description: Read-only, backend-composed display label (the invoice number, or id while still
            a draft).
          example: INV-2026-0001
        invoice_number:
          type: string
          description: 'Empty (`""`) while the invoice is in `draft`. Stamped by the

            per-tenant `InvoiceNumberingSchema` on the draft → finalized transition

            (resolution order: invoice type → company → tenant-default schema).

            Once assigned, the value is immutable. Cancellation from `draft`

            does NOT consume a number — the row keeps the empty string and

            relies on `status="cancelled"` as the audit signal.

            '
          example: 2026-R-0042
        business_partner:
          $ref: '#/components/schemas/Reference'
        company:
          oneOf:
          - $ref: '#/components/schemas/ReferenceValue'
          - type: 'null'
          description: 'Issuing Company (BG-7 Seller). Snapshotted onto the immutable

            `seller_*` columns at draft → finalized. Null until the auto-default

            resolves (Phase D seed guarantees a value for new tenants).

            '
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: Sales channel selected for this invoice, null if none.
        status:
          type: string
          enum:
          - draft
          - finalized
          - paid
          - overdue
          - cancelled
          example: draft
        issue_date:
          type: string
          format: date
          example: '2026-05-01'
        due_date:
          type: string
          format: date
          example: '2026-05-31'
        dunning_level:
          type: integer
          example: 0
          description: Dunning escalation level (0 = none, 1..N = reminder ladder step).
        document_type_code:
          type: string
          enum:
          - '380'
          - '381'
          - '384'
          - '386'
          - '326'
          example: '380'
        currency:
          type: string
          example: EUR
        payment_means_code:
          type: string
          example: '30'
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/LineItemResponse'
        tax_exclusive:
          $ref: '#/components/schemas/MonetaryAmount'
          description: 'EN 16931 BT-109 — sum of line nets, as a `MonetaryAmount`

            carrying the invoice''s currency (BT-5). A draft whose totals

            haven''t been computed yet returns the identity element

            (`{"amount": 0, "currency": null}`).

            '
        tax_total:
          $ref: '#/components/schemas/MonetaryAmount'
          description: EN 16931 BT-110.
        tax_inclusive:
          $ref: '#/components/schemas/MonetaryAmount'
          description: EN 16931 BT-112. BT-115 (payable_amount) equals this until prepaid / rounding ships.
        vat_breakdown:
          type: array
          items:
            $ref: '#/components/schemas/VatBreakdownLine'
          description: 'Per-VAT-rate breakdown (UStG §14). Server-computed by

            `recompute_totals()`; read-only on the API. The renderer prefers

            this persisted list over recomputation.

            '
        notes:
          type: array
          items:
            type: string
          description: EN 16931 BT-22 invoice notes (0..n).
        payment_terms:
          oneOf:
          - $ref: '#/components/schemas/PaymentTerms'
          - type: 'null'
          description: Structured BT-20 sub-block. `null` when unset.
        data:
          type: object
          additionalProperties: true
        addresses:
          type: object
          description: 'Snapshotted address per slot the invoice type declared. Slot keys are

            a subset of the type''s `address_slots` (`invoice` | `shipping` |

            `service`). Empty object when no slot is configured for the invoice

            type or the partner has no eligible address.

            '
          additionalProperties:
            $ref: '#/components/schemas/InvoiceAddress'
        created_at:
          type: integer
          example: 1746144000
        updated_at:
          type: integer
          example: 1746144000
        created_by:
          $ref: '#/components/schemas/ReferenceValue'
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
    InvoicingPeriod:
      type: object
      description: Invoice-level invoicing period (EN 16931 BG-14).
      properties:
        start_date:
          type:
          - string
          - 'null'
          format: date
        end_date:
          type:
          - string
          - 'null'
          format: date
    BillingReference:
      type: object
      description: Preceding-invoice reference (EN 16931 BG-3 / BT-25 / BT-26).
      required:
      - preceding_invoice_number
      properties:
        preceding_invoice_number:
          type: string
        preceding_invoice_issue_date:
          type:
          - string
          - 'null'
          format: date
    PartyContact:
      type: object
      description: Contact group (EN 16931 BG-6 / BG-9).
      properties:
        name:
          type:
          - string
          - 'null'
        phone:
          type:
          - string
          - 'null'
        email:
          type:
          - string
          - 'null'
    ElectronicAddress:
      type: object
      description: 'Electronic address (EN 16931 BT-34 / BT-49). `scheme_id` follows the

        EAS code list: `EM` = email, `9930` = German VAT, `0088` = GLN, etc.

        '
      required:
      - value
      - scheme_id
      properties:
        value:
          type: string
        scheme_id:
          type: string
    Payee:
      type: object
      description: 'Third-party payee (EN 16931 BG-10). Populated only when the party

        receiving payment differs from the seller (e.g. factoring).

        '
      required:
      - name
      properties:
        name:
          type: string
        identifier:
          type:
          - string
          - 'null'
        legal_registration_id:
          type:
          - string
          - 'null'
    LineItemCreate:
      type: object
      description: 'Line item input. Either `unit_price` OR `product` must be supplied —

        when only `product` is set, the server resolves the unit price from the

        product''s own pricing and snapshots the result onto the line. When both

        are set, the explicit `unit_price` wins (manual override).

        '
      required:
      - description
      - quantity
      properties:
        description:
          type: string
          minLength: 1
          example: Consulting services
        quantity:
          $ref: '#/components/schemas/Decimal'
        unit_price:
          anyOf:
          - $ref: '#/components/schemas/Decimal'
          - type: 'null'
          description: Optional. Filled by the resolver when `product` is supplied alone.
        tax_rate:
          $ref: '#/components/schemas/Decimal'
        product:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: 'Reference to the catalog product whose price should be applied to

            this line. When set without an explicit `unit_price`, the invoice

            service auto-fills `unit_price` (and `tax_rate` when the line came

            in with the default 0) from the resolver result.

            '
    InvoiceAddressInput:
      type: object
      description: 'Address payload accepted as a slot value on InvoiceCreate / InvoiceUpdate.

        The full address is snapshotted onto the invoice — the client is free to

        submit a partner-address pulled from the address book (in which case

        `source_address_id` should be the partner address''s `_id`) or a free-text

        address (omit `source_address_id`).

        '
      required:
      - street_line_1
      - city
      - country_code
      properties:
        source_address_id:
          type:
          - string
          - 'null'
          example: addr_a1b2c3
        street_line_1:
          type: string
          minLength: 1
          example: Musterstraße 1
        street_line_2:
          type: string
        city:
          type: string
          minLength: 1
          example: Berlin
        state_province:
          type: string
        postal_code:
          type: string
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          example: DE
    InvoiceCreate:
      type: object
      description: 'EN 16931 aligned invoice header. All seller_* fields and

        `payment_instructions` are NOT accepted on input — they are sourced

        from the `invoice.seller.*` tenant-config keys and refreshed on every

        save while the invoice is in draft. `issue_date`, `due_date`,

        `currency`, `document_type_code` and `payment_means_code` are optional;

        the service fills in tenant-config defaults when omitted.

        '
      required:
      - business_partner
      properties:
        business_partner:
          $ref: '#/components/schemas/Reference'
        company:
          oneOf:
          - $ref: '#/components/schemas/ReferenceValue'
          - type: 'null'
          description: 'Issuing Company (EN 16931 BG-7 Seller). Auto-resolved from the

            tenant''s invoice-company config when omitted on create. Per the

            no-bare-id rule (architecture.md#Embedded References) the wire

            shape is the full Reference object.

            '
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: 'Optional tenant-defined sales channel selected for this invoice.

            Snapshotted on the invoice.

            '
        issue_date:
          type:
          - string
          - 'null'
          format: date
          example: '2026-05-01'
        due_date:
          type:
          - string
          - 'null'
          format: date
          example: '2026-05-31'
        tax_point_date:
          type:
          - string
          - 'null'
          format: date
          description: EN 16931 BT-7 — value-added-tax point date.
        actual_delivery_date:
          type:
          - string
          - 'null'
          format: date
          description: EN 16931 BT-72 — actual delivery date.
        document_type_code:
          type:
          - string
          - 'null'
          enum:
          - '380'
          - '381'
          - '384'
          - '386'
          - '326'
          - null
          description: 'EN 16931 BT-3 (UNCL 1001). 380=commercial invoice, 381=credit note,

            384=corrected invoice, 386=prepayment invoice, 326=partial invoice.

            '
        currency:
          type:
          - string
          - 'null'
          minLength: 3
          maxLength: 3
          example: EUR
        buyer_reference:
          type:
          - string
          - 'null'
          description: EN 16931 BT-10. Often the buyer's PO number or Leitweg-ID.
        purchase_order_reference:
          type:
          - string
          - 'null'
          description: EN 16931 BT-13.
        contract_reference:
          type:
          - string
          - 'null'
          description: EN 16931 BT-12.
        project_reference:
          type:
          - string
          - 'null'
          description: EN 16931 BT-11.
        invoicing_period:
          anyOf:
          - $ref: '#/components/schemas/InvoicingPeriod'
          - type: 'null'
        preceding_invoice:
          anyOf:
          - $ref: '#/components/schemas/BillingReference'
          - type: 'null'
        buyer_name:
          type:
          - string
          - 'null'
          description: EN 16931 BT-44 — buyer name. Defaults to the partner's company/full name.
        buyer_legal_registration_id:
          type:
          - string
          - 'null'
          description: EN 16931 BT-47.
        buyer_identifier:
          type:
          - string
          - 'null'
          description: EN 16931 BT-46 — party identifier (GLN, DUNS, etc.).
        buyer_vat_id:
          type:
          - string
          - 'null'
          description: EN 16931 BT-48.
        buyer_vat_id_type:
          type:
          - string
          - 'null'
          description: 'EN 16931 VAT scheme of `buyer_vat_id` — the identifier registry `schema_key` (e.g.
            `vat_eu`, `vat_gb`). Defaulted from the partner''s VAT-class identifier when omitted.

            '
        buyer_contact:
          anyOf:
          - $ref: '#/components/schemas/PartyContact'
          - type: 'null'
        buyer_electronic_address:
          anyOf:
          - $ref: '#/components/schemas/ElectronicAddress'
          - type: 'null'
        payee:
          anyOf:
          - $ref: '#/components/schemas/Payee'
          - type: 'null'
        payment_means_code:
          type:
          - string
          - 'null'
          description: EN 16931 BT-81 (UNCL 4461). Defaults from tenant config.
        payment_terms_text:
          type:
          - string
          - 'null'
          description: EN 16931 BT-20.
        payment_terms:
          anyOf:
          - $ref: '#/components/schemas/PaymentTerms'
          - type: 'null'
          description: 'Structured skonto / late-payment block. Additive to BT-20

            `payment_terms_text` — both render when supplied.

            '
        remittance_information:
          type:
          - string
          - 'null'
          description: EN 16931 BT-83 — payment reference / Verwendungszweck.
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/LineItemCreate'
        notes:
          type: array
          items:
            type: string
          default: []
          description: 'EN 16931 BT-22 — invoice notes (0..n). Each entry is rendered as

            its own paragraph below the totals. The author handles

            translation; the renderer prints what is supplied.

            '
        type_id:
          type:
          - string
          - 'null'
        schema_version:
          type:
          - string
          - 'null'
        data:
          type: object
          additionalProperties: true
          default: {}
        addresses:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/InvoiceAddressInput'
          description: 'Optional. When omitted, the server snapshots the partner''s preferred

            addresses for each slot declared by the invoice type. When supplied,

            the slot keys must be a subset of the invoice type''s `address_slots`.

            '
    InvoiceUpdate:
      type: object
      description: 'Partial header update. While the invoice is in `draft` any of the

        fields below may be patched; once `status` leaves `draft`, only the

        status itself can be stepped further (finalized → paid / overdue /

        cancelled, etc.) — all other fields are frozen.

        '
      required: []
      properties:
        business_partner:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
        company:
          oneOf:
          - $ref: '#/components/schemas/ReferenceValue'
          - type: 'null'
          description: 'Re-pick the issuing Company. Validated against the tenant''s

            per-doc-type allowed list. Only editable while the invoice is

            in draft. Per the no-bare-id rule the wire shape is the full

            Reference object.

            '
        channel:
          anyOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: 'Re-pick the sales channel, or null to clear. Only editable while

            the invoice is in draft.

            '
        issue_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
        dunning_level:
          type: integer
          minimum: 0
          description: Dunning escalation level (0 = none, 1..N = reminder ladder step).
        tax_point_date:
          type: string
          format: date
        actual_delivery_date:
          type: string
          format: date
        document_type_code:
          type: string
          enum:
          - '380'
          - '381'
          - '384'
          - '386'
          - '326'
        currency:
          type: string
          minLength: 3
          maxLength: 3
        buyer_reference:
          type: string
        purchase_order_reference:
          type: string
        contract_reference:
          type: string
        project_reference:
          type: string
        invoicing_period:
          $ref: '#/components/schemas/InvoicingPeriod'
        preceding_invoice:
          $ref: '#/components/schemas/BillingReference'
        buyer_name:
          type: string
        buyer_legal_registration_id:
          type: string
        buyer_identifier:
          type: string
        buyer_vat_id:
          type: string
        buyer_vat_id_type:
          type: string
          description: 'EN 16931 VAT scheme of `buyer_vat_id` — the identifier registry `schema_key` (e.g.
            `vat_eu`, `vat_gb`).

            '
        buyer_contact:
          $ref: '#/components/schemas/PartyContact'
        buyer_electronic_address:
          $ref: '#/components/schemas/ElectronicAddress'
        payee:
          $ref: '#/components/schemas/Payee'
        payment_means_code:
          type: string
        payment_terms_text:
          type: string
        payment_terms:
          anyOf:
          - $ref: '#/components/schemas/PaymentTerms'
          - type: 'null'
        remittance_information:
          type: string
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/LineItemCreate'
        notes:
          type:
          - array
          - 'null'
          items:
            type: string
          description: 'Replaces the persisted list. `null` / omitted = no change; `[]`

            clears the field.

            '
        status:
          type: string
          enum:
          - draft
          - finalized
          - paid
          - overdue
          - cancelled
        type_id:
          type:
          - string
          - 'null'
        schema_version:
          type:
          - string
          - 'null'
        data:
          type: object
          additionalProperties: true
        addresses:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/InvoiceAddressInput'
    DocumentType:
      type: string
      enum:
      - invoice
      - delivery_slip
      - offer
      - pro_forma_invoice
      - intermediate_receipt
      description: 'The kind of business document this document represents.

        MVP only renders PDFs for `invoice`; the other values are reserved

        for upcoming templates.

        '
    DocumentStatus:
      type: string
      enum:
      - draft
      - pdf_pending
      - pdf_ready
      - pdf_failed
      description: 'Lifecycle state. New documents start at `pdf_pending`; the

        async PDF worker promotes them to `pdf_ready` (or `pdf_failed`

        on permanent error). `draft` is reserved for future use.

        '
    DocumentResponse:
      type: object
      required:
      - _id
      - _class
      - invoice_id
      - document_type
      - status
      - file
      - template_version
      - created_at
      - updated_at
      - tenant
      properties:
        _id:
          type: string
          example: doc_a1b2c3d4e5f6789012345
        _class:
          type: string
          example: document
        invoice_id:
          type: string
          example: inv_a1b2c3d4e5f6
        document_type:
          $ref: '#/components/schemas/DocumentType'
        status:
          $ref: '#/components/schemas/DocumentStatus'
        file:
          oneOf:
          - $ref: '#/components/schemas/Reference'
          - type: 'null'
          description: 'Reference to the rendered PDF. `null` until the document-generator worker reports
            the output via `PATCH .../documents/{id}`.

            '
        template_version:
          type: string
          example: 1.0.0
        created_at:
          type: integer
        updated_at:
          type: integer
        tenant:
          $ref: '#/components/schemas/Reference'
    DocumentCreate:
      type: object
      required:
      - document_type
      properties:
        document_type:
          $ref: '#/components/schemas/DocumentType'
    FileStatus:
      type: string
      enum:
      - pending
      - ready
      - quarantined
      - failed
      - soft_deleted
      description: 'Lifecycle state of a file. `pending` is set when the upload URL is

        issued; the S3 ObjectCreated event handler promotes to `ready` after

        validation, or to `failed`/`quarantined` if validation or malware

        scanning rejects the object.

        '
    FileResponse:
      type: object
      required:
      - _id
      - _class
      - _name
      - tenant
      - parent
      - type
      - file_name
      - file_size
      - mime_type
      - sha256
      - status
      - retention_class
      - legal_hold
      - description
      - href
      - created_at
      - updated_at
      - created_by
      properties:
        _id:
          type: string
          example: fil_a1b2c3d4e5f6
        _class:
          type: string
          enum:
          - file
        _name:
          $ref: '#/components/schemas/EntityName'
          example: invoice.pdf
        tenant:
          $ref: '#/components/schemas/ReferenceValue'
        parent:
          $ref: '#/components/schemas/ReferenceValue'
        type:
          type: string
          example: business_partner.avatar
        file_name:
          type: string
        file_size:
          type: integer
        mime_type:
          type: string
        sha256:
          type: string
          nullable: true
          description: Hex-encoded SHA-256, populated once `status=ready`.
        status:
          $ref: '#/components/schemas/FileStatus'
        retention_class:
          type: string
          enum:
          - transient
          - standard
          - archival
        legal_hold:
          type: boolean
        description:
          type: string
          nullable: true
        href:
          type: string
          format: uri
          description: Self-link to the download endpoint.
        created_at:
          type: integer
        updated_at:
          type: integer
        created_by:
          allOf:
          - $ref: '#/components/schemas/ReferenceValue'
          nullable: true
    PaginatedFiles:
      type: object
      required:
      - items
      - next_token
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FileResponse'
        next_token:
          type:
          - string
          - 'null'
    FileCreateRequest:
      type: object
      required:
      - parent
      - type
      - file_name
      - file_size
      - mime_type
      properties:
        parent:
          $ref: '#/components/schemas/ReferenceValue'
        type:
          type: string
          example: business_partner.avatar
        file_name:
          type: string
        file_size:
          type: integer
          minimum: 1
        mime_type:
          type: string
          example: image/png
        description:
          type: string
          nullable: true
    PresignedPost:
      type: object
      required:
      - url
      - fields
      - expires_at
      description: 'S3 presigned-POST form. The client must POST `multipart/form-data`

        with every field from `fields` plus a final `file` part containing

        the binary body.

        '
      properties:
        url:
          type: string
          format: uri
        fields:
          type: object
          additionalProperties:
            type: string
        expires_at:
          type: integer
          description: Unix epoch seconds when the URL expires.
    FileCreateResponse:
      allOf:
      - $ref: '#/components/schemas/FileResponse'
      - type: object
        required:
        - upload
        properties:
          upload:
            $ref: '#/components/schemas/PresignedPost'
    FileDownloadResponse:
      type: object
      required:
      - url
      - expires_at
      properties:
        url:
          type: string
          format: uri
        expires_at:
          type: integer
    IdentifierSchema:
      type: object
      required:
      - schema_key
      - display_name
      - applicable_entity_classes
      - is_vat_scheme
      - has_validator
      properties:
        schema_key:
          type: string
          example: vat_eu
        display_name:
          type: object
          additionalProperties:
            type: string
          example:
            en: VAT ID (EU)
        applicable_entity_classes:
          type: array
          items:
            type: string
          example:
          - company
          - business_partner
        is_vat_scheme:
          type: boolean
          example: true
        has_validator:
          type: boolean
          example: true
        recommended:
          type: boolean
          description: 'Whether the schema is recommended for the request, sorted first. Country-agnostic
            schemes (e.g. `gln`) are recommended regardless of country; country-specific VAT schemes are
            recommended only when they match the supplied `country` (so a VAT scheme is never recommended
            when `country` is omitted). Every scheme stays selectable — a company may hold a foreign VAT
            registration.

            '
          example: true
    CodelistHeader:
      type: object
      required:
      - _id
      - _class
      - _name
      - namespace
      - version
      - scope
      - status
      - display_name
      - loading
      properties:
        _id:
          type: string
          example: cl_iso_3166_1_2020
        _class:
          type: string
          example: codelist
        _name:
          $ref: '#/components/schemas/EntityName'
          example: iso_3166_1
        namespace:
          type: string
          example: iso/3166-1
        version:
          type: string
          example: '2020'
        authority:
          type: string
        industry:
          type: array
          items:
            type: string
        scope:
          type: string
          enum:
          - global
          - tenant
        display_name:
          type: object
          additionalProperties:
            type: string
        description:
          type: object
          additionalProperties:
            type: string
        loading:
          type: string
          enum:
          - eager
          - lazy
        status:
          type: string
          enum:
          - draft
          - active
          - deprecated
        is_stub:
          type: boolean
        created_at:
          type: integer
        updated_at:
          type: integer
    CodelistCatalogResponse:
      type: object
      required:
      - items
      - count
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CodelistHeader'
        count:
          type: integer
    CodeValueResponse:
      type: object
      required:
      - _id
      - _class
      - namespace
      - version
      - code
      - status
      - display_name
      - codelist
      properties:
        _id:
          type: string
          example: EUR
        _class:
          type: string
          example: code_value
        codelist:
          $ref: '#/components/schemas/ReferenceValue'
        namespace:
          type: string
        version:
          type: string
        code:
          type: string
        display_name:
          type: object
          additionalProperties:
            type: string
        status:
          type: string
          enum:
          - active
          - deprecated
          - retired
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: integer
        updated_at:
          type: integer
    CodeValuesResponse:
      type: object
      required:
      - items
      - count
      - total
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CodeValueResponse'
        count:
          type: integer
        total:
          type: integer
    MigrationResponse:
      type: object
      required:
      - from
      - chain
      - terminal
      properties:
        from:
          type: object
          required:
          - namespace
          - version
          - code
          properties:
            namespace:
              type: string
            version:
              type: string
            code:
              type: string
        chain:
          type: array
          items:
            $ref: '#/components/schemas/CodeValueResponse'
        terminal:
          $ref: '#/components/schemas/CodeValueResponse'
    ValidateRequest:
      type: object
      required:
      - namespace
      - code
      properties:
        namespace:
          type: string
        version:
          type: string
          default: latest
        code:
          type: string
        locale:
          type: string
          default: en
    ValidateResponse:
      type: object
      required:
      - valid
      - namespace
      - version
      - code
      properties:
        valid:
          type: boolean
        namespace:
          type: string
        version:
          type: string
        code:
          type: string
    CodelistCreateRequest:
      type: object
      required:
      - slug
      - codelist
      properties:
        slug:
          type: string
          example: my_categories
        codelist:
          type: object
          required:
          - version
          properties:
            version:
              type: string
            published_at:
              type: string
              format: date
            authority:
              type: string
            industry:
              type: array
              items:
                type: string
            display_name:
              type: object
              additionalProperties:
                type: string
            description:
              type: object
              additionalProperties:
                type: string
            licence:
              type: string
            attribution:
              type: string
    CodelistUpdateRequest:
      type: object
      required:
      - version
      properties:
        version:
          type: string
        published_at:
          type: string
          format: date
        authority:
          type: string
        industry:
          type: array
          items:
            type: string
        display_name:
          type: object
          additionalProperties:
            type: string
        description:
          type: object
          additionalProperties:
            type: string
        licence:
          type: string
        attribution:
          type: string
        status:
          type: string
          enum:
          - draft
          - active
          - deprecated
    CodeValueCreateRequest:
      type: object
      required:
      - code
      properties:
        code:
          type: string
        display_name:
          type: object
          additionalProperties:
            type: string
        description:
          type: object
          additionalProperties:
            type: string
        parent_code:
          type: string
        valid_from:
          type: string
          format: date
        valid_to:
          type: string
          format: date
        metadata:
          type: object
          additionalProperties: true
    CodeValueUpdateRequest:
      type: object
      properties:
        display_name:
          type: object
          additionalProperties:
            type: string
        description:
          type: object
          additionalProperties:
            type: string
        parent_code:
          type: string
        valid_from:
          type: string
          format: date
        valid_to:
          type: string
          format: date
        status:
          type: string
          enum:
          - active
          - deprecated
          - retired
        replaced_by_code:
          type: string
        metadata:
          type: object
          additionalProperties: true
    DeprecateRequest:
      type: object
      properties:
        replaced_by_code:
          type: string
  responses:
    Conflict:
      description: Resource already exists or conflict with current state
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableEntity:
      description: 'Request body failed validation. When `error` is `"validation_error"`, `details.errors`
        is an array of per-field failures (see `ValidationErrorDetails`). Other `error` codes (e.g. `"identifier_validation_error"`)
        carry domain-specific context in `details`.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  parameters:
    UserIdParam:
      name: user_id
      in: path
      required: true
      schema:
        type: string
        example: usr_a1b2c3d4e5f6
    LimitParam:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    NextTokenParam:
      name: next_token
      in: query
      required: false
      schema:
        type: string
        description: Base64-encoded pagination cursor from previous response
    PartnerTypeParam:
      name: partner_type
      in: query
      required: false
      schema:
        type: string
        enum:
        - person
        - company
    StatusParam:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
        - active
        - inactive
    SearchQueryParam:
      name: q
      in: query
      required: false
      description: 'Full-text search term. When provided, the endpoint delegates to the

        shared search index (matches across name/title/email/identifier

        fields, tenant-scoped) and returns the matching entities in the

        same response shape as the unfiltered list. Pagination (`limit`,

        `next_token`) is not honoured in this mode; results are capped at

        the search index size limit (20 items in MVP).

        '
      schema:
        type: string
        minLength: 1
    BusinessPartnerIdParam:
      name: business_partner_id
      in: path
      required: true
      schema:
        type: string
        example: bp_a1b2c3d4e5f6
    AddressIdParam:
      name: address_id
      in: path
      required: true
      schema:
        type: string
        example: addr_a1b2c3d4e5f6
x-tagGroups:
- name: Getting started
  tags:
  - Auth
  - API Keys
- name: Core records
  tags:
  - Business Partners
  - Addresses
  - Products
  - Invoices
  - Documents
  - Projects
  - CustomObjects
  - Files
- name: Reference data
  tags:
  - Codelists
  - Identifiers
- name: Staying in sync
  tags:
  - Changes
