openapi: 3.1.0
info:
  title: AZDAN E-Invoicing — Tenant API
  version: 0.1.0
  description: >-
    Every API an AZDAN tenant can use: the **Tenant Console API** (what the portal does on a signed-in user's behalf)
    and the **Integration API** (ERP and system-to-system access with an API key), plus sign-in, multi-factor
    authentication and account activation.


    This reference is generated from the running platform — its routes and validation rules — and checked on every
    build, so it lists exactly the endpoints that exist, with the request rules the server enforces.


    ## Environments

    | Environment | Base URL | Peppol network |

    |---|---|---|

    | Development | `https://peppol-dev.azdanapps.com` | Peppol test network |

    | Production | `https://peppol.azdanapps.com` | Peppol production network |


    Paths and contracts are identical in both environments. Integration requests choose the Peppol network with
    `environment` (`test` | `production`).


    ## Authentication

    **Integration API — API key.** Create a key in the Tenant Console (*API Keys*) and send it as `Authorization: Bearer
    azd_live_…` or `azd_test_…`. Each operation lists the scopes it accepts in `x-azdan-scopes`; any one of them is
    enough. Use **Authorize → ApiKeyBearer** to try these operations here.


    **Tenant Console API — user session.** `POST /api/auth/login` returns a session token (and sets the `azdan_session`
    cookie). Send it as `Authorization: Bearer <token>`, or rely on the cookie and add `X-CSRF-Token` (the `azdan_csrf`
    cookie value) on POST, PUT, PATCH and DELETE. Sessions last 8 hours.


    **Multi-factor authentication is mandatory.** An account with MFA signs in in two steps: `POST /api/auth/login`
    returns `{ mfaRequired: true, ticket }`, and `POST /api/auth/mfa/verify` exchanges the ticket and a 6-digit code for
    the session. An account without MFA can only set it up (`/api/auth/mfa/status`, `/setup`, `/enable`, `/api/auth/me`)
    until it is enabled; every other call answers `403 MFA_ENROLLMENT_REQUIRED`.


    ## Roles

    `tenant_admin` manages the tenant: users, API keys, webhooks, company and participant data, EmaraTax linkage.
    `tenant_user` works with transactions, documents and reports, limited to the companies and participants assigned to
    them. Every response is scoped to the caller's own tenant.


    ## Errors

    Errors share one shape: `{ "error": { "code", "message", "details?", "issues?" } }`. A request that fails validation
    answers `400 VALIDATION`, with each failed field in `issues`. Codes are stable and safe to branch on; messages are
    written to be shown to users.


    ## Rate limits

    Requests are rate-limited per API key or per client. A limited request answers `429` with `RateLimit-*` and
    `Retry-After` headers; retry after the indicated time.


    ## Conventions

    JSON in UTF-8; times in ISO 8601 UTC. Identifiers in the examples are shortened and masked (`txn_8F2K…`,
    `1004****59`).
  contact:
    name: AZDAN e-Invoicing support
    email: peppol@azdan.com
servers:
  - url: https://peppol-dev.azdanapps.com
    description: Development
  - url: https://peppol.azdanapps.com
    description: Production
tags:
  - name: Authentication
    description: Sign-in, session, current principal and multi-factor authentication for both portals.
  - name: Account activation
    description: 'One-time activation links sent to new users: validate the link, then set a password.'
  - name: Dashboard & compliance
    description: Tenant home-screen figures, analytics and the compliance score.
  - name: Transactions
    description: >-
      Outbound and inbound Peppol transactions of the signed-in tenant: list, detail (status dimensions, journey,
      documents), retry of a failed transaction, and the access point's own transaction log.
  - name: Documents & files
    description: Stored files of a transaction (invoice XML/PDF, SBD, TDD, MLS) and short-lived download URLs.
  - name: Invoice sending
    description: Validate and send invoices and credit notes from the portal (session authenticated).
  - name: Reports
    description: Submission, reconciliation, exception-aging, TDD and VAT reports.
  - name: Webhooks
    description: Webhook endpoints, delivery history, test, replay and signing-secret rotation.
  - name: Users & API keys
    description: Tenant users and integration API keys. Secrets are shown once, on creation.
  - name: Company & participants
    description: Subsidiaries (companies), their Peppol participants and onboarding companies.
  - name: Master data
    description: Customers, items and company profiles used when composing documents.
  - name: Mappings
    description: 'ERP-to-PINT field mappings: drafts, samples, simulation, activation and rollback.'
  - name: EmaraTax (FTA)
    description: Linking, re-verifying and offboarding taxpayers through EmaraTax (UAE FTA).
  - name: Settings & integrations
    description: Notification settings, tenant preferences, NetSuite connection and the audit log.
  - name: Integration API · Peppol
    description: >-
      The public, API-key authenticated Integration API: validate, submit and track Peppol documents. Specified in full
      by the public Integration API reference (/docs).
  - name: Integration API · Documents
    description: API-key document downloads for integrations.
x-tagGroups:
  - name: Getting started
    tags:
      - Authentication
      - Account activation
  - name: Tenant Console API (session)
    tags:
      - Dashboard & compliance
      - Transactions
      - Documents & files
      - Invoice sending
      - Reports
      - Webhooks
      - Users & API keys
      - Company & participants
      - Master data
      - Mappings
      - EmaraTax (FTA)
      - Settings & integrations
  - name: Integration API (API key)
    tags:
      - Integration API · Peppol
      - Integration API · Documents
paths:
  /api/auth/login:
    post:
      operationId: postAuthLogin
      summary: Sign in with email and password
      description: >-
        Step 1 of portal sign-in. Looks the user up by lower-cased email and accepts only `status: active` accounts
        whose bcrypt hash matches.


        - **Portal matrix** — with `portal: "tenant"` a `super_admin` is refused, with `portal: "admin"` every
        non-super-admin is refused (`403 WRONG_PORTAL`). Without `portal` no matrix is applied.

        - **Suspended tenant** — after the password matched, a tenant principal whose tenant is not active gets `403
        TENANT_SUSPENDED`.

        - **MFA enabled** — no session is issued. The response is `{ mfaRequired: true, ticket, methods }`: `ticket` is
        an HS256 JWT with audience `azdan-mfa` and `typ: mfa_ticket`, valid `MFA_TICKET_TTL_S` (default 300 s),
        redeemable only at `POST /api/auth/mfa/verify`; it is never accepted as a session.

        - **MFA not enabled** — `lastLoginAt` is updated, a session JWT is signed (claims `sub`, `role`, `tenantId`,
        `email`, `tv` token version, `scope` `admin`|`tenant`; expiry `JWT_EXPIRES_IN`, default 8 h), set as the
        HTTP-only `azdan_session` cookie and returned as `token`.

        - **Mandatory MFA** (`AUTH_MFA_REQUIRED`, default on) — on that branch the response says `mfaEnrollmentRequired:
        true`: the session can call only `GET /api/auth/me`, `GET /api/auth/mfa/status`, `POST /api/auth/mfa/setup` and
        `POST /api/auth/mfa/enable`; every other route answers `403 MFA_ENROLLMENT_REQUIRED` until MFA is enabled. The
        portals send the user straight to MFA setup.


        Every attempt is audited (`auth.login`, success/failure/denied). The readable `azdan_csrf` cookie is **not** set
        here; it is issued by the first safe (`GET`) request to an `/api/*` route behind the CSRF middleware.


        **Rate limit:** per IP, `RATE_LIMIT_LOGIN_MAX` (default 10) per `RATE_LIMIT_LOGIN_WINDOW_MS` (default 60 s), on
        top of the global `/api` limit. In demo mode (`USE_MOCK_DATA=true`) only built-in demo principals are accepted
        and none carries MFA.


        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                  maxLength: 200
                captchaId:
                  type: string
                  maxLength: 64
                captchaAnswer:
                  type: string
                  maxLength: 16
                portal:
                  type: string
                  enum:
                    - admin
                    - tenant
              required:
                - email
                - password
            example:
              email: o***@example.com
              password: ••••••••••••
              portal: tenant
      responses:
        '200':
          description: >-
            Signed in (session cookie set), or an MFA challenge when the account has MFA enabled (no cookie). The
            example is the session form; the challenge form is `{ "mfaRequired": true, "ticket": "eyJhbGciOi…",
            "methods": ["totp", "recovery_code"] }`.
          headers:
            Set-Cookie:
              description: >-
                `azdan_session=<jwt>; Path=/; HttpOnly; SameSite=Lax` (+ `Secure` in production). Name, domain,
                SameSite, Secure and max-age come from `AUTH_COOKIE_*` (max-age default 8 h).
              schema:
                type: string
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required:
                      - token
                      - principal
                      - mfaEnabled
                    properties:
                      token:
                        type: string
                        description: Session JWT (same value as the cookie).
                      principal:
                        type: object
                        properties:
                          sub:
                            type: string
                          role:
                            type: string
                            enum:
                              - super_admin
                              - tenant_admin
                              - tenant_user
                          tenantId:
                            type:
                              - string
                              - 'null'
                          email:
                            type: string
                      mfaEnabled:
                        type: boolean
                        description: Always false on this branch.
                      mfaEnrollmentRequired:
                        type: boolean
                        description: 'Mandatory MFA: true when this session may only enrol (MFA setup) until MFA is enabled.'
                  - type: object
                    required:
                      - mfaRequired
                      - ticket
                      - methods
                    properties:
                      mfaRequired:
                        const: true
                      ticket:
                        type: string
                        description: Short-lived MFA ticket for POST /api/auth/mfa/verify.
                      methods:
                        type: array
                        items:
                          type: string
                          enum:
                            - totp
                            - recovery_code
              example:
                token: eyJhbGciOi…
                principal:
                  sub: user_4f1c…
                  role: tenant_admin
                  tenantId: tnt_7Q2X…
                  email: o***@example.com
                mfaEnabled: false
                mfaEnrollmentRequired: true
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          description: Unknown email, wrong password, or the account is not active.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Invalid credentials
        '403':
          description: Wrong portal for this role (`WRONG_PORTAL`), or the tenant is suspended (`TENANT_SUSPENDED`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: WRONG_PORTAL
                  message: Super Admin accounts must sign in via the Admin Portal.
        '429':
          description: >-
            Rate limit exceeded (login, per IP). Answered by express-rate-limit's default handler: plain-text body `Too
            many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/mfa/verify:
    post:
      operationId: postAuthMfaVerify
      summary: Complete sign-in with an MFA code
      description: >-
        Step 2 of sign-in for MFA-enabled accounts and the only place their session is minted. Verifies the MFA ticket
        from `POST /api/auth/login` (audience `azdan-mfa`, `typ: mfa_ticket`), re-checks that the user is still active
        and MFA-enabled and that the tenant is not suspended, then accepts either:


        - a 6-digit TOTP code for the active secret, or

        - one of the single-use recovery codes (the matched hash is removed).


        On success `lastLoginAt` is updated, the `azdan_session` cookie is set and the token returned. Each attempt is
        audited (`auth.mfa.challenge`).


        **Rate limit:** per IP, `RATE_LIMIT_MFA_MAX` (default 5) per `RATE_LIMIT_MFA_WINDOW_MS` (default 5 min) —
        tighter than login, because the code space is 6 digits.


        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ticket:
                  type: string
                code:
                  type: string
            example:
              ticket: eyJhbGciOi…
              code: '492817'
      responses:
        '200':
          description: Signed in. Session cookie set.
          headers:
            Set-Cookie:
              description: >-
                `azdan_session=<jwt>; Path=/; HttpOnly; SameSite=Lax` (+ `Secure` in production). Name, domain,
                SameSite, Secure and max-age come from `AUTH_COOKIE_*` (max-age default 8 h).
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  principal:
                    type: object
                    properties:
                      sub:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      email:
                        type: string
              example:
                token: eyJhbGciOi…
                principal:
                  sub: user_4f1c…
                  role: tenant_admin
                  tenantId: tnt_7Q2X…
                  email: o***@example.com
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          description: Ticket expired or invalid, MFA not active for the account, account no longer active, or wrong code.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Invalid verification code
        '403':
          description: The tenant was suspended during the ticket's lifetime.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: TENANT_SUSPENDED
                  message: >-
                    Your organisation's AZDAN account is suspended. Contact AZDAN at sales@azdan.com to request
                    reactivation.
        '429':
          description: >-
            Rate limit exceeded (MFA verify, per IP). Answered by express-rate-limit's default handler: plain-text body
            `Too many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/register:
    post:
      operationId: postAuthRegister
      summary: create a free AZDAN account (self-registration, Flow A)
      description: |-
        create a free AZDAN account (self-registration, Flow A).

        Creates an ACTIVE tenant on the Free plan of the public price list, its administrator
        (invited until the email address is verified) and the Service Agreement receipt, and emails
        a single-use 24-hour verification link. The reply is `{ ok: true }` whether or not the
        address was already known (an existing account is told by email). `website` is a honeypot;
        `captchaToken` is checked when CAPTCHA_SECRET_KEY is set. Rate-limited per IP and globally.

        **Access:** Public — no credentials.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  minLength: 3
                  maxLength: 254
                password:
                  type: string
                  minLength: 1
                  maxLength: 200
                displayName:
                  type: string
                  minLength: 1
                  maxLength: 120
                companyName:
                  type: string
                  minLength: 1
                  maxLength: 200
                phone:
                  type: string
                  maxLength: 32
                locale:
                  type: string
                  enum:
                    - en
                    - ar
                termsAccepted:
                  type: boolean
                termsVersion:
                  type: string
                  maxLength: 16
                website:
                  type: string
                  maxLength: 200
                captchaToken:
                  type: string
                  maxLength: 4096
              required:
                - email
                - password
                - displayName
                - companyName
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/register/validate:
    post:
      operationId: postAuthRegisterValidate
      summary: 'the verification page: who is verifying, or why the link cannot be used'
      description: |-
        the verification page: who is verifying, or why the link cannot be used.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  minLength: 20
                  maxLength: 200
              required:
                - token
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/register/verify:
    post:
      operationId: postAuthRegisterVerify
      summary: verify the email address; the link stops working
      description: |-
        verify the email address; the link stops working.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  minLength: 20
                  maxLength: 200
              required:
                - token
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/captcha:
    get:
      operationId: getAuthCaptcha
      summary: a new image CAPTCHA for a public form that sends email on request
      description: |-
        a new image CAPTCHA for a public form that sends email on request.

        Returns `{ captchaId, image, expiresInSeconds }`; `image` is a PNG data URL. The form sends
        `captchaId` and the visitor's `captchaAnswer` back. One attempt per image.

        **Access:** Public — no credentials. Rate-limited per IP and globally in app.js.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/register/resend:
    post:
      operationId: postAuthRegisterResend
      summary: a fresh verification link, at most one a minute
      description: |-
        a fresh verification link, at most one a minute. Needs a
        correct image CAPTCHA (`captchaId`, `captchaAnswer`, purpose `resend`).

        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/reactivation-request:
    post:
      operationId: postAuthReactivationRequest
      summary: a suspended customer asks AZDAN to reactivate the account (public, rate-limited in app.js)
      description: |-
        a suspended customer asks AZDAN to reactivate the
        account (public, rate-limited in app.js). Records a lifecycle event and tells sales; the
        account is NOT reactivated here. The reply is the same for every address. Needs a correct
        image CAPTCHA (`captchaId`, `captchaAnswer`, purpose `reactivation`).

        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/password-reset/request:
    post:
      operationId: postAuthPasswordResetRequest
      summary: email a reset link to a Tenant Console account
      description: |-
        email a reset link to a Tenant Console account.

        Needs a correct image CAPTCHA (`captchaId`, `captchaAnswer`, purpose `password_reset`). The
        reply is `{ ok: true }` for every address, and the email goes out after the reply, so neither
        the answer nor its timing says whether an account exists.

        **Access:** Public — no credentials.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/password-reset/validate:
    post:
      operationId: postAuthPasswordResetValidate
      summary: 'the reset page: whose password, or why not'
      description: |-
        the reset page: whose password, or why not.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  minLength: 20
                  maxLength: 200
              required:
                - token
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/password-reset/complete:
    post:
      operationId: postAuthPasswordResetComplete
      summary: set the new password
      description: |-
        set the new password. The link stops working, every
        session of the user is revoked and nobody is signed in: MFA applies at the next sign-in.

        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/activation/validate:
    post:
      operationId: postAuthActivationValidate
      summary: Check an account activation link
      description: >-
        Called by the activation page before it shows the set-password form. The token from the emailed link
        (`/portal/activate?token=…`) is hashed (SHA-256) and matched against the user's stored hash; only the hash is
        ever stored.


        Valid when the user is still `invited`, the link has not been used and has not expired
        (`ACTIVATION_TOKEN_TTL_HOURS`, default 72 h). Issuing a new link replaces the hash, so older links stop working.
        Nothing is changed by this call. `Cache-Control: no-store`.


        **Rate limit:** per IP, same budget as login (`RATE_LIMIT_LOGIN_*`, shared prefix `/api/auth/activation`).


        **Access:** Public — no credentials.
      tags:
        - Account activation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  minLength: 20
                  maxLength: 200
              required:
                - token
            example:
              token: q3Vt9yJx…
      responses:
        '200':
          description: The link can be used.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  username:
                    type: string
                  tenantName:
                    type: string
              example:
                valid: true
                username: o***@example.com
                tenantName: Example Trading LLC
        '400':
          description: Unknown token, already used, superseded by a newer link, or the user is no longer `invited`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: ACTIVATION_LINK_INVALID
                  message: This activation link is not valid. It may have been used already, or a newer link was sent.
        '410':
          description: The link has expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: ACTIVATION_LINK_EXPIRED
                  message: This activation link has expired. Ask AZDAN (peppol@azdan.com) to send a new one.
        '429':
          description: >-
            Rate limit exceeded (activation, per IP). Answered by express-rate-limit's default handler: plain-text body
            `Too many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/activation/complete:
    post:
      operationId: postAuthActivationComplete
      summary: Set the password and activate the account
      description: >-
        Re-validates the link exactly like `validate`, then enforces the password policy: at least 12 and at most 200
        characters, letters and at least one digit, and not containing the email's local part (when it is 4+
        characters). The tenant must be active.


        The password is bcrypt-hashed (cost 12) and the user made `active` in one conditional update on the same token
        hash and `invited` status, so two parallel submissions activate once; the token is cleared (single use). Side
        effects: audit `user.activated`; when the user is the tenant's primary administrator, a tenant lifecycle event
        `admin_activated`. No session is created — the user signs in afterwards. `Cache-Control: no-store`.


        **Rate limit:** per IP, same budget as login.


        **Access:** Public — no credentials.
      tags:
        - Account activation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                password:
                  type: string
            example:
              token: q3Vt9yJx…
              password: ••••••••••••••
      responses:
        '200':
          description: Activated. The link no longer works.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  username:
                    type: string
              example:
                ok: true
                username: o***@example.com
        '400':
          description: Invalid link (`ACTIVATION_LINK_INVALID`) or the password fails the policy (`PASSWORD_POLICY`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: PASSWORD_POLICY
                  message: Use at least 12 characters.
        '403':
          description: The tenant is not active.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: TENANT_SUSPENDED
                  message: >-
                    Your organisation's AZDAN account is suspended. Contact AZDAN at sales@azdan.com to request
                    reactivation.
        '410':
          description: The link has expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: ACTIVATION_LINK_EXPIRED
                  message: This activation link has expired. Ask AZDAN (peppol@azdan.com) to send a new one.
        '429':
          description: >-
            Rate limit exceeded (activation, per IP). Answered by express-rate-limit's default handler: plain-text body
            `Too many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/logout:
    post:
      operationId: postAuthLogout
      summary: Sign out (clear the session cookie)
      description: >-
        Clears the `azdan_session` cookie with the same attributes it was set with and answers `{ ok: true }`. No
        authentication is required and nothing is checked or stored server-side: the JWT itself is **not** revoked (its
        `tokenVersion` is not bumped), so a copy of the token stays valid until it expires. The `azdan_csrf` cookie is
        left in place.


        **Access:** Public — no credentials.
      tags:
        - Authentication
      responses:
        '200':
          description: Cookie cleared.
          headers:
            Set-Cookie:
              description: '`azdan_session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`'
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
              example:
                ok: true
        '500':
          $ref: '#/components/responses/ErrInternal'
      security: []
      x-azdan-access: public
  /api/auth/me:
    get:
      operationId: getAuthMe
      summary: Get the signed-in principal
      description: >-
        Returns the principal built from the verified session (Bearer JWT or `azdan_session` cookie) after the account,
        token-version and tenant-suspension checks: `sub`, `role`, `tenantId`, `scope` (`admin` | `tenant`) and `email`
        — never the token version. For tenant principals it adds `tenant` with the tenant's display name (falling back
        to the legal name); that lookup is best-effort and its failure never fails the call. `mfa.enrollmentRequired` is
        true while mandatory MFA still has to be set up — this route stays available then, so the portals can route the
        user to MFA setup. `preferences.timeZone` is the user's display time zone (an IANA name, `null` = the consoles'
        default, Asia/Dubai); best-effort, like `tenant`.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      responses:
        '200':
          description: The current principal.
          content:
            application/json:
              schema:
                type: object
                properties:
                  principal:
                    type: object
                    properties:
                      sub:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      scope:
                        type: string
                      email:
                        type: string
                  tenant:
                    type: object
                    properties:
                      tenantId:
                        type: string
                      displayName:
                        type: string
                  mfa:
                    type: object
                    properties:
                      enrollmentRequired:
                        type: boolean
                  preferences:
                    type: object
                    properties:
                      timeZone:
                        type: string
              example:
                principal:
                  sub: user_4f1c…
                  role: tenant_admin
                  tenantId: tnt_7Q2X…
                  scope: tenant
                  email: o***@example.com
                tenant:
                  tenantId: tnt_7Q2X…
                  displayName: Example Trading LLC
                mfa:
                  enrollmentRequired: false
                preferences:
                  timeZone: Asia/Dubai
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/me/preferences:
    patch:
      operationId: patchAuthMePreferences
      summary: Save the signed-in user's display preferences
      description: >-
        Stores the user's display time zone on the account (`users.preferences.timeZone`): an IANA name such as
        `Asia/Dubai` or `Europe/London`, or `null` for the consoles' default (Asia/Dubai). It changes only how the
        consoles show dates — every timestamp is stored and returned in UTC (ISO-8601). Report periods stay in UAE time.
        In demo mode nothing is stored.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                timeZone:
                  type:
                    - string
                    - 'null'
                  minLength: 1
                  maxLength: 64
              required:
                - timeZone
              additionalProperties: false
            example:
              timeZone: Europe/London
      responses:
        '200':
          description: The saved preferences.
          content:
            application/json:
              schema:
                type: object
                properties:
                  preferences:
                    type: object
                    properties:
                      timeZone:
                        type: string
              example:
                preferences:
                  timeZone: Europe/London
        '400':
          description: Unknown time zone or extra fields.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: BAD_REQUEST
                  message: Invalid request body
        '401':
          description: Not signed in, or the user no longer exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Unknown user
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/me/profile:
    get:
      operationId: getAuthMeProfile
      summary: Get the signed-in user's profile
      description: >-
        The user's own name (`displayName`), contact details (`phone`, `jobTitle` — `users.profile`), account facts
        (e-mail, role, tenant, MFA state, last sign-in, created) and display preferences. Per user; independent of any
        tenant setting. In demo mode the answer is built from the token alone.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      responses:
        '200':
          description: The profile and preferences.
          content:
            application/json:
              schema:
                type: object
                properties:
                  profile:
                    type: object
                    properties:
                      userId:
                        type: string
                      email:
                        type: string
                      displayName:
                        type: string
                      phone:
                        type: string
                      jobTitle:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      mfaEnabled:
                        type: boolean
                      lastLoginAt:
                        type: string
                      createdAt:
                        type: string
                  preferences:
                    type: object
                    properties:
                      timeZone:
                        type: string
              example:
                profile:
                  userId: usr_01J9…
                  email: a***@example.com
                  displayName: Amal Haddad
                  phone: +971 4 123 4567
                  jobTitle: Finance lead
                  role: tenant_admin
                  tenantId: tnt_7Q2X…
                  mfaEnabled: true
                  lastLoginAt: '2026-10-27T08:00:00.000Z'
                  createdAt: '2026-09-01T08:00:00.000Z'
                preferences:
                  timeZone: Asia/Dubai
        '401':
          description: Not signed in, or the user no longer exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Unknown user
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
    patch:
      operationId: patchAuthMeProfile
      summary: Edit the signed-in user's profile
      description: >-
        Changes only the fields sent: `displayName` (1–120 characters), `phone` (digits, spaces, brackets and dashes,
        optional leading `+`; an empty string clears it) and `jobTitle` (up to 80 characters; empty clears it). The
        e-mail and role cannot be changed here. Audited as `user.profile.update`. In demo mode nothing is stored.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName:
                  type: string
                  minLength: 1
                  maxLength: 120
                phone:
                  type: string
                  maxLength: 32
                  pattern: ^(\+?[0-9][0-9 ()-]{5,30})?$
                jobTitle:
                  type: string
                  maxLength: 80
              additionalProperties: false
            example:
              displayName: Amal Haddad
              phone: +971 4 123 4567
              jobTitle: Finance lead
      responses:
        '200':
          description: The saved profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  profile:
                    type: object
                    properties:
                      userId:
                        type: string
                      email:
                        type: string
                      displayName:
                        type: string
                      phone:
                        type: string
                      jobTitle:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      mfaEnabled:
                        type: boolean
                      lastLoginAt:
                        type: string
                      createdAt:
                        type: string
              example:
                profile:
                  userId: usr_01J9…
                  email: a***@example.com
                  displayName: Amal Haddad
                  phone: +971 4 123 4567
                  jobTitle: Finance lead
                  role: tenant_admin
                  tenantId: tnt_7Q2X…
                  mfaEnabled: true
                  lastLoginAt: '2026-10-27T08:00:00.000Z'
                  createdAt: '2026-09-01T08:00:00.000Z'
        '400':
          description: A field is invalid, unknown, or nothing was sent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: BAD_REQUEST
                  message: Invalid request body
        '401':
          description: Not signed in, or the user no longer exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Unknown user
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/me/password:
    post:
      operationId: postAuthMePassword
      summary: Change the signed-in user's own password
      description: >-
        Any signed-in user (every tenant role and `super_admin`) changes their own password with `currentPassword`,
        `newPassword` and the current 6-digit code from their authenticator app (or an unused recovery code) as `code` —
        or in the `X-Step-Up-Code` header, which the consoles send for critical actions.


        Checks, in order: the current password against the bcrypt hash (`400 PASSWORD_INCORRECT`; five wrong passwords
        in fifteen minutes answer `429 PASSWORD_LOCKED`); the policy of account activation — 12 to 200 characters, not
        containing the e-mail's local part — plus not equal to the current password (`400 PASSWORD_POLICY`, the message
        says which rule); then the code, last so a slip does not use one up (verified like a step-up confirmation:
        single use, five wrong codes lock the user out of step-up for ten minutes — `403 STEP_UP_REQUIRED` / `403
        STEP_UP_INVALID` / `429 STEP_UP_LOCKED`, audited as `auth.stepup`).


        On success the new bcrypt hash is stored, `tokenVersion` is bumped so **every other session of the user is
        signed out**, and a fresh session for this caller is minted: set as the `azdan_session` cookie and returned as
        `token` (the consoles' proxies store it in their own session cookie, so the browser stays signed in). Audited as
        `user.password.change` (success and failure, with a reason; never the passwords). Rate-limited per IP like
        `/mfa/verify` (`RATE_LIMIT_MFA_*`). Refused in demo mode.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                currentPassword:
                  type: string
                  minLength: 1
                  maxLength: 200
                newPassword:
                  type: string
                  minLength: 1
                  maxLength: 200
                code:
                  type: string
                  minLength: 6
                  maxLength: 20
              required:
                - currentPassword
                - newPassword
              additionalProperties: false
            example:
              currentPassword: ••••••••••••
              newPassword: ••••••••••••••••
              code: '492817'
      responses:
        '200':
          description: Password changed; other sessions revoked; a fresh session for this caller.
          headers:
            Set-Cookie:
              description: >-
                `azdan_session=<jwt>; Path=/; HttpOnly; SameSite=Lax` (+ `Secure` in production). Name, domain,
                SameSite, Secure and max-age come from `AUTH_COOKIE_*` (max-age default 8 h).
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  sessionsRevoked:
                    type: boolean
                  token:
                    type: string
              example:
                ok: true
                sessionsRevoked: true
                token: eyJhbGciOi…
        '400':
          description: Wrong current password, or the new password breaks the policy (the message says which rule), or demo mode.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: PASSWORD_INCORRECT
                  message: The current password is incorrect.
        '401':
          description: Not signed in, or the user no longer exists or is not active.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Unknown user
        '403':
          description: No code, or a wrong / already-used code.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: STEP_UP_INVALID
                  message: That code is incorrect or has already been used. Enter the current code.
        '429':
          description: >-
            Five wrong codes (`STEP_UP_LOCKED`, ten minutes) or five wrong passwords (`PASSWORD_LOCKED`, fifteen
            minutes).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: PASSWORD_LOCKED
                  message: Too many incorrect passwords. Wait a few minutes and try again.
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/mfa/status:
    get:
      operationId: getAuthMfaStatus
      summary: Get the MFA state of the signed-in user
      description: >-
        `enabled` — MFA is active; `pending` — `POST /api/auth/mfa/setup` was called but the first code was not yet
        confirmed; `enrolledAt` — when MFA was enabled; `required` — MFA is mandatory (`AUTH_MFA_REQUIRED`). Available
        while enrolment is still required. In demo mode the answer is `{ enabled: false, pending: false, supported:
        false }`.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      responses:
        '200':
          description: MFA state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                  pending:
                    type: boolean
                  enrolledAt:
                    type: string
                  supported:
                    type: boolean
                  required:
                    type: boolean
              example:
                enabled: true
                pending: false
                enrolledAt: '2026-09-02T08:14:22.000Z'
                supported: true
                required: true
        '401':
          description: Not signed in, or the user no longer exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: UNAUTHORIZED
                  message: Unknown user
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/mfa/setup:
    post:
      operationId: postAuthMfaSetup
      summary: Start MFA enrolment (generate a TOTP secret)
      description: >-
        Generates a fresh TOTP secret, stores it encrypted (`MFA_ENCRYPTION_KEY`, falling back to a key derived from the
        JWT secret) as a **pending** secret — inactive until confirmed with `POST /api/auth/mfa/enable` — and returns it
        once with the `otpauth://` URL and a server-rendered QR code (SVG data URI). Calling it again replaces the
        pending secret. The issuer in the QR code is `MFA_ISSUER` (default "AZDAN ePeppol"); outside production it names
        the deployment — "AZDAN ePeppol (Dev)", "AZDAN ePeppol (Staging)" — so Authenticator entries for different
        servers are told apart.


        Refused when MFA is already enabled (disable first) and in demo mode. Audited as `auth.mfa.setup`; the secret is
        never logged or audited. No request body.


        This route is mounted before the CSRF middleware, so cookie sessions do not need `X-CSRF-Token` here.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      responses:
        '200':
          description: Pending secret created. Show it once, then confirm with /mfa/enable.
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                  otpauthUrl:
                    type: string
                  qrCodeDataUri:
                    type: string
                  issuer:
                    type: string
                  account:
                    type: string
                  digits:
                    type: integer
                  period:
                    type: integer
              example:
                secret: JBSWY3DP…
                otpauthUrl: >-
                  otpauth://totp/AZDAN%20ePeppol:o***%40example.com?secret=JBSWY3DP…&issuer=AZDAN%20ePeppol&digits=6&period=30
                qrCodeDataUri: data:image/svg+xml;base64,PHN2ZyB4bWxucz0i…
                issuer: AZDAN ePeppol
                account: o***@example.com
                digits: 6
                period: 30
        '400':
          description: MFA is already enabled, or demo mode.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: BAD_REQUEST
                  message: MFA is already enabled. Disable it first to re-enrol.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/mfa/enable:
    post:
      operationId: postAuthMfaEnable
      summary: Confirm the first TOTP code and enable MFA
      description: >-
        Checks `code` against the pending secret from `/mfa/setup`. On success the pending secret becomes the active
        one, `mfaEnabled` is set with `mfaEnrolledAt`, and 8 recovery codes (`xxxxx-xxxxx`, hex) are generated — stored
        only as SHA-256 hashes and returned **once**. Audited as `auth.mfa.enable` (success and failure). Sessions
        already issued stay valid, and the mandatory-MFA enrolment gate lifts on the next request (no new sign-in
        needed). Mounted before the CSRF middleware.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
            example:
              code: '492817'
      responses:
        '200':
          description: MFA enabled. Store the recovery codes now; they are not shown again.
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                  recoveryCodes:
                    type: array
                    items:
                      type: string
              example:
                enabled: true
                recoveryCodes:
                  - 3f9a1-c07be
                  - 8d2e4-51a09
        '400':
          description: >-
            Wrong code, MFA already enabled, no pending enrolment (`Start enrolment with /mfa/setup first`), or demo
            mode.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: BAD_REQUEST
                  message: That code is incorrect. Check your authenticator and try again.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/auth/mfa/disable:
    post:
      operationId: postAuthMfaDisable
      summary: Disable MFA after re-verifying with a code
      description: >-
        Requires a current 6-digit TOTP code or an unused recovery code (a recovery code used here is consumed). On
        success the active and pending secrets, the recovery codes and `mfaEnrolledAt` are cleared. With mandatory MFA
        the user is back to enrolment-only (`403 MFA_ENROLLMENT_REQUIRED` elsewhere) from the next request until MFA is
        set up again. Audited as `auth.mfa.disable` (success and failure). Mounted before the CSRF middleware.


        **Access:** Any signed-in user (portal session).
      tags:
        - Authentication
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
            example:
              code: '492817'
      responses:
        '200':
          description: MFA disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
              example:
                enabled: false
        '400':
          description: Wrong code, MFA not enabled, or demo mode.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: BAD_REQUEST
                  message: Invalid verification code
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: session
  /api/tenant/ap-submit/raw:
    post:
      operationId: postTenantApSubmitRaw
      summary: Submit a raw UBL document with Peppol routing headers
      description: >-
        Machine submit for clients that send the document bytes themselves. Despite the name it is not a blind pipe: the
        body is read into memory (max `AP_SUBMIT_MAX_BYTES`, default 15 MB), bound to the `X-Peppol-*` identities and
        sent through the same pipeline as `POST /api/integrations/peppol/invoices` (PINT-AE validation → persisted
        transaction → AP Core). The tenant comes from the API key, never from the request.


        **Checks, in order:** `Content-Type` is `application/xml`, `text/xml` or `application/octet-stream`;
        `Content-Length` present, non-zero and within the limit; `X-Peppol-Sender` is an active outbound/both
        participant of the tenant in the requested environment (`403 SENDER_NOT_OWNED`); the body is not a pre-built
        StandardBusinessDocument (`422 PREBUILT_SBD_NOT_SUPPORTED`); receiver, document type, process and a 2-letter C1
        country are present; the document type is the PINT-AE Billing Invoice or CreditNote (`400
        UNSUPPORTED_DOC_TYPE`), the process `urn:peppol:bis:billing` (`400 UNSUPPORTED_PROFILE`); `X-Peppol-C1-Country`
        equals the participant's registered C1 country (`400 C1_COUNTRY_MISMATCH`).


        **FTA C5 accreditation paths (test environment only):** UAE TaxData sent to the FTA sandbox receiver
        `0242:000773-100` with the exact TaxData document type and tax-reporting process is forwarded byte-for-byte
        without PINT validation; with `X-Azdan-Accreditation-Test: FTA-C5-TS07 | FTA-C5-TS08` and `X-Payload-SHA256`, an
        invoice missing only the Seller name is forwarded between two participants of the same tenant. Every other shape
        is refused on these paths.


        **Result:** `202` with `ok: true` when AP Core accepted the message; `502` with `ok: false` and a sanitized
        `upstream.report` when AP Core rejected it (the transaction is recorded as failed).


        **Scope:** `transactions:submit`. **Rate limit:** `AP_SUBMIT_RATE_PER_MIN` (default 60) per minute per API-key
        prefix (per IP without a Bearer header), shared with `/api/integrations/peppol/*`. Not CSRF-checked (Bearer
        only).


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).
      tags:
        - Integration API · Peppol
      parameters:
        - name: X-Peppol-Sender
          in: header
          required: true
          description: Sender participant id; an optional `iso6523-actorid-upis::` prefix is stripped.
          schema:
            type: string
            example: 0235:1004****59
        - name: X-Peppol-Receiver
          in: header
          required: true
          description: Receiver participant id.
          schema:
            type: string
            example: 0235:1002****17
        - name: X-Peppol-DocType
          in: header
          required: true
          description: Peppol document type identifier.
          schema:
            type: string
            example: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
        - name: X-Peppol-Process
          in: header
          required: true
          description: Peppol process identifier.
          schema:
            type: string
            example: urn:peppol:bis:billing
        - name: X-Peppol-C1-Country
          in: header
          required: true
          description: ISO 3166-1 alpha-2 country of C1.
          schema:
            type: string
            example: AE
        - name: X-Peppol-Environment
          in: header
          required: false
          description: '`test` (default) or `production` (`prod`/`live` also mean production; anything else is test).'
          schema:
            type: string
            enum:
              - test
              - production
        - name: Content-Length
          in: header
          required: true
          description: Body size in bytes; required, > 0 and ≤ `AP_SUBMIT_MAX_BYTES`.
          schema:
            type: integer
        - name: X-Azdan-Accreditation-Test
          in: header
          required: false
          description: 'FTA C5 accreditation only: exactly `FTA-C5-TS07` or `FTA-C5-TS08`.'
          schema:
            type: string
            enum:
              - FTA-C5-TS07
              - FTA-C5-TS08
        - name: X-Payload-SHA256
          in: header
          required: false
          description: Hex SHA-256 of the body; required with `X-Azdan-Accreditation-Test`.
          schema:
            type: string
        - name: X-Azdan-Test-Run-Id
          in: header
          required: false
          description: Optional C5 evidence label (validated, recorded with the transaction).
          schema:
            type: string
        - name: X-Azdan-Test-Scenario
          in: header
          required: false
          description: Optional C5 evidence label.
          schema:
            type: string
      requestBody:
        required: true
        description: The raw UBL document bytes (UTF-8). Not JSON.
        content:
          application/xml:
            schema:
              type: string
              format: binary
            example: |-
              <?xml version="1.0" encoding="UTF-8"?>
              <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">…</Invoice>
          text/xml:
            schema:
              type: string
              format: binary
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '202':
          description: Validated and accepted by AP Core.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  transactionId:
                    type: string
                  tenantId:
                    type: string
                  status:
                    type: string
                  payloadFormat:
                    type: string
                  as4MessageId:
                    type: string
                  sbdhInstanceId:
                    type: string
                  documents:
                    type: object
                    properties:
                      invoiceXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: string
                          fileName:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                  upstream:
                    type: object
                    properties:
                      status:
                        type: integer
              example:
                ok: true
                transactionId: txn_1789562400000_9f3a…
                tenantId: tnt_7Q2X…
                status: sending
                payloadFormat: raw-ubl
                as4MessageId: f0debc61-215c-4e0a-9d1b-…@peppol.azdanapps.com
                sbdhInstanceId: 6c1d2e3f-4a5b-…
                documents:
                  invoiceXml:
                    available: true
                    fileId: file_01J9Z3…
                    fileName: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a…/files/invoice-xml
                upstream:
                  status: 200
        '400':
          description: >-
            Bad content type, missing/zero/oversized `Content-Length`, missing routing header, unsupported document type
            or process, C1 mismatch, or PINT-AE validation failure.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            message:
                              type: string
                            xpath:
                              type: string
              example:
                error:
                  code: UNSUPPORTED_DOC_TYPE
                  message: >-
                    X-Peppol-DocType 'urn:…' is not a document type this platform validates and submits. Only the
                    PINT-AE Billing Invoice and CreditNote are accepted.
                  details:
                    - code: UNSUPPORTED_DOC_TYPE
                      message: …
                      xpath: ''
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: >-
            The sender is not an active outbound participant of this tenant in that environment, or the key lacks
            `transactions:submit`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                error:
                  code: SENDER_NOT_OWNED
                  message: SENDER_NOT_OWNED
        '422':
          description: A pre-built StandardBusinessDocument was sent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            message:
                              type: string
                            xpath:
                              type: string
              example:
                error:
                  code: PREBUILT_SBD_NOT_SUPPORTED
                  message: >-
                    Pre-built StandardBusinessDocument submissions are not accepted: their routing identities cannot be
                    bound to the validated document. …
                  details:
                    - code: PREBUILT_SBD_NOT_SUPPORTED
                      message: Submit the UBL document without an SBD wrapper.
                      xpath: /
        '429':
          description: >-
            Rate limit exceeded (submit, per API key). Answered by express-rate-limit's default handler: plain-text body
            `Too many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          description: 'AP Core rejected the message. Same shape as 202 with `ok: false`.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  transactionId:
                    type: string
                  tenantId:
                    type: string
                  status:
                    type: string
                  payloadFormat:
                    type: string
                  as4MessageId:
                    type: 'null'
                  sbdhInstanceId:
                    type: 'null'
                  upstream:
                    type: object
                    properties:
                      status:
                        type: integer
                      report:
                        type: object
                        properties:
                          errors:
                            type: array
                            items:
                              type: object
                              properties:
                                code:
                                  type: string
                                message:
                                  type: string
              example:
                ok: false
                transactionId: txn_1789562400000_9f3a…
                tenantId: tnt_7Q2X…
                status: failed
                payloadFormat: raw-ubl
                as4MessageId: null
                sbdhInstanceId: null
                upstream:
                  status: 422
                  report:
                    errors:
                      - code: PEPPOL-EN16931-R001
                        message: …
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
  /api/integrations/peppol/validate:
    post:
      operationId: validateDocument
      summary: Validate a Peppol document
      description: |-
        Dry-run validation. Checks your document against PINT/BIS Schematron
        rules without creating a transaction or contacting the Peppol network.
        Safe to call repeatedly during development.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `peppol:validate`, `peppol:submit`, `transactions:submit`, `transactions:read`
      tags:
        - Integration API · Peppol
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
            examples:
              jsonMode:
                summary: JSON mode — Minimal Tax Invoice (380)
                description: >-
                  The smallest request the API accepts for a UAE tax invoice. Start here and add only what your business
                  case needs.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000005
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        cityName: Dubai
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              xmlMode:
                summary: XML mode — UAE Tax Invoice (380)
                description: >-
                  The standard VAT invoice. Carries all 51 MoF mandatory Tax Invoice fields. The xml value is the UBL
                  2.1 document the API builds for the JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <Invoice
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>INV-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000001</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:DueDate>2026-10-16</cbc:DueDate>
                      <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
                      <cbc:Note>Thank you for your business.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000103</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000203</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:PaymentMeans>
                        <cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
                        <cac:PayeeFinancialAccount><cbc:ID>AE070331234567890123456</cbc:ID></cac:PayeeFinancialAccount>
                      </cac:PaymentMeans>
                      <cac:PaymentTerms>
                        <cbc:Note>Net 30 days</cbc:Note>
                      </cac:PaymentTerms>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1050.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1050.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:InvoiceLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">20.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">120.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:InvoiceLine>
                    </Invoice>
              creditNoteJsonMode:
                summary: JSON mode — UAE Tax Credit Note (381) (381)
                description: >-
                  Reverses a tax invoice. Amounts stay POSITIVE; the document type says it is a credit. Needs a reason
                  code and the original invoice reference.
                value:
                  inputType: json
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: CN-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000002
                    issueDate: '2026-09-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Credit for returned services.
                    creditNoteReasonCode: DL8.61.1.D
                    billingReference:
                      invoiceId: INV-2026-000123
                      issueDate: '2026-09-16'
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              creditNoteXmlMode:
                summary: XML mode — UAE Tax Credit Note (381)
                description: >-
                  Reverses a tax invoice. Amounts stay POSITIVE; the document type says it is a credit. Needs a reason
                  code and the original invoice reference. The xml value is the UBL 2.1 document the API builds for the
                  JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <CreditNote
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CN-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000002</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
                      <cbc:Note>Credit for returned services.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:DiscrepancyResponse>
                        <cbc:ResponseCode>DL8.61.1.D</cbc:ResponseCode>
                      </cac:DiscrepancyResponse>
                      <cac:BillingReference>
                        <cac:InvoiceDocumentReference>
                          <cbc:ID>INV-2026-000123</cbc:ID>
                          <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                        </cac:InvoiceDocumentReference>
                      </cac:BillingReference>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000103</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000203</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1050.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1050.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:CreditNoteLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:CreditedQuantity unitCode="EA">10</cbc:CreditedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">0.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">100.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:CreditNoteLine>
                    </CreditNote>
              commercialInvoiceJsonMode:
                summary: JSON mode — UAE Commercial Invoice (480) (480)
                description: >-
                  Issued when no VAT is charged. Carries all 49 MoF mandatory Commercial Invoice fields: the seller
                  states its TIN instead of a VAT TRN, the buyer its legal registration, and every tax category is E, O
                  or Z.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: commercial
                    invoiceNumber: CINV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000003
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Thank you for your business.
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    paymentTerms:
                      note: Net 30 days
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxRegistrationId: '1000000001'
                      taxSchemeId: TIN
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      legalRegistrationId: CN-7654321
                      legalRegistrationType: CL
                      legalRegistrationAuthorityName: Abu Dhabi Department of Economic Development
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                          grossPriceAmount: '120.00'
                          baseQuantity: '1'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: Z
                          rate: '0'
                    taxTotal:
                      taxAmount: '0.00'
                      taxSubtotals:
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1000.00'
                      payableAmount: '1000.00'
              commercialInvoiceXmlMode:
                summary: XML mode — UAE Commercial Invoice (480)
                description: >-
                  Issued when no VAT is charged. Carries all 49 MoF mandatory Commercial Invoice fields: the seller
                  states its TIN instead of a VAT TRN, the buyer its legal registration, and every tax category is E, O
                  or Z. The xml value is the UBL 2.1 document the API builds for the JSON example of the same name; it
                  passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <Invoice
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CINV-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000003</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:DueDate>2026-10-16</cbc:DueDate>
                      <cbc:InvoiceTypeCode>480</cbc:InvoiceTypeCode>
                      <cbc:Note>Thank you for your business.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>1000000001</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>TIN</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="CL" schemeAgencyName="Abu Dhabi Department of Economic Development">CN-7654321</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:PaymentMeans>
                        <cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
                        <cac:PayeeFinancialAccount><cbc:ID>AE070331234567890123456</cbc:ID></cac:PayeeFinancialAccount>
                      </cac:PaymentMeans>
                      <cac:PaymentTerms>
                        <cbc:Note>Net 30 days</cbc:Note>
                      </cac:PaymentTerms>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1000.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1000.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:InvoiceLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">20.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">120.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:InvoiceLine>
                    </Invoice>
              commercialCreditNoteJsonMode:
                summary: JSON mode — UAE Commercial Credit Note (81) (81)
                description: Reverses a commercial invoice. Same identity rules as 480.
                value:
                  inputType: json
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: commercial
                    invoiceNumber: CCN-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000004
                    issueDate: '2026-09-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Credit for returned goods.
                    creditNoteReasonCode: DL8.61.1.D
                    billingReference:
                      invoiceId: CINV-2026-000123
                      issueDate: '2026-09-16'
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxRegistrationId: '1000000001'
                      taxSchemeId: TIN
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      legalRegistrationId: CN-7654321
                      legalRegistrationType: CL
                      legalRegistrationAuthorityName: Abu Dhabi Department of Economic Development
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: Z
                          rate: '0'
                    taxTotal:
                      taxAmount: '0.00'
                      taxSubtotals:
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1000.00'
                      payableAmount: '1000.00'
              commercialCreditNoteXmlMode:
                summary: XML mode — UAE Commercial Credit Note (81)
                description: >-
                  Reverses a commercial invoice. Same identity rules as 480. The xml value is the UBL 2.1 document the
                  API builds for the JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <CreditNote
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CCN-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000004</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:CreditNoteTypeCode>81</cbc:CreditNoteTypeCode>
                      <cbc:Note>Credit for returned goods.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:DiscrepancyResponse>
                        <cbc:ResponseCode>DL8.61.1.D</cbc:ResponseCode>
                      </cac:DiscrepancyResponse>
                      <cac:BillingReference>
                        <cac:InvoiceDocumentReference>
                          <cbc:ID>CINV-2026-000123</cbc:ID>
                          <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                        </cac:InvoiceDocumentReference>
                      </cac:BillingReference>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>1000000001</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>TIN</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="CL" schemeAgencyName="Abu Dhabi Department of Economic Development">CN-7654321</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1000.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1000.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:CreditNoteLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:CreditedQuantity unitCode="EA">10</cbc:CreditedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">0.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">100.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:CreditNoteLine>
                    </CreditNote>
              uaeScenarioJsonMode:
                summary: JSON mode — Combined scenarios (380)
                description: >-
                  Positions 1, 4, 6 and 8 declared together (mask 10010101): the free-zone beneficiary (BTAE-01), the
                  invoice period (IBG-14), the disclosed-agent principal (BTAE-14) and a deliver-to address abroad
                  (IBG-13). Scenarios combine, and each companion element stays mandatory under its own rule — none is
                  derived from a value the invoice already carries.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000018
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionType:
                      freeTradeZone: true
                      summaryInvoice: true
                      disclosedAgentBilling: true
                      export: true
                    invoicePeriod:
                      startDate: '2026-09-01'
                      endDate: '2026-09-30'
                      frequencyCode: MTH
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                      beneficiaryId: FZ-BEN-778901
                    agent:
                      principalId: PRINCIPAL-4471
                    delivery:
                      date: '2026-09-25'
                      address:
                        streetName: Hamngatan 4
                        additionalStreetName: Warehouse B
                        cityName: Goteborg
                        postalZone: '41106'
                        countrySubentity: Vastra Gotaland
                        countryCode: SE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              exemptionReasonJsonMode:
                summary: JSON mode — Mixed VAT categories (380)
                description: >-
                  Standard rated (S), zero rated (Z) and exempt (E) lines on one invoice. The VAT breakdown has one
                  entry per category; the exempt line carries a VAT exemption reason code and no rate.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000017
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                      - id: '2'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: International freight
                          description: Zero-rated international transport of goods
                        tax:
                          category: Z
                          rate: '0'
                      - id: '3'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Financial service fee
                          description: Exempt financial service
                        tax:
                          category: E
                          exemptionReasonCode: VATEX-AE-EXEMPT-FINANCIAL
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                        - category: E
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '3000.00'
                      taxExclusiveAmount: '3000.00'
                      taxInclusiveAmount: '3050.00'
                      payableAmount: '3050.00'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
              examples:
                valid:
                  summary: Valid document
                  value:
                    valid: true
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    warnings: []
                    phormSkipped: false
                    normalized:
                      sender: '0235:1000000001'
                      receiver: '0235:1000000002'
                      invoiceNumber: INV-2026-000123
                      issueDate: '2026-09-16'
                      currency: AED
                      payableAmount: 1050
                      invoiceTypeCode: '380'
                      dueDate: '2026-10-16'
                      supplierName: AZDAN Sample Supplier LLC
                      customerName: Emirates Buyer Trading LLC
                      netAmount: 1000
                      taxAmount: 50
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:validate
        - peppol:submit
        - transactions:submit
        - transactions:read
  /api/integrations/peppol/invoices:
    post:
      operationId: submitInvoice
      summary: Submit a Peppol invoice or credit note
      description: |-
        Submit a document for Peppol delivery. Validates, creates a transaction,
        and hands off to the access point for AS4 delivery.

        Returns **202 Accepted** — delivery is asynchronous. Subscribe to webhooks
        for real-time status updates.

        Send `Idempotency-Key` as an HTTP header. The key must uniquely identify
        one logical submission. Reuse the same key only when retrying the exact
        same request. Use a new key for a new invoice or credit note.

        The header is authoritative. The legacy request-body field
        `idempotencyKey` is deprecated and supported only for backward
        compatibility; if both are present they must match.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `peppol:submit`, `transactions:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitRequest'
            examples:
              jsonMode:
                summary: JSON mode — Minimal Tax Invoice (380)
                description: >-
                  The smallest request the API accepts for a UAE tax invoice. Start here and add only what your business
                  case needs.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000005
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        cityName: Dubai
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              xmlMode:
                summary: XML mode — UAE Tax Invoice (380)
                description: >-
                  The standard VAT invoice. Carries all 51 MoF mandatory Tax Invoice fields. The xml value is the UBL
                  2.1 document the API builds for the JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <Invoice
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>INV-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000001</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:DueDate>2026-10-16</cbc:DueDate>
                      <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
                      <cbc:Note>Thank you for your business.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000103</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000203</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:PaymentMeans>
                        <cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
                        <cac:PayeeFinancialAccount><cbc:ID>AE070331234567890123456</cbc:ID></cac:PayeeFinancialAccount>
                      </cac:PaymentMeans>
                      <cac:PaymentTerms>
                        <cbc:Note>Net 30 days</cbc:Note>
                      </cac:PaymentTerms>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1050.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1050.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:InvoiceLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">20.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">120.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:InvoiceLine>
                    </Invoice>
              creditNoteJsonMode:
                summary: JSON mode — UAE Tax Credit Note (381) (381)
                description: >-
                  Reverses a tax invoice. Amounts stay POSITIVE; the document type says it is a credit. Needs a reason
                  code and the original invoice reference.
                value:
                  inputType: json
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: CN-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000002
                    issueDate: '2026-09-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Credit for returned services.
                    creditNoteReasonCode: DL8.61.1.D
                    billingReference:
                      invoiceId: INV-2026-000123
                      issueDate: '2026-09-16'
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              creditNoteXmlMode:
                summary: XML mode — UAE Tax Credit Note (381)
                description: >-
                  Reverses a tax invoice. Amounts stay POSITIVE; the document type says it is a credit. Needs a reason
                  code and the original invoice reference. The xml value is the UBL 2.1 document the API builds for the
                  JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <CreditNote
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CN-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000002</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
                      <cbc:Note>Credit for returned services.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:DiscrepancyResponse>
                        <cbc:ResponseCode>DL8.61.1.D</cbc:ResponseCode>
                      </cac:DiscrepancyResponse>
                      <cac:BillingReference>
                        <cac:InvoiceDocumentReference>
                          <cbc:ID>INV-2026-000123</cbc:ID>
                          <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                        </cac:InvoiceDocumentReference>
                      </cac:BillingReference>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000103</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>100000000000203</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1050.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1050.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:CreditNoteLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:CreditedQuantity unitCode="EA">10</cbc:CreditedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>S</cbc:ID>
                            <cbc:Percent>5.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">0.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">100.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:CreditNoteLine>
                    </CreditNote>
              commercialInvoiceJsonMode:
                summary: JSON mode — UAE Commercial Invoice (480) (480)
                description: >-
                  Issued when no VAT is charged. Carries all 49 MoF mandatory Commercial Invoice fields: the seller
                  states its TIN instead of a VAT TRN, the buyer its legal registration, and every tax category is E, O
                  or Z.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: commercial
                    invoiceNumber: CINV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000003
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Thank you for your business.
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    paymentTerms:
                      note: Net 30 days
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxRegistrationId: '1000000001'
                      taxSchemeId: TIN
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      legalRegistrationId: CN-7654321
                      legalRegistrationType: CL
                      legalRegistrationAuthorityName: Abu Dhabi Department of Economic Development
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                          grossPriceAmount: '120.00'
                          baseQuantity: '1'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: Z
                          rate: '0'
                    taxTotal:
                      taxAmount: '0.00'
                      taxSubtotals:
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1000.00'
                      payableAmount: '1000.00'
              commercialInvoiceXmlMode:
                summary: XML mode — UAE Commercial Invoice (480)
                description: >-
                  Issued when no VAT is charged. Carries all 49 MoF mandatory Commercial Invoice fields: the seller
                  states its TIN instead of a VAT TRN, the buyer its legal registration, and every tax category is E, O
                  or Z. The xml value is the UBL 2.1 document the API builds for the JSON example of the same name; it
                  passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <Invoice
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CINV-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000003</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:DueDate>2026-10-16</cbc:DueDate>
                      <cbc:InvoiceTypeCode>480</cbc:InvoiceTypeCode>
                      <cbc:Note>Thank you for your business.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>1000000001</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>TIN</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="CL" schemeAgencyName="Abu Dhabi Department of Economic Development">CN-7654321</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:PaymentMeans>
                        <cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
                        <cac:PayeeFinancialAccount><cbc:ID>AE070331234567890123456</cbc:ID></cac:PayeeFinancialAccount>
                      </cac:PaymentMeans>
                      <cac:PaymentTerms>
                        <cbc:Note>Net 30 days</cbc:Note>
                      </cac:PaymentTerms>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1000.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1000.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:InvoiceLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">20.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">120.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:InvoiceLine>
                    </Invoice>
              commercialCreditNoteJsonMode:
                summary: JSON mode — UAE Commercial Credit Note (81) (81)
                description: Reverses a commercial invoice. Same identity rules as 480.
                value:
                  inputType: json
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: commercial
                    invoiceNumber: CCN-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000004
                    issueDate: '2026-09-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    note: Credit for returned goods.
                    creditNoteReasonCode: DL8.61.1.D
                    billingReference:
                      invoiceId: CINV-2026-000123
                      issueDate: '2026-09-16'
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxRegistrationId: '1000000001'
                      taxSchemeId: TIN
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      legalRegistrationId: CN-7654321
                      legalRegistrationType: CL
                      legalRegistrationAuthorityName: Abu Dhabi Department of Economic Development
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: Z
                          rate: '0'
                    taxTotal:
                      taxAmount: '0.00'
                      taxSubtotals:
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1000.00'
                      payableAmount: '1000.00'
              commercialCreditNoteXmlMode:
                summary: XML mode — UAE Commercial Credit Note (81)
                description: >-
                  Reverses a commercial invoice. Same identity rules as 480. The xml value is the UBL 2.1 document the
                  API builds for the JSON example of the same name; it passes PINT-AE 1.0.4.
                value:
                  inputType: xml
                  environment: test
                  documentType: creditNote
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  xml: |-
                    <?xml version="1.0" encoding="UTF-8"?>
                    <CreditNote
                      xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
                      xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
                      xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
                      <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
                      <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
                      <cbc:ProfileExecutionID>00000000</cbc:ProfileExecutionID>
                      <cbc:ID>CCN-2026-000123</cbc:ID>
                      <cbc:UUID>3f2504e0-4f89-41d3-9a0c-000000000004</cbc:UUID>
                      <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                      <cbc:CreditNoteTypeCode>81</cbc:CreditNoteTypeCode>
                      <cbc:Note>Credit for returned goods.</cbc:Note>
                      <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
                      <cac:DiscrepancyResponse>
                        <cbc:ResponseCode>DL8.61.1.D</cbc:ResponseCode>
                      </cac:DiscrepancyResponse>
                      <cac:BillingReference>
                        <cac:InvoiceDocumentReference>
                          <cbc:ID>CINV-2026-000123</cbc:ID>
                          <cbc:IssueDate>2026-09-16</cbc:IssueDate>
                        </cac:InvoiceDocumentReference>
                      </cac:BillingReference>
                      <cac:AccountingSupplierParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000001</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>AZDAN Sample Supplier LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Sheikh Zayed Road 12</cbc:StreetName>
                            <cbc:AdditionalStreetName>Office 401</cbc:AdditionalStreetName>
                            <cbc:CityName>Dubai</cbc:CityName>
                            <cbc:PostalZone>00000</cbc:PostalZone>
                            <cbc:CountrySubentity>DXB</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyTaxScheme>
                            <cbc:CompanyID>1000000001</cbc:CompanyID>
                            <cac:TaxScheme><cbc:ID>TIN</cbc:ID></cac:TaxScheme>
                          </cac:PartyTaxScheme>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>AZDAN Sample Supplier LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="TL" schemeAgencyName="Department of Economic Development Dubai">CN-1234567</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingSupplierParty>
                      <cac:AccountingCustomerParty>
                        <cac:Party>
                          <cbc:EndpointID schemeID="0235">1000000002</cbc:EndpointID>
                          <cac:PartyName><cbc:Name>Emirates Buyer Trading LLC</cbc:Name></cac:PartyName>
                          <cac:PostalAddress>
                            <cbc:StreetName>Corniche Road 88</cbc:StreetName>
                            <cbc:CityName>Abu Dhabi</cbc:CityName>
                            <cbc:PostalZone>11111</cbc:PostalZone>
                            <cbc:CountrySubentity>AUH</cbc:CountrySubentity>
                            <cac:Country><cbc:IdentificationCode>AE</cbc:IdentificationCode></cac:Country>
                          </cac:PostalAddress>
                          <cac:PartyLegalEntity>
                            <cbc:RegistrationName>Emirates Buyer Trading LLC</cbc:RegistrationName>
                            <cbc:CompanyID schemeAgencyID="CL" schemeAgencyName="Abu Dhabi Department of Economic Development">CN-7654321</cbc:CompanyID>
                          </cac:PartyLegalEntity>
                        </cac:Party></cac:AccountingCustomerParty>
                      <cac:TaxTotal>
                        <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                        <cac:TaxSubtotal>
                          <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
                          <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          <cac:TaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:TaxCategory>
                        </cac:TaxSubtotal>
                      </cac:TaxTotal>
                      <cac:LegalMonetaryTotal>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
                        <cbc:TaxInclusiveAmount currencyID="AED">1000.00</cbc:TaxInclusiveAmount>
                        <cbc:PayableAmount currencyID="AED">1000.00</cbc:PayableAmount>
                      </cac:LegalMonetaryTotal>
                      <cac:CreditNoteLine>
                        <cbc:ID>1</cbc:ID>
                        <cbc:CreditedQuantity unitCode="EA">10</cbc:CreditedQuantity>
                        <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
                        <cac:Item>
                            <cbc:Description>Professional services for September 2026</cbc:Description>
                          <cbc:Name>Consulting services</cbc:Name>
                          <cac:ClassifiedTaxCategory>
                            <cbc:ID>Z</cbc:ID>
                            <cbc:Percent>0.00</cbc:Percent>
                            <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
                          </cac:ClassifiedTaxCategory>
                        </cac:Item>
                        <cac:Price>
                          <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
                          <cbc:BaseQuantity unitCode="EA">1</cbc:BaseQuantity>
                          <cac:AllowanceCharge>
                            <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
                            <cbc:Amount currencyID="AED">0.00</cbc:Amount>
                            <cbc:BaseAmount currencyID="AED">100.00</cbc:BaseAmount>
                          </cac:AllowanceCharge>
                        </cac:Price>
                        <cac:ItemPriceExtension>
                          <cbc:Amount currencyID="AED">1000.00</cbc:Amount>
                          <cac:TaxTotal>
                            <cbc:TaxAmount currencyID="AED">0.00</cbc:TaxAmount>
                          </cac:TaxTotal>
                        </cac:ItemPriceExtension>
                      </cac:CreditNoteLine>
                    </CreditNote>
              uaeScenarioJsonMode:
                summary: JSON mode — Combined scenarios (380)
                description: >-
                  Positions 1, 4, 6 and 8 declared together (mask 10010101): the free-zone beneficiary (BTAE-01), the
                  invoice period (IBG-14), the disclosed-agent principal (BTAE-14) and a deliver-to address abroad
                  (IBG-13). Scenarios combine, and each companion element stays mandatory under its own rule — none is
                  derived from a value the invoice already carries.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000018
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionType:
                      freeTradeZone: true
                      summaryInvoice: true
                      disclosedAgentBilling: true
                      export: true
                    invoicePeriod:
                      startDate: '2026-09-01'
                      endDate: '2026-09-30'
                      frequencyCode: MTH
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                      beneficiaryId: FZ-BEN-778901
                    agent:
                      principalId: PRINCIPAL-4471
                    delivery:
                      date: '2026-09-25'
                      address:
                        streetName: Hamngatan 4
                        additionalStreetName: Warehouse B
                        cityName: Goteborg
                        postalZone: '41106'
                        countrySubentity: Vastra Gotaland
                        countryCode: SE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '1000.00'
                      taxExclusiveAmount: '1000.00'
                      taxInclusiveAmount: '1050.00'
                      payableAmount: '1050.00'
              exemptionReasonJsonMode:
                summary: JSON mode — Mixed VAT categories (380)
                description: >-
                  Standard rated (S), zero rated (Z) and exempt (E) lines on one invoice. The VAT breakdown has one
                  entry per category; the exempt line carries a VAT exemption reason code and no rate.
                value:
                  inputType: json
                  environment: test
                  documentType: invoice
                  senderPeppolId: '0235:1000000001'
                  receiverPeppolId: '0235:1000000002'
                  payload:
                    invoiceKind: tax
                    invoiceNumber: INV-2026-000123
                    uniqueIdentifierNumber: 3f2504e0-4f89-41d3-9a0c-000000000017
                    issueDate: '2026-09-16'
                    dueDate: '2026-10-16'
                    currency: AED
                    transactionTypeCode: '00000000'
                    paymentMeansCode: '30'
                    paymentAccountId: AE070331234567890123456
                    supplier:
                      name: AZDAN Sample Supplier LLC
                      schemeId: '0235'
                      peppolId: '1000000001'
                      taxId: '100000000000103'
                      legalRegistrationId: CN-1234567
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development Dubai
                      address:
                        streetName: Sheikh Zayed Road 12
                        additionalStreetName: Office 401
                        cityName: Dubai
                        postalZone: '00000'
                        countrySubentity: DXB
                        countryCode: AE
                    customer:
                      name: Emirates Buyer Trading LLC
                      schemeId: '0235'
                      peppolId: '1000000002'
                      taxId: '100000000000203'
                      address:
                        streetName: Corniche Road 88
                        cityName: Abu Dhabi
                        postalZone: '11111'
                        countrySubentity: AUH
                        countryCode: AE
                    lines:
                      - id: '1'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Consulting services
                          description: Professional services for September 2026
                        tax:
                          category: S
                          rate: '5'
                      - id: '2'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: International freight
                          description: Zero-rated international transport of goods
                        tax:
                          category: Z
                          rate: '0'
                      - id: '3'
                        quantity: '10'
                        unitCode: EA
                        lineExtensionAmount: '1000.00'
                        price:
                          priceAmount: '100.00'
                        item:
                          name: Financial service fee
                          description: Exempt financial service
                        tax:
                          category: E
                          exemptionReasonCode: VATEX-AE-EXEMPT-FINANCIAL
                    taxTotal:
                      taxAmount: '50.00'
                      taxSubtotals:
                        - category: S
                          percent: '5.00'
                          taxableAmount: '1000.00'
                          taxAmount: '50.00'
                        - category: Z
                          percent: '0.00'
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                        - category: E
                          taxableAmount: '1000.00'
                          taxAmount: '0.00'
                    legalMonetaryTotal:
                      lineExtensionAmount: '3000.00'
                      taxExclusiveAmount: '3000.00'
                      taxInclusiveAmount: '3050.00'
                      payableAmount: '3050.00'
      responses:
        '202':
          description: Accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
              examples:
                accepted:
                  summary: Accepted for delivery
                  value:
                    transactionId: txn_1789562400000_9f3a1c2b
                    status: queued
                    valid: true
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    correlationId: cor_1789562400000_4d5e6f70
                    as4MessageId: f0debc61-215c-4e0a-9d1b-2a7c3e4f5a6b@peppol.azdanapps.com
                    sbdhInstanceId: 6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f
                    idempotent: false
                    warnings: []
                    validatorPhormSkipped: false
                    message: Invoice accepted for Peppol delivery.
                    documents:
                      invoiceXml:
                        available: true
                        fileId: file_01J9Z3Q7M8K2N4P6R8T0V2X4Y6
                        fileName: txn_1789562400000_9f3a1c2b.xml
                        contentType: application/xml
                        downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a1c2b/files/invoice-xml
                      invoicePdf:
                        available: false
                        fileId: null
                        fileName: null
                        contentType: application/pdf
                        downloadUrl: null
                        reason: not_generated
                      sbdXml:
                        available: false
                        fileId: null
                        fileName: null
                        contentType: application/xml
                        downloadUrl: null
                        reason: not_stored
                      tddXml:
                        available: false
                        fileId: null
                        fileName: null
                        contentType: application/xml
                        downloadUrl: null
                        reason: not_generated
                      mlsXml:
                        available: false
                        fileId: null
                        fileName: null
                        contentType: application/xml
                        downloadUrl: null
                        reason: not_received
                      payloadXml:
                        available: false
                        privileged: true
                        requiredScope: peppol:payload:read
                        contentType: application/xml
                        downloadUrl: null
                        reason: not_in_object_storage
                idempotentReplay:
                  summary: Same Idempotency-Key again — the original transaction, nothing sent twice
                  value:
                    transactionId: txn_1789562400000_9f3a1c2b
                    status: sending
                    valid: true
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    correlationId: cor_1789562400000_4d5e6f70
                    as4MessageId: f0debc61-215c-4e0a-9d1b-2a7c3e4f5a6b@peppol.azdanapps.com
                    sbdhInstanceId: 6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f
                    idempotent: true
                    warnings: []
                    validatorPhormSkipped: false
                    message: Existing transaction returned (idempotent replay).
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '502':
          $ref: '#/components/responses/UpstreamError'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:submit
        - transactions:submit
  /api/integrations/peppol/transactions:
    get:
      operationId: listTransactions
      summary: List transactions
      description: |-
        Paginated list of the tenant's transactions. Supports two pagination modes:

        - **Cursor mode** (default): Pass `limit` and optionally `cursor`.
          Recommended for sync jobs and large datasets.
        - **Page mode**: Pass `page` and `pageSize`. Backward compatible for
          UI browsing.

        If both `page`/`pageSize` and `limit`/`cursor` are absent, cursor mode
        is used with default limit=50.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `transactions:read`, `peppol:read`, `peppol:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - $ref: '#/components/parameters/CursorLimit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Sort'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/IncludeParam'
        - $ref: '#/components/parameters/FilterDirection'
        - $ref: '#/components/parameters/FilterStatus'
        - $ref: '#/components/parameters/FilterEnvironment'
        - $ref: '#/components/parameters/FilterDocumentType'
        - $ref: '#/components/parameters/FilterSenderPeppolId'
        - $ref: '#/components/parameters/FilterReceiverPeppolId'
        - $ref: '#/components/parameters/FilterPeppolParticipantId'
        - $ref: '#/components/parameters/FilterSubsidiaryId'
        - $ref: '#/components/parameters/FilterCorrelationId'
        - $ref: '#/components/parameters/FilterAs4MessageId'
        - $ref: '#/components/parameters/FilterApCoreMessageId'
        - $ref: '#/components/parameters/FilterSbdhInstanceId'
        - $ref: '#/components/parameters/FilterIdempotencyKey'
        - $ref: '#/components/parameters/FilterDateFrom'
        - $ref: '#/components/parameters/FilterDateTo'
        - $ref: '#/components/parameters/FilterMessageId'
        - $ref: '#/components/parameters/FilterSearch'
      responses:
        '200':
          description: Transaction list
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CursorListResponse'
                  - $ref: '#/components/schemas/PageListResponse'
              examples:
                cursorMode:
                  summary: Cursor pagination
                  value:
                    items:
                      - transactionId: txn_1715690640123_a1b2c3d4
                        direction: outbound
                        status: delivered
                        documentType: invoice
                        sender:
                          participantId: '0235:1000000001'
                          name: Acme Trading LLC
                        receiver:
                          participantId: '0235:1000000002'
                          name: BuyerCo
                        environment: production
                        createdAt: '2026-05-14T10:00:00.000Z'
                    pagination:
                      mode: cursor
                      limit: 50
                      nextCursor: eyJzIjoiY3JlYXRlZEF0Iiw...
                      prevCursor: null
                      hasNext: true
                pageMode:
                  summary: Page pagination (backward compatible)
                  value:
                    transactions:
                      - transactionId: txn_1715690640123_a1b2c3d4
                        direction: outbound
                        status: delivered
                    pagination:
                      page: 1
                      pageSize: 25
                      total: 142
                      totalPages: 6
                      hasMore: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/integrations/peppol/transactions/by-message/{messageId}:
    get:
      operationId: getTransactionByMessageId
      summary: Look up transaction by AS4 message ID
      description: |-
        Find a transaction by its AS4/AP Core message ID. Useful for correlating
        transport-level IDs from AP logs or network receipts.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `transactions:read`, `peppol:read`, `peppol:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - name: messageId
          in: path
          required: true
          schema:
            type: string
          example: f0debc61-215c-4d16-9d33-ca5d126ccbb3@peppol.azdanapps.com
      responses:
        '200':
          description: Transaction detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transaction'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/integrations/peppol/transactions/by-idempotency/{idempotencyKey}:
    get:
      operationId: getTransactionByIdempotencyKey
      summary: Look up transaction by Idempotency-Key
      description: |-
        Return the transaction that was originally created with the given
        `Idempotency-Key`. Keys are scoped per tenant, so this is a deterministic
        single-row lookup. Use this to confirm whether a previous submit attempt
        succeeded without retrying it.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).
      tags:
        - Integration API · Peppol
      parameters:
        - name: idempotencyKey
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 120
          example: erp-invoice-2026-0001
      responses:
        '200':
          description: Transaction summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transaction'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/integrations/peppol/transactions/{transactionId}/tdd:
    get:
      operationId: getTransactionTdd
      summary: Tax Data Document generated for a transaction
      description: >-
        Tax Data Documents are generated automatically by AZDAN. Do not submit a TDD. Use the invoice transaction's TDD
        endpoint to inspect the generated TDD status and download it when available.


        Resolution follows the explicit parent linkage recorded by the access point (never a time or sender guess), so
        on a self-send the TDD returned is the one generated for THIS outbound document. The download route returns JSON
        with a short-lived signed URL and needs the privileged peppol:payload:read scope; documents.tddXml /
        …/files/tdd-xml offer the same file under the ordinary document-read scopes.


        On the enriched detail of an invoice, ap.isTdd is always false and ap.tddStatus is always null — they describe
        the row itself, not the TDD generated for it. The generated TDD's delivery status is tdd.status on the TDD
        endpoint.


        Required scope: any of x-azdan-scopes (a default key qualifies).


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).
      tags:
        - Integration API · Peppol
      parameters:
        - $ref: '#/components/parameters/TransactionIdPath'
      responses:
        '200':
          description: TDD metadata
          content:
            application/json:
              schema:
                type: object
                required:
                  - tdd
                properties:
                  tdd:
                    $ref: '#/components/schemas/TddMetadata'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/integrations/peppol/transactions/{transactionId}/tdd/download:
    get:
      operationId: getTransactionTddDownload
      summary: Signed download link for the generated Tax Data Document
      description: >-
        Same resolution as …/tdd, plus a short-lived signed downloadUrl for the TDD XML. Returns JSON (not a redirect).
        Resolution follows the explicit parent linkage recorded by the access point (never a time or sender guess), so
        on a self-send the TDD returned is the one generated for THIS outbound document. The download route returns JSON
        with a short-lived signed URL and needs the privileged peppol:payload:read scope; documents.tddXml /
        …/files/tdd-xml offer the same file under the ordinary document-read scopes.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).
      tags:
        - Integration API · Peppol
      parameters:
        - $ref: '#/components/parameters/TransactionIdPath'
        - name: ttlSeconds
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: Requested link lifetime; the server may cap it.
      responses:
        '200':
          description: TDD metadata with a signed download URL
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/TddMetadata'
                  - type: object
                    properties:
                      downloadUrl:
                        type: string
                        format: uri
                      ttlSeconds:
                        type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:payload:read
  /api/integrations/peppol/transactions/{transactionId}/files/{docKind}:
    get:
      operationId: getIntegrationsPeppolTransactionsByTransactionIdFilesByDocKind
      summary: API-key authenticated aliases for the unified documents contract
      description: >-
        API-key authenticated aliases for the unified documents contract. These

        mirror the tenant portal download routes but live under the integration

        namespace so ERP/webhook consumers can use their AZDAN API key.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).


        Specified per document kind in the public Integration API reference:
        `/api/integrations/peppol/transactions/{transactionId}/files/invoice-xml`,
        `/api/integrations/peppol/transactions/{transactionId}/files/invoice-pdf`,
        `/api/integrations/peppol/transactions/{transactionId}/files/sbd-xml`,
        `/api/integrations/peppol/transactions/{transactionId}/files/tdd-xml`,
        `/api/integrations/peppol/transactions/{transactionId}/files/mls-xml`.


        **API-key scopes (any one):** `peppol:read`, `transactions:read`, `peppol:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
        - name: docKind
          in: path
          required: true
          schema:
            type: string
            enum:
              - invoice-xml
              - invoice-pdf
              - sbd-xml
              - tdd-xml
              - mls-xml
      responses:
        '200':
          description: File streamed through the Platform API.
          headers:
            Content-Disposition:
              schema:
                type: string
                example: attachment; filename="INV-10001.xml"
            Cache-Control:
              schema:
                type: string
                example: no-store
          content:
            application/xml:
              schema:
                type: string
                format: binary
        '302':
          description: Redirect to a short-lived signed download URL.
          headers:
            Location:
              description: Short-lived signed download URL.
              schema:
                type: string
                format: uri
            Cache-Control:
              schema:
                type: string
                example: no-store
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:read
        - transactions:read
        - peppol:submit
  /api/integrations/peppol/transactions/{transactionId}:
    get:
      operationId: getTransaction
      summary: Get enriched transaction detail
      description: |-
        Returns a full enriched view of a single transaction including:
        - `transaction` — platform transaction fields
        - `platform` — correlation/idempotency/webhook summary
        - `ap` — AP Core link status, AS4/SBDH IDs, timestamps, errors
        - `storage` — primary storage info (S3 key, content type, size)
        - `files` — list of associated files (payload, SBDH, MLR, etc.)
        - `links` — HATEOAS links to related endpoints (self, events, xml)

        AP matching uses a prioritized strategy:
        1. Exact match on `sbdhInstanceId`
        2. Exact match on `apCoreMessageId` (AS4 message ID)
        3. Fuzzy match on sender+receiver+docType+process+createdAt±5min
        4. If multiple fuzzy matches, returns `ap.linkAmbiguous: true`


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `transactions:read`, `peppol:read`, `peppol:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
          example: txn_1715690640123_a1b2c3d4
      responses:
        '200':
          description: Enriched transaction detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrichedTransactionDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/integrations/peppol/transactions/{transactionId}/xml:
    get:
      operationId: getTransactionXml
      summary: Download XML business payload
      description: |-
        Generates a time-limited presigned S3 URL for the XML business payload
        and redirects (303) to it. The `Content-Type` header is set to
        `application/xml`. The presigned URL expires after 60 seconds.

        Requires scope: `peppol:payload:read`.

        Returns 409 if the payload is not available in S3 (e.g., only exists
        in a legacy local filesystem path).


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).
      tags:
        - Integration API · Peppol
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '303':
          description: Redirect to presigned S3 URL
          headers:
            Location:
              description: Presigned S3 download URL (expires in 60s)
              schema:
                type: string
                format: uri
            Content-Type:
              schema:
                type: string
                example: application/xml
            Cache-Control:
              schema:
                type: string
                example: no-store
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Payload not available in object storage yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: PAYLOAD_NOT_IN_OBJECT_STORAGE
                  message: XML payload is not available in object storage.
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:payload:read
  /api/integrations/peppol/transactions/{transactionId}/events:
    get:
      operationId: getTransactionEvents
      summary: Get webhook events for a transaction
      description: |-
        Retrieve the webhook event timeline for a transaction. Shows every
        lifecycle event emitted, delivery status, and any errors. Events are
        sorted chronologically (oldest first).


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).

        **API-key scopes (any one):** `transactions:read`, `peppol:read`, `peppol:submit`
      tags:
        - Integration API · Peppol
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Event timeline
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventsResponse'
              examples:
                timeline:
                  summary: Full outbound lifecycle
                  value:
                    events:
                      - eventId: evt_txn_submitted_abc123
                        type: transaction.submitted
                        deliveryStatus: delivered
                        payload:
                          event: transaction.submitted
                          transactionId: txn_1715690640123_a1b2c3d4
                          status: queued
                          direction: outbound
                          environment: production
                          sender: '0235:1000000001'
                          receiver: '0235:1000000002'
                        createdAt: '2026-05-14T10:00:00.000Z'
                        deliveredAt: '2026-05-14T10:00:01.000Z'
                        attempts: 1
                        lastError: null
                      - eventId: evt_ap_a1b2c3d4e5f6g7h8
                        type: ap.outbound.delivered
                        deliveryStatus: delivered
                        payload:
                          event: ap.outbound.delivered
                          status: delivered
                          direction: outbound
                        createdAt: '2026-05-14T10:00:04.000Z'
                        deliveredAt: '2026-05-14T10:00:05.000Z'
                        attempts: 1
                        lastError: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - transactions:read
        - peppol:read
        - peppol:submit
  /api/tenant/transactions/{transactionId}/files/{docKind}:
    get:
      operationId: getTenantTransactionsByTransactionIdFilesByDocKind
      summary: Download a transaction document (legacy API-key URL)
      description: >-
        Backward-compatible URL for Postman imports and webhook payloads that still point integrations at
        `/api/tenant/transactions/{id}/files/{kind}`. The canonical API-key URL is `GET
        /api/integrations/peppol/transactions/{transactionId}/files/{docKind}`; both run the same handler.


        Only requests with `Authorization: Bearer azd_live_…` / `azd_test_…` are handled here; any other caller (tenant
        JWT or session cookie) falls through to the Tenant Console download route on the same path, which uses session
        authentication.


        The file must belong to both the transaction and the key's tenant. The bytes are streamed (`Content-Disposition:
        attachment`, `Cache-Control: no-store`) or the call answers `302` to a short-lived signed URL; storage
        coordinates are never returned. A missing document answers `404` naming the document and why it is absent
        (`details.reason`: `not_generated`, `metadata_only`, `not_received`, `not_stored_for_outbound`, `not_stored`,
        `transaction_not_found`). Each download is audited (`storage.document.download`).


        **Scopes (any one):** `peppol:read`, `transactions:read`, `peppol:submit`. Rate-limited with the shared
        per-API-key submit limiter.


        **Access:** Tenant API key (`Authorization: Bearer azd_…`).


        Specified per document kind in the public Integration API reference:
        `/api/tenant/transactions/{transactionId}/files/invoice-xml`,
        `/api/tenant/transactions/{transactionId}/files/invoice-pdf`,
        `/api/tenant/transactions/{transactionId}/files/sbd-xml`,
        `/api/tenant/transactions/{transactionId}/files/tdd-xml`,
        `/api/tenant/transactions/{transactionId}/files/mls-xml`.


        **API-key scopes (any one):** `peppol:read`, `transactions:read`, `peppol:submit`
      tags:
        - Integration API · Documents
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
        - name: docKind
          in: path
          required: true
          schema:
            type: string
            enum:
              - invoice-xml
              - invoice-pdf
              - sbd-xml
              - tdd-xml
              - mls-xml
      responses:
        '200':
          description: File streamed. `Content-Type` is the stored type (`application/xml` or `application/pdf`).
          headers:
            Content-Disposition:
              schema:
                type: string
                example: attachment; filename="INV-10001.xml"
            Cache-Control:
              schema:
                type: string
                example: no-store
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '302':
          description: Redirect to a short-lived signed download URL.
          headers:
            Location:
              schema:
                type: string
                format: uri
            Cache-Control:
              schema:
                type: string
                example: no-store
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          description: The document is not available for this transaction, or the transaction is not the key's tenant's.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: object
                        properties:
                          document:
                            type: string
                          available:
                            type: boolean
                          reason:
                            type: string
                          expected:
                            type: boolean
              example:
                error:
                  code: NOT_FOUND
                  message: Tax Data Document has not been generated for this transaction
                  details:
                    document: tddXml
                    available: false
                    reason: not_generated
                    expected: true
        '429':
          description: >-
            Rate limit exceeded (per API key). Answered by express-rate-limit's default handler: plain-text body `Too
            many requests, please try again later.` with standard `RateLimit-*` headers.
          content:
            text/plain:
              schema:
                type: string
                format: binary
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - ApiKeyBearer: []
      x-azdan-access: apiKey
      x-azdan-scopes:
        - peppol:read
        - transactions:read
        - peppol:submit
  /api/tenant/config/preferences/{tenantId}:
    get:
      operationId: getTenantConfigPreferencesByTenantId
      summary: Get the tenant's own preferences
      description: >-
        Returns the caller's own `tenant_preferences` document. The `{tenantId}` path segment must equal the principal's
        tenant, otherwise 403 (`cross-tenant access denied`). Webhook entries are returned as stored, including
        `secretEnc`. 404 when none has been written.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: tenantId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The tenant preferences document.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  schemaVersion:
                    type: integer
                  retries:
                    type: object
                    properties:
                      outbound.max-attempts:
                        type: integer
                  notifications:
                    type: object
                    properties:
                      email.failures:
                        type: array
                        items:
                          type: string
                  version:
                    type: integer
                  updatedAt:
                    type: string
                  updatedBy:
                    type: string
                  updatedFromIp:
                    type: string
                  changeRef:
                    type: string
              example:
                _id: tnt_7Q2X…
                schemaVersion: 1
                retries:
                  outbound.max-attempts: 5
                notifications:
                  email.failures:
                    - o***@example.com
                version: 3
                updatedAt: '2026-10-21T09:14:03.000Z'
                updatedBy: usr_3Hd…
                updatedFromIp: 10.0.1.***
                changeRef: 6717a5e1…77c0
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          description: Not found. This route answers with a bare code and no `message`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: NOT_FOUND
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    put:
      operationId: putTenantConfigPreferencesByTenantId
      summary: Create or update the tenant's own preferences
      description: >-
        Writes the caller's own `tenant_preferences` document (same pipeline as the admin route: `reason` and
        `expectedVersion` required, JSON Schema validation, optimistic concurrency, change history and audit).
        `{tenantId}` must equal the principal's tenant. The route itself checks only tenant ownership, not the tenant
        role, so any tenant user of the tenant can write.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: tenantId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                expectedVersion:
                  type: integer
                reason:
                  type: string
                payload:
                  type: object
                  properties:
                    retries:
                      type: object
                      properties:
                        outbound.max-attempts:
                          type: integer
                    notifications:
                      type: object
                      properties:
                        email.failures:
                          type: array
                          items:
                            type: string
            example:
              expectedVersion: 2
              reason: Add finance mailbox for failure alerts
              payload:
                retries:
                  outbound.max-attempts: 5
                notifications:
                  email.failures:
                    - o***@example.com
      responses:
        '200':
          description: The document after the write.
          content:
            application/json:
              schema:
                type: object
                properties:
                  doc:
                    type: object
                    properties:
                      _id:
                        type: string
                      schemaVersion:
                        type: integer
                      retries:
                        type: object
                        properties:
                          outbound.max-attempts:
                            type: integer
                      notifications:
                        type: object
                        properties:
                          email.failures:
                            type: array
                            items:
                              type: string
                      version:
                        type: integer
                      updatedAt:
                        type: string
                      updatedBy:
                        type: string
                      updatedFromIp:
                        type: string
                      changeRef:
                        type: string
                  changed:
                    type: boolean
              example:
                doc:
                  _id: tnt_7Q2X…
                  schemaVersion: 1
                  retries:
                    outbound.max-attempts: 5
                  notifications:
                    email.failures:
                      - o***@example.com
                  version: 3
                  updatedAt: '2026-10-21T09:14:03.000Z'
                  updatedBy: usr_3Hd…
                  updatedFromIp: 10.0.1.***
                  changeRef: 6717a5e1…77c0
                changed: true
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/terms:
    get:
      operationId: getTenantTerms
      summary: 'Where the signed-in tenant stands against the current agreement: the current version,…'
      description: |-
        Where the signed-in tenant stands against the current agreement: the
        current version, the acceptance on record (version, date, who), whether an
        acceptance is still required, and whether THIS user may give it.

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/terms/accept:
    post:
      operationId: postTenantTermsAccept
      summary: Records the administrator's acceptance of the current agreement for the tenant…
      description: |-
        Records the administrator's acceptance of the current agreement for the
        tenant (receipt row + tenant summary) and lifts the sign-in gate on the
        next request. 409 TERMS_VERSION_OUTDATED when `version` is not the current one.

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                version:
                  type: string
                  minLength: 1
                  maxLength: 16
                accepted:
                  const: true
                locale:
                  type: string
                  enum:
                    - en
                    - ar
              required:
                - version
                - accepted
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/onboarding/confirm:
    post:
      operationId: postTenantFtaOnboardingConfirm
      summary: 'Confirm onboarding: register the Peppol participant and link in EmaraTax'
      description: >-
        Step 3 of onboarding for a signed-in tenant, after the taxpayer saw FTA's verification. Requires `acknowledged:
        true` (literal) and a session in status `verified` that has not expired.


        1. Environment guard (session's pinned plane = live gateway plane).

        2. The participant id is **derived** from the FTA-verified TIN (`0235:<TIN>`); a `peppolId` in the body may only
        confirm it (`400 FTA_PEPPOL_ID_MISMATCH`).

        3. A tenant that already holds an active linkage for this TIN is refused (`409
        FTA_EXISTING_LINKAGE_ACTION_REQUIRED` — use the reverify/offboard routes).

        4. Register the participant (idempotent; reused when this tenant already owns it, `409
        PEPPOL_ID_ALREADY_ASSIGNED` when another tenant does) on the given or default subsidiary, C1 country defaulting
        to the subsidiary's, else `AE`.

        5. Publish it on the AZDAN SMP + SML (unless `FTA_ONBOARDING_SMP_PUBLISH=off`).

        6. Call FTA API-5 `crregupdate` action 1. Once FTA has reported `Active`, a re-confirmation never calls API-5
        again.

        7. Record the durable FTA profile and spend the session.


        `linked` is true only when FTA's `onboardingStatus` is `Active`. A registry step that is pending, blocked
        (`crreg.status: "contract_blocked"` with `missingContractFacts`) or failed is reported as data in `crreg`;
        `crreg.retryable: true` means the same session may submit again. Side effects: audit `fta.onboarding.confirmed`;
        the `fta_onboarded` email when linked.


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
                acknowledged:
                  const: true
                peppolId:
                  type: string
                  pattern: ^[A-Za-z0-9]{2,8}:.+$
                subsidiaryId:
                  type: string
                  minLength: 1
                  maxLength: 120
                c1CountryCode:
                  type: string
                direction:
                  type: string
                  enum:
                    - inbound
                    - outbound
                    - both
                label:
                  type: string
                  maxLength: 120
                termsAccepted:
                  type: boolean
                termsVersion:
                  type: string
                  maxLength: 16
                termsLocale:
                  type: string
                  enum:
                    - en
                    - ar
                clientIp:
                  type: string
                  maxLength: 64
                userAgent:
                  type: string
                  maxLength: 512
                linkToken:
                  type: string
                  minLength: 20
                  maxLength: 200
                targetConfirmed:
                  type: boolean
              required:
                - sessionId
                - acknowledged
            example:
              sessionId: kM3v…
              acknowledged: true
              peppolId: 0235:1004****59
              subsidiaryId: sub_…
              c1CountryCode: AE
              direction: both
              label: Head office
      responses:
        '200':
          description: Participant registered; `crreg` and `linked` report the EmaraTax outcome.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  participant:
                    type: object
                    properties:
                      participantId:
                        type: string
                      peppolId:
                        type: string
                      environment:
                        type: string
                      direction:
                        type: string
                      status:
                        type: string
                      reused:
                        type: boolean
                  crreg:
                    type: object
                    properties:
                      status:
                        type: string
                      msgCode:
                        type: string
                      errorCode:
                        type: 'null'
                      onboardingStatus:
                        type: string
                      recordUpdateTimestamp:
                        type: string
                      retryable:
                        type: boolean
                      reused:
                        type: boolean
                      action:
                        type: integer
                      missingContractFacts:
                        type: 'null'
                  smpPublish:
                    type: object
                    properties:
                      status:
                        type: string
                      verifiedPublished:
                        type: boolean
                      held:
                        type: boolean
                      documentTypes:
                        type: array
                        items:
                          type: string
                  linked:
                    type: boolean
              example:
                ok: true
                correlationId: fta-3c9e…
                participant:
                  participantId: ppt_…
                  peppolId: 0235:1004****59
                  environment: production
                  direction: both
                  status: active
                  reused: false
                crreg:
                  status: ok
                  msgCode: …
                  errorCode: null
                  onboardingStatus: Active
                  recordUpdateTimestamp: '2026-10-24T09:25:31Z'
                  retryable: false
                  reused: false
                  action: 1
                  missingContractFacts: null
                smpPublish:
                  status: published
                  verifiedPublished: true
                  held: false
                  documentTypes:
                    - urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##…
                linked: true
        '400':
          description: '`acknowledged` missing/false, or `peppolId` differs from the derived id.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_PEPPOL_ID_MISMATCH
                  message: …
                  correlationId: fta-3c9e…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: …
                  correlationId: fta-3c9e…
        '409':
          description: >-
            Session not verified, participant id underivable or owned by another tenant, an existing linkage requires
            reverify/offboard, or the environment changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_NOT_VERIFIED
                  message: …
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          description: SMP publication not configured, environment unknown, or the profile could not be persisted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_PEPPOL_PUBLISH_NOT_CONFIGURED
                  message: …
                  correlationId: fta-3c9e…
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/link/start:
    post:
      operationId: postTenantFtaLinkStart
      summary: mint the link intent for the signed-in administrator and tell the portal where EmaraTax is
      description: |-
        mint the link intent for the signed-in administrator
        and tell the portal where EmaraTax is. The clear token is returned ONCE, for the portal
        to place in its HttpOnly cookie; it is never stored and never echoed again.

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/link/status:
    get:
      operationId: getTenantFtaLinkStatus
      summary: the Connect screen's facts (no credential, no token)
      description: |-
        the Connect screen's facts (no credential, no token).

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/linkage/context:
    post:
      operationId: postTenantFtaLinkageContext
      summary: Bind a session to the tenant and get the linkage context
      description: >-
        The first call the portal makes for a session once the user is signed in. Answers whether the tenant already
        holds an FTA linkage for **this session's taxpayer** in the session's environment.


        - The taxpayer is taken from the session (bound TIN, else the TIN EmaraTax sent on a management redirect, else
        the FTA-verified TIN) — never typed by the user. A management session without a TIN is bound only when the
        tenant has exactly one linked taxpayer in that environment.

        - **No linkage** → `existingLinkage: false`; onboarding continues. A management redirect without a linkage →
        `409 FTA_LINKAGE_NOT_FOUND`.

        - **Linkage found** → the session is **bound** (claimed) to the tenant and TIN, and the response carries the
        durable profile, offboarding/reverification state, and whether API-5 (`registryReady`) and the SMP removal
        (`smpReady`) can run on this deployment (missing configuration names only).

        - A session claimed by another tenant → `403 FTA_SESSION_TENANT_MISMATCH`; a management TIN that is not this
        tenant's → `403 FTA_MANAGEMENT_TIN_MISMATCH`. A consumed session still answers (`consumed: true`) so the outcome
        screen can render.


        No external call (FTA/SMP). Any tenant role may call it.


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
              required:
                - sessionId
            example:
              sessionId: kM3v…
      responses:
        '200':
          description: 'Context. Example: existing linkage, management entry.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  existingLinkage:
                    type: boolean
                  consumed:
                    type: boolean
                  environment:
                    type: string
                  entry:
                    type: string
                  managementTin:
                    type: string
                  requestId:
                    type: string
                  linkage:
                    type: object
                    properties:
                      tenantId:
                        type: string
                      environment:
                        type: string
                      tin:
                        type: string
                      peppolId:
                        type: string
                      participantId:
                        type: string
                      aspName:
                        type: string
                      claimedAt:
                        type: string
                  operation:
                    type: 'null'
                  verification:
                    type: 'null'
                  verificationClassification:
                    type: 'null'
                  verificationTinMismatch:
                    type: boolean
                  verifyAttemptsRemaining:
                    type: integer
                  hintedTinMatches:
                    type: boolean
                  profile:
                    type: object
                    properties:
                      tin:
                        type: string
                      peppolId:
                        type: string
                      participantId:
                        type: string
                      peppolStatus:
                        type: string
                      entityNameEn:
                        type: string
                      entityNameAr:
                        type: string
                      vatTrn:
                        type: string
                      legalType:
                        type: string
                      effectiveDate:
                        type: string
                      registeredAdminEmail:
                        type: string
                      mobile:
                        type: string
                      onboardingStatus:
                        type: string
                      linkedAt:
                        type: string
                      overallStatus:
                        type: string
                  offboarding:
                    type: object
                    properties:
                      stage:
                        type: string
                      pending:
                        type: boolean
                      participantRemoved:
                        type: boolean
                      smpUnpublished:
                        type: boolean
                      smpUnpublish:
                        type: 'null'
                      registry:
                        type: 'null'
                  reverification:
                    type: object
                    properties:
                      lastCompletedAt:
                        type: 'null'
                      count:
                        type: integer
                  registryReady:
                    type: boolean
                  registryBlockers:
                    type: array
                    items: {}
                  smpReady:
                    type: boolean
                  smpBlockers:
                    type: array
                    items: {}
                  summary:
                    type: object
                    properties:
                      tin:
                        type: string
                      peppolId:
                        type: string
                      verificationStatus:
                        type: string
                      linkageStatus:
                        type: string
                      aspName:
                        type: string
              example:
                ok: true
                correlationId: fta-3c9e…
                existingLinkage: true
                consumed: false
                environment: production
                entry: management
                managementTin: 1004****59
                requestId: far_…
                linkage:
                  tenantId: tnt_7Q2X…
                  environment: production
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  participantId: ppt_…
                  aspName: AZDAN
                  claimedAt: '2026-10-24T09:23:55.000Z'
                operation: null
                verification: null
                verificationClassification: null
                verificationTinMismatch: false
                verifyAttemptsRemaining: 3
                hintedTinMatches: true
                profile:
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  participantId: ppt_…
                  peppolStatus: active
                  entityNameEn: Example Trading LLC
                  entityNameAr: …
                  vatTrn: 1004****59003
                  legalType: LLC
                  effectiveDate: '2026-01-01'
                  registeredAdminEmail: o***@example.com
                  mobile: +97155****67
                  onboardingStatus: Active
                  linkedAt: '2026-09-02T10:11:00.000Z'
                  overallStatus: linked
                offboarding:
                  stage: none
                  pending: false
                  participantRemoved: false
                  smpUnpublished: false
                  smpUnpublish: null
                  registry: null
                reverification:
                  lastCompletedAt: null
                  count: 0
                registryReady: true
                registryBlockers: []
                smpReady: true
                smpBlockers: []
                summary:
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  verificationStatus: 'True'
                  linkageStatus: linked
                  aspName: AZDAN
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: No tenant, session claimed by another tenant, or management TIN not this tenant's.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_TENANT_MISMATCH
                  message: This linking session belongs to a different account.
                  correlationId: fta-3c9e…
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: Unknown or expired linking session.
                  correlationId: fta-3c9e…
        '409':
          description: No linkage for a management entry, environment changed, or the session cannot be claimed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_LINKAGE_NOT_FOUND
                  message: No existing e-Invoicing linkage was found for this account. …
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: This EmaraTax authorization has expired. …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/linkage/select-action:
    post:
      operationId: postTenantFtaLinkageSelectAction
      summary: Choose reverify or offboard for an existing linkage
      description: >-
        Records the business operation for a bound session: `reverify` (FTA API-5 action 2) or `offboard` (action 3).
        The action number is decided server-side; only the word is accepted.


        Re-runs the context checks, then refuses: a consumed session (`409 FTA_SESSION_ALREADY_USED`), no existing
        linkage (`409 FTA_LINKAGE_NOT_FOUND`), `reverify` while an offboarding is still pending with FTA (`409
        FTA_OFFBOARDING_IN_PROGRESS`), and a deployment where API-5 cannot run (`503 FTA_CRREG_NOT_READY`). The
        session's operation moves to `reverify_verification_pending` / `offboard_verification_pending`. Audited as
        `fta.linkage.action.selected`. `changed: false` when the same action was already selected.


        Only a `tenant_admin` of the tenant may run this step (`403 FTA_ROLE_NOT_PERMITTED` otherwise, audited).


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
                action:
                  type: string
                  enum:
                    - reverify
                    - offboard
              required:
                - sessionId
                - action
            example:
              sessionId: kM3v…
              action: reverify
      responses:
        '200':
          description: Operation selected.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  action:
                    type: string
                  operation:
                    type: object
                    properties:
                      type:
                        type: string
                      state:
                        type: string
                      selectedAt:
                        type: string
                      reasonPresent:
                        type: boolean
                      acknowledgedAt:
                        type: 'null'
                      confirmAttempts:
                        type: integer
                      inFlight:
                        type: boolean
                      result:
                        type: 'null'
                      updatedAt:
                        type: string
                  linkage:
                    type: object
                    properties:
                      tenantId:
                        type: string
                      environment:
                        type: string
                      tin:
                        type: string
                      peppolId:
                        type: string
                      participantId:
                        type: string
                      aspName:
                        type: string
                      claimedAt:
                        type: string
                  changed:
                    type: boolean
              example:
                ok: true
                correlationId: fta-3c9e…
                action: reverify
                operation:
                  type: reverify
                  state: reverify_verification_pending
                  selectedAt: '2026-10-24T09:24:10.000Z'
                  reasonPresent: false
                  acknowledgedAt: null
                  confirmAttempts: 0
                  inFlight: false
                  result: null
                  updatedAt: '2026-10-24T09:24:10.000Z'
                linkage:
                  tenantId: tnt_7Q2X…
                  environment: production
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  participantId: ppt_…
                  aspName: AZDAN
                  claimedAt: '2026-10-24T09:23:55.000Z'
                changed: true
        '400':
          description: Unsupported action.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_LINKAGE_ACTION_INVALID
                  message: Choose either to reverify the linkage or to offboard.
                  correlationId: fta-3c9e…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Not a `tenant_admin`, or the session belongs to another tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_ROLE_NOT_PERMITTED
                  message: Only an account administrator can manage the e-Invoicing linkage.
                  correlationId: fta-3c9e…
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: Unknown or expired linking session.
                  correlationId: fta-3c9e…
        '409':
          description: Session used, no linkage, offboarding pending, operation in progress or invalid transition.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_OFFBOARDING_IN_PROGRESS
                  message: >-
                    An offboarding for this account has not completed with the Federal Tax Authority yet. Complete the
                    offboarding first.
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          description: API-5 cannot run on this deployment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_CRREG_NOT_READY
                  message: >-
                    Linkage changes are temporarily unavailable while the service-provider configuration is being
                    completed.
                  correlationId: fta-3c9e…
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/linkage/verify:
    post:
      operationId: postTenantFtaLinkageVerify
      summary: Verify the linked taxpayer with FTA (API-4) for reverify or offboard
      description: >-
        Runs FTA API-1 → API-4 with the session's **new** authcode against the tenant's **linked** TIN (no `tin` input).
        The answer must name exactly that TIN (`409 FTA_TIN_MISMATCH` otherwise). For a linked taxpayer FTA's "already
        linked" answer is accepted only when the email supplied equals the registered admin email FTA verified at
        onboarding.


        Requires a selected operation (`409 FTA_LINKAGE_ACTION_REQUIRED`) in its `*_verification_pending` state; on
        success the operation moves to `reverify_verified` / `offboard_verified`. Calling again after success returns
        the stored verification with `reused: true` without calling FTA. Same attempt cap (3) and errors as the
        onboarding verification. Audited as `fta.reverify.verified` / `fta.offboard.verified` (or `.failed`).


        Only a `tenant_admin` of the tenant may run this step (`403 FTA_ROLE_NOT_PERMITTED` otherwise, audited).


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
                email:
                  type: string
                  minLength: 3
                  maxLength: 254
                mobile:
                  type: string
                  maxLength: 32
              required:
                - sessionId
                - email
            example:
              sessionId: kM3v…
              email: o***@example.com
              mobile: +97155****67
      responses:
        '200':
          description: Verified for the selected operation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  verification:
                    type: object
                    properties:
                      tin:
                        type: string
                      email:
                        type: string
                      mobile:
                        type: string
                      entityNameEn:
                        type: string
                      entityNameAr:
                        type: string
                      vatTrn:
                        type: string
                      effectiveDate:
                        type: string
                      legalType:
                        type: string
                      verificationStatus:
                        type: string
                      msgCode:
                        type: string
                      msgDesc:
                        type: string
                      classification:
                        type: string
                  operation:
                    type: object
                    properties:
                      type:
                        type: string
                      state:
                        type: string
                      selectedAt:
                        type: string
                      reasonPresent:
                        type: boolean
                      acknowledgedAt:
                        type: 'null'
                      confirmAttempts:
                        type: integer
                      inFlight:
                        type: boolean
                      result:
                        type: 'null'
                      updatedAt:
                        type: string
                  reused:
                    type: boolean
              example:
                ok: true
                correlationId: fta-3c9e…
                verification:
                  tin: 1004****59
                  email: o***@example.com
                  mobile: +97155****67
                  entityNameEn: Example Trading LLC
                  entityNameAr: …
                  vatTrn: 1004****59003
                  effectiveDate: '2026-01-01'
                  legalType: LLC
                  verificationStatus: 'True'
                  msgCode: …
                  msgDesc: …
                  classification: existing_linkage_verified
                operation:
                  type: reverify
                  state: reverify_verified
                  selectedAt: '2026-10-24T09:24:10.000Z'
                  reasonPresent: false
                  acknowledgedAt: null
                  confirmAttempts: 0
                  inFlight: false
                  result: null
                  updatedAt: '2026-10-24T09:24:10.000Z'
                reused: false
        '400':
          description: Field validation failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      fields:
                        type: object
                        properties:
                          email:
                            type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_INPUT_INVALID
                  message: Please correct the highlighted fields.
                  fields:
                    email: Enter the registered admin email address.
                  correlationId: fta-3c9e…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Not a `tenant_admin`, or the session belongs to another tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_ROLE_NOT_PERMITTED
                  message: Only an account administrator can manage the e-Invoicing linkage.
                  correlationId: fta-3c9e…
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: Unknown or expired linking session.
                  correlationId: fta-3c9e…
        '409':
          description: No action selected, session used/unclaimed, invalid transition, or FTA verified a different TIN.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_LINKAGE_ACTION_REQUIRED
                  message: Choose reverify or offboard first.
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: …
                  correlationId: fta-3c9e…
        '422':
          description: FTA did not verify the taxpayer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      msgCode:
                        type: string
                      msgDesc:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_VERIFICATION_FAILED
                  message: …
                  msgCode: …
                  msgDesc: …
                  correlationId: fta-3c9e…
        '429':
          description: Verification attempts for this session are spent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_TOO_MANY_ATTEMPTS
                  message: …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          description: An FTA gateway call failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_UPSTREAM_ERROR
                  message: …
                  correlationId: fta-3c9e…
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/linkage/reverify/confirm:
    post:
      operationId: postTenantFtaLinkageReverifyConfirm
      summary: Confirm reverification (FTA API-5 action 2)
      description: >-
        Requires `acknowledged: true`, a non-empty `reason` (whitespace-collapsed, max 500 characters), a verified
        reverify operation, and the TIN chain profile TIN = bound TIN = FTA-verified TIN. Then calls FTA API-5
        `crregupdate` action 2 and records the outcome on the durable profile.


        - FTA confirmed → `outcome: "reverified"`, `completed: true`; session spent; tenant lifecycle records the
        reverification.

        - Sent but unconfirmed → `200` with `outcome: "unconfirmed"`, `completed: false`.

        - Failed → `502` with `outcome: "failed"` and the `crreg` view in the body; when `retryable` the session stays
        verified and the same request may be repeated, otherwise the session is spent. The linkage is unchanged either
        way.


        Repeating a finished confirmation returns the recorded result. Audited (`fta.reverify.completed` / `.failed`).


        Only a `tenant_admin` of the tenant may run this step (`403 FTA_ROLE_NOT_PERMITTED` otherwise, audited).


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
                acknowledged:
                  const: true
              required:
                - sessionId
                - reason
                - acknowledged
            example:
              sessionId: kM3v…
              reason: Annual reverification requested by EmaraTax
              acknowledged: true
      responses:
        '200':
          description: Reverified (or sent and unconfirmed — see `outcome`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  requestId:
                    type: string
                  outcome:
                    type: string
                  completed:
                    type: boolean
                  crreg:
                    type: object
                    properties:
                      status:
                        type: string
                      action:
                        type: integer
                      msgCode:
                        type: string
                      errorCode:
                        type: 'null'
                      onboardingStatus:
                        type: string
                      verificationStatus:
                        type: string
                      recordUpdateTimestamp:
                        type: string
                      retryable:
                        type: boolean
                      diagnosticId:
                        type: string
                  summary:
                    type: object
                    properties:
                      tin:
                        type: string
                      peppolId:
                        type: string
                      verificationStatus:
                        type: string
                      linkageStatus:
                        type: string
                      aspName:
                        type: string
                  profileRecorded:
                    type: boolean
              example:
                ok: true
                correlationId: fta-3c9e…
                requestId: far_…
                outcome: reverified
                completed: true
                crreg:
                  status: ok
                  action: 2
                  msgCode: …
                  errorCode: null
                  onboardingStatus: Active
                  verificationStatus: 'True'
                  recordUpdateTimestamp: '2026-10-24T09:41:07Z'
                  retryable: false
                  diagnosticId: fxd_…
                summary:
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  verificationStatus: 'True'
                  linkageStatus: linked
                  aspName: AZDAN
                profileRecorded: true
        '400':
          description: Reason missing or acknowledgement absent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_REASON_REQUIRED
                  message: A reason is required.
                  correlationId: fta-3c9e…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Not a `tenant_admin`, or the session belongs to another tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_ROLE_NOT_PERMITTED
                  message: Only an account administrator can manage the e-Invoicing linkage.
                  correlationId: fta-3c9e…
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: Unknown or expired linking session.
                  correlationId: fta-3c9e…
        '409':
          description: Not verified, wrong operation, in progress, or TIN chain mismatch.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_OPERATION_IN_PROGRESS
                  message: This request is already being processed. Please wait for it to finish.
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          description: 'FTA did not accept or could not be reached. Body: the error plus `outcome` and `crreg`.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
                      retryable:
                        type: boolean
                  outcome:
                    type: string
                  crreg:
                    type: object
                    properties:
                      status:
                        type: string
                      action:
                        type: integer
                      msgCode:
                        type: string
                      errorCode:
                        type: 'null'
                      onboardingStatus:
                        type: 'null'
                      verificationStatus:
                        type: string
                      recordUpdateTimestamp:
                        type: string
                      retryable:
                        type: boolean
                      diagnosticId:
                        type: string
              example:
                error:
                  code: FTA_CENTRAL_REGISTRATION_FAILED
                  message: The Federal Tax Authority could not be reached. Your linkage is unchanged — please try again.
                  correlationId: fta-3c9e…
                  retryable: true
                outcome: failed
                crreg:
                  status: failed
                  action: 2
                  msgCode: …
                  errorCode: null
                  onboardingStatus: null
                  verificationStatus: 'True'
                  recordUpdateTimestamp: '2026-10-24T09:41:07Z'
                  retryable: true
                  diagnosticId: fxd_…
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/linkage/offboard/confirm:
    post:
      operationId: postTenantFtaLinkageOffboardConfirm
      summary: 'Confirm offboarding: remove from Peppol, then delink in EmaraTax'
      description: >-
        Same preconditions as reverify confirmation (acknowledgement, reason, verified offboard operation, TIN chain).
        Then, in the order the FTA End-User Process requires:


        1. **Peppol removal** — (a) the participant is offboarded from the AZDAN SMP (one `DELETE` with
        `delete-in-sml=true`; the platform reads back a 404; already absent counts as success) and the outcome is
        persisted; (b) the tenant's participant row `0235:<TIN>` in this environment is deactivated (never
        hard-deleted). If (a) fails nothing local changes and nothing is sent to FTA; if (b) fails a retry repeats only
        (b). Either failure answers `502` with `participantRemoval`/`smpUnpublish` in the body.

        2. **FTA API-5** `crregupdate` action 3.


        - FTA confirmed → `outcome: "offboarded"`, `completed: true`; session spent; the tenant lifecycle hook runs
        (offboarding the tenant's last taxpayer suspends the account and revokes its sessions) and the `fta_offboarded`
        email is queued.

        - FTA did not confirm → **partial state** `200` with `outcome: "fta_delink_pending"`, `completed: false`; the
        participant stays deactivated and the profile records the stage. When `retryable` the same session may retry
        step 2 only; otherwise an operator follow-up and the `fta_offboard_pending` email.


        If FTA already confirmed a delink for this TIN that the profile missed, the confirmation is recovered from
        diagnostics without calling FTA again. Audited (`fta.offboard.*`).


        Only a `tenant_admin` of the tenant may run this step (`403 FTA_ROLE_NOT_PERMITTED` otherwise, audited).


        `tenantId` always comes from the session principal; body fields `tenantId`, `environment` and `tin` are ignored
        (stripped by the schema). Responses carry `Cache-Control: no-store, private, max-age=0`. Errors are `{ error: {
        code, message, correlationId } }` with no upstream body. Counts against the tenant write limiter
        (`RATE_LIMIT_WRITE_MAX`, default 60/min per tenant).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                  minLength: 1
                  maxLength: 512
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
                acknowledged:
                  const: true
              required:
                - sessionId
                - reason
                - acknowledged
            example:
              sessionId: kM3v…
              reason: Moving to another accredited service provider
              acknowledged: true
      responses:
        '200':
          description: Offboarded, or the partial `fta_delink_pending` state — check `completed`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  correlationId:
                    type: string
                  requestId:
                    type: string
                  outcome:
                    type: string
                  completed:
                    type: boolean
                  participantRemoval:
                    type: object
                    properties:
                      status:
                        type: string
                      participantId:
                        type: string
                      peppolId:
                        type: string
                      previousStatus:
                        type: string
                      errorCode:
                        type: 'null'
                      at:
                        type: string
                  smpUnpublish:
                    type: object
                    properties:
                      status:
                        type: string
                      at:
                        type: string
                      serviceGroupId:
                        type: string
                      endpoint:
                        type: string
                      steps:
                        type: 'null'
                      verifiedAbsent:
                        type: boolean
                      errorCode:
                        type: 'null'
                  crreg:
                    type: object
                    properties:
                      status:
                        type: string
                      action:
                        type: integer
                      msgCode:
                        type: string
                      errorCode:
                        type: 'null'
                      onboardingStatus:
                        type: string
                      verificationStatus:
                        type: string
                      recordUpdateTimestamp:
                        type: string
                      retryable:
                        type: boolean
                      diagnosticId:
                        type: string
                  summary:
                    type: object
                    properties:
                      tin:
                        type: string
                      peppolId:
                        type: string
                      verificationStatus:
                        type: string
                      linkageStatus:
                        type: string
                      aspName:
                        type: string
                  profileRecorded:
                    type: boolean
              example:
                ok: true
                correlationId: fta-3c9e…
                requestId: far_…
                outcome: offboarded
                completed: true
                participantRemoval:
                  status: removed
                  participantId: ppt_…
                  peppolId: 0235:1004****59
                  previousStatus: active
                  errorCode: null
                  at: '2026-10-24T09:44:02.000Z'
                smpUnpublish:
                  status: unpublished
                  at: '2026-10-24T09:44:01.000Z'
                  serviceGroupId: iso6523-actorid-upis::0235:1004****59
                  endpoint: https://smp.example.net
                  steps: null
                  verifiedAbsent: true
                  errorCode: null
                crreg:
                  status: ok
                  action: 3
                  msgCode: …
                  errorCode: null
                  onboardingStatus: Inactive
                  verificationStatus: 'True'
                  recordUpdateTimestamp: '2026-10-24T09:41:07Z'
                  retryable: false
                  diagnosticId: fxd_…
                summary:
                  tin: 1004****59
                  peppolId: 0235:1004****59
                  verificationStatus: 'True'
                  linkageStatus: offboarded
                  aspName: AZDAN
                profileRecorded: true
        '400':
          description: Reason missing or acknowledgement absent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_REASON_REQUIRED
                  message: A reason is required.
                  correlationId: fta-3c9e…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Not a `tenant_admin`, or the session belongs to another tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_ROLE_NOT_PERMITTED
                  message: Only an account administrator can manage the e-Invoicing linkage.
                  correlationId: fta-3c9e…
        '404':
          description: Unknown session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_NOT_FOUND
                  message: Unknown or expired linking session.
                  correlationId: fta-3c9e…
        '409':
          description: Not verified, wrong operation, in progress, or TIN chain mismatch.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_OPERATION_IN_PROGRESS
                  message: This request is already being processed. Please wait for it to finish.
                  correlationId: fta-3c9e…
        '410':
          description: The session expired.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
              example:
                error:
                  code: FTA_SESSION_EXPIRED
                  message: …
                  correlationId: fta-3c9e…
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          description: >-
            The Peppol removal step failed; nothing was sent to FTA. Body: the error plus `outcome` and
            `participantRemoval`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      correlationId:
                        type: string
                      retryable:
                        type: boolean
                  outcome:
                    type: string
                  participantRemoval:
                    type: object
                    properties:
                      status:
                        type: string
                      participantId:
                        type: string
                      peppolId:
                        type: string
                      previousStatus:
                        type: 'null'
                      errorCode:
                        type: 'null'
                      at:
                        type: 'null'
              example:
                error:
                  code: FTA_PEPPOL_REMOVAL_FAILED
                  message: …
                  correlationId: fta-3c9e…
                  retryable: true
                outcome: failed
                participantRemoval:
                  status: not_attempted
                  participantId: ppt_…
                  peppolId: 0235:1004****59
                  previousStatus: null
                  errorCode: null
                  at: null
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/fta/profile:
    get:
      operationId: getTenantFtaProfile
      summary: Get the tenant's durable EmaraTax profiles
      description: >-
        Read-only FTA / EmaraTax status of the signed-in tenant: one profile per (environment, TIN), sorted by
        environment then TIN. Each carries the FTA-verified entity facts, ASP link, Peppol participant, central-registry
        (API-5) state, offboarding/reverification state and operation history, plus derived `overallStatus`,
        `nextAction`, `timeline` and `configurationHealthy` (FTA credentials configured). No credentials, session or
        authcode. `Cache-Control: no-store`. The example is abridged.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - EmaraTax (FTA)
      responses:
        '200':
          description: Profiles (possibly empty).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  profiles:
                    type: array
                    items:
                      type: object
                      properties:
                        tenantId:
                          type: string
                        environment:
                          type: string
                        tin:
                          type: string
                        verification:
                          type: object
                          properties:
                            status:
                              type: string
                            tin:
                              type: string
                            vatTrn:
                              type: string
                            entityNameEn:
                              type: string
                            entityNameAr:
                              type: string
                            legalType:
                              type: string
                            effectiveDate:
                              type: string
                            verificationStatus:
                              type: string
                            registeredAdminEmail:
                              type: string
                            mobile:
                              type: string
                            verifiedAt:
                              type: string
                        aspLink:
                          type: object
                          properties:
                            status:
                              type: string
                            linkedAt:
                              type: string
                            offboardedAt:
                              type: 'null'
                        peppol:
                          type: object
                          properties:
                            status:
                              type: string
                            participantId:
                              type: string
                            peppolId:
                              type: string
                            participantCreatedAt:
                              type: string
                        centralRegistry:
                          type: object
                          properties:
                            status:
                              type: string
                            onboardingStatus:
                              type: string
                            lastAction:
                              type: integer
                            activatedAt:
                              type: string
                        overallStatus:
                          type: string
                        nextAction:
                          type: 'null'
                        configurationHealthy:
                          type: boolean
              example:
                ok: true
                profiles:
                  - tenantId: tnt_7Q2X…
                    environment: production
                    tin: 1004****59
                    verification:
                      status: verified
                      tin: 1004****59
                      vatTrn: 1004****59003
                      entityNameEn: Example Trading LLC
                      entityNameAr: …
                      legalType: LLC
                      effectiveDate: '2026-01-01'
                      verificationStatus: 'True'
                      registeredAdminEmail: o***@example.com
                      mobile: +97155****67
                      verifiedAt: '2026-09-02T10:09:12.000Z'
                    aspLink:
                      status: linked
                      linkedAt: '2026-09-02T10:11:00.000Z'
                      offboardedAt: null
                    peppol:
                      status: active
                      participantId: ppt_…
                      peppolId: 0235:1004****59
                      participantCreatedAt: '2026-09-02T10:10:40.000Z'
                    centralRegistry:
                      status: ok
                      onboardingStatus: Active
                      lastAction: 1
                      activatedAt: '2026-09-02T10:11:00.000Z'
                    overallStatus: linked
                    nextAction: null
                    configurationHealthy: true
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/ap-transactions:
    get:
      operationId: getTenantApTransactions
      summary: List the tenant's access point transactions
      description: >-
        The access point's own transaction log for the caller's tenant: inbound rows whose receiver and outbound rows
        whose sender is one of the tenant's active Peppol participants (tenant_user: only participants on their
        subsidiary/participant allow-lists, re-read from the database on each call). Live and archived rows, merged
        newest first, with the same filters and page-number pagination as the admin list. Object-storage coordinates and
        the spool path are removed: `storage` keeps only `provider`, `fileType`, `sizeBytes`, `legacy`, and
        `documentPath` is `null`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 25
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - inbound
              - outbound
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: peppolId
          in: query
          required: false
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
      responses:
        '200':
          description: A page of AP rows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        source:
                          type: string
                        direction:
                          type: string
                        tenantId:
                          type: string
                        sender:
                          type: string
                        receiver:
                          type: string
                        documentType:
                          type: string
                        processId:
                          type: string
                        status:
                          type: string
                        mlsType:
                          type: string
                        mlsTo:
                          type: string
                        mlsResponseCode:
                          type: string
                        mlsOutboundTransactionId:
                          type: string
                        tddTransactionId:
                          type: string
                        tddSbdhInstanceId:
                          type: string
                        tddStatus:
                          type: string
                        tddCreatedAt:
                          type: string
                        tddLastError:
                          type: 'null'
                        receivedAt:
                          type: string
                        createdAt:
                          type: string
                        completedAt:
                          type: string
                        errorDetails:
                          type: 'null'
                        documentPath:
                          type: 'null'
                        documentSize:
                          type: integer
                        as4MessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        correlationId:
                          type: 'null'
                        storage:
                          type: object
                          properties:
                            provider:
                              type: string
                            fileType:
                              type: string
                            sizeBytes:
                              type: integer
                            legacy:
                              type: boolean
                        archived:
                          type: boolean
                        documentKind:
                          type: string
                        statusDetail:
                          type: object
                          properties:
                            documentKind:
                              type: string
                            overall:
                              type: string
                            processing:
                              type: object
                              properties:
                                platformStatus:
                                  type: 'null'
                                apStatus:
                                  type: string
                                source:
                                  type: string
                                createdAt:
                                  type: string
                                failureStage:
                                  type: 'null'
                            validation:
                              type: object
                              properties:
                                state:
                                  type: string
                                engine:
                                  type: string
                                at:
                                  type: string
                                warnings:
                                  type: integer
                                error:
                                  type: 'null'
                            delivery:
                              type: object
                              properties:
                                state:
                                  type: string
                                apStatus:
                                  type: string
                                platformStatus:
                                  type: 'null'
                                attempts:
                                  type: 'null'
                                lastAttemptAt:
                                  type: 'null'
                                completedAt:
                                  type: string
                                failedAt:
                                  type: 'null'
                                errorCode:
                                  type: 'null'
                                error:
                                  type: 'null'
                            mls:
                              type: object
                              properties:
                                role:
                                  type: string
                                state:
                                  type: string
                                code:
                                  type: string
                                raw:
                                  type: string
                                at:
                                  type: 'null'
                                mlsId:
                                  type: 'null'
                                errorCode:
                                  type: 'null'
                                errorMessage:
                                  type: 'null'
                                policy:
                                  type: string
                            reporting:
                              type: object
                              properties:
                                state:
                                  type: string
                                raw:
                                  type: string
                                at:
                                  type: string
                                error:
                                  type: 'null'
                                ftaResponse:
                                  type: 'null'
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - id: 6716f0aa…41d2
                    source: inbound_transactions
                    direction: inbound
                    tenantId: tnt_7Q2X…
                    sender: 0235:1004****59
                    receiver: 0235:1007****21
                    documentType: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    status: forwarded
                    mlsType: always
                    mlsTo: 0235:1004****59
                    mlsResponseCode: AP
                    mlsOutboundTransactionId: 6716f0b9…08ce
                    tddTransactionId: 6716f0c4…5a17
                    tddSbdhInstanceId: 3f1c9e2a-…
                    tddStatus: sent
                    tddCreatedAt: '2026-10-21T09:14:41.000Z'
                    tddLastError: null
                    receivedAt: '2026-10-21T09:14:03.000Z'
                    createdAt: '2026-10-21T09:14:03.000Z'
                    completedAt: '2026-10-21T09:14:41.000Z'
                    errorDetails: null
                    documentPath: null
                    documentSize: 18342
                    as4MessageId: a91f…@azdan-ap
                    sbdhInstanceId: b7d2e0f4-…
                    correlationId: null
                    storage:
                      provider: oci
                      fileType: payload
                      sizeBytes: 18342
                      legacy: false
                    archived: false
                    documentKind: invoice
                    statusDetail:
                      documentKind: invoice
                      overall: delivered
                      processing:
                        platformStatus: null
                        apStatus: forwarded
                        source: ap
                        createdAt: '2026-10-21T09:14:03.000Z'
                        failureStage: null
                      validation:
                        state: passed
                        engine: ap_core
                        at: '2026-10-21T09:14:03.000Z'
                        warnings: 0
                        error: null
                      delivery:
                        state: delivered
                        apStatus: forwarded
                        platformStatus: null
                        attempts: null
                        lastAttemptAt: null
                        completedAt: '2026-10-21T09:14:41.000Z'
                        failedAt: null
                        errorCode: null
                        error: null
                      mls:
                        role: sent
                        state: accepted
                        code: AP
                        raw: AP
                        at: null
                        mlsId: null
                        errorCode: null
                        errorMessage: null
                        policy: always
                      reporting:
                        state: reported
                        raw: sent
                        at: '2026-10-21T09:14:41.000Z'
                        error: null
                        ftaResponse: null
                  - id: 6716e8d1…77b3
                    source: outbound_transactions
                    direction: outbound
                    tenantId: tnt_7Q2X…
                    sender: 0235:1007****21
                    receiver: 0235:1004****59
                    documentType: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    status: sent
                    mlsStatus: received_ap
                    mlsReceivedAt: '2026-10-21T09:14:41.000Z'
                    mlsId: mls_…
                    mlsInboundTransactionId: null
                    mlsType: always
                    mlsErrorCode: null
                    mlsErrorMessage: null
                    mlsAs4MessageId: c03e…@ap.example
                    mlsRefToMessageId: e81a…@azdan-ap
                    mlsLastAttemptAt: '2026-10-21T09:14:41.000Z'
                    mlsAttemptCount: 1
                    parentTransactionId: null
                    parentSbdhInstanceId: null
                    parentAs4MessageId: null
                    parentDocumentType: null
                    generatedFrom: null
                    tddTypeCode: null
                    precedingTddTransactionId: null
                    precedingTddSbdhInstanceId: null
                    supersededByTransactionId: null
                    tddReason: null
                    tddFollowUpStatus: null
                    tddFollowUpError: null
                    receivedAt: '2026-10-21T09:14:03.000Z'
                    createdAt: '2026-10-21T09:14:03.000Z'
                    completedAt: '2026-10-21T09:14:41.000Z'
                    errorDetails: null
                    sendingError: null
                    as4ErrorCode: null
                    as4ErrorDetail: null
                    retryCount: 1
                    lastAttemptAt: '2026-10-21T09:14:03.000Z'
                    failureCategory: null
                    documentPath: null
                    documentSize: 21004
                    as4MessageId: e81a…@azdan-ap
                    sbdhInstanceId: 5aa0c7e1-…
                    correlationId: txn_8F2K…
                    storage:
                      provider: oci
                      fileType: sbdh
                      sizeBytes: 21004
                      legacy: false
                    archived: false
                    documentKind: invoice
                    statusDetail:
                      documentKind: invoice
                      overall: accepted
                      processing:
                        platformStatus: null
                        apStatus: sent
                        source: ap
                        createdAt: '2026-10-21T09:14:03.000Z'
                        failureStage: null
                      validation:
                        state: passed
                        engine: ap_core
                        at: '2026-10-21T09:14:03.000Z'
                        warnings: 0
                        error: null
                      delivery:
                        state: delivered
                        apStatus: sent
                        platformStatus: null
                        attempts: 1
                        lastAttemptAt: '2026-10-21T09:14:03.000Z'
                        completedAt: '2026-10-21T09:14:41.000Z'
                        failedAt: null
                        errorCode: null
                        error: null
                      mls:
                        role: received
                        state: accepted
                        code: AP
                        raw: received_ap
                        at: '2026-10-21T09:14:41.000Z'
                        mlsId: mls_…
                        errorCode: null
                        errorMessage: null
                        policy: always
                      reporting:
                        state: unknown
                page:
                  number: 1
                  size: 25
                  total: 2
                  totalPages: 1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/ap-transactions/{id}/tdd:
    get:
      operationId: getTenantApTransactionsByIdTdd
      summary: Get the UAE tax data document (TDD) of a transaction
      description: >-
        Resolves the TDD the access point generated for a transaction. `{id}` may be a platform transaction id or an AP
        inbound/outbound id. For an outbound invoice with several TDDs (Submit, then Withdraw/Resubmit) the current one
        is returned. Ownership is checked against the tenant's Peppol participants (not the tenant_user allow-lists).
        Side effect: if the TDD XML is not yet registered, it is uploaded to object storage and a `transaction_files`
        row is created before responding.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: TDD metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tdd:
                    type: object
                    properties:
                      fileId:
                        type: string
                      transactionId:
                        type: string
                      parentTransactionId:
                        type: string
                      apTddTransactionId:
                        type: string
                      status:
                        type: string
                      documentTypeId:
                        type: string
                      processId:
                        type: string
                      generatedAt:
                        type: string
                      sha256:
                        type: string
                      sizeBytes:
                        type: integer
                      contentType:
                        type: string
                      source:
                        type: string
                      sbdhInstanceId:
                        type: string
                      apCoreMessageId:
                        type: string
                      tddDownloadUrl:
                        type: string
              example:
                tdd:
                  fileId: fil_5Rk…
                  transactionId: txn_8F2K…
                  parentTransactionId: txn_8F2K…
                  apTddTransactionId: 6716f0c4…5a17
                  status: sent
                  documentTypeId: urn:peppol:edec:taxdata:…
                  processId: urn:peppol:edec:taxdata:…
                  generatedAt: '2026-10-21T09:14:41.000Z'
                  sha256: e2b7…9a01
                  sizeBytes: 6120
                  contentType: application/xml
                  source: ap_core_generated_tdd
                  sbdhInstanceId: 0d4f8b11-…
                  apCoreMessageId: f7c2…@azdan-ap
                  tddDownloadUrl: /api/tenant/ap-transactions/txn_8F2K…/tdd/download
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/ap-transactions/{id}/tdd/download:
    get:
      operationId: getTenantApTransactionsByIdTddDownload
      summary: Get a signed download URL for a transaction's TDD
      description: >-
        Same resolution and side effects as `GET …/{id}/tdd`, then creates a read-only OCI pre-authenticated request
        (PAR) for the TDD XML and returns its URL. The URL always expires after 15 minutes: `ttlSeconds` is accepted but
        ignored by the signer. Unlike the metadata route, the TDD fields are returned flat, not under `tdd`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: ttlSeconds
          in: query
          description: 'Accepted but ignored: every link is valid for 900 seconds.'
          schema:
            type: integer
      responses:
        '200':
          description: TDD metadata and a signed URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  fileId:
                    type: string
                  transactionId:
                    type: string
                  parentTransactionId:
                    type: string
                  apTddTransactionId:
                    type: string
                  status:
                    type: string
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  generatedAt:
                    type: string
                  sha256:
                    type: string
                  sizeBytes:
                    type: integer
                  contentType:
                    type: string
                  source:
                    type: string
                  sbdhInstanceId:
                    type: string
                  apCoreMessageId:
                    type: string
                  tddDownloadUrl:
                    type: string
                  downloadUrl:
                    type: string
                  ttlSeconds:
                    type: integer
              example:
                fileId: fil_5Rk…
                transactionId: txn_8F2K…
                parentTransactionId: txn_8F2K…
                apTddTransactionId: 6716f0c4…5a17
                status: sent
                documentTypeId: urn:peppol:edec:taxdata:…
                processId: urn:peppol:edec:taxdata:…
                generatedAt: '2026-10-21T09:14:41.000Z'
                sha256: e2b7…9a01
                sizeBytes: 6120
                contentType: application/xml
                source: ap_core_generated_tdd
                sbdhInstanceId: 0d4f8b11-…
                apCoreMessageId: f7c2…@azdan-ap
                tddDownloadUrl: /api/tenant/ap-transactions/txn_8F2K…/tdd/download
                downloadUrl: https://objectstorage.me-dubai-1.oraclecloud.com/p/••••/n/axq…/b/azdan-einvoice-…/o/…/tdd.xml
                ttlSeconds: 900
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          $ref: '#/components/responses/ErrBadGateway'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/ap-transactions/{id}:
    get:
      operationId: getTenantApTransactionsById
      summary: Get one of the tenant's access point transactions
      description: >-
        Returns one AP row by AP id from the collection named in `source`, only when its owning participant (receiver
        for inbound, sender for outbound) is visible to the caller; otherwise 404. Storage coordinates and the spool
        path are removed as in the list.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: source
          in: query
          required: true
          schema:
            type: string
            enum:
              - inbound_transactions
              - outbound_transactions
      responses:
        '200':
          description: The AP row.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  source:
                    type: string
                  direction:
                    type: string
                  tenantId:
                    type: string
                  sender:
                    type: string
                  receiver:
                    type: string
                  documentType:
                    type: string
                  processId:
                    type: string
                  status:
                    type: string
                  mlsStatus:
                    type: string
                  mlsReceivedAt:
                    type: string
                  mlsId:
                    type: string
                  mlsInboundTransactionId:
                    type: 'null'
                  mlsType:
                    type: string
                  mlsErrorCode:
                    type: 'null'
                  mlsErrorMessage:
                    type: 'null'
                  mlsAs4MessageId:
                    type: string
                  mlsRefToMessageId:
                    type: string
                  mlsLastAttemptAt:
                    type: string
                  mlsAttemptCount:
                    type: integer
                  parentTransactionId:
                    type: 'null'
                  parentSbdhInstanceId:
                    type: 'null'
                  parentAs4MessageId:
                    type: 'null'
                  parentDocumentType:
                    type: 'null'
                  generatedFrom:
                    type: 'null'
                  tddTypeCode:
                    type: 'null'
                  precedingTddTransactionId:
                    type: 'null'
                  precedingTddSbdhInstanceId:
                    type: 'null'
                  supersededByTransactionId:
                    type: 'null'
                  tddReason:
                    type: 'null'
                  tddFollowUpStatus:
                    type: 'null'
                  tddFollowUpError:
                    type: 'null'
                  receivedAt:
                    type: string
                  createdAt:
                    type: string
                  completedAt:
                    type: string
                  errorDetails:
                    type: 'null'
                  sendingError:
                    type: 'null'
                  as4ErrorCode:
                    type: 'null'
                  as4ErrorDetail:
                    type: 'null'
                  retryCount:
                    type: integer
                  lastAttemptAt:
                    type: string
                  failureCategory:
                    type: 'null'
                  documentPath:
                    type: 'null'
                  documentSize:
                    type: integer
                  as4MessageId:
                    type: string
                  sbdhInstanceId:
                    type: string
                  correlationId:
                    type: string
                  storage:
                    type: object
                    properties:
                      provider:
                        type: string
                      fileType:
                        type: string
                      sizeBytes:
                        type: integer
                      legacy:
                        type: boolean
                  archived:
                    type: boolean
                  documentKind:
                    type: string
                  statusDetail:
                    type: object
                    properties:
                      documentKind:
                        type: string
                      overall:
                        type: string
                      processing:
                        type: object
                        properties:
                          platformStatus:
                            type: 'null'
                          apStatus:
                            type: string
                          source:
                            type: string
                          createdAt:
                            type: string
                          failureStage:
                            type: 'null'
                      validation:
                        type: object
                        properties:
                          state:
                            type: string
                          engine:
                            type: string
                          at:
                            type: string
                          warnings:
                            type: integer
                          error:
                            type: 'null'
                      delivery:
                        type: object
                        properties:
                          state:
                            type: string
                          apStatus:
                            type: string
                          platformStatus:
                            type: 'null'
                          attempts:
                            type: integer
                          lastAttemptAt:
                            type: string
                          completedAt:
                            type: string
                          failedAt:
                            type: 'null'
                          errorCode:
                            type: 'null'
                          error:
                            type: 'null'
                      mls:
                        type: object
                        properties:
                          role:
                            type: string
                          state:
                            type: string
                          code:
                            type: string
                          raw:
                            type: string
                          at:
                            type: string
                          mlsId:
                            type: string
                          errorCode:
                            type: 'null'
                          errorMessage:
                            type: 'null'
                          policy:
                            type: string
                      reporting:
                        type: object
                        properties:
                          state:
                            type: string
              example:
                id: 6716e8d1…77b3
                source: outbound_transactions
                direction: outbound
                tenantId: tnt_7Q2X…
                sender: 0235:1007****21
                receiver: 0235:1004****59
                documentType: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                status: sent
                mlsStatus: received_ap
                mlsReceivedAt: '2026-10-21T09:14:41.000Z'
                mlsId: mls_…
                mlsInboundTransactionId: null
                mlsType: always
                mlsErrorCode: null
                mlsErrorMessage: null
                mlsAs4MessageId: c03e…@ap.example
                mlsRefToMessageId: e81a…@azdan-ap
                mlsLastAttemptAt: '2026-10-21T09:14:41.000Z'
                mlsAttemptCount: 1
                parentTransactionId: null
                parentSbdhInstanceId: null
                parentAs4MessageId: null
                parentDocumentType: null
                generatedFrom: null
                tddTypeCode: null
                precedingTddTransactionId: null
                precedingTddSbdhInstanceId: null
                supersededByTransactionId: null
                tddReason: null
                tddFollowUpStatus: null
                tddFollowUpError: null
                receivedAt: '2026-10-21T09:14:03.000Z'
                createdAt: '2026-10-21T09:14:03.000Z'
                completedAt: '2026-10-21T09:14:41.000Z'
                errorDetails: null
                sendingError: null
                as4ErrorCode: null
                as4ErrorDetail: null
                retryCount: 1
                lastAttemptAt: '2026-10-21T09:14:03.000Z'
                failureCategory: null
                documentPath: null
                documentSize: 21004
                as4MessageId: e81a…@azdan-ap
                sbdhInstanceId: 5aa0c7e1-…
                correlationId: txn_8F2K…
                storage:
                  provider: oci
                  fileType: sbdh
                  sizeBytes: 21004
                  legacy: false
                archived: false
                documentKind: invoice
                statusDetail:
                  documentKind: invoice
                  overall: accepted
                  processing:
                    platformStatus: null
                    apStatus: sent
                    source: ap
                    createdAt: '2026-10-21T09:14:03.000Z'
                    failureStage: null
                  validation:
                    state: passed
                    engine: ap_core
                    at: '2026-10-21T09:14:03.000Z'
                    warnings: 0
                    error: null
                  delivery:
                    state: delivered
                    apStatus: sent
                    platformStatus: null
                    attempts: 1
                    lastAttemptAt: '2026-10-21T09:14:03.000Z'
                    completedAt: '2026-10-21T09:14:41.000Z'
                    failedAt: null
                    errorCode: null
                    error: null
                  mls:
                    role: received
                    state: accepted
                    code: AP
                    raw: received_ap
                    at: '2026-10-21T09:14:41.000Z'
                    mlsId: mls_…
                    errorCode: null
                    errorMessage: null
                    policy: always
                  reporting:
                    state: unknown
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/invoices/templates/{kind}:
    get:
      operationId: getTenantInvoicesTemplatesByKind
      summary: Download an invoice starter template
      description: >-
        Static documentation artifacts — no tenant data is read. `kind=xml` returns a complete PINT-AE sample
        (`application/xml`, as an attachment; `documentType=creditNote` returns a credit note instead of an invoice),
        `kind=csv` returns the CSV import template (`text/csv`, one row per invoice line) and `kind=spec` returns the
        CSV column contract as JSON (shown below). Any other `kind` answers 400 `UNKNOWN_TEMPLATE`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Invoice sending
      parameters:
        - name: kind
          in: path
          required: true
          schema:
            type: string
        - name: documentType
          in: query
          description: 'For `kind=xml` only: which sample to build.'
          schema:
            type: string
            enum:
              - invoice
              - creditNote
            default: invoice
      responses:
        '200':
          description: The template. JSON for `spec`; an XML or CSV file download for `xml` / `csv`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  columns:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        required:
                          type: boolean
                        scope:
                          type: string
                        example:
                          type: string
                  rules:
                    type: array
                    items:
                      type: string
              example:
                columns:
                  - name: invoice_number
                    required: true
                    scope: invoice
                    example: INV-2026-0001
                  - name: customer_peppol_id
                    required: true
                    scope: invoice
                    example: '0235:1000000002'
                rules:
                  - One row per invoice line.
                  - >-
                    Rows that share the same invoice_number belong to the same invoice; the invoice-level columns are
                    read from that group's first row.
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/invoices/validate:
    post:
      operationId: postTenantInvoicesValidate
      summary: Validate an invoice without sending it
      description: >-
        Dry-run of the same validation pipeline as `POST /api/integrations/peppol/validate` (inputs `xml`, `json` or
        `mapped`), authenticated with the user's session instead of an API key. Nothing is persisted and no receiver is
        contacted. A `tenant_user` may only name a `senderPeppolId` on their allow-list; otherwise 403
        `SENDER_NOT_IN_USER_SCOPE` and a `denied` audit row. The sender must also belong to the tenant (403
        `SENDER_NOT_OWNED`). A document that fails validation answers 400 with `valid: false` and the rule details.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Invoice sending
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              anyOf:
                - type: object
                  properties:
                    inputType:
                      const: xml
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    xml:
                      type: string
                      minLength: 20
                      maxLength: 15728640
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - xml
                  additionalProperties: false
                - type: object
                  properties:
                    inputType:
                      const: json
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    payload:
                      type: object
                      additionalProperties: {}
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - payload
                  additionalProperties: false
                - type: object
                  properties:
                    inputType:
                      const: mapped
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    payload:
                      type: object
                      additionalProperties: {}
                    mappingId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    sourceSystem:
                      type: string
                      minLength: 1
                      maxLength: 60
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - payload
                  additionalProperties: false
            example:
              inputType: json
              environment: production
              documentType: invoice
              senderPeppolId: 0235:1004****59
              receiverPeppolId: 0235:1002****03
              payload:
                invoiceNumber: INV-2026-000123
                issueDate: '2026-10-20'
                dueDate: '2026-11-19'
                currency: AED
                buyer:
                  name: Sample Buyer FZ-LLC
                  peppolId: 0235:1002****03
                lines:
                  - description: Consulting services
                    quantity: 1
                    unitPrice: 1000
                    vatCategory: S
                    vatRate: 5
      responses:
        '200':
          description: The document is valid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  warnings:
                    type: array
                    items: {}
                  phormSkipped:
                    type: boolean
                  normalized:
                    type: object
                    properties:
                      sender:
                        type: string
                      receiver:
                        type: string
                      invoiceNumber:
                        type: string
                      issueDate:
                        type: string
                      currency:
                        type: string
                      payableAmount:
                        type: integer
                      invoiceTypeCode:
                        type: string
                      dueDate:
                        type: string
                      supplierName:
                        type: string
                      customerName:
                        type: string
                      netAmount:
                        type: integer
                      taxAmount:
                        type: integer
              example:
                valid: true
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                warnings: []
                phormSkipped: false
                normalized:
                  sender: 0235:1004****59
                  receiver: 0235:1002****03
                  invoiceNumber: INV-2026-000123
                  issueDate: '2026-10-20'
                  currency: AED
                  payableAmount: 1050
                  invoiceTypeCode: '380'
                  dueDate: '2026-11-19'
                  supplierName: Sample Trading LLC
                  customerName: Sample Buyer FZ-LLC
                  netAmount: 1000
                  taxAmount: 50
        '400':
          description: The document failed validation (or the request body is invalid — standard error shape).
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            message:
                              type: string
                            xpath:
                              type: string
                  warnings:
                    type: array
                    items: {}
              example:
                valid: false
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                error:
                  code: PINT_VALIDATION_FAILED
                  message: Document failed Peppol/PINT validation.
                  details:
                    - code: ibr-144-ae
                      message: Buyer address must contain street, city and country subdivision.
                      xpath: /Invoice/cac:AccountingCustomerParty
                warnings: []
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/invoices/send:
    post:
      operationId: postTenantInvoicesSend
      summary: Send an invoice over Peppol
      description: >-
        Validates and submits the document through the same pipeline as `POST /api/integrations/peppol/invoices`
        (storage, AP Core hand-off, outbox events, audit), authenticated with the user's session. The `Idempotency-Key`
        header is authoritative; a different `body.idempotencyKey` answers 400 `IDEMPOTENCY_KEY_CONFLICT`. A repeated
        key returns the original transaction with `idempotent: true`, except a failed transaction that never reached the
        access point, which is retried (or re-created with `forceResubmit: true`). A `tenant_user` may only send from an
        allowed participant (403 `SENDER_NOT_IN_USER_SCOPE`). The `documents` links in the response use the Integration
        API URLs (`/api/integrations/peppol/…`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Invoice sending
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Optional de-duplication key (1–120 characters), scoped to the tenant. A repeat with the same key returns the
            original transaction (`idempotent: true`) instead of sending again.
          schema:
            type: string
            maxLength: 120
            example: INV-2026-000123
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              anyOf:
                - type: object
                  properties:
                    inputType:
                      const: xml
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    xml:
                      type: string
                      minLength: 20
                      maxLength: 15728640
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - xml
                  additionalProperties: false
                - type: object
                  properties:
                    inputType:
                      const: json
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    payload:
                      type: object
                      additionalProperties: {}
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - payload
                  additionalProperties: false
                - type: object
                  properties:
                    inputType:
                      const: mapped
                      type: string
                    environment:
                      type: string
                      enum:
                        - test
                        - production
                      default: test
                    documentType:
                      type: string
                      enum:
                        - invoice
                        - creditNote
                    senderPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    receiverPeppolId:
                      type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    payload:
                      type: object
                      additionalProperties: {}
                    mappingId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    sourceSystem:
                      type: string
                      minLength: 1
                      maxLength: 60
                    correlationId:
                      type: string
                      minLength: 1
                      maxLength: 120
                    idempotencyKey:
                      type: string
                      minLength: 1
                      maxLength: 120
                    forceResubmit:
                      type: boolean
                  required:
                    - inputType
                    - documentType
                    - senderPeppolId
                    - receiverPeppolId
                    - payload
                  additionalProperties: false
            example:
              inputType: json
              environment: production
              documentType: invoice
              senderPeppolId: 0235:1004****59
              receiverPeppolId: 0235:1002****03
              payload:
                invoiceNumber: INV-2026-000123
                issueDate: '2026-10-20'
                dueDate: '2026-11-19'
                currency: AED
                buyer:
                  name: Sample Buyer FZ-LLC
                  peppolId: 0235:1002****03
                lines:
                  - description: Consulting services
                    quantity: 1
                    unitPrice: 1000
                    vatCategory: S
                    vatRate: 5
      responses:
        '202':
          description: Accepted for Peppol delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transactionId:
                    type: string
                  status:
                    type: string
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  correlationId:
                    type: string
                  as4MessageId:
                    type: string
                  sbdhInstanceId:
                    type: string
                  idempotent:
                    type: boolean
                  warnings:
                    type: array
                    items: {}
                  validatorPhormSkipped:
                    type: boolean
                  documents:
                    type: object
                    properties:
                      invoiceXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: string
                          fileName:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                      invoicePdf:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      sbdXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      tddXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      mlsXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      payloadXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          privileged:
                            type: boolean
                          requiredScope:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                  message:
                    type: string
              example:
                transactionId: txn_1789562400000_9f3a…
                status: queued
                valid: true
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                correlationId: cor_1789562400000_4d5e…
                as4MessageId: f0debc61-…@peppol.azdanapps.com
                sbdhInstanceId: 6c1d2e3f-…
                idempotent: false
                warnings: []
                validatorPhormSkipped: false
                documents:
                  invoiceXml:
                    available: true
                    fileId: file_01J9Z3…
                    fileName: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a…/files/invoice-xml
                  invoicePdf:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/pdf
                    downloadUrl: null
                    reason: not_generated
                  sbdXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_stored
                  tddXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_generated
                  mlsXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_received
                  payloadXml:
                    available: true
                    privileged: true
                    requiredScope: peppol:payload:read
                    contentType: application/xml
                    downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a…/xml
                message: Invoice accepted for Peppol delivery.
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '422':
          $ref: '#/components/responses/ErrUnprocessable'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          $ref: '#/components/responses/ErrBadGateway'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/notification-settings:
    get:
      operationId: getTenantNotificationSettings
      summary: Get the tenant's email notification settings
      description: >-
        Returns the tenant's notification settings (defaults when never saved, flagged `isDefault`), the resolved
        recipients (tenant admins, extra addresses and the effective list), and the platform's mail status: whether
        email is enabled and configured, which kinds are sent, and whether outside production all mail is redirected to
        a test mailbox. Readable by any tenant user.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: Settings view.
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      types:
                        type: object
                        properties:
                          deliveryFailed:
                            type: boolean
                          mlsRejected:
                            type: boolean
                          inboundReceived:
                            type: boolean
                          ftaLinkage:
                            type: boolean
                      notifyAdmins:
                        type: boolean
                      extraRecipients:
                        type: array
                        items:
                          type: string
                      updatedAt:
                        type: string
                      isDefault:
                        type: boolean
                  recipients:
                    type: object
                    properties:
                      admins:
                        type: array
                        items:
                          type: string
                      extra:
                        type: array
                        items:
                          type: string
                      effective:
                        type: array
                        items:
                          type: string
                  tenantName:
                    type: string
                  platform:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      configured:
                        type: boolean
                      kinds:
                        type: array
                        items:
                          type: string
                      redirected:
                        type: boolean
                  limits:
                    type: object
                    properties:
                      maxExtraRecipients:
                        type: integer
              example:
                settings:
                  enabled: true
                  types:
                    deliveryFailed: true
                    mlsRejected: true
                    inboundReceived: false
                    ftaLinkage: true
                  notifyAdmins: true
                  extraRecipients:
                    - f***@example.com
                  updatedAt: '2026-10-21T09:14:03.000Z'
                  isDefault: false
                recipients:
                  admins:
                    - o***@example.com
                  extra:
                    - f***@example.com
                  effective:
                    - o***@example.com
                    - f***@example.com
                tenantName: AZDAN Demo
                platform:
                  enabled: true
                  configured: true
                  kinds:
                    - delivery_failed
                    - mls_rejected
                    - inbound_received
                    - fta_onboarded
                    - fta_reverified
                    - fta_offboarded
                    - fta_offboard_pending
                    - tenant_suspended
                    - tenant_reactivated
                    - tenant_welcome
                  redirected: false
                limits:
                  maxExtraRecipients: 10
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    put:
      operationId: putTenantNotificationSettings
      summary: Save the tenant's email notification settings
      description: >-
        Replaces the tenant's notification settings: master switch, per-type switches, whether tenant admins receive
        mail, and extra recipients (trimmed, lower-cased, de-duplicated, at most 10 after normalisation). `tenant_admin`
        only; denials are audited. Returns the same view as the GET. The audit row records which fields changed and a
        recipient count, never addresses.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                types:
                  type: object
                  properties:
                    deliveryFailed:
                      type: boolean
                    mlsRejected:
                      type: boolean
                    inboundReceived:
                      type: boolean
                    ftaLinkage:
                      type: boolean
                  required:
                    - deliveryFailed
                    - mlsRejected
                    - inboundReceived
                    - ftaLinkage
                  additionalProperties: false
                notifyAdmins:
                  type: boolean
                extraRecipients:
                  type: array
                  items:
                    type: string
                    maxLength: 254
                  maxItems: 50
                  default: []
              required:
                - enabled
                - types
                - notifyAdmins
              additionalProperties: false
            example:
              enabled: true
              types:
                deliveryFailed: true
                mlsRejected: true
                inboundReceived: false
                ftaLinkage: true
              notifyAdmins: true
              extraRecipients:
                - f***@example.com
      responses:
        '200':
          description: The updated settings view (same shape as the GET).
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      types:
                        type: object
                        properties:
                          deliveryFailed:
                            type: boolean
                          mlsRejected:
                            type: boolean
                          inboundReceived:
                            type: boolean
                          ftaLinkage:
                            type: boolean
                      notifyAdmins:
                        type: boolean
                      extraRecipients:
                        type: array
                        items:
                          type: string
                      updatedAt:
                        type: string
                      isDefault:
                        type: boolean
                  recipients:
                    type: object
                    properties:
                      admins:
                        type: array
                        items:
                          type: string
                      extra:
                        type: array
                        items:
                          type: string
                      effective:
                        type: array
                        items:
                          type: string
                  tenantName:
                    type: string
                  platform:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      configured:
                        type: boolean
                      kinds:
                        type: array
                        items:
                          type: string
                      redirected:
                        type: boolean
                  limits:
                    type: object
                    properties:
                      maxExtraRecipients:
                        type: integer
              example:
                settings:
                  enabled: true
                  types:
                    deliveryFailed: true
                    mlsRejected: true
                    inboundReceived: false
                    ftaLinkage: true
                  notifyAdmins: true
                  extraRecipients:
                    - f***@example.com
                  updatedAt: '2026-10-21T09:14:03.000Z'
                  isDefault: false
                recipients:
                  admins:
                    - o***@example.com
                  extra:
                    - f***@example.com
                  effective:
                    - o***@example.com
                    - f***@example.com
                tenantName: AZDAN Demo
                platform:
                  enabled: true
                  configured: true
                  kinds:
                    - delivery_failed
                    - mls_rejected
                  redirected: false
                limits:
                  maxExtraRecipients: 10
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/notification-settings/test:
    post:
      operationId: postTenantNotificationSettingsTest
      summary: Send a test notification email
      description: >-
        Sends a bilingual test email to the recipients the settings name (admins when `notifyAdmins`, plus extra
        addresses), regardless of the master switch. At most once a minute per tenant. Each send is logged in the mail
        notification log. `tenant_admin` only. Per-recipient results include the mail server's error text on failure.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Send results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sent:
                    type: integer
                  failed:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        recipient:
                          type: string
                        status:
                          type: string
                  redirected:
                    type: boolean
              example:
                sent: 1
                failed: 1
                results:
                  - recipient: o***@example.com
                    status: sent
                  - recipient: f***@example.com
                    status: failed
                    error: 550 Mailbox unavailable
                redirected: false
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '429':
          $ref: '#/components/responses/ErrTooManyRequests'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/notification-settings/history:
    get:
      operationId: getTenantNotificationSettingsHistory
      summary: List the tenant's recent notification emails
      description: >-
        The tenant's own notification emails, newest first (`limit` 1–100, default 25), excluding ones skipped by
        settings. Shows whether each failed but not the mail server's error text.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Recent emails.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        kind:
                          type: string
                        recipient:
                          type: string
                        status:
                          type: string
                        attempts:
                          type: integer
                        transactionId:
                          type: string
                        createdAt:
                          type: string
                        sentAt:
                          type: string
                        failed:
                          type: boolean
              example:
                items:
                  - id: 6716f2b0…3c11
                    kind: delivery_failed
                    recipient: o***@example.com
                    status: sent
                    attempts: 1
                    transactionId: txn_8F2K…
                    createdAt: '2026-10-21T09:14:03.000Z'
                    sentAt: '2026-10-21T09:14:41.000Z'
                    failed: false
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/company-profiles:
    get:
      operationId: getTenantCompanyProfiles
      summary: List company invoicing profiles
      description: >-
        Lists the tenant's subsidiaries (default first) with their seller invoicing profile and a server-derived
        `missingFields` checklist naming the PINT-AE rule behind each gap (e.g. `ibr-150-ae`). `uaeScheme` is true when
        the subsidiary has an active outbound participant with EAS `0235`, which makes trade-licence fields mandatory.
        tenant_user sees only subsidiaries on their allow-list. Not paginated.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      responses:
        '200':
          description: Company profiles.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        subsidiaryId:
                          type: string
                        name:
                          type: string
                        legalName:
                          type: string
                        country:
                          type: string
                        taxRegistrationNumber:
                          type: string
                        defaultCurrency:
                          type: string
                        isDefault:
                          type: boolean
                        status:
                          type: string
                        invoiceProfile:
                          type: object
                          properties:
                            legalRegistrationId:
                              type: string
                            legalRegistrationType:
                              type: string
                            legalRegistrationAuthorityName:
                              type: string
                            address:
                              type: object
                              properties:
                                street:
                                  type: string
                                additionalStreetLine:
                                  type: string
                                city:
                                  type: string
                                region:
                                  type: string
                                postalCode:
                                  type: string
                                country:
                                  type: string
                            defaultPaymentMeansCode:
                              type: string
                            defaultPaymentAccountId:
                              type: string
                            defaultPaymentTermsNote:
                              type: string
                            updatedAt:
                              type: string
                        uaeScheme:
                          type: boolean
                        missingFields:
                          type: array
                          items:
                            type: object
                            properties:
                              field:
                                type: string
                              label:
                                type: string
                              rule:
                                type: string
                              why:
                                type: string
              example:
                items:
                  - subsidiaryId: sub_2Lm…
                    name: AZDAN Demo
                    legalName: AZDAN Demo Trading L.L.C.
                    country: AE
                    taxRegistrationNumber: 1007****03
                    defaultCurrency: AED
                    isDefault: true
                    status: active
                    invoiceProfile:
                      legalRegistrationId: CN-98****
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: ''
                      address:
                        street: Al Wasl Road
                        additionalStreetLine: ''
                        city: Dubai
                        region: DXB
                        postalCode: '00000'
                        country: AE
                      defaultPaymentMeansCode: '30'
                      defaultPaymentAccountId: AE07****0001
                      defaultPaymentTermsNote: Net 30
                      updatedAt: '2026-10-21T09:14:03.000Z'
                    uaeScheme: true
                    missingFields:
                      - field: invoiceProfile.legalRegistrationAuthorityName
                        label: Issuing authority
                        rule: ibr-172-ae
                        why: Mandatory for a trade licence; for a passport it must be the ISO 3166-1 issuing country.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/company-profiles/{subsidiaryId}:
    put:
      operationId: putTenantCompanyProfilesBySubsidiaryId
      summary: Update a company invoicing profile
      description: >-
        Updates the seller invoicing profile of one of the tenant's subsidiaries; only the fields sent are changed
        (partial update despite PUT). `address`, when sent, replaces the whole address and its `country` sets the
        subsidiary's country. `legalName`, `taxRegistrationNumber` and `defaultCurrency` are written to the subsidiary
        itself. `tenant_admin` only; audited. Returns the recomputed profile with `missingFields`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: subsidiaryId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                legalName:
                  type: string
                  maxLength: 200
                taxRegistrationNumber:
                  type: string
                  maxLength: 80
                defaultCurrency:
                  anyOf:
                    - type: string
                      pattern: ^[A-Za-z]{3}$
                    - const: ''
                      type: string
                legalRegistrationId:
                  type: string
                  maxLength: 80
                legalRegistrationType:
                  type: string
                  enum:
                    - ''
                    - TL
                    - EID
                    - PAS
                    - CD
                legalRegistrationAuthorityName:
                  type: string
                  maxLength: 200
                address:
                  type: object
                  properties:
                    street:
                      type: string
                      maxLength: 200
                    additionalStreetLine:
                      type: string
                      maxLength: 200
                    city:
                      type: string
                      maxLength: 120
                    region:
                      type: string
                      maxLength: 120
                    postalCode:
                      type: string
                      maxLength: 40
                    country:
                      anyOf:
                        - type: string
                          pattern: ^[A-Za-z]{2}$
                        - const: ''
                          type: string
                  additionalProperties: false
                defaultPaymentMeansCode:
                  anyOf:
                    - type: string
                      pattern: ^[0-9]{1,3}$
                    - const: ''
                      type: string
                defaultPaymentAccountId:
                  type: string
                  maxLength: 80
                defaultPaymentTermsNote:
                  type: string
                  maxLength: 500
              additionalProperties: false
            example:
              legalRegistrationId: CN-98****
              legalRegistrationType: TL
              legalRegistrationAuthorityName: Dubai Economy and Tourism
              address:
                street: Al Wasl Road
                city: Dubai
                region: DXB
                postalCode: '00000'
                country: AE
              defaultPaymentMeansCode: '30'
              defaultPaymentAccountId: AE07****0001
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  companyProfile:
                    type: object
                    properties:
                      subsidiaryId:
                        type: string
                      name:
                        type: string
                      legalName:
                        type: string
                      country:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      defaultCurrency:
                        type: string
                      isDefault:
                        type: boolean
                      status:
                        type: string
                      invoiceProfile:
                        type: object
                        properties:
                          legalRegistrationId:
                            type: string
                          legalRegistrationType:
                            type: string
                          legalRegistrationAuthorityName:
                            type: string
                          address:
                            type: object
                            properties:
                              street:
                                type: string
                              additionalStreetLine:
                                type: string
                              city:
                                type: string
                              region:
                                type: string
                              postalCode:
                                type: string
                              country:
                                type: string
                          defaultPaymentMeansCode:
                            type: string
                          defaultPaymentAccountId:
                            type: string
                          defaultPaymentTermsNote:
                            type: string
                          updatedAt:
                            type: string
                      uaeScheme:
                        type: boolean
                      missingFields:
                        type: array
                        items: {}
              example:
                companyProfile:
                  subsidiaryId: sub_2Lm…
                  name: AZDAN Demo
                  legalName: AZDAN Demo Trading L.L.C.
                  country: AE
                  taxRegistrationNumber: 1007****03
                  defaultCurrency: AED
                  isDefault: true
                  status: active
                  invoiceProfile:
                    legalRegistrationId: CN-98****
                    legalRegistrationType: TL
                    legalRegistrationAuthorityName: Dubai Economy and Tourism
                    address:
                      street: Al Wasl Road
                      additionalStreetLine: ''
                      city: Dubai
                      region: DXB
                      postalCode: '00000'
                      country: AE
                    defaultPaymentMeansCode: '30'
                    defaultPaymentAccountId: AE07****0001
                    defaultPaymentTermsNote: Net 30
                    updatedAt: '2026-10-21T09:14:03.000Z'
                  uaeScheme: true
                  missingFields: []
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/customers:
    get:
      operationId: getTenantCustomers
      summary: List customers
      description: >-
        Lists the tenant's customers sorted by display name. Defaults to `status=active` (`all` includes disabled);
        archived (deleted) customers never appear. `q` searches display name, legal name, Peppol ID and TRN
        (case-insensitive); `subsidiaryId` narrows to one company. tenant_user sees tenant-wide customers plus those of
        subsidiaries on their allow-list. Page-number pagination (default 50, max 200).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
            maxLength: 120
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - disabled
              - all
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
            maxLength: 80
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
      responses:
        '200':
          description: A page of customers.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        customerId:
                          type: string
                        subsidiaryId:
                          type: string
                        displayName:
                          type: string
                        legalName:
                          type: string
                        peppolId:
                          type: string
                        schemeId:
                          type: string
                        endpointValue:
                          type: string
                        taxRegistrationNumber:
                          type: string
                        legalRegistrationId:
                          type: string
                        legalRegistrationType:
                          type: string
                        legalRegistrationAuthorityName:
                          type: string
                        address:
                          type: object
                          properties:
                            street:
                              type: string
                            additionalStreetLine:
                              type: string
                            city:
                              type: string
                            region:
                              type: string
                            postalCode:
                              type: string
                            country:
                              type: string
                        contact:
                          type: object
                          properties:
                            email:
                              type: string
                            phone:
                              type: string
                        notes:
                          type: string
                        status:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - customerId: cust_a81f…
                    subsidiaryId: ''
                    displayName: Gulf Trading
                    legalName: Gulf Trading L.L.C.
                    peppolId: 0235:1004****59
                    schemeId: '0235'
                    endpointValue: 1004****59
                    taxRegistrationNumber: 1004****59
                    legalRegistrationId: CN-12****
                    legalRegistrationType: TL
                    legalRegistrationAuthorityName: Dubai Economy and Tourism
                    address:
                      street: Sheikh Zayed Road
                      additionalStreetLine: ''
                      city: Dubai
                      region: DXB
                      postalCode: '00000'
                      country: AE
                    contact:
                      email: o***@example.com
                      phone: +9714*****00
                    notes: ''
                    status: active
                    createdAt: '2026-08-04T07:30:00.000Z'
                    updatedAt: '2026-10-21T09:14:03.000Z'
                page:
                  number: 1
                  size: 50
                  total: 1
                  totalPages: 1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantCustomers
      summary: Create a customer
      description: >-
        Creates a customer. `peppolId` must be `NNNN:value` with an EAS scheme on the OpenPeppol active list and is
        split into `schemeId` / `endpointValue`; country codes are upper-cased. A Peppol ID already used by another
        customer of the tenant returns 409. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName:
                  type: string
                  minLength: 1
                  maxLength: 200
                legalName:
                  type: string
                  maxLength: 200
                subsidiaryId:
                  type: string
                  maxLength: 80
                peppolId:
                  anyOf:
                    - type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    - const: ''
                      type: string
                taxRegistrationNumber:
                  type: string
                  maxLength: 80
                legalRegistrationId:
                  type: string
                  maxLength: 80
                legalRegistrationType:
                  type: string
                  enum:
                    - ''
                    - TL
                    - CL
                    - EID
                    - PAS
                    - CD
                legalRegistrationAuthorityName:
                  type: string
                  maxLength: 200
                address:
                  type: object
                  properties:
                    street:
                      type: string
                      maxLength: 200
                    additionalStreetLine:
                      type: string
                      maxLength: 200
                    city:
                      type: string
                      maxLength: 120
                    region:
                      type: string
                      maxLength: 120
                    postalCode:
                      type: string
                      maxLength: 40
                    country:
                      anyOf:
                        - type: string
                          pattern: ^[A-Za-z]{2}$
                        - const: ''
                          type: string
                  additionalProperties: false
                contact:
                  type: object
                  properties:
                    email:
                      anyOf:
                        - type: string
                          format: email
                          maxLength: 200
                        - const: ''
                          type: string
                    phone:
                      type: string
                      maxLength: 60
                  additionalProperties: false
                notes:
                  type: string
                  maxLength: 1000
                status:
                  type: string
                  enum:
                    - active
                    - disabled
              required:
                - displayName
              additionalProperties: false
            example:
              displayName: Gulf Trading
              legalName: Gulf Trading L.L.C.
              peppolId: 0235:1004****59
              taxRegistrationNumber: 1004****59
              legalRegistrationId: CN-12****
              legalRegistrationType: TL
              legalRegistrationAuthorityName: Dubai Economy and Tourism
              address:
                street: Sheikh Zayed Road
                city: Dubai
                region: DXB
                postalCode: '00000'
                country: AE
              contact:
                email: o***@example.com
                phone: +9714*****00
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer:
                    type: object
                    properties:
                      customerId:
                        type: string
                      subsidiaryId:
                        type: string
                      displayName:
                        type: string
                      legalName:
                        type: string
                      peppolId:
                        type: string
                      schemeId:
                        type: string
                      endpointValue:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      legalRegistrationId:
                        type: string
                      legalRegistrationType:
                        type: string
                      legalRegistrationAuthorityName:
                        type: string
                      address:
                        type: object
                        properties:
                          street:
                            type: string
                          additionalStreetLine:
                            type: string
                          city:
                            type: string
                          region:
                            type: string
                          postalCode:
                            type: string
                          country:
                            type: string
                      contact:
                        type: object
                        properties:
                          email:
                            type: string
                          phone:
                            type: string
                      notes:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                customer:
                  customerId: cust_a81f…
                  subsidiaryId: ''
                  displayName: Gulf Trading
                  legalName: Gulf Trading L.L.C.
                  peppolId: 0235:1004****59
                  schemeId: '0235'
                  endpointValue: 1004****59
                  taxRegistrationNumber: 1004****59
                  legalRegistrationId: CN-12****
                  legalRegistrationType: TL
                  legalRegistrationAuthorityName: Dubai Economy and Tourism
                  address:
                    street: Sheikh Zayed Road
                    additionalStreetLine: ''
                    city: Dubai
                    region: DXB
                    postalCode: '00000'
                    country: AE
                  contact:
                    email: o***@example.com
                    phone: +9714*****00
                  notes: ''
                  status: active
                  createdAt: '2026-08-04T07:30:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/customers/{customerId}:
    get:
      operationId: getTenantCustomersByCustomerId
      summary: Get a customer
      description: >-
        Returns one non-archived customer. A subsidiary-scoped customer outside a tenant_user's allow-list returns 404,
        the same as a missing one.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The customer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer:
                    type: object
                    properties:
                      customerId:
                        type: string
                      subsidiaryId:
                        type: string
                      displayName:
                        type: string
                      legalName:
                        type: string
                      peppolId:
                        type: string
                      schemeId:
                        type: string
                      endpointValue:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      legalRegistrationId:
                        type: string
                      legalRegistrationType:
                        type: string
                      legalRegistrationAuthorityName:
                        type: string
                      address:
                        type: object
                        properties:
                          street:
                            type: string
                          additionalStreetLine:
                            type: string
                          city:
                            type: string
                          region:
                            type: string
                          postalCode:
                            type: string
                          country:
                            type: string
                      contact:
                        type: object
                        properties:
                          email:
                            type: string
                          phone:
                            type: string
                      notes:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                customer:
                  customerId: cust_a81f…
                  subsidiaryId: ''
                  displayName: Gulf Trading
                  legalName: Gulf Trading L.L.C.
                  peppolId: 0235:1004****59
                  schemeId: '0235'
                  endpointValue: 1004****59
                  taxRegistrationNumber: 1004****59
                  legalRegistrationId: CN-12****
                  legalRegistrationType: TL
                  legalRegistrationAuthorityName: Dubai Economy and Tourism
                  address:
                    street: Sheikh Zayed Road
                    additionalStreetLine: ''
                    city: Dubai
                    region: DXB
                    postalCode: '00000'
                    country: AE
                  contact:
                    email: o***@example.com
                    phone: +9714*****00
                  notes: ''
                  status: active
                  createdAt: '2026-08-04T07:30:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    delete:
      operationId: deleteTenantCustomersByCustomerId
      summary: Archive a customer
      description: >-
        Soft delete: sets `deletedAt` and `status: disabled`. The customer disappears from lists and detail reads; the
        record is kept. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  customerId:
                    type: string
              example:
                ok: true
                customerId: cust_a81f…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    patch:
      operationId: patchTenantCustomersByCustomerId
      summary: Update a customer
      description: >-
        Updates only the fields sent. Sending `address` or `contact` replaces that whole sub-object; sending `peppolId`
        re-derives `schemeId` / `endpointValue`. `tenant_admin` only; audited with the list of changed fields.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: customerId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName:
                  type: string
                  minLength: 1
                  maxLength: 200
                legalName:
                  type: string
                  maxLength: 200
                subsidiaryId:
                  type: string
                  maxLength: 80
                peppolId:
                  anyOf:
                    - type: string
                      minLength: 3
                      maxLength: 200
                      pattern: ^[0-9]{4}:.+$
                    - const: ''
                      type: string
                taxRegistrationNumber:
                  type: string
                  maxLength: 80
                legalRegistrationId:
                  type: string
                  maxLength: 80
                legalRegistrationType:
                  type: string
                  enum:
                    - ''
                    - TL
                    - CL
                    - EID
                    - PAS
                    - CD
                legalRegistrationAuthorityName:
                  type: string
                  maxLength: 200
                address:
                  type: object
                  properties:
                    street:
                      type: string
                      maxLength: 200
                    additionalStreetLine:
                      type: string
                      maxLength: 200
                    city:
                      type: string
                      maxLength: 120
                    region:
                      type: string
                      maxLength: 120
                    postalCode:
                      type: string
                      maxLength: 40
                    country:
                      anyOf:
                        - type: string
                          pattern: ^[A-Za-z]{2}$
                        - const: ''
                          type: string
                  additionalProperties: false
                contact:
                  type: object
                  properties:
                    email:
                      anyOf:
                        - type: string
                          format: email
                          maxLength: 200
                        - const: ''
                          type: string
                    phone:
                      type: string
                      maxLength: 60
                  additionalProperties: false
                notes:
                  type: string
                  maxLength: 1000
                status:
                  type: string
                  enum:
                    - active
                    - disabled
              additionalProperties: false
            example:
              contact:
                email: o***@example.com
                phone: +9714*****00
              status: active
      responses:
        '200':
          description: The updated customer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer:
                    type: object
                    properties:
                      customerId:
                        type: string
                      subsidiaryId:
                        type: string
                      displayName:
                        type: string
                      legalName:
                        type: string
                      peppolId:
                        type: string
                      schemeId:
                        type: string
                      endpointValue:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      legalRegistrationId:
                        type: string
                      legalRegistrationType:
                        type: string
                      legalRegistrationAuthorityName:
                        type: string
                      address:
                        type: object
                        properties:
                          street:
                            type: string
                          additionalStreetLine:
                            type: string
                          city:
                            type: string
                          region:
                            type: string
                          postalCode:
                            type: string
                          country:
                            type: string
                      contact:
                        type: object
                        properties:
                          email:
                            type: string
                          phone:
                            type: string
                      notes:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                customer:
                  customerId: cust_a81f…
                  subsidiaryId: ''
                  displayName: Gulf Trading
                  legalName: Gulf Trading L.L.C.
                  peppolId: 0235:1004****59
                  schemeId: '0235'
                  endpointValue: 1004****59
                  taxRegistrationNumber: 1004****59
                  legalRegistrationId: CN-12****
                  legalRegistrationType: TL
                  legalRegistrationAuthorityName: Dubai Economy and Tourism
                  address:
                    street: Sheikh Zayed Road
                    additionalStreetLine: ''
                    city: Dubai
                    region: DXB
                    postalCode: '00000'
                    country: AE
                  contact:
                    email: o***@example.com
                    phone: +9714*****00
                  notes: ''
                  status: active
                  createdAt: '2026-08-04T07:30:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/items:
    get:
      operationId: getTenantItems
      summary: List items
      description: >-
        Lists the tenant's items sorted by name. Defaults to `status=active` (`all` includes disabled); archived items
        never appear. `q` searches name, description and code; `subsidiaryId` narrows to one company. tenant_user sees
        tenant-wide items plus those of subsidiaries on their allow-list. Page-number pagination (default 50, max 200).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
            maxLength: 120
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - disabled
              - all
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
            maxLength: 80
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
      responses:
        '200':
          description: A page of items.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        itemId:
                          type: string
                        subsidiaryId:
                          type: string
                        code:
                          type: string
                        name:
                          type: string
                        description:
                          type: string
                        unitCode:
                          type: string
                        unitLabel:
                          type: string
                        defaultUnitPrice:
                          type: string
                        currency:
                          type: string
                        vatCategory:
                          type: string
                        vatPercent:
                          type: string
                        status:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - itemId: item_3c9d…
                    subsidiaryId: ''
                    code: SRV-001
                    name: Consulting services
                    description: Hourly consulting
                    unitCode: HUR
                    unitLabel: Hour
                    defaultUnitPrice: '450.00'
                    currency: AED
                    vatCategory: S
                    vatPercent: '5'
                    status: active
                    createdAt: '2026-08-04T07:35:00.000Z'
                    updatedAt: '2026-10-21T09:14:03.000Z'
                page:
                  number: 1
                  size: 50
                  total: 1
                  totalPages: 1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantItems
      summary: Create an item
      description: >-
        Creates a catalogue item. Prices and VAT percentages are decimal strings, kept as strings end to end; `currency`
        is upper-cased. An item code already used in the tenant returns 409. `unitCode` reads back as `EA` when empty.
        `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type: string
                  maxLength: 1000
                code:
                  type: string
                  maxLength: 80
                subsidiaryId:
                  type: string
                  maxLength: 80
                unitCode:
                  type: string
                  maxLength: 12
                unitLabel:
                  type: string
                  maxLength: 60
                defaultUnitPrice:
                  anyOf:
                    - type: string
                      pattern: ^-?\d{1,15}(\.\d{1,6})?$
                    - const: ''
                      type: string
                currency:
                  anyOf:
                    - type: string
                      pattern: ^[A-Za-z]{3}$
                    - const: ''
                      type: string
                vatCategory:
                  type: string
                  maxLength: 4
                vatPercent:
                  anyOf:
                    - type: string
                      pattern: ^\d{1,3}(\.\d{1,4})?$
                    - const: ''
                      type: string
                status:
                  type: string
                  enum:
                    - active
                    - disabled
              required:
                - name
              additionalProperties: false
            example:
              name: Consulting services
              description: Hourly consulting
              code: SRV-001
              unitCode: HUR
              unitLabel: Hour
              defaultUnitPrice: '450.00'
              currency: AED
              vatCategory: S
              vatPercent: '5'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  item:
                    type: object
                    properties:
                      itemId:
                        type: string
                      subsidiaryId:
                        type: string
                      code:
                        type: string
                      name:
                        type: string
                      description:
                        type: string
                      unitCode:
                        type: string
                      unitLabel:
                        type: string
                      defaultUnitPrice:
                        type: string
                      currency:
                        type: string
                      vatCategory:
                        type: string
                      vatPercent:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                item:
                  itemId: item_3c9d…
                  subsidiaryId: ''
                  code: SRV-001
                  name: Consulting services
                  description: Hourly consulting
                  unitCode: HUR
                  unitLabel: Hour
                  defaultUnitPrice: '450.00'
                  currency: AED
                  vatCategory: S
                  vatPercent: '5'
                  status: active
                  createdAt: '2026-08-04T07:35:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/items/{itemId}:
    get:
      operationId: getTenantItemsByItemId
      summary: Get an item
      description: >-
        Returns one non-archived item. A subsidiary-scoped item outside a tenant_user's allow-list returns 404, the same
        as a missing one.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The item.
          content:
            application/json:
              schema:
                type: object
                properties:
                  item:
                    type: object
                    properties:
                      itemId:
                        type: string
                      subsidiaryId:
                        type: string
                      code:
                        type: string
                      name:
                        type: string
                      description:
                        type: string
                      unitCode:
                        type: string
                      unitLabel:
                        type: string
                      defaultUnitPrice:
                        type: string
                      currency:
                        type: string
                      vatCategory:
                        type: string
                      vatPercent:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                item:
                  itemId: item_3c9d…
                  subsidiaryId: ''
                  code: SRV-001
                  name: Consulting services
                  description: Hourly consulting
                  unitCode: HUR
                  unitLabel: Hour
                  defaultUnitPrice: '450.00'
                  currency: AED
                  vatCategory: S
                  vatPercent: '5'
                  status: active
                  createdAt: '2026-08-04T07:35:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    delete:
      operationId: deleteTenantItemsByItemId
      summary: Archive an item
      description: >-
        Soft delete: sets `deletedAt` and `status: disabled`. The item disappears from lists and detail reads; the
        record is kept. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  itemId:
                    type: string
              example:
                ok: true
                itemId: item_3c9d…
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    patch:
      operationId: patchTenantItemsByItemId
      summary: Update an item
      description: |-
        Updates only the fields sent. `tenant_admin` only; audited with the list of changed fields.

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Master data
      parameters:
        - name: itemId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type: string
                  maxLength: 1000
                code:
                  type: string
                  maxLength: 80
                subsidiaryId:
                  type: string
                  maxLength: 80
                unitCode:
                  type: string
                  maxLength: 12
                unitLabel:
                  type: string
                  maxLength: 60
                defaultUnitPrice:
                  anyOf:
                    - type: string
                      pattern: ^-?\d{1,15}(\.\d{1,6})?$
                    - const: ''
                      type: string
                currency:
                  anyOf:
                    - type: string
                      pattern: ^[A-Za-z]{3}$
                    - const: ''
                      type: string
                vatCategory:
                  type: string
                  maxLength: 4
                vatPercent:
                  anyOf:
                    - type: string
                      pattern: ^\d{1,3}(\.\d{1,4})?$
                    - const: ''
                      type: string
                status:
                  type: string
                  enum:
                    - active
                    - disabled
              additionalProperties: false
            example:
              defaultUnitPrice: '475.00'
              vatPercent: '5'
      responses:
        '200':
          description: The updated item.
          content:
            application/json:
              schema:
                type: object
                properties:
                  item:
                    type: object
                    properties:
                      itemId:
                        type: string
                      subsidiaryId:
                        type: string
                      code:
                        type: string
                      name:
                        type: string
                      description:
                        type: string
                      unitCode:
                        type: string
                      unitLabel:
                        type: string
                      defaultUnitPrice:
                        type: string
                      currency:
                        type: string
                      vatCategory:
                        type: string
                      vatPercent:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                item:
                  itemId: item_3c9d…
                  subsidiaryId: ''
                  code: SRV-001
                  name: Consulting services
                  description: Hourly consulting
                  unitCode: HUR
                  unitLabel: Hour
                  defaultUnitPrice: '475.00'
                  currency: AED
                  vatCategory: S
                  vatPercent: '5'
                  status: active
                  createdAt: '2026-08-04T07:35:00.000Z'
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions/{transactionId}/files:
    get:
      operationId: getTenantTransactionsByTransactionIdFiles
      summary: List the stored files of a transaction
      description: >-
        The `transaction_files` rows whose `transactionId` equals the path id, oldest first, in the tenant-safe
        projection (no object-storage coordinates, KMS key id or spool path; `encryption.mode` and
        `legacyLocalPathPending` only). A transaction outside a `tenant_user`'s scope answers 404, like an unknown id.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Documents & files
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The transaction's files.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        fileId:
                          type: string
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        subsidiaryId:
                          type: string
                        direction:
                          type: string
                        environment:
                          type: string
                        transactionType:
                          type: string
                        fileType:
                          type: string
                        role:
                          type: string
                        originalName:
                          type: string
                        filename:
                          type: string
                        contentType:
                          type: string
                        sizeBytes:
                          type: integer
                        sha256:
                          type: string
                        source:
                          type: string
                        parentTransactionId:
                          type: string
                        apTddTransactionId:
                          type: string
                        apCoreMessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        storageProvider:
                          type: string
                        status:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        encryption:
                          type: object
                          properties:
                            mode:
                              type: string
                        legacyLocalPathPending:
                          type: boolean
              example:
                items:
                  - fileId: file_01J9Z3…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: ''
                    fileType: payload
                    role: payload
                    originalName: txn_1789562400000_9f3a….xml
                    filename: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    sizeBytes: 18432
                    sha256: 3f7a9c…
                    source: ''
                    parentTransactionId: ''
                    apTddTransactionId: ''
                    apCoreMessageId: ''
                    sbdhInstanceId: ''
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:00:01.000Z'
                    updatedAt: '2026-10-20T09:00:01.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
                  - fileId: file_01J9Z5…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: taxdata
                    fileType: tdd_xml
                    role: tdd_xml
                    originalName: 6c1d2e3f-….xml
                    filename: 6c1d2e3f-….xml
                    contentType: application/xml
                    sizeBytes: 4210
                    sha256: a91e04…
                    source: ap_core_generated_tdd
                    parentTransactionId: txn_1789562400000_9f3a…
                    apTddTransactionId: b7e2c1d0-…
                    apCoreMessageId: 2a4b6c8d-…@peppol.azdanapps.com
                    sbdhInstanceId: 9d8c7b6a-…
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:03:12.000Z'
                    updatedAt: '2026-10-20T09:03:12.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/files/{fileId}/preview:
    get:
      operationId: getTenantFilesByFileIdPreview
      summary: Preview a stored file inline
      description: >-
        Reads the first 512 KiB of the object from object storage and returns a structured preview, so a browser can
        show XML without following a signed URL. XML / SBDH files are parsed (SBDH header + UBL summary) and returned as
        `rawText`; JSON is pretty-printed; PDF and binary files return metadata only (`summary.reason:
        "binary-not-inlined"`). Only object-storage files can be previewed (400 otherwise). Each preview is audit-logged
        (`storage.file.preview`). Files outside a `tenant_user`'s scope answer 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Documents & files
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The file metadata and preview.
          content:
            application/json:
              schema:
                type: object
                properties:
                  file:
                    type: object
                    properties:
                      fileId:
                        type: string
                      transactionId:
                        type: string
                      tenantId:
                        type: string
                      subsidiaryId:
                        type: string
                      direction:
                        type: string
                      environment:
                        type: string
                      transactionType:
                        type: string
                      fileType:
                        type: string
                      role:
                        type: string
                      originalName:
                        type: string
                      filename:
                        type: string
                      contentType:
                        type: string
                      sizeBytes:
                        type: integer
                      sha256:
                        type: string
                      source:
                        type: string
                      parentTransactionId:
                        type: string
                      apTddTransactionId:
                        type: string
                      apCoreMessageId:
                        type: string
                      sbdhInstanceId:
                        type: string
                      storageProvider:
                        type: string
                      status:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
                      encryption:
                        type: object
                        properties:
                          mode:
                            type: string
                      legacyLocalPathPending:
                        type: boolean
                  preview:
                    type: object
                    properties:
                      kind:
                        type: string
                      summary:
                        type: object
                        properties:
                          hasSbdh:
                            type: boolean
                          documentRoot:
                            type: string
                          senderId:
                            type: 'null'
                          receiverId:
                            type: 'null'
                          instanceIdentifier:
                            type: 'null'
                          documentId:
                            type: string
                          issueDate:
                            type: string
                          payableAmount:
                            type: object
                            properties:
                              currency:
                                type: string
                              value:
                                type: string
                      sbdhHeader:
                        type: 'null'
                      documentSummary:
                        type: object
                        properties:
                          rootElement:
                            type: string
                          id:
                            type: string
                          issueDate:
                            type: string
                          documentCurrencyCode:
                            type: string
                          payableAmount:
                            type: object
                            properties:
                              currency:
                                type: string
                              value:
                                type: string
                      rawText:
                        type: string
                      truncated:
                        type: boolean
              example:
                file:
                  fileId: file_01J9Z3…
                  transactionId: txn_1789562400000_9f3a…
                  tenantId: tnt_7Q2X…
                  subsidiaryId: sub_1789…_a1b2
                  direction: outbound
                  environment: production
                  transactionType: ''
                  fileType: payload
                  role: payload
                  originalName: txn_1789562400000_9f3a….xml
                  filename: txn_1789562400000_9f3a….xml
                  contentType: application/xml
                  sizeBytes: 18432
                  sha256: 3f7a9c…
                  source: ''
                  parentTransactionId: ''
                  apTddTransactionId: ''
                  apCoreMessageId: ''
                  sbdhInstanceId: ''
                  storageProvider: oci
                  status: uploaded
                  createdAt: '2026-10-20T09:00:01.000Z'
                  updatedAt: '2026-10-20T09:00:01.000Z'
                  encryption:
                    mode: NONE
                  legacyLocalPathPending: false
                preview:
                  kind: xml
                  summary:
                    hasSbdh: false
                    documentRoot: Invoice
                    senderId: null
                    receiverId: null
                    instanceIdentifier: null
                    documentId: INV-2026-000123
                    issueDate: '2026-10-20'
                    payableAmount:
                      currency: AED
                      value: '1050.00'
                  sbdhHeader: null
                  documentSummary:
                    rootElement: Invoice
                    id: INV-2026-000123
                    issueDate: '2026-10-20'
                    documentCurrencyCode: AED
                    payableAmount:
                      currency: AED
                      value: '1050.00'
                  rawText: >-
                    <?xml version="1.0" encoding="UTF-8"?><Invoice
                    xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">…
                  truncated: false
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/files/{fileId}/download-url:
    post:
      operationId: postTenantFilesByFileIdDownloadUrl
      summary: Create a short-lived download URL for a file
      description: >-
        Mints a pre-authenticated object-storage URL for one file. `ttlSeconds` is 30–300 (the storage profile default
        applies when omitted). Only object-storage files qualify (400 otherwise). Audit-logged
        (`storage.file.download_url`). Files outside a `tenant_user`'s scope answer 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Documents & files
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ttlSeconds:
                  type: integer
                  minimum: 30
                  maximum: 300
              additionalProperties: false
              default: {}
            example:
              ttlSeconds: 120
      responses:
        '200':
          description: The signed URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                  ttlSeconds:
                    type: integer
                  fileId:
                    type: string
              example:
                url: https://objectstorage.me-dubai-1.oraclecloud.com/p/AbCd…/n/…/b/…/o/…
                ttlSeconds: 120
                fileId: file_01J9Z3…
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          $ref: '#/components/responses/ErrBadGateway'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/subsidiaries:
    get:
      operationId: getTenantSubsidiaries
      summary: List the tenant's companies
      description: >-
        Every non-deleted subsidiary (company) of the tenant, default first, then by name, with `participantCount` and
        `locationCount`. A `tenant_user` only receives the companies on their `allowedSubsidiaryIds`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      responses:
        '200':
          description: The companies.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        subsidiaryId:
                          type: string
                        tenantId:
                          type: string
                        name:
                          type: string
                        legalName:
                          type: string
                        code:
                          type: string
                        country:
                          type: string
                        taxRegistrationNumber:
                          type: string
                        netsuiteSubsidiaryId:
                          type: string
                        defaultCurrency:
                          type: string
                        status:
                          type: string
                        isDefault:
                          type: boolean
                        sellerProfile:
                          type: object
                          properties:
                            legalRegistrationId:
                              type: string
                            legalRegistrationType:
                              type: string
                            legalRegistrationAuthorityName:
                              type: string
                            address:
                              type: object
                              properties:
                                streetName:
                                  type: string
                                cityName:
                                  type: string
                                countrySubentity:
                                  type: string
                                postalZone:
                                  type: string
                        deletedAt:
                          type: 'null'
                        createdBy:
                          type: 'null'
                        updatedBy:
                          type: 'null'
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        participantCount:
                          type: integer
                        locationCount:
                          type: integer
              example:
                items:
                  - _id: 671f2c…
                    subsidiaryId: sub_1789…_a1b2
                    tenantId: tnt_7Q2X…
                    name: Sample Trading
                    legalName: Sample Trading LLC
                    code: AE-HQ
                    country: AE
                    taxRegistrationNumber: 1004****59
                    netsuiteSubsidiaryId: ''
                    defaultCurrency: AED
                    status: active
                    isDefault: true
                    sellerProfile:
                      legalRegistrationId: CN-12****7
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development
                      address:
                        streetName: Sheikh Zayed Road
                        cityName: Dubai
                        countrySubentity: DXB
                        postalZone: '00000'
                    deletedAt: null
                    createdBy: null
                    updatedBy: null
                    createdAt: '2026-09-01T08:00:00.000Z'
                    updatedAt: '2026-10-20T09:15:00.000Z'
                    participantCount: 2
                    locationCount: 1
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantSubsidiaries
      summary: Create a company
      description: >-
        Creates a subsidiary (company) in the caller's tenant — the body cannot name another tenant. `code`, when given,
        must be unique among the tenant's companies (409 `SUBSIDIARY_CODE_TAKEN`). `isDefault: true` clears the previous
        default. `sellerProfile` is the seller identity used on the company's outbound invoices. Other refusals answer
        400 `{ error: { code } }` (`NAME_REQUIRED`, `SUBSIDIARY_CONSTRAINT`, `TENANT_NOT_FOUND`). Audit-logged
        (`subsidiary.create`). Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                legalName:
                  type: string
                code:
                  type: string
                country:
                  type: string
                  maxLength: 2
                  minLength: 2
                taxRegistrationNumber:
                  type: string
                netsuiteSubsidiaryId:
                  type: string
                defaultCurrency:
                  type: string
                isDefault:
                  type: boolean
                sellerProfile:
                  type: object
                  properties:
                    legalRegistrationId:
                      type: string
                    legalRegistrationType:
                      type: string
                      enum:
                        - TL
                        - EID
                        - PAS
                        - CD
                    legalRegistrationAuthorityName:
                      type: string
                    address:
                      type: object
                      properties:
                        streetName:
                          type: string
                        cityName:
                          type: string
                        countrySubentity:
                          type: string
                        postalZone:
                          type: string
              required:
                - name
            example:
              name: Sample Trading
              legalName: Sample Trading LLC
              code: AE-HQ
              country: AE
              taxRegistrationNumber: 1004****59
              defaultCurrency: AED
              isDefault: true
              sellerProfile:
                legalRegistrationId: CN-12****7
                legalRegistrationType: TL
                legalRegistrationAuthorityName: Department of Economic Development
                address:
                  streetName: Sheikh Zayed Road
                  cityName: Dubai
                  countrySubentity: DXB
                  postalZone: '00000'
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  subsidiary:
                    type: object
                    properties:
                      _id:
                        type: string
                      subsidiaryId:
                        type: string
                      tenantId:
                        type: string
                      name:
                        type: string
                      legalName:
                        type: string
                      code:
                        type: string
                      country:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      netsuiteSubsidiaryId:
                        type: string
                      defaultCurrency:
                        type: string
                      status:
                        type: string
                      isDefault:
                        type: boolean
                      sellerProfile:
                        type: object
                        properties:
                          legalRegistrationId:
                            type: string
                          legalRegistrationType:
                            type: string
                          legalRegistrationAuthorityName:
                            type: string
                          address:
                            type: object
                            properties:
                              streetName:
                                type: string
                              cityName:
                                type: string
                              countrySubentity:
                                type: string
                              postalZone:
                                type: string
                      deletedAt:
                        type: 'null'
                      createdBy:
                        type: 'null'
                      updatedBy:
                        type: 'null'
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                ok: true
                subsidiary:
                  _id: 671f2c…
                  subsidiaryId: sub_1789…_a1b2
                  tenantId: tnt_7Q2X…
                  name: Sample Trading
                  legalName: Sample Trading LLC
                  code: AE-HQ
                  country: AE
                  taxRegistrationNumber: 1004****59
                  netsuiteSubsidiaryId: ''
                  defaultCurrency: AED
                  status: active
                  isDefault: true
                  sellerProfile:
                    legalRegistrationId: CN-12****7
                    legalRegistrationType: TL
                    legalRegistrationAuthorityName: Department of Economic Development
                    address:
                      streetName: Sheikh Zayed Road
                      cityName: Dubai
                      countrySubentity: DXB
                      postalZone: '00000'
                  deletedAt: null
                  createdBy: null
                  updatedBy: null
                  createdAt: '2026-09-01T08:00:00.000Z'
                  updatedAt: '2026-10-20T09:15:00.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          description: The company code is already used (`SUBSIDIARY_CODE_TAKEN`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: SUBSIDIARY_CODE_TAKEN
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/peppol/validate:
    post:
      operationId: postTenantPeppolValidate
      summary: Validate a Peppol document with the session (sender resolved)
      description: >-
        Same validation as `POST /api/integrations/peppol/validate`, with the user's session. `senderPeppolId` may be
        omitted: the server picks the tenant's default participant, else the first outbound-capable one, else the first
        one (participants with status `disabled` are ignored). A `senderPeppolId` that is not one of the tenant's
        participants answers 400, as does a tenant with no participant. Unlike `POST /api/tenant/invoices/validate`, no
        `tenant_user` allow-list check is applied.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Invoice sending
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                inputType:
                  type: string
                environment:
                  type: string
                documentType:
                  type: string
                receiverPeppolId:
                  type: string
                payload:
                  type: object
                  properties:
                    invoiceNumber:
                      type: string
                    issueDate:
                      type: string
                    dueDate:
                      type: string
                    currency:
                      type: string
                    buyer:
                      type: object
                      properties:
                        name:
                          type: string
                        peppolId:
                          type: string
                    lines:
                      type: array
                      items:
                        type: object
                        properties:
                          description:
                            type: string
                          quantity:
                            type: integer
                          unitPrice:
                            type: integer
                          vatCategory:
                            type: string
                          vatRate:
                            type: integer
            example:
              inputType: json
              environment: production
              documentType: invoice
              receiverPeppolId: 0235:1002****03
              payload:
                invoiceNumber: INV-2026-000123
                issueDate: '2026-10-20'
                dueDate: '2026-11-19'
                currency: AED
                buyer:
                  name: Sample Buyer FZ-LLC
                  peppolId: 0235:1002****03
                lines:
                  - description: Consulting services
                    quantity: 1
                    unitPrice: 1000
                    vatCategory: S
                    vatRate: 5
      responses:
        '200':
          description: The document is valid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  warnings:
                    type: array
                    items: {}
                  phormSkipped:
                    type: boolean
                  normalized:
                    type: object
                    properties:
                      sender:
                        type: string
                      receiver:
                        type: string
                      invoiceNumber:
                        type: string
                      issueDate:
                        type: string
                      currency:
                        type: string
                      payableAmount:
                        type: integer
                      invoiceTypeCode:
                        type: string
                      dueDate:
                        type: string
                      supplierName:
                        type: string
                      customerName:
                        type: string
                      netAmount:
                        type: integer
                      taxAmount:
                        type: integer
              example:
                valid: true
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                warnings: []
                phormSkipped: false
                normalized:
                  sender: 0235:1004****59
                  receiver: 0235:1002****03
                  invoiceNumber: INV-2026-000123
                  issueDate: '2026-10-20'
                  currency: AED
                  payableAmount: 1050
                  invoiceTypeCode: '380'
                  dueDate: '2026-11-19'
                  supplierName: Sample Trading LLC
                  customerName: Sample Buyer FZ-LLC
                  netAmount: 1000
                  taxAmount: 50
        '400':
          description: The document failed validation, or the sender could not be resolved (standard error shape).
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            message:
                              type: string
                            xpath:
                              type: string
                  warnings:
                    type: array
                    items: {}
              example:
                valid: false
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                error:
                  code: PINT_VALIDATION_FAILED
                  message: Document failed Peppol/PINT validation.
                  details:
                    - code: ibr-144-ae
                      message: Buyer address must contain street, city and country subdivision.
                      xpath: /Invoice/cac:AccountingCustomerParty
                warnings: []
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/peppol/invoices:
    post:
      operationId: postTenantPeppolInvoices
      summary: Submit a Peppol invoice with the session (sender resolved)
      description: >-
        Same submission pipeline as `POST /api/integrations/peppol/invoices`, with the user's session and the sender
        resolved as in `POST /api/tenant/peppol/validate`. A non-empty `Idempotency-Key` header replaces
        `body.idempotencyKey` (no conflict check). No `tenant_user` allow-list check is applied. The `documents` links
        use the Integration API URLs.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Invoice sending
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Optional de-duplication key (1–120 characters), scoped to the tenant. A repeat with the same key returns the
            original transaction (`idempotent: true`) instead of sending again.
          schema:
            type: string
            maxLength: 120
            example: INV-2026-000123
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                inputType:
                  type: string
                environment:
                  type: string
                documentType:
                  type: string
                receiverPeppolId:
                  type: string
                payload:
                  type: object
                  properties:
                    invoiceNumber:
                      type: string
                    issueDate:
                      type: string
                    dueDate:
                      type: string
                    currency:
                      type: string
                    buyer:
                      type: object
                      properties:
                        name:
                          type: string
                        peppolId:
                          type: string
                    lines:
                      type: array
                      items:
                        type: object
                        properties:
                          description:
                            type: string
                          quantity:
                            type: integer
                          unitPrice:
                            type: integer
                          vatCategory:
                            type: string
                          vatRate:
                            type: integer
            example:
              inputType: json
              environment: production
              documentType: invoice
              receiverPeppolId: 0235:1002****03
              payload:
                invoiceNumber: INV-2026-000123
                issueDate: '2026-10-20'
                dueDate: '2026-11-19'
                currency: AED
                buyer:
                  name: Sample Buyer FZ-LLC
                  peppolId: 0235:1002****03
                lines:
                  - description: Consulting services
                    quantity: 1
                    unitPrice: 1000
                    vatCategory: S
                    vatRate: 5
      responses:
        '202':
          description: Accepted for Peppol delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transactionId:
                    type: string
                  status:
                    type: string
                  valid:
                    type: boolean
                  documentTypeId:
                    type: string
                  processId:
                    type: string
                  correlationId:
                    type: string
                  as4MessageId:
                    type: string
                  sbdhInstanceId:
                    type: string
                  idempotent:
                    type: boolean
                  warnings:
                    type: array
                    items: {}
                  validatorPhormSkipped:
                    type: boolean
                  documents:
                    type: object
                    properties:
                      invoiceXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: string
                          fileName:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                      invoicePdf:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      sbdXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      tddXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      mlsXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      payloadXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          privileged:
                            type: boolean
                          requiredScope:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                  message:
                    type: string
              example:
                transactionId: txn_1789562400000_9f3a…
                status: queued
                valid: true
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                correlationId: cor_1789562400000_4d5e…
                as4MessageId: f0debc61-…@peppol.azdanapps.com
                sbdhInstanceId: 6c1d2e3f-…
                idempotent: false
                warnings: []
                validatorPhormSkipped: false
                documents:
                  invoiceXml:
                    available: true
                    fileId: file_01J9Z3…
                    fileName: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a…/files/invoice-xml
                  invoicePdf:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/pdf
                    downloadUrl: null
                    reason: not_generated
                  sbdXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_stored
                  tddXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_generated
                  mlsXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_received
                  payloadXml:
                    available: true
                    privileged: true
                    requiredScope: peppol:payload:read
                    contentType: application/xml
                    downloadUrl: /api/integrations/peppol/transactions/txn_1789562400000_9f3a…/xml
                message: Invoice accepted for Peppol delivery.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '422':
          $ref: '#/components/responses/ErrUnprocessable'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          $ref: '#/components/responses/ErrBadGateway'
        '503':
          $ref: '#/components/responses/ErrUnavailable'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite:
    get:
      operationId: getTenantIntegrationsNetsuite
      summary: Get the NetSuite connection status
      description: >-
        Returns the tenant's NetSuite connection without secrets: configuration, status (`not_configured`, `configured`,
        `connected`, `expired`, `reconnect_required`, `disconnected`), token expiry, account metadata and last
        test/delivery outcome. The client secret and tokens are reported only as `hasClientSecret` / `hasRefreshToken`.
        When nothing has been saved, only `configured: false, status: "not_configured"` is returned. Readable by any
        tenant user.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: Connection status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connectionId:
                    type: string
                  status:
                    type: string
                  configured:
                    type: boolean
                  connected:
                    type: boolean
                  accountId:
                    type: string
                  environment:
                    type: string
                  restletUrl:
                    type: string
                  scriptId:
                    type: string
                  deployId:
                    type: string
                  scopes:
                    type: string
                  authType:
                    type: string
                  clientId:
                    type: string
                  hasClientSecret:
                    type: boolean
                  keyId:
                    type: 'null'
                  hasRefreshToken:
                    type: boolean
                  accessTokenExpiresAt:
                    type: string
                  tokenExpiresAt:
                    type: string
                  connectedAt:
                    type: string
                  reconnectRequired:
                    type: boolean
                  accountName:
                    type: string
                  authorizedUser:
                    type: string
                  authorizedRole:
                    type: string
                  metadataFetchedAt:
                    type: string
                  lastTokenRefreshAt:
                    type: string
                  lastSuccessfulDeliveryAt:
                    type: string
                  lastTestAt:
                    type: string
                  lastTestOk:
                    type: boolean
                  lastError:
                    type: 'null'
                  updatedAt:
                    type: string
              example:
                connectionId: nsc_1729502043000_5e2d…
                status: connected
                configured: true
                connected: true
                accountId: 1234567_SB1
                environment: sandbox
                restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
                scriptId: customscript_azdan_inbound
                deployId: customdeploy_azdan_inbound
                scopes: restlets
                authType: oauth2_authorization_code
                clientId: a3f9…
                hasClientSecret: true
                keyId: null
                hasRefreshToken: true
                accessTokenExpiresAt: '2026-10-21T10:14:03.000Z'
                tokenExpiresAt: '2026-10-21T10:14:03.000Z'
                connectedAt: '2026-10-21T09:14:03.000Z'
                reconnectRequired: false
                accountName: AZDAN Demo (Sandbox)
                authorizedUser: o***@example.com
                authorizedRole: Administrator
                metadataFetchedAt: '2026-10-21T09:14:03.000Z'
                lastTokenRefreshAt: '2026-10-21T09:14:03.000Z'
                lastSuccessfulDeliveryAt: '2026-10-20T16:40:00.000Z'
                lastTestAt: '2026-10-21T09:14:41.000Z'
                lastTestOk: true
                lastError: null
                updatedAt: '2026-10-21T09:14:41.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite/config:
    put:
      operationId: putTenantIntegrationsNetsuiteConfig
      summary: Save the NetSuite connection configuration
      description: >-
        Saves the manual configuration (account id, environment, RESTlet / authorize / token URLs, script and deploy
        ids, scopes, client id, key id, client secret). Only fields sent are changed. URLs are checked against the SSRF
        rules. The client secret is AES-256-GCM encrypted at rest and is kept when the field is empty or omitted. Status
        moves from `not_configured` to `configured` once account id, client id and secret are present. tenant_admin
        only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                accountId:
                  type: string
                  maxLength: 64
                environment:
                  type: string
                  enum:
                    - production
                    - sandbox
                restletUrl:
                  anyOf:
                    - type: string
                      format: uri
                      maxLength: 2048
                    - const: ''
                      type: string
                scriptId:
                  type: string
                  maxLength: 64
                deployId:
                  type: string
                  maxLength: 64
                scopes:
                  type: string
                  maxLength: 256
                authorizeUrl:
                  anyOf:
                    - type: string
                      format: uri
                      maxLength: 2048
                    - const: ''
                      type: string
                tokenUrl:
                  anyOf:
                    - type: string
                      format: uri
                      maxLength: 2048
                    - const: ''
                      type: string
                clientId:
                  type: string
                  maxLength: 256
                clientSecret:
                  type: string
                  maxLength: 2048
                keyId:
                  type: string
                  maxLength: 256
              additionalProperties: false
            example:
              accountId: 1234567_SB1
              environment: sandbox
              restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
              scriptId: customscript_azdan_inbound
              deployId: customdeploy_azdan_inbound
              scopes: restlets
              clientId: a3f9…
              clientSecret: ••••
      responses:
        '200':
          description: The connection after saving.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connectionId:
                    type: string
                  status:
                    type: string
                  configured:
                    type: boolean
                  connected:
                    type: boolean
                  accountId:
                    type: string
                  environment:
                    type: string
                  restletUrl:
                    type: string
                  scriptId:
                    type: string
                  deployId:
                    type: string
                  scopes:
                    type: string
                  authType:
                    type: string
                  clientId:
                    type: string
                  hasClientSecret:
                    type: boolean
                  keyId:
                    type: 'null'
                  hasRefreshToken:
                    type: boolean
                  accessTokenExpiresAt:
                    type: 'null'
                  tokenExpiresAt:
                    type: 'null'
                  connectedAt:
                    type: 'null'
                  reconnectRequired:
                    type: boolean
                  accountName:
                    type: 'null'
                  authorizedUser:
                    type: 'null'
                  authorizedRole:
                    type: 'null'
                  metadataFetchedAt:
                    type: 'null'
                  lastTokenRefreshAt:
                    type: 'null'
                  lastSuccessfulDeliveryAt:
                    type: string
                  lastTestAt:
                    type: string
                  lastTestOk:
                    type: boolean
                  lastError:
                    type: 'null'
                  updatedAt:
                    type: string
              example:
                connectionId: nsc_1729502043000_5e2d…
                status: configured
                configured: true
                connected: false
                accountId: 1234567_SB1
                environment: sandbox
                restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
                scriptId: customscript_azdan_inbound
                deployId: customdeploy_azdan_inbound
                scopes: restlets
                authType: oauth2_authorization_code
                clientId: a3f9…
                hasClientSecret: true
                keyId: null
                hasRefreshToken: false
                accessTokenExpiresAt: null
                tokenExpiresAt: null
                connectedAt: null
                reconnectRequired: false
                accountName: null
                authorizedUser: null
                authorizedRole: null
                metadataFetchedAt: null
                lastTokenRefreshAt: null
                lastSuccessfulDeliveryAt: '2026-10-20T16:40:00.000Z'
                lastTestAt: '2026-10-21T09:14:41.000Z'
                lastTestOk: true
                lastError: null
                updatedAt: '2026-10-21T09:14:41.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite/oauth/start:
    post:
      operationId: postTenantIntegrationsNetsuiteOauthStart
      summary: Start the NetSuite OAuth authorization
      description: >-
        Begins the OAuth 2.0 authorization-code flow: stores a one-time `state` (valid 10 minutes, bound to tenant,
        user, connection, environment and account) and returns the NetSuite authorize URL to open in the browser. The
        redirect URI comes from server configuration (`NETSUITE_OAUTH_REDIRECT_URI` or the tenant portal origin); a
        `redirectUri` in the body is accepted but not used. tenant_admin only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                redirectUri:
                  type: string
                  format: uri
                  maxLength: 2048
              additionalProperties: false
            example: {}
      responses:
        '200':
          description: Authorize URL and state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  authorizeUrl:
                    type: string
                  state:
                    type: string
                  redirectUri:
                    type: string
              example:
                authorizeUrl: >-
                  https://1234567-sb1.app.netsuite.com/app/login/oauth2/authorize.nl?response_type=code&client_id=a3f9…&redirect_uri=…&scope=restlets&state=9d1e…
                state: 9d1e…
                redirectUri: https://portal.example.com/portal/integrations/netsuite/callback
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite/oauth/callback:
    post:
      operationId: postTenantIntegrationsNetsuiteOauthCallback
      summary: Complete the NetSuite OAuth authorization
      description: >-
        Completes the flow with the `state` and `code` NetSuite returned. The state must match, be unexpired and belong
        to the same tenant, user, connection, environment and account. The code is exchanged server-side using the
        stored redirect URI; tokens are stored encrypted and account metadata is fetched from NetSuite's userinfo
        endpoint (falling back to `company` / `entity` / `role` from the body). The one-time state is cleared either
        way. A NetSuite `error` sets status back to `configured`; a failed exchange sets `reconnect_required`. A
        `redirectUri` in the body is not used. tenant_admin only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                state:
                  type: string
                  minLength: 1
                  maxLength: 128
                code:
                  type: string
                  minLength: 1
                  maxLength: 4096
                redirectUri:
                  type: string
                  format: uri
                  maxLength: 2048
                error:
                  type: string
                  maxLength: 256
                role:
                  type: string
                  maxLength: 256
                entity:
                  type: string
                  maxLength: 256
                company:
                  type: string
                  maxLength: 512
              additionalProperties: false
            example:
              state: 9d1e…
              code: ••••
              company: AZDAN Demo (Sandbox)
              role: '3'
              entity: '-5'
      responses:
        '200':
          description: The connected connection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connectionId:
                    type: string
                  status:
                    type: string
                  configured:
                    type: boolean
                  connected:
                    type: boolean
                  accountId:
                    type: string
                  environment:
                    type: string
                  restletUrl:
                    type: string
                  scriptId:
                    type: string
                  deployId:
                    type: string
                  scopes:
                    type: string
                  authType:
                    type: string
                  clientId:
                    type: string
                  hasClientSecret:
                    type: boolean
                  keyId:
                    type: 'null'
                  hasRefreshToken:
                    type: boolean
                  accessTokenExpiresAt:
                    type: string
                  tokenExpiresAt:
                    type: string
                  connectedAt:
                    type: string
                  reconnectRequired:
                    type: boolean
                  accountName:
                    type: string
                  authorizedUser:
                    type: string
                  authorizedRole:
                    type: string
                  metadataFetchedAt:
                    type: string
                  lastTokenRefreshAt:
                    type: string
                  lastSuccessfulDeliveryAt:
                    type: string
                  lastTestAt:
                    type: string
                  lastTestOk:
                    type: boolean
                  lastError:
                    type: 'null'
                  updatedAt:
                    type: string
              example:
                connectionId: nsc_1729502043000_5e2d…
                status: connected
                configured: true
                connected: true
                accountId: 1234567_SB1
                environment: sandbox
                restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
                scriptId: customscript_azdan_inbound
                deployId: customdeploy_azdan_inbound
                scopes: restlets
                authType: oauth2_authorization_code
                clientId: a3f9…
                hasClientSecret: true
                keyId: null
                hasRefreshToken: true
                accessTokenExpiresAt: '2026-10-21T10:14:03.000Z'
                tokenExpiresAt: '2026-10-21T10:14:03.000Z'
                connectedAt: '2026-10-21T09:14:03.000Z'
                reconnectRequired: false
                accountName: AZDAN Demo (Sandbox)
                authorizedUser: o***@example.com
                authorizedRole: Administrator
                metadataFetchedAt: '2026-10-21T09:14:03.000Z'
                lastTokenRefreshAt: '2026-10-21T09:14:03.000Z'
                lastSuccessfulDeliveryAt: '2026-10-20T16:40:00.000Z'
                lastTestAt: '2026-10-21T09:14:41.000Z'
                lastTestOk: true
                lastError: null
                updatedAt: '2026-10-21T09:14:41.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
        '502':
          $ref: '#/components/responses/ErrBadGateway'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite/test:
    post:
      operationId: postTenantIntegrationsNetsuiteTest
      summary: Test the NetSuite connection
      description: >-
        Checks that a valid access token can be obtained, refreshing it when it is close to expiry. Records `lastTestAt`
        / `lastTestOk` (and `lastError` on failure) on the connection. A failed check is HTTP 200 with `ok: false` and a
        code such as `DISCONNECTED`, `NETSUITE_TOKEN_REFRESH_FAILED` or `DECRYPT_FAILED`. tenant_admin only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Test result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  code:
                    type: string
                  status:
                    type: string
                  connection:
                    type: object
                    properties:
                      connectionId:
                        type: string
                      status:
                        type: string
                      configured:
                        type: boolean
                      connected:
                        type: boolean
                      accountId:
                        type: string
                      environment:
                        type: string
                      restletUrl:
                        type: string
                      scriptId:
                        type: string
                      deployId:
                        type: string
                      scopes:
                        type: string
                      authType:
                        type: string
                      clientId:
                        type: string
                      hasClientSecret:
                        type: boolean
                      keyId:
                        type: 'null'
                      hasRefreshToken:
                        type: boolean
                      accessTokenExpiresAt:
                        type: string
                      tokenExpiresAt:
                        type: string
                      connectedAt:
                        type: string
                      reconnectRequired:
                        type: boolean
                      accountName:
                        type: string
                      authorizedUser:
                        type: string
                      authorizedRole:
                        type: string
                      metadataFetchedAt:
                        type: string
                      lastTokenRefreshAt:
                        type: string
                      lastSuccessfulDeliveryAt:
                        type: string
                      lastTestAt:
                        type: string
                      lastTestOk:
                        type: boolean
                      lastError:
                        type: 'null'
                      updatedAt:
                        type: string
              example:
                ok: true
                code: ok
                status: connected
                connection:
                  connectionId: nsc_1729502043000_5e2d…
                  status: connected
                  configured: true
                  connected: true
                  accountId: 1234567_SB1
                  environment: sandbox
                  restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
                  scriptId: customscript_azdan_inbound
                  deployId: customdeploy_azdan_inbound
                  scopes: restlets
                  authType: oauth2_authorization_code
                  clientId: a3f9…
                  hasClientSecret: true
                  keyId: null
                  hasRefreshToken: true
                  accessTokenExpiresAt: '2026-10-21T10:14:03.000Z'
                  tokenExpiresAt: '2026-10-21T10:14:03.000Z'
                  connectedAt: '2026-10-21T09:14:03.000Z'
                  reconnectRequired: false
                  accountName: AZDAN Demo (Sandbox)
                  authorizedUser: o***@example.com
                  authorizedRole: Administrator
                  metadataFetchedAt: '2026-10-21T09:14:03.000Z'
                  lastTokenRefreshAt: '2026-10-21T09:14:03.000Z'
                  lastSuccessfulDeliveryAt: '2026-10-20T16:40:00.000Z'
                  lastTestAt: '2026-10-21T09:14:41.000Z'
                  lastTestOk: true
                  lastError: null
                  updatedAt: '2026-10-21T09:14:41.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/integrations/netsuite/disconnect:
    post:
      operationId: postTenantIntegrationsNetsuiteDisconnect
      summary: Disconnect NetSuite
      description: >-
        Deletes the stored access and refresh tokens and any pending OAuth state, and sets status `disconnected`. The
        saved configuration and client secret are kept, so the tenant can reconnect with `oauth/start`. When no
        connection exists, returns `{ ok: true, status: "not_configured" }`. Tokens are not revoked at NetSuite.
        tenant_admin only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Disconnected.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  status:
                    type: string
                  connection:
                    type: object
                    properties:
                      connectionId:
                        type: string
                      status:
                        type: string
                      configured:
                        type: boolean
                      connected:
                        type: boolean
                      accountId:
                        type: string
                      environment:
                        type: string
                      restletUrl:
                        type: string
                      scriptId:
                        type: string
                      deployId:
                        type: string
                      scopes:
                        type: string
                      authType:
                        type: string
                      clientId:
                        type: string
                      hasClientSecret:
                        type: boolean
                      keyId:
                        type: 'null'
                      hasRefreshToken:
                        type: boolean
                      accessTokenExpiresAt:
                        type: 'null'
                      tokenExpiresAt:
                        type: 'null'
                      connectedAt:
                        type: string
                      reconnectRequired:
                        type: boolean
                      accountName:
                        type: string
                      authorizedUser:
                        type: string
                      authorizedRole:
                        type: string
                      metadataFetchedAt:
                        type: string
                      lastTokenRefreshAt:
                        type: string
                      lastSuccessfulDeliveryAt:
                        type: string
                      lastTestAt:
                        type: string
                      lastTestOk:
                        type: boolean
                      lastError:
                        type: 'null'
                      updatedAt:
                        type: string
              example:
                ok: true
                status: disconnected
                connection:
                  connectionId: nsc_1729502043000_5e2d…
                  status: disconnected
                  configured: true
                  connected: false
                  accountId: 1234567_SB1
                  environment: sandbox
                  restletUrl: https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=…&deploy=1
                  scriptId: customscript_azdan_inbound
                  deployId: customdeploy_azdan_inbound
                  scopes: restlets
                  authType: oauth2_authorization_code
                  clientId: a3f9…
                  hasClientSecret: true
                  keyId: null
                  hasRefreshToken: false
                  accessTokenExpiresAt: null
                  tokenExpiresAt: null
                  connectedAt: '2026-10-21T09:14:03.000Z'
                  reconnectRequired: false
                  accountName: AZDAN Demo (Sandbox)
                  authorizedUser: o***@example.com
                  authorizedRole: Administrator
                  metadataFetchedAt: '2026-10-21T09:14:03.000Z'
                  lastTokenRefreshAt: '2026-10-21T09:14:03.000Z'
                  lastSuccessfulDeliveryAt: '2026-10-20T16:40:00.000Z'
                  lastTestAt: '2026-10-21T09:14:41.000Z'
                  lastTestOk: true
                  lastError: null
                  updatedAt: '2026-10-21T09:14:41.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/users:
    get:
      operationId: getTenantUsers
      summary: List the tenant's users
      description: >-
        All users of the caller's tenant, newest first, with safe fields only (no password hash or MFA secrets).
        Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      responses:
        '200':
          description: The users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        userId:
                          type: string
                        email:
                          type: string
                        displayName:
                          type: string
                        role:
                          type: string
                        tenantId:
                          type: string
                        allowedSubsidiaryIds:
                          type: array
                          items:
                            type: string
                        allowedPeppolParticipantIds:
                          type: array
                          items: {}
                        status:
                          type: string
                        mfaEnabled:
                          type: boolean
                        lastLoginAt:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
              example:
                items:
                  - _id: 671f5c…
                    userId: user_1789562400000_5e6f…
                    email: o***@example.com
                    displayName: Finance operator
                    role: tenant_user
                    tenantId: tnt_7Q2X…
                    allowedSubsidiaryIds:
                      - sub_1789…_a1b2
                    allowedPeppolParticipantIds: []
                    status: active
                    mfaEnabled: false
                    lastLoginAt: '2026-10-23T06:10:00.000Z'
                    createdAt: '2026-09-10T12:00:00.000Z'
                    updatedAt: '2026-10-20T08:00:00.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantUsers
      summary: Create a tenant user
      description: >-
        Creates an active `tenant_admin` or `tenant_user` in the caller's tenant. The email is lower-cased and must be
        unused platform-wide (409 `USER_EXISTS`). `allowedSubsidiaryIds` is silently reduced to companies of this
        tenant. A server-generated temporary password is returned **once** in `tempPassword`; only its bcrypt hash is
        stored. Audit-logged (`user.create`). Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 200
                role:
                  type: string
                  enum:
                    - tenant_admin
                    - tenant_user
                    - tenant_developer
                    - tenant_viewer
                displayName:
                  type: string
                  maxLength: 120
                allowedSubsidiaryIds:
                  type: array
                  items:
                    type: string
                    maxLength: 80
                  maxItems: 50
              required:
                - email
                - role
              additionalProperties: false
            example:
              email: o***@example.com
              role: tenant_user
              displayName: Finance operator
              allowedSubsidiaryIds:
                - sub_1789…_a1b2
      responses:
        '201':
          description: Created. The temporary password is shown only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  user:
                    type: object
                    properties:
                      _id:
                        type: string
                      userId:
                        type: string
                      email:
                        type: string
                      displayName:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      allowedSubsidiaryIds:
                        type: array
                        items:
                          type: string
                      allowedPeppolParticipantIds:
                        type: array
                        items: {}
                      status:
                        type: string
                      mfaEnabled:
                        type: boolean
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
                      tokenVersion:
                        type: integer
                      activation:
                        type: object
                        properties:
                          tokenHash:
                            type: 'null'
                          expiresAt:
                            type: 'null'
                          sentAt:
                            type: 'null'
                          usedAt:
                            type: 'null'
                  tempPassword:
                    type: string
              example:
                ok: true
                user:
                  _id: 671f5c…
                  userId: user_1789562400000_5e6f…
                  email: o***@example.com
                  displayName: Finance operator
                  role: tenant_user
                  tenantId: tnt_7Q2X…
                  allowedSubsidiaryIds:
                    - sub_1789…_a1b2
                  allowedPeppolParticipantIds: []
                  status: active
                  mfaEnabled: false
                  createdAt: '2026-09-10T12:00:00.000Z'
                  updatedAt: '2026-10-20T08:00:00.000Z'
                  tokenVersion: 0
                  activation:
                    tokenHash: null
                    expiresAt: null
                    sentAt: null
                    usedAt: null
                tempPassword: Xk7#…(16 chars)
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/users/{userId}:
    patch:
      operationId: patchTenantUsersByUserId
      summary: Update a tenant user's role, name or company access
      description: >-
        Updates `role`, `displayName` (blank resets it to the email) and/or `allowedSubsidiaryIds` (reduced to this
        tenant's companies) of a user in the caller's tenant. An admin cannot change their own role (409
        `CANNOT_CHANGE_OWN_ROLE`). Audit-logged (`user.update`). Requires the `tenant_admin` role; a `tenant_user` gets
        403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role:
                  type: string
                  enum:
                    - tenant_admin
                    - tenant_user
                    - tenant_developer
                    - tenant_viewer
                displayName:
                  type: string
                  maxLength: 120
                allowedSubsidiaryIds:
                  type: array
                  items:
                    type: string
                    maxLength: 80
                  maxItems: 50
              additionalProperties: false
            example:
              role: tenant_user
              allowedSubsidiaryIds:
                - sub_1789…_a1b2
      responses:
        '200':
          description: The updated user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  user:
                    type: object
                    properties:
                      _id:
                        type: string
                      userId:
                        type: string
                      email:
                        type: string
                      displayName:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      allowedSubsidiaryIds:
                        type: array
                        items:
                          type: string
                      allowedPeppolParticipantIds:
                        type: array
                        items: {}
                      status:
                        type: string
                      mfaEnabled:
                        type: boolean
                      lastLoginAt:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                ok: true
                user:
                  _id: 671f5c…
                  userId: user_1789562400000_5e6f…
                  email: o***@example.com
                  displayName: Finance operator
                  role: tenant_user
                  tenantId: tnt_7Q2X…
                  allowedSubsidiaryIds:
                    - sub_1789…_a1b2
                  allowedPeppolParticipantIds: []
                  status: active
                  mfaEnabled: false
                  lastLoginAt: '2026-10-23T06:10:00.000Z'
                  createdAt: '2026-09-10T12:00:00.000Z'
                  updatedAt: '2026-10-20T08:00:00.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/users/{userId}/status:
    patch:
      operationId: patchTenantUsersByUserIdStatus
      summary: Activate or disable a tenant user
      description: >-
        Sets `status` to `active` or `disabled`. Disabling revokes the user's sessions immediately. An admin cannot
        change their own status (409 `CANNOT_CHANGE_OWN_STATUS`). Audit-logged (`user.activate` / `user.deactivate`).
        Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - active
                    - disabled
              required:
                - status
              additionalProperties: false
            example:
              status: disabled
      responses:
        '200':
          description: The updated user.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  user:
                    type: object
                    properties:
                      _id:
                        type: string
                      userId:
                        type: string
                      email:
                        type: string
                      displayName:
                        type: string
                      role:
                        type: string
                      tenantId:
                        type: string
                      allowedSubsidiaryIds:
                        type: array
                        items:
                          type: string
                      allowedPeppolParticipantIds:
                        type: array
                        items: {}
                      status:
                        type: string
                      mfaEnabled:
                        type: boolean
                      lastLoginAt:
                        type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                ok: true
                user:
                  _id: 671f5c…
                  userId: user_1789562400000_5e6f…
                  email: o***@example.com
                  displayName: Finance operator
                  role: tenant_user
                  tenantId: tnt_7Q2X…
                  allowedSubsidiaryIds:
                    - sub_1789…_a1b2
                  allowedPeppolParticipantIds: []
                  status: disabled
                  mfaEnabled: false
                  lastLoginAt: '2026-10-23T06:10:00.000Z'
                  createdAt: '2026-09-10T12:00:00.000Z'
                  updatedAt: '2026-10-20T08:00:00.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/users/{userId}/reset-password:
    post:
      operationId: postTenantUsersByUserIdResetPassword
      summary: Reset a tenant user's password
      description: >-
        Replaces the user's password with a new server-generated temporary password, returned **once** in
        `tempPassword`, and revokes their sessions. An admin cannot reset their own password here (409
        `CANNOT_RESET_OWN_PASSWORD`). Audit-logged (`user.password_reset`). Requires the `tenant_admin` role; a
        `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The new temporary password, shown only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  tempPassword:
                    type: string
              example:
                ok: true
                tempPassword: Pq9=…(16 chars)
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings:
    get:
      operationId: getTenantMappings
      summary: List field mappings
      description: >-
        Lists the tenant's ERP-to-PINT field mappings, most recently updated first, as a bare array of summaries (not
        paginated). Readable by any tenant user.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      responses:
        '200':
          description: Mapping summaries.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                    sourceSystem:
                      type: string
                    docType:
                      type: string
                    activeVersion:
                      type: integer
                    versionCount:
                      type: integer
                    updatedAt:
                      type: string
              example:
                - id: map_1729502043000_7a1c…
                  name: NetSuite invoices
                  sourceSystem: netsuite
                  docType: invoice
                  activeVersion: 1
                  versionCount: 2
                  updatedAt: '2026-10-21T09:14:03.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantMappings
      summary: Create a field mapping
      description: >-
        Creates a mapping with version 1 as a Draft. With a `sampleRecord`, the source schema is inferred from it,
        connections are auto-suggested by field-name synonyms and the record is stored as the first sample; without one
        the draft is empty. `docType` defaults to `invoice`, `sourceSystem` to `custom`. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                docType:
                  type: string
                  enum:
                    - invoice
                    - creditNote
                sourceSystem:
                  type: string
                  maxLength: 60
                sampleRecord:
                  type: object
                  additionalProperties: {}
                sampleLabel:
                  type: string
                  maxLength: 120
              required:
                - name
              additionalProperties: false
            example:
              name: NetSuite invoices
              docType: invoice
              sourceSystem: netsuite
              sampleLabel: Gulf Trading
              sampleRecord:
                tranid: INV-1042
                trandate: '2026-10-20'
                item:
                  - amount: 450
                    taxcode: VAT_AE:S
      responses:
        '201':
          description: The created mapping.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  sourceSystem:
                    type: string
                  docType:
                    type: string
                  activeVersion:
                    type: 'null'
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        version:
                          type: integer
                        status:
                          type: string
                        author:
                          type: string
                        date:
                          type: string
                        note:
                          type: string
                        connections:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              source:
                                type: string
                              target:
                                type: string
                              transform:
                                type: object
                                properties:
                                  type:
                                    type: string
                  sourceSchema:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        label:
                          type: string
                        repeating:
                          type: boolean
                        fields:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              type:
                                type: string
                  targetSchema:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        label:
                          type: string
                        fields:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              type:
                                type: string
                              required:
                                type: boolean
              example:
                id: map_1729502043000_7a1c…
                name: NetSuite invoices
                sourceSystem: netsuite
                docType: invoice
                activeVersion: null
                versions:
                  - version: 1
                    status: Draft
                    author: o***@example.com
                    date: '2026-10-21T09:14:03.000Z'
                    note: Auto-mapped from sample record.
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                sourceSchema:
                  - key: header
                    label: Header
                    repeating: false
                    fields:
                      - id: tranid
                        type: text
                      - id: trandate
                        type: date
                  - key: item
                    label: item
                    repeating: true
                    fields:
                      - id: item[].amount
                        type: number
                      - id: item[].taxcode
                        type: text
                targetSchema:
                  - key: document
                    label: Document
                    fields:
                      - id: invoiceNumber
                        type: text
                        required: true
                      - id: issueDate
                        type: date
                        required: true
                      - id: currency
                        type: text
                        required: false
                        defaultValue: AED
                  - key: supplier
                    label: Supplier
                    derived: true
                    fields: []
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: SAMPLE_EMPTY
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}:
    get:
      operationId: getTenantMappingsByMappingId
      summary: Get a field mapping
      description: >-
        Returns a mapping with all its versions (connections included), the inferred source schema and the PINT target
        schema for its document type.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The mapping.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  sourceSystem:
                    type: string
                  docType:
                    type: string
                  activeVersion:
                    type: integer
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        version:
                          type: integer
                        status:
                          type: string
                        author:
                          type: string
                        date:
                          type: string
                        note:
                          type: string
                        connections:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              source:
                                type: string
                              target:
                                type: string
                              transform:
                                type: object
                                properties:
                                  type:
                                    type: string
                  sourceSchema:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        label:
                          type: string
                        repeating:
                          type: boolean
                        fields:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              type:
                                type: string
                  targetSchema:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        label:
                          type: string
                        fields:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              type:
                                type: string
                              required:
                                type: boolean
              example:
                id: map_1729502043000_7a1c…
                name: NetSuite invoices
                sourceSystem: netsuite
                docType: invoice
                activeVersion: 1
                versions:
                  - version: 1
                    status: Active
                    author: o***@example.com
                    date: '2026-10-01T08:00:00.000Z'
                    note: Go-live (approved by Finance lead)
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                  - version: 2
                    status: Draft
                    author: o***@example.com
                    date: '2026-10-21T09:14:03.000Z'
                    note: Draft in progress.
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                sourceSchema:
                  - key: header
                    label: Header
                    repeating: false
                    fields:
                      - id: tranid
                        type: text
                      - id: trandate
                        type: date
                  - key: item
                    label: item
                    repeating: true
                    fields:
                      - id: item[].amount
                        type: number
                      - id: item[].taxcode
                        type: text
                targetSchema:
                  - key: document
                    label: Document
                    fields:
                      - id: invoiceNumber
                        type: text
                        required: true
                      - id: issueDate
                        type: date
                        required: true
                      - id: currency
                        type: text
                        required: false
                        defaultValue: AED
                  - key: supplier
                    label: Supplier
                    derived: true
                    fields: []
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/versions:
    get:
      operationId: getTenantMappingsByMappingIdVersions
      summary: List a mapping's versions
      description: >-
        Returns the mapping's versions as a bare array (Draft, Active, Archived), each with author, date, note and
        connections.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Versions.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    version:
                      type: integer
                    status:
                      type: string
                    author:
                      type: string
                    date:
                      type: string
                    note:
                      type: string
                    connections:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                          source:
                            type: string
                          target:
                            type: string
                          transform:
                            type: object
                            properties:
                              type:
                                type: string
              example:
                - version: 1
                  status: Active
                  author: o***@example.com
                  date: '2026-10-01T08:00:00.000Z'
                  note: Go-live (approved by Finance lead)
                  connections:
                    - id: c1
                      source: header.tranid
                      target: invoiceNumber
                      transform:
                        type: direct
                    - id: c2
                      source: header.trandate
                      target: issueDate
                      transform:
                        type: direct
                    - id: c3
                      source: item[].taxcode
                      target: lines[].taxCategory
                      transform:
                        type: lookup
                        pairs:
                          - from: VAT_AE:S
                            to: S
                          - from: VAT_AE:Z
                            to: Z
                - version: 2
                  status: Draft
                  author: o***@example.com
                  date: '2026-10-21T09:14:03.000Z'
                  note: Draft in progress.
                  connections:
                    - id: c1
                      source: header.tranid
                      target: invoiceNumber
                      transform:
                        type: direct
                    - id: c2
                      source: header.trandate
                      target: issueDate
                      transform:
                        type: direct
                    - id: c3
                      source: item[].taxcode
                      target: lines[].taxCategory
                      transform:
                        type: lookup
                        pairs:
                          - from: VAT_AE:S
                            to: S
                          - from: VAT_AE:Z
                            to: Z
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/samples:
    get:
      operationId: getTenantMappingsByMappingIdSamples
      summary: List a mapping's sample records
      description: >-
        Returns the stored sample ERP records (at most 10) as a bare array, each with its id, counterparty label and the
        full record.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Samples.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    counterparty:
                      type: string
                    record:
                      type: object
                      properties:
                        tranid:
                          type: string
                        trandate:
                          type: string
                        item:
                          type: array
                          items:
                            type: object
                            properties:
                              amount:
                                type: integer
                              taxcode:
                                type: string
              example:
                - id: sample_1729502043000_0b9e…
                  counterparty: Gulf Trading
                  record:
                    tranid: INV-1042
                    trandate: '2026-10-20'
                    item:
                      - amount: 450
                        taxcode: VAT_AE:S
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantMappingsByMappingIdSamples
      summary: Add a sample record to a mapping
      description: >-
        Stores another sample ERP record (maximum 10 per mapping) and merges any new source fields it reveals into the
        source schema. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                counterparty:
                  type: string
                  maxLength: 120
                record:
                  type: object
                  additionalProperties: {}
              required:
                - record
              additionalProperties: false
            example:
              counterparty: Gulf Trading
              record:
                tranid: INV-1042
                trandate: '2026-10-20'
                item:
                  - amount: 450
                    taxcode: VAT_AE:S
      responses:
        '201':
          description: The stored sample and the merged source schema.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sample:
                    type: object
                    properties:
                      id:
                        type: string
                      counterparty:
                        type: string
                      record:
                        type: object
                        properties:
                          tranid:
                            type: string
                          trandate:
                            type: string
                          item:
                            type: array
                            items:
                              type: object
                              properties:
                                amount:
                                  type: integer
                                taxcode:
                                  type: string
                  sourceSchema:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        label:
                          type: string
                        repeating:
                          type: boolean
                        fields:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              type:
                                type: string
              example:
                sample:
                  id: sample_1729502043000_0b9e…
                  counterparty: Gulf Trading
                  record:
                    tranid: INV-1042
                    trandate: '2026-10-20'
                    item:
                      - amount: 450
                        taxcode: VAT_AE:S
                sourceSchema:
                  - key: header
                    label: Header
                    repeating: false
                    fields:
                      - id: tranid
                        type: text
                      - id: trandate
                        type: date
                  - key: item
                    label: item
                    repeating: true
                    fields:
                      - id: item[].amount
                        type: number
                      - id: item[].taxcode
                        type: text
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: SAMPLE_LIMIT
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/draft:
    put:
      operationId: putTenantMappingsByMappingIdDraft
      summary: Save a mapping draft
      description: >-
        Replaces the connections of the mapping's Draft version, creating a new Draft (next version number) when none
        exists. Connections are checked structurally first; blocking issues (`MAP_MALFORMED_CONNECTION`,
        `MAP_DUPLICATE_TARGET`, `MAP_BAD_FORMULA`) return 400 with `issues`. The Draft never runs in the submission
        path. `tenant_admin` only. Not audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                connections:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        maxLength: 80
                      source:
                        type: string
                        maxLength: 300
                      target:
                        type: string
                        maxLength: 300
                      transform:
                        type: object
                        properties:
                          type:
                            type: string
                            enum:
                              - direct
                              - formula
                              - lookup
                              - conditional
                          expression:
                            type: string
                            maxLength: 500
                          pairs:
                            type: array
                            items:
                              type: object
                              properties:
                                from:
                                  type: string
                                  maxLength: 200
                                to:
                                  type: string
                                  maxLength: 200
                              required:
                                - from
                                - to
                            maxItems: 100
                          field:
                            type: string
                            maxLength: 200
                          value:
                            type: string
                            maxLength: 200
                          then:
                            type: string
                            maxLength: 200
                          else:
                            type: string
                            maxLength: 200
                        required:
                          - type
                        additionalProperties: false
                    required:
                      - id
                      - source
                      - target
                    additionalProperties: false
                  maxItems: 200
              required:
                - connections
              additionalProperties: false
            example:
              connections:
                - id: c1
                  source: header.tranid
                  target: invoiceNumber
                  transform:
                    type: direct
                - id: c2
                  source: header.trandate
                  target: issueDate
                  transform:
                    type: direct
                - id: c3
                  source: item[].taxcode
                  target: lines[].taxCategory
                  transform:
                    type: lookup
                    pairs:
                      - from: VAT_AE:S
                        to: S
                      - from: VAT_AE:Z
                        to: Z
      responses:
        '200':
          description: The saved Draft version.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: object
                    properties:
                      version:
                        type: integer
                      status:
                        type: string
                      author:
                        type: string
                      date:
                        type: string
                      note:
                        type: string
                      connections:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                            source:
                              type: string
                            target:
                              type: string
                            transform:
                              type: object
                              properties:
                                type:
                                  type: string
              example:
                version:
                  version: 2
                  status: Draft
                  author: o***@example.com
                  date: '2026-10-21T09:14:03.000Z'
                  note: Draft in progress.
                  connections:
                    - id: c1
                      source: header.tranid
                      target: invoiceNumber
                      transform:
                        type: direct
                    - id: c2
                      source: header.trandate
                      target: issueDate
                      transform:
                        type: direct
                    - id: c3
                      source: item[].taxcode
                      target: lines[].taxCategory
                      transform:
                        type: lookup
                        pairs:
                          - from: VAT_AE:S
                            to: S
                          - from: VAT_AE:Z
                            to: Z
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      issues:
                        type: array
                        items:
                          type: object
                          properties:
                            code:
                              type: string
                            severity:
                              type: string
                            targetPath:
                              type: string
                            sourceField:
                              type: string
              example:
                error:
                  code: MAP_DUPLICATE_TARGET
                  issues:
                    - code: MAP_DUPLICATE_TARGET
                      severity: danger
                      targetPath: invoiceNumber
                      sourceField: header.tranid
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/simulate:
    post:
      operationId: postTenantMappingsByMappingIdSimulate
      summary: Simulate a mapping against a sample record
      description: >-
        Applies the given connections to a stored sample (`sampleId`, or the first sample) and runs the result through
        the same validator that gates submission, returning the mapped values, the issues (genuine PINT rule failures as
        `danger`) and a 0–100 readiness score. The supplier comes from the tenant's default company; when its profile is
        incomplete a placeholder supplier is used and `MAP_SUPPLIER_PLACEHOLDER` is added as a warning. Nothing is
        saved. Open to any tenant user.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                connections:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        maxLength: 80
                      source:
                        type: string
                        maxLength: 300
                      target:
                        type: string
                        maxLength: 300
                      transform:
                        type: object
                        properties:
                          type:
                            type: string
                            enum:
                              - direct
                              - formula
                              - lookup
                              - conditional
                          expression:
                            type: string
                            maxLength: 500
                          pairs:
                            type: array
                            items:
                              type: object
                              properties:
                                from:
                                  type: string
                                  maxLength: 200
                                to:
                                  type: string
                                  maxLength: 200
                              required:
                                - from
                                - to
                            maxItems: 100
                          field:
                            type: string
                            maxLength: 200
                          value:
                            type: string
                            maxLength: 200
                          then:
                            type: string
                            maxLength: 200
                          else:
                            type: string
                            maxLength: 200
                        required:
                          - type
                        additionalProperties: false
                    required:
                      - id
                      - source
                      - target
                    additionalProperties: false
                  maxItems: 200
                sampleId:
                  type: string
                  maxLength: 80
              required:
                - connections
              additionalProperties: false
            example:
              connections:
                - id: c1
                  source: header.tranid
                  target: invoiceNumber
                  transform:
                    type: direct
                - id: c2
                  source: header.trandate
                  target: issueDate
                  transform:
                    type: direct
                - id: c3
                  source: item[].taxcode
                  target: lines[].taxCategory
                  transform:
                    type: lookup
                    pairs:
                      - from: VAT_AE:S
                        to: S
                      - from: VAT_AE:Z
                        to: Z
              sampleId: sample_1729502043000_0b9e…
      responses:
        '200':
          description: Simulation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  values:
                    type: array
                    items:
                      type: object
                      properties:
                        targetPath:
                          type: string
                        value:
                          type: string
                        sourceField:
                          type: string
                        transform:
                          type: string
                  issues:
                    type: array
                    items:
                      type: object
                      properties:
                        targetPath:
                          type: string
                        sourceField:
                          type: 'null'
                        code:
                          type: string
                        severity:
                          type: string
                        message:
                          type: string
                  score:
                    type: integer
              example:
                values:
                  - targetPath: invoiceNumber
                    value: INV-1042
                    sourceField: header.tranid
                    transform: direct
                  - targetPath: taxTotal.taxAmount
                    value: '22.5'
                    sourceField: null
                    transform: direct
                issues:
                  - targetPath: customer.taxId
                    sourceField: null
                    code: PINT_BR_AE_…
                    severity: danger
                    message: Buyer VAT identifier is required
                score: 85
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: NO_SAMPLES
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/activate:
    post:
      operationId: postTenantMappingsByMappingIdActivate
      summary: Activate a mapping draft
      description: >-
        Makes the given Draft version the Active one (the previous Active becomes Archived); from then on it is applied
        to `inputType: "mapped"` submissions. Every required PINT target must be mapped, otherwise 400
        `REQUIRED_UNMAPPED` with the `unmapped` list. `approver` is free text from the request and is appended to the
        note and audit row. `tenant_admin` only.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                version:
                  type: integer
                  exclusiveMinimum: 0
                approver:
                  type: string
                  maxLength: 120
                note:
                  type: string
                  maxLength: 500
              required:
                - version
                - approver
                - note
              additionalProperties: false
            example:
              version: 2
              approver: Finance lead
              note: Adds VAT lookup
      responses:
        '200':
          description: All versions after activation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        version:
                          type: integer
                        status:
                          type: string
                        author:
                          type: string
                        date:
                          type: string
                        note:
                          type: string
                        connections:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              source:
                                type: string
                              target:
                                type: string
                              transform:
                                type: object
                                properties:
                                  type:
                                    type: string
                  activatedVersion:
                    type: integer
              example:
                versions:
                  - version: 1
                    status: Archived
                    author: o***@example.com
                    date: '2026-10-01T08:00:00.000Z'
                    note: Go-live (approved by Finance lead)
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                  - version: 2
                    status: Active
                    author: o***@example.com
                    date: '2026-10-21T09:14:03.000Z'
                    note: Adds VAT lookup (approved by Finance lead)
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                activatedVersion: 2
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      unmapped:
                        type: array
                        items:
                          type: string
              example:
                error:
                  code: REQUIRED_UNMAPPED
                  unmapped:
                    - customer.taxId
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/rollback:
    post:
      operationId: postTenantMappingsByMappingIdRollback
      summary: Roll a mapping back to an archived version
      description: >-
        Copies the connections of an Archived version into a new version that becomes Active (the current Active is
        archived). `toVersion` must be Archived (`NOT_ARCHIVED` otherwise). `approver` (free text) becomes the new
        version's author. `tenant_admin` only; audited.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                toVersion:
                  type: integer
                  exclusiveMinimum: 0
                approver:
                  type: string
                  maxLength: 120
                note:
                  type: string
                  maxLength: 500
              required:
                - toVersion
                - approver
                - note
              additionalProperties: false
            example:
              toVersion: 1
              approver: Finance lead
              note: VAT lookup rejected by receiver
      responses:
        '200':
          description: All versions after the rollback.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      type: object
                      properties:
                        version:
                          type: integer
                        status:
                          type: string
                        author:
                          type: string
                        date:
                          type: string
                        note:
                          type: string
                        connections:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              source:
                                type: string
                              target:
                                type: string
                              transform:
                                type: object
                                properties:
                                  type:
                                    type: string
                  activatedVersion:
                    type: integer
              example:
                versions:
                  - version: 1
                    status: Archived
                    author: o***@example.com
                    date: '2026-10-01T08:00:00.000Z'
                    note: Go-live (approved by Finance lead)
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                  - version: 2
                    status: Archived
                    author: o***@example.com
                    date: '2026-10-21T09:14:03.000Z'
                    note: Draft in progress.
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                  - version: 3
                    status: Active
                    author: Finance lead
                    date: '2026-10-21T09:14:41.000Z'
                    note: Rolled back to v1 — VAT lookup rejected by receiver (approved by Finance lead)
                    connections:
                      - id: c1
                        source: header.tranid
                        target: invoiceNumber
                        transform:
                          type: direct
                      - id: c2
                        source: header.trandate
                        target: issueDate
                        transform:
                          type: direct
                      - id: c3
                        source: item[].taxcode
                        target: lines[].taxCategory
                        transform:
                          type: lookup
                          pairs:
                            - from: VAT_AE:S
                              to: S
                            - from: VAT_AE:Z
                              to: Z
                activatedVersion: 3
        '400':
          description: >-
            Rejected by a mapping rule. The body carries only `error.code` (no `message`), plus `unmapped` or `issues`
            where relevant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: NOT_ARCHIVED
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/mappings/{mappingId}/diff:
    get:
      operationId: getTenantMappingsByMappingIdDiff
      summary: Compare two mapping versions
      description: >-
        Diffs the connections of versions `from` and `to` by target field: connections only in `to` are `added`, only in
        `from` are `removed`, and those whose source or transform differ are `changed`. Both query parameters are
        required integers.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Mappings
      parameters:
        - name: mappingId
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: true
          description: Base version number.
          schema:
            type: integer
        - name: to
          in: query
          required: true
          description: Compared version number.
          schema:
            type: integer
      responses:
        '200':
          description: Connection diff.
          content:
            application/json:
              schema:
                type: object
                properties:
                  added:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        source:
                          type: string
                        target:
                          type: string
                        transform:
                          type: object
                          properties:
                            type:
                              type: string
                            pairs:
                              type: array
                              items:
                                type: object
                                properties:
                                  from:
                                    type: string
                                  to:
                                    type: string
                  removed:
                    type: array
                    items: {}
                  changed:
                    type: array
                    items:
                      type: object
                      properties:
                        target:
                          type: string
                        from:
                          type: object
                          properties:
                            id:
                              type: string
                            source:
                              type: string
                            target:
                              type: string
                        to:
                          type: object
                          properties:
                            id:
                              type: string
                            source:
                              type: string
                            target:
                              type: string
                            transform:
                              type: object
                              properties:
                                type:
                                  type: string
              example:
                added:
                  - id: c3
                    source: item[].taxcode
                    target: lines[].taxCategory
                    transform:
                      type: lookup
                      pairs:
                        - from: VAT_AE:S
                          to: S
                        - from: VAT_AE:Z
                          to: Z
                removed: []
                changed:
                  - target: issueDate
                    from:
                      id: c2
                      source: header.createddate
                      target: issueDate
                    to:
                      id: c2
                      source: header.trandate
                      target: issueDate
                      transform:
                        type: direct
        '400':
          description: '`from` or `to` is missing or not an integer (bare code, no `message`).'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: BAD_VERSIONS
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/insights-schedules:
    get:
      operationId: getTenantInsightsSchedules
      summary: List scheduled report emails
      description: >-
        The schedules (this tenant) with their next and last run and the last 10 runs, plus `allowedRecipients`: the
        addresses that may be chosen (the tenant's contact (shared onboarding) email and its active portal users (tenant
        admins and users)) and the limits (20 schedules, 10 recipients each).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: Schedules and allowed recipients.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        scheduleId:
                          type: string
                        scope:
                          type: string
                        report:
                          type: string
                        frequency:
                          type: string
                        weekday:
                          type: integer
                        hour:
                          type: integer
                        filters:
                          type: object
                          properties:
                            environment:
                              type: string
                            direction:
                              type: string
                        recipients:
                          type: array
                          items:
                            type: string
                        enabled:
                          type: boolean
                        nextRunAt:
                          type: string
                        lastStatus:
                          type: string
                  allowedRecipients:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        label:
                          type: string
                  limits:
                    type: object
                    properties:
                      schedules:
                        type: integer
                      recipients:
                        type: integer
              example:
                items:
                  - scheduleId: sch_3f2a9c1d7b4e6a80
                    scope: tenant
                    report: volume
                    frequency: weekly
                    weekday: 1
                    hour: 8
                    filters:
                      environment: production
                      direction: all
                    recipients:
                      - finance@acme.ae
                    enabled: true
                    nextRunAt: '2026-11-02T04:00:00.000Z'
                    lastStatus: sent
                allowedRecipients:
                  - email: finance@acme.ae
                    label: Finance (user)
                limits:
                  schedules: 20
                  recipients: 10
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantInsightsSchedules
      summary: Create a scheduled report email
      description: >-
        Sends one report by email on a schedule: `report` = `volume`, `vat`, `delivery`, `partners`; `frequency` =
        `weekly` (`weekday` 1–7, ISO, Monday = 1) or `monthly` (`dayOfMonth` 1–28); `hour` 0–23 Dubai time (default
        08:00). Each run covers the last full period (previous ISO week or previous calendar month, Dubai time) compared
        with the period before, and the email has a KPI summary, a link to the report and the Excel export attached.
        `recipients` (1–10) must be the tenant's contact (shared onboarding) email and its active portal users (tenant
        admins and users); they are checked again at every send, so a removed user stops receiving reports. Optional
        `filters`: `environment`, `direction`. Write access: tenant_admin or super_admin (403 otherwise). Audit-logged.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '201':
          description: The schedule.
          content:
            application/json:
              schema:
                type: object
                properties:
                  scheduleId:
                    type: string
                  scope:
                    type: string
                  report:
                    type: string
                  frequency:
                    type: string
                  weekday:
                    type: integer
                  hour:
                    type: integer
                  filters:
                    type: object
                    properties:
                      environment:
                        type: string
                      direction:
                        type: string
                  recipients:
                    type: array
                    items:
                      type: string
                  enabled:
                    type: boolean
                  nextRunAt:
                    type: string
                  lastStatus:
                    type: string
              example:
                scheduleId: sch_3f2a9c1d7b4e6a80
                scope: tenant
                report: volume
                frequency: weekly
                weekday: 1
                hour: 8
                filters:
                  environment: production
                  direction: all
                recipients:
                  - finance@acme.ae
                enabled: true
                nextRunAt: '2026-11-02T04:00:00.000Z'
                lastStatus: sent
        '400':
          description: Invalid body or `RECIPIENT_NOT_ALLOWED` (rejected addresses in `details.rejected`).
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Not allowed to change schedules.
        '409':
          description: '`SCHEDULE_LIMIT` (20).'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/insights-schedules/{scheduleId}:
    delete:
      operationId: deleteTenantInsightsSchedulesByScheduleId
      summary: Delete a scheduled report email
      description: |-
        Removes the schedule. Audit-logged.

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: scheduleId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
              example:
                deleted: true
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          description: No such schedule for this caller.
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    patch:
      operationId: patchTenantInsightsSchedulesByScheduleId
      summary: Change a scheduled report email
      description: >-
        Any of the create fields; the next run is recalculated (none while `enabled` is false). Recipients are
        re-checked. Only the caller's own schedules (this tenant). Audit-logged.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: scheduleId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The updated schedule.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          description: No such schedule for this caller.
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/insights-schedules/{scheduleId}/send-now:
    post:
      operationId: postTenantInsightsSchedulesByScheduleIdSendNow
      summary: Send a scheduled report now
      description: >-
        Builds and emails the report for the last full period immediately, without moving the schedule. Once a minute
        per schedule. Returns the run: `status` `sent`, `skipped` (no recipient is still allowed) or `failed` (with
        `error`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: scheduleId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The run.
          content:
            application/json:
              schema:
                type: object
                properties:
                  at:
                    type: string
                  trigger:
                    type: string
                  status:
                    type: string
                  from:
                    type: string
                  to:
                    type: string
                  recipients:
                    type: integer
                  error:
                    type: 'null'
              example:
                at: '2026-10-25T18:00:00.000Z'
                trigger: manual
                status: sent
                from: '2026-10-19'
                to: '2026-10-25'
                recipients: 2
                error: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '429':
          description: '`TOO_SOON`: sent less than a minute ago.'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/insights/{report}:
    get:
      operationId: getTenantInsightsByReport
      summary: Get an Insights report
      description: >-
        Exact aggregation over every matching platform transaction (no row cap, unlike `/api/tenant/reports/*`).
        `report` is `volume` (Volume & trends: business documents and Tax Data Documents per period, sent vs received,
        delivery rate, by document kind, daily calendar, weekday × hour heatmap) or `vat` (VAT & invoice value: sales =
        sent documents, purchases = received, credit notes netted, output/input VAT, VAT by category, largest documents,
        coverage; AED in the KPIs and charts, other currencies listed separately, never converted), `delivery`
        (Delivery, MLS & failures: sent business documents, delivery rate, time to delivery median/95th percentile and
        distribution, failures by stage and reason with the latest affected documents, and the receiver's Message Level
        Status from the AP — accepted/acknowledged/rejected/awaiting — with response time against the 10-minute SLA and
        the latest rejections) or `partners` (Partners & FTA reporting: top counterparties by documents with AED value
        net of credit notes, delivery rate, share and last activity, names from the tenant's customer list, partners
        first seen in the period; Tax Data Documents sent to the FTA with the FTA's answer from the AP MLS —
        accepted/rejected/awaiting/not delivered — response times and the latest rejections). AP figures come from
        linking the period's platform rows to AP rows (live + archive); at most the 5,000 most recent rows are linked
        and the response says when the AP figures are a sample. Period: `from`/`to` are Dubai calendar days, both
        inclusive (default: the last 30 days, at most 731). `granularity` = `auto` (day up to 45 days, week up to 200,
        then month), `day`, `week` (ISO, from Monday) or `month`. With `compare` (default `true`) each KPI carries the
        previous period of the same length and the change in %. Filters: `environment` (Peppol network), `direction`,
        `subsidiaryId`. tenant_user callers only see their allowed companies / Peppol identities. Results are cached for
        30 s. Unknown query parameters are rejected.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: report
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
        - name: to
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum:
              - auto
              - day
              - week
              - month
        - name: environment
          in: query
          required: false
          schema:
            type: string
            enum:
              - test
              - production
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - inbound
              - outbound
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
            maxLength: 80
        - name: compare
          in: query
          required: false
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  report:
                    type: string
                  title:
                    type: string
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                      granularity:
                        type: string
                      timezone:
                        type: string
                  previous:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                  generatedAt:
                    type: string
                  kpis:
                    type: object
                    properties:
                      documents:
                        type: object
                        properties:
                          current:
                            type: integer
                          previous:
                            type: integer
                          change:
                            type: number
                      deliveryRate:
                        type: object
                        properties:
                          current:
                            type: number
                          previous:
                            type: number
                          change:
                            type: number
                  series:
                    type: array
                    items:
                      type: object
                      properties:
                        bucket:
                          type: string
                        sent:
                          type: integer
                        received:
                          type: integer
                        invoice:
                          type: integer
                        credit_note:
                          type: integer
                        tdd:
                          type: integer
              example:
                report: volume
                title: Volume & trends
                period:
                  from: '2026-09-26'
                  to: '2026-10-25'
                  granularity: day
                  timezone: Asia/Dubai
                previous:
                  from: '2026-08-27'
                  to: '2026-09-25'
                generatedAt: '2026-10-25T18:00:00.000Z'
                kpis:
                  documents:
                    current: 412
                    previous: 380
                    change: 8.4
                  deliveryRate:
                    current: 97.6
                    previous: 96.1
                    change: 1.5
                series:
                  - bucket: '2026-10-20'
                    sent: 18
                    received: 4
                    invoice: 19
                    credit_note: 3
                    tdd: 21
        '400':
          description: Unknown report or parameter, `from` after `to`, or a period over 731 days.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/insights/{report}/export:
    get:
      operationId: getTenantInsightsByReportExport
      summary: Export an Insights report
      description: >-
        The same report and filters as `GET /api/tenant/insights/{report}`, as a file. `format=xlsx` (default): an Excel
        workbook with one sheet per table (typed numbers, dates and AED amounts, frozen header). `format=csv`: the main
        table only, UTF-8 with a byte-order mark, formula-like cells neutralised. Sent as an attachment named
        `azdan-{report}-{from}_{to}.{ext}`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: report
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
        - name: to
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}$
        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum:
              - auto
              - day
              - week
              - month
        - name: environment
          in: query
          required: false
          schema:
            type: string
            enum:
              - test
              - production
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - inbound
              - outbound
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
            maxLength: 80
        - name: compare
          in: query
          required: false
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - csv
              - xlsx
      responses:
        '200':
          description: The file (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` or `text/csv`).
        '400':
          description: Unknown report, format or parameter.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/analytics:
    get:
      operationId: getTenantAnalytics
      summary: Get console analytics for a date range
      description: >-
        One aggregation over the tenant's platform transactions (all rows, including tax-data and MLS transport rows)
        feeding the console charts: daily series (every day in range, zero-filled), a documentType → direction → status
        flow, VAT by category with coverage, failure stages and the top 8 failure reasons, and totals. Default range:
        the 91 days ending today; `to` is inclusive. Amounts are summed as stored (`amount`, `taxAmount`). tenant_user
        callers only see their allowed companies / Peppol identities. Unknown query parameters are rejected (strict
        schema).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Dashboard & compliance
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}(T.*)?$
        - name: to
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-\d{2}-\d{2}(T.*)?$
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
            maxLength: 80
        - name: environment
          in: query
          required: false
          schema:
            type: string
            enum:
              - test
              - production
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - inbound
              - outbound
      responses:
        '200':
          description: The console datasets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  range:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                  totals:
                    type: object
                    properties:
                      documents:
                        type: integer
                      delivered:
                        type: integer
                      failed:
                        type: integer
                      pending:
                        type: integer
                      value:
                        type: number
                      vat:
                        type: number
                  timeseries:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                        outbound:
                          type: integer
                        inbound:
                          type: integer
                        delivered:
                          type: integer
                        failed:
                          type: integer
                        pending:
                          type: integer
                        value:
                          type: number
                        vat:
                          type: number
                  flow:
                    type: object
                    properties:
                      nodes:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                      links:
                        type: array
                        items:
                          type: object
                          properties:
                            source:
                              type: string
                            target:
                              type: string
                            value:
                              type: integer
                  vat:
                    type: object
                    properties:
                      byCategory:
                        type: array
                        items:
                          type: object
                          properties:
                            category:
                              type: string
                            taxableAmount:
                              type: integer
                            taxAmount:
                              type: number
                      coverage:
                        type: object
                        properties:
                          total:
                            type: integer
                          withVat:
                            type: integer
                  exceptions:
                    type: object
                    properties:
                      byStage:
                        type: array
                        items:
                          type: object
                          properties:
                            stage:
                              type: string
                            count:
                              type: integer
                      topReasons:
                        type: array
                        items:
                          type: object
                          properties:
                            reason:
                              type: string
                            count:
                              type: integer
              example:
                range:
                  from: '2026-09-24'
                  to: '2026-10-24'
                totals:
                  documents: 412
                  delivered: 398
                  failed: 6
                  pending: 8
                  value: 1824500.75
                  vat: 86880.04
                timeseries:
                  - date: '2026-10-20'
                    outbound: 18
                    inbound: 4
                    delivered: 21
                    failed: 1
                    pending: 0
                    value: 61250.5
                    vat: 2916.69
                flow:
                  nodes:
                    - name: invoice
                    - name: outbound
                    - name: delivered
                  links:
                    - source: invoice
                      target: outbound
                      value: 350
                    - source: outbound
                      target: delivered
                      value: 342
                vat:
                  byCategory:
                    - category: S
                      taxableAmount: 1737620
                      taxAmount: 86880.04
                  coverage:
                    total: 412
                    withVat: 389
                exceptions:
                  byStage:
                    - stage: ap_core_response
                      count: 4
                  topReasons:
                    - reason: Receiver endpoint not found in SMP
                      count: 3
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/dashboard:
    get:
      operationId: getTenantDashboard
      summary: Get the tenant dashboard summary
      description: >-
        Status and direction counts over all of the tenant's platform transactions, the 24-hour success rate (share of
        rows created in the last 24 h whose status falls in the SUCCESS bucket; 1 when there were none), the 10 most
        recent transactions (projected ledger rows, without AP enrichment or `statusDetail`) and the tenant's webhooks
        (secrets removed). `queued` counts `queued` + `sending`. Not narrowed by a `tenant_user`'s company scope.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Dashboard & compliance
      responses:
        '200':
          description: The dashboard summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenantId:
                    type: string
                  counts:
                    type: object
                    properties:
                      total:
                        type: integer
                      delivered:
                        type: integer
                      failed:
                        type: integer
                      queued:
                        type: integer
                      inbound:
                        type: integer
                      outbound:
                        type: integer
                  successRate24h:
                    type: number
                  recentTransactions:
                    type: array
                    items:
                      type: object
                      properties:
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        subsidiaryId:
                          type: string
                        direction:
                          type: string
                        status:
                          type: string
                        documentType:
                          type: string
                        documentTypeId:
                          type: string
                        processId:
                          type: string
                        environment:
                          type: string
                        c1CountryCode:
                          type: string
                        sender:
                          type: object
                          properties:
                            participantId:
                              type: string
                            name:
                              type: string
                        receiver:
                          type: object
                          properties:
                            participantId:
                              type: string
                        correlationId:
                          type: string
                        apCoreMessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        sizeBytes:
                          type: integer
                        amount:
                          type: integer
                        currency:
                          type: string
                        taxAmount:
                          type: integer
                        taxExclusiveAmount:
                          type: integer
                        taxBreakdown:
                          type: array
                          items:
                            type: object
                            properties:
                              category:
                                type: string
                              percent:
                                type: string
                              taxableAmount:
                                type: integer
                              taxAmount:
                                type: integer
                        document:
                          type: object
                          properties:
                            number:
                              type: string
                            issueDate:
                              type: string
                            dueDate:
                              type: string
                            currency:
                              type: string
                            typeCode:
                              type: string
                            supplierName:
                              type: string
                            customerName:
                              type: string
                            netAmount:
                              type: integer
                            taxAmount:
                              type: integer
                            payableAmount:
                              type: integer
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        deliveredAt:
                          type: string
                        attempts:
                          type: integer
                        lastAttemptAt:
                          type: string
                  webhooks:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        webhookId:
                          type: string
                        tenantId:
                          type: string
                        name:
                          type: string
                        url:
                          type: string
                        events:
                          type: array
                          items:
                            type: string
                        subsidiaryId:
                          type: 'null'
                        payloadVersion:
                          type: string
                        status:
                          type: string
                        signingSecretRotatedAt:
                          type: 'null'
                        previousSecretValidUntil:
                          type: 'null'
                        health:
                          type: object
                          properties:
                            consecutiveFailures:
                              type: integer
                            successRate24h:
                              type: integer
                            lastDeliveryAt:
                              type: string
                            lastSuccessAt:
                              type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        hasSigningSecret:
                          type: boolean
                        auth:
                          type: object
                          properties:
                            type:
                              type: string
                            configured:
                              type: boolean
                            display:
                              type: string
              example:
                tenantId: tnt_7Q2X…
                counts:
                  total: 1287
                  delivered: 1240
                  failed: 21
                  queued: 6
                  inbound: 312
                  outbound: 975
                successRate24h: 0.97
                recentTransactions:
                  - transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    status: delivered
                    documentType: invoice
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    environment: production
                    c1CountryCode: AE
                    sender:
                      participantId: 0235:1004****59
                      name: Sample Trading LLC
                    receiver:
                      participantId: 0235:1002****03
                    correlationId: cor_1789562400000_4d5e…
                    apCoreMessageId: f0debc61-…@peppol.azdanapps.com
                    sbdhInstanceId: 6c1d2e3f-…
                    sizeBytes: 18432
                    amount: 1050
                    currency: AED
                    taxAmount: 50
                    taxExclusiveAmount: 1000
                    taxBreakdown:
                      - category: S
                        percent: '5'
                        taxableAmount: 1000
                        taxAmount: 50
                    document:
                      number: INV-2026-000123
                      issueDate: '2026-10-20'
                      dueDate: '2026-11-19'
                      currency: AED
                      typeCode: '380'
                      supplierName: Sample Trading LLC
                      customerName: Sample Buyer FZ-LLC
                      netAmount: 1000
                      taxAmount: 50
                      payableAmount: 1050
                    createdAt: '2026-10-20T09:00:00.000Z'
                    updatedAt: '2026-10-20T09:02:41.000Z'
                    deliveredAt: '2026-10-20T09:00:04.000Z'
                    attempts: 1
                    lastAttemptAt: '2026-10-20T09:00:02.000Z'
                webhooks:
                  - _id: 671f3a…
                    webhookId: whk_1789562400000_k3j9a1
                    tenantId: tnt_7Q2X…
                    name: ERP production
                    url: https://erp.example.com/hooks/azdan
                    events:
                      - document.delivered
                      - document.failed
                      - mls.rejected
                    subsidiaryId: null
                    payloadVersion: '2'
                    status: active
                    signingSecretRotatedAt: null
                    previousSecretValidUntil: null
                    health:
                      consecutiveFailures: 0
                      successRate24h: 1
                      lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                      lastSuccessAt: '2026-10-20T09:02:45.000Z'
                    createdAt: '2026-09-02T10:00:00.000Z'
                    updatedAt: '2026-10-01T11:30:00.000Z'
                    hasSigningSecret: true
                    auth:
                      type: bearer
                      configured: true
                      display: Bearer ••••••••••••
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions:
    get:
      operationId: getTenantTransactions
      summary: List transactions
      description: >-
        Paged ledger of the tenant's platform transactions, newest first, each enriched from AP Core (AP status, MLS,
        link state) and carrying `documentKind`, `statusDetail` and the status `bucket`. Filters: `direction`, `status`,
        `bucket`, `subsidiaryId`, `peppolParticipantId`, `environment`, `from` / `to` (created time) and `q`
        (transaction id, document type, correlation id, participant ids, document number, customer name).
        `view=documents` leaves out MLS and tax-data transport rows. On page 1, when no `subsidiaryId`, `environment`,
        `q` or `peppolParticipantId` filter is set, up to 50 AP-only rows (traffic that never went through the Platform
        API, `source: "ap-core"`, `linkState: "ap_only"`) are merged in; `page.total` counts platform rows only. In list
        rows outbound `statusDetail.reporting` is `unknown` (the TDD is only resolved in the detail). tenant_user
        callers only see rows of their allowed companies / Peppol identities; with no allow-list the list is empty.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 25
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - inbound
              - outbound
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: bucket
          in: query
          required: false
          schema:
            type: string
            enum:
              - SUCCESS
              - FAILED
              - PENDING
              - OTHER
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
        - name: peppolParticipantId
          in: query
          required: false
          schema:
            type: string
        - name: environment
          in: query
          required: false
          schema:
            type: string
            enum:
              - test
              - production
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: q
          in: query
          required: false
          schema:
            type: string
        - name: view
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - documents
        - name: documentKind
          in: query
          required: false
          schema:
            type: string
            enum:
              - invoice
              - credit_note
              - self_billing_invoice
              - self_billing_credit_note
              - tdd
              - mls
              - other
        - name: counterparty
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 120
        - name: customerId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 80
        - name: amountMin
          in: query
          required: false
          schema:
            type: number
            minimum: 0
        - name: amountMax
          in: query
          required: false
          schema:
            type: number
            minimum: 0
      responses:
        '200':
          description: One page of transactions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        subsidiaryId:
                          type: string
                        direction:
                          type: string
                        status:
                          type: string
                        documentType:
                          type: string
                        documentTypeId:
                          type: string
                        processId:
                          type: string
                        environment:
                          type: string
                        c1CountryCode:
                          type: string
                        sender:
                          type: object
                          properties:
                            participantId:
                              type: string
                            name:
                              type: string
                        receiver:
                          type: object
                          properties:
                            participantId:
                              type: string
                        correlationId:
                          type: string
                        apCoreMessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        sizeBytes:
                          type: integer
                        amount:
                          type: integer
                        currency:
                          type: string
                        taxAmount:
                          type: integer
                        taxExclusiveAmount:
                          type: integer
                        taxBreakdown:
                          type: array
                          items:
                            type: object
                            properties:
                              category:
                                type: string
                              percent:
                                type: string
                              taxableAmount:
                                type: integer
                              taxAmount:
                                type: integer
                        document:
                          type: object
                          properties:
                            number:
                              type: string
                            issueDate:
                              type: string
                            dueDate:
                              type: string
                            currency:
                              type: string
                            typeCode:
                              type: string
                            supplierName:
                              type: string
                            customerName:
                              type: string
                            netAmount:
                              type: integer
                            taxAmount:
                              type: integer
                            payableAmount:
                              type: integer
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        deliveredAt:
                          type: string
                        attempts:
                          type: integer
                        lastAttemptAt:
                          type: string
                        source:
                          type: string
                        apStatus:
                          type: string
                        apTransactionId:
                          type: string
                        apMessageId:
                          type: string
                        mlsStatus:
                          type: string
                        mlsType:
                          type: string
                        mlsResponseCode:
                          type: 'null'
                        tddStatus:
                          type: 'null'
                        isTdd:
                          type: boolean
                        linkState:
                          type: string
                        documentKind:
                          type: string
                        statusDetail:
                          type: object
                          properties:
                            documentKind:
                              type: string
                            overall:
                              type: string
                            processing:
                              type: object
                              properties:
                                platformStatus:
                                  type: string
                                apStatus:
                                  type: string
                                source:
                                  type: string
                                createdAt:
                                  type: string
                                failureStage:
                                  type: 'null'
                            validation:
                              type: object
                              properties:
                                state:
                                  type: string
                                engine:
                                  type: string
                                at:
                                  type: string
                                warnings:
                                  type: integer
                                error:
                                  type: 'null'
                            delivery:
                              type: object
                              properties:
                                state:
                                  type: string
                                apStatus:
                                  type: string
                                platformStatus:
                                  type: string
                                attempts:
                                  type: integer
                                lastAttemptAt:
                                  type: string
                                completedAt:
                                  type: string
                                failedAt:
                                  type: 'null'
                                errorCode:
                                  type: 'null'
                                error:
                                  type: 'null'
                            mls:
                              type: object
                              properties:
                                role:
                                  type: string
                                state:
                                  type: string
                                code:
                                  type: string
                                raw:
                                  type: string
                                at:
                                  type: string
                                mlsId:
                                  type: string
                                errorCode:
                                  type: 'null'
                                errorMessage:
                                  type: 'null'
                                policy:
                                  type: string
                            reporting:
                              type: object
                              properties:
                                state:
                                  type: string
                        bucket:
                          type: string
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    status: delivered
                    documentType: invoice
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    environment: production
                    c1CountryCode: AE
                    sender:
                      participantId: 0235:1004****59
                      name: Sample Trading LLC
                    receiver:
                      participantId: 0235:1002****03
                    correlationId: cor_1789562400000_4d5e…
                    apCoreMessageId: f0debc61-…@peppol.azdanapps.com
                    sbdhInstanceId: 6c1d2e3f-…
                    sizeBytes: 18432
                    amount: 1050
                    currency: AED
                    taxAmount: 50
                    taxExclusiveAmount: 1000
                    taxBreakdown:
                      - category: S
                        percent: '5'
                        taxableAmount: 1000
                        taxAmount: 50
                    document:
                      number: INV-2026-000123
                      issueDate: '2026-10-20'
                      dueDate: '2026-11-19'
                      currency: AED
                      typeCode: '380'
                      supplierName: Sample Trading LLC
                      customerName: Sample Buyer FZ-LLC
                      netAmount: 1000
                      taxAmount: 50
                      payableAmount: 1050
                    createdAt: '2026-10-20T09:00:00.000Z'
                    updatedAt: '2026-10-20T09:02:41.000Z'
                    deliveredAt: '2026-10-20T09:00:04.000Z'
                    attempts: 1
                    lastAttemptAt: '2026-10-20T09:00:02.000Z'
                    source: platform
                    apStatus: sent
                    apTransactionId: a3c5e7f9-…
                    apMessageId: f0debc61-…@peppol.azdanapps.com
                    mlsStatus: received_ap
                    mlsType: ALWAYS_SEND
                    mlsResponseCode: null
                    tddStatus: null
                    isTdd: false
                    linkState: linked
                    documentKind: invoice
                    statusDetail:
                      documentKind: invoice
                      overall: accepted
                      processing:
                        platformStatus: delivered
                        apStatus: sent
                        source: platform+ap
                        createdAt: '2026-10-20T09:00:00.000Z'
                        failureStage: null
                      validation:
                        state: passed
                        engine: internal
                        at: '2026-10-20T08:59:59.000Z'
                        warnings: 0
                        error: null
                      delivery:
                        state: delivered
                        apStatus: sent
                        platformStatus: delivered
                        attempts: 1
                        lastAttemptAt: '2026-10-20T09:00:02.000Z'
                        completedAt: '2026-10-20T09:00:04.000Z'
                        failedAt: null
                        errorCode: null
                        error: null
                      mls:
                        role: received
                        state: accepted
                        code: AP
                        raw: received_ap
                        at: '2026-10-20T09:02:40.000Z'
                        mlsId: mls-3e1f…
                        errorCode: null
                        errorMessage: null
                        policy: ALWAYS_SEND
                      reporting:
                        state: unknown
                    bucket: SUCCESS
                  - transactionId: e41d9b2c-…
                    source: ap-core
                    direction: inbound
                    status: forwarded
                    apStatus: forwarded
                    apTransactionId: e41d9b2c-…
                    apMessageId: 77aa01bc-…@ap.example.com
                    sender:
                      participantId: 0235:1003****11
                      name: null
                    receiver:
                      participantId: 0235:1004****59
                      name: null
                    documentType: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    documentTypeId: >-
                      urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                    processId: urn:peppol:bis:billing
                    sbdhInstanceId: 1b2c3d4e-…
                    mlsStatus: null
                    mlsType: ALWAYS_SEND
                    mlsResponseCode: AP
                    tddStatus: sent
                    isTdd: false
                    correlationId: null
                    documentKind: invoice
                    statusDetail:
                      documentKind: invoice
                      overall: delivered
                      processing:
                        platformStatus: null
                        apStatus: forwarded
                        source: ap
                        createdAt: '2026-10-19T14:20:00.000Z'
                        failureStage: null
                      validation:
                        state: passed
                        engine: ap_core
                        at: '2026-10-19T14:20:00.000Z'
                        warnings: 0
                        error: null
                      delivery:
                        state: delivered
                        apStatus: forwarded
                        platformStatus: null
                        attempts: 1
                        lastAttemptAt: null
                        completedAt: '2026-10-19T14:20:03.000Z'
                        failedAt: null
                        errorCode: null
                        error: null
                      mls:
                        role: sent
                        state: accepted
                        code: AP
                        raw: AP
                        at: null
                        mlsId: null
                        errorCode: null
                        errorMessage: null
                        policy: ALWAYS_SEND
                      reporting:
                        state: reported
                        raw: sent
                        at: '2026-10-19T14:20:05.000Z'
                        error: null
                        ftaResponse: null
                    createdAt: '2026-10-19T14:20:00.000Z'
                    completedAt: '2026-10-19T14:20:03.000Z'
                    sizeBytes: 15220
                    tenantId: tnt_7Q2X…
                    linkState: ap_only
                page:
                  number: 1
                  size: 25
                  total: 1287
                  totalPages: 52
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions/{transactionId}:
    get:
      operationId: getTenantTransactionsByTransactionId
      summary: Get a transaction with its full status story
      description: >-
        Everything known about one transaction: the projected ledger row (`transaction`), its files (`files`, including
        the TDD / MLS files generated for it), status events, the linked AP Core row (`ap`), payload `availability`, the
        unified `documents` download links, and the derived status: `statusDetail` (separate `overall`, `processing`,
        `validation`, `delivery`, `mls`, `reporting` dimensions plus `documentKind`), the delivery `journey` timeline,
        `apLog` and the resolved `tdd`. Top-level `status` is the derived portal status (`delivered`, `failed`,
        `sending`, `quarantined`, `validated`), not the ledger status. An id that is not a `txn_…` platform id is also
        looked up as an AP Core transaction id (AP-only rows, `source: "ap-core"`). Out of a `tenant_user`'s scope
        answers 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: >-
            The transaction detail. Example: an outbound invoice accepted by the receiver (MLS AP) whose TDD the FTA
            accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transaction:
                    type: object
                    properties:
                      transactionId:
                        type: string
                      tenantId:
                        type: string
                      subsidiaryId:
                        type: string
                      direction:
                        type: string
                      status:
                        type: string
                      documentType:
                        type: string
                      documentTypeId:
                        type: string
                      processId:
                        type: string
                      environment:
                        type: string
                      c1CountryCode:
                        type: string
                      sender:
                        type: object
                        properties:
                          participantId:
                            type: string
                          name:
                            type: string
                      receiver:
                        type: object
                        properties:
                          participantId:
                            type: string
                      correlationId:
                        type: string
                      apCoreMessageId:
                        type: string
                      sbdhInstanceId:
                        type: string
                      sizeBytes:
                        type: integer
                      amount:
                        type: integer
                      currency:
                        type: string
                      taxAmount:
                        type: integer
                      taxExclusiveAmount:
                        type: integer
                      taxBreakdown:
                        type: array
                        items:
                          type: object
                          properties:
                            category:
                              type: string
                            percent:
                              type: string
                            taxableAmount:
                              type: integer
                            taxAmount:
                              type: integer
                      document:
                        type: object
                        properties:
                          number:
                            type: string
                          issueDate:
                            type: string
                          dueDate:
                            type: string
                          currency:
                            type: string
                          typeCode:
                            type: string
                          supplierName:
                            type: string
                          customerName:
                            type: string
                          netAmount:
                            type: integer
                          taxAmount:
                            type: integer
                          payableAmount:
                            type: integer
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
                      deliveredAt:
                        type: string
                      attempts:
                        type: integer
                      lastAttemptAt:
                        type: string
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        fileId:
                          type: string
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        subsidiaryId:
                          type: string
                        direction:
                          type: string
                        environment:
                          type: string
                        transactionType:
                          type: string
                        fileType:
                          type: string
                        role:
                          type: string
                        originalName:
                          type: string
                        filename:
                          type: string
                        contentType:
                          type: string
                        sizeBytes:
                          type: integer
                        sha256:
                          type: string
                        source:
                          type: string
                        parentTransactionId:
                          type: string
                        apTddTransactionId:
                          type: string
                        apCoreMessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        storageProvider:
                          type: string
                        status:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        encryption:
                          type: object
                          properties:
                            mode:
                              type: string
                        legacyLocalPathPending:
                          type: boolean
                  statusEvents:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        type:
                          type: string
                        status:
                          type: string
                        detail:
                          type: string
                        actor:
                          type: string
                        createdAt:
                          type: string
                  ap:
                    type: object
                    properties:
                      linked:
                        type: boolean
                      apTransactionId:
                        type: string
                      archived:
                        type: boolean
                      direction:
                        type: string
                      apStatus:
                        type: string
                      docTypeId:
                        type: string
                      processId:
                        type: string
                      sender:
                        type: string
                      receiver:
                        type: string
                      sbdhInstanceId:
                        type: string
                      as4MessageId:
                        type: string
                      correlationId:
                        type: string
                      platformTransactionId:
                        type: string
                      mlsStatus:
                        type: string
                      mlsType:
                        type: string
                      mlsReceivedAt:
                        type: string
                      mlsId:
                        type: string
                      mlsErrorCode:
                        type: 'null'
                      mlsErrorMessage:
                        type: 'null'
                      mlsResponseCode:
                        type: 'null'
                      attemptCount:
                        type: integer
                      lastAttemptAt:
                        type: string
                      tddStatus:
                        type: 'null'
                      createdAt:
                        type: string
                      completedAt:
                        type: string
                      errorDetails:
                        type: 'null'
                      documentSize:
                        type: integer
                      isTdd:
                        type: boolean
                  availability:
                    type: object
                    properties:
                      payloadAvailable:
                        type: boolean
                      payloadStatus:
                        type: string
                      xmlDownloadable:
                        type: boolean
                      objectStorageIndexed:
                        type: boolean
                      s3Indexed:
                        type: boolean
                      fileCount:
                        type: integer
                      primaryFileId:
                        type: string
                      storageError:
                        type: 'null'
                  documents:
                    type: object
                    properties:
                      invoiceXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: string
                          fileName:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                      invoicePdf:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      sbdXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                      tddXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: string
                          fileName:
                            type: string
                          contentType:
                            type: string
                          downloadUrl:
                            type: string
                      mlsXml:
                        type: object
                        properties:
                          available:
                            type: boolean
                          fileId:
                            type: 'null'
                          fileName:
                            type: 'null'
                          contentType:
                            type: string
                          downloadUrl:
                            type: 'null'
                          reason:
                            type: string
                  documentKind:
                    type: string
                  statusDetail:
                    type: object
                    properties:
                      documentKind:
                        type: string
                      overall:
                        type: string
                      processing:
                        type: object
                        properties:
                          platformStatus:
                            type: string
                          apStatus:
                            type: string
                          source:
                            type: string
                          createdAt:
                            type: string
                          failureStage:
                            type: 'null'
                      validation:
                        type: object
                        properties:
                          state:
                            type: string
                          engine:
                            type: string
                          at:
                            type: string
                          warnings:
                            type: integer
                          error:
                            type: 'null'
                      delivery:
                        type: object
                        properties:
                          state:
                            type: string
                          apStatus:
                            type: string
                          platformStatus:
                            type: string
                          attempts:
                            type: integer
                          lastAttemptAt:
                            type: string
                          completedAt:
                            type: string
                          failedAt:
                            type: 'null'
                          errorCode:
                            type: 'null'
                          error:
                            type: 'null'
                      mls:
                        type: object
                        properties:
                          role:
                            type: string
                          state:
                            type: string
                          code:
                            type: string
                          raw:
                            type: string
                          at:
                            type: string
                          mlsId:
                            type: string
                          errorCode:
                            type: 'null'
                          errorMessage:
                            type: 'null'
                          policy:
                            type: string
                      reporting:
                        type: object
                        properties:
                          state:
                            type: string
                          raw:
                            type: string
                          typeCode:
                            type: string
                          at:
                            type: string
                          error:
                            type: 'null'
                          followUpStatus:
                            type: 'null'
                          followUpError:
                            type: 'null'
                          ftaResponse:
                            type: object
                            properties:
                              state:
                                type: string
                              code:
                                type: string
                              at:
                                type: string
                              errorMessage:
                                type: 'null'
                  status:
                    type: string
                  deliveryStatus:
                    type: string
                  reportingStatus:
                    type: string
                  responseStatus:
                    type: string
                  journey:
                    type: array
                    items:
                      type: object
                      properties:
                        stage:
                          type: string
                        track:
                          type: string
                        status:
                          type: string
                        at:
                          type: string
                        detail:
                          type: string
                  apLog:
                    type: object
                    properties:
                      source:
                        type: string
                      apTransactionId:
                        type: string
                      apStatus:
                        type: string
                      as4MessageId:
                        type: string
                      sbdhInstanceId:
                        type: string
                      correlationId:
                        type: string
                      mlsStatus:
                        type: string
                      mlsType:
                        type: string
                      mlsReceivedAt:
                        type: string
                      mlsVerdict:
                        type: string
                      attempts:
                        type: integer
                      linkState:
                        type: string
                      failureReason:
                        type: 'null'
                      failureStage:
                        type: 'null'
                  tdd:
                    type: object
                    properties:
                      typeCode:
                        type: string
                      sendStatus:
                        type: string
                      mlsStatus:
                        type: string
                      mlsType:
                        type: string
                      mlsReceivedAt:
                        type: string
                      mlsVerdict:
                        type: string
                      error:
                        type: 'null'
                      followUpStatus:
                        type: 'null'
                      followUpError:
                        type: 'null'
                      chain:
                        type: array
                        items:
                          type: object
                          properties:
                            apTransactionId:
                              type: string
                            typeCode:
                              type: string
                            sendStatus:
                              type: string
                            mlsStatus:
                              type: string
                            mlsReceivedAt:
                              type: string
                            mlsErrorMessage:
                              type: 'null'
              example:
                transaction:
                  transactionId: txn_1789562400000_9f3a…
                  tenantId: tnt_7Q2X…
                  subsidiaryId: sub_1789…_a1b2
                  direction: outbound
                  status: delivered
                  documentType: invoice
                  documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                  processId: urn:peppol:bis:billing
                  environment: production
                  c1CountryCode: AE
                  sender:
                    participantId: 0235:1004****59
                    name: Sample Trading LLC
                  receiver:
                    participantId: 0235:1002****03
                  correlationId: cor_1789562400000_4d5e…
                  apCoreMessageId: f0debc61-…@peppol.azdanapps.com
                  sbdhInstanceId: 6c1d2e3f-…
                  sizeBytes: 18432
                  amount: 1050
                  currency: AED
                  taxAmount: 50
                  taxExclusiveAmount: 1000
                  taxBreakdown:
                    - category: S
                      percent: '5'
                      taxableAmount: 1000
                      taxAmount: 50
                  document:
                    number: INV-2026-000123
                    issueDate: '2026-10-20'
                    dueDate: '2026-11-19'
                    currency: AED
                    typeCode: '380'
                    supplierName: Sample Trading LLC
                    customerName: Sample Buyer FZ-LLC
                    netAmount: 1000
                    taxAmount: 50
                    payableAmount: 1050
                  createdAt: '2026-10-20T09:00:00.000Z'
                  updatedAt: '2026-10-20T09:02:41.000Z'
                  deliveredAt: '2026-10-20T09:00:04.000Z'
                  attempts: 1
                  lastAttemptAt: '2026-10-20T09:00:02.000Z'
                files:
                  - fileId: file_01J9Z3…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: ''
                    fileType: payload
                    role: payload
                    originalName: txn_1789562400000_9f3a….xml
                    filename: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    sizeBytes: 18432
                    sha256: 3f7a9c…
                    source: ''
                    parentTransactionId: ''
                    apTddTransactionId: ''
                    apCoreMessageId: ''
                    sbdhInstanceId: ''
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:00:01.000Z'
                    updatedAt: '2026-10-20T09:00:01.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
                  - fileId: file_01J9Z5…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: taxdata
                    fileType: tdd_xml
                    role: tdd_xml
                    originalName: 6c1d2e3f-….xml
                    filename: 6c1d2e3f-….xml
                    contentType: application/xml
                    sizeBytes: 4210
                    sha256: a91e04…
                    source: ap_core_generated_tdd
                    parentTransactionId: txn_1789562400000_9f3a…
                    apTddTransactionId: b7e2c1d0-…
                    apCoreMessageId: 2a4b6c8d-…@peppol.azdanapps.com
                    sbdhInstanceId: 9d8c7b6a-…
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:03:12.000Z'
                    updatedAt: '2026-10-20T09:03:12.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
                statusEvents:
                  - _id: 671f7e…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    type: status
                    status: delivered
                    detail: AP Core reported sent
                    actor: system
                    createdAt: '2026-10-20T09:00:04.000Z'
                ap:
                  linked: true
                  apTransactionId: a3c5e7f9-…
                  archived: false
                  direction: outbound
                  apStatus: sent
                  docTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                  processId: urn:peppol:bis:billing
                  sender: 0235:1004****59
                  receiver: 0235:1002****03
                  sbdhInstanceId: 6c1d2e3f-…
                  as4MessageId: f0debc61-…@peppol.azdanapps.com
                  correlationId: cor_1789562400000_4d5e…
                  platformTransactionId: txn_1789562400000_9f3a…
                  mlsStatus: received_ap
                  mlsType: ALWAYS_SEND
                  mlsReceivedAt: '2026-10-20T09:02:40.000Z'
                  mlsId: mls-3e1f…
                  mlsErrorCode: null
                  mlsErrorMessage: null
                  mlsResponseCode: null
                  attemptCount: 1
                  lastAttemptAt: '2026-10-20T09:00:02.000Z'
                  tddStatus: null
                  createdAt: '2026-10-20T09:00:01.000Z'
                  completedAt: '2026-10-20T09:00:04.000Z'
                  errorDetails: null
                  documentSize: 18432
                  isTdd: false
                availability:
                  payloadAvailable: true
                  payloadStatus: uploaded
                  xmlDownloadable: true
                  objectStorageIndexed: true
                  s3Indexed: true
                  fileCount: 2
                  primaryFileId: file_01J9Z3…
                  storageError: null
                documents:
                  invoiceXml:
                    available: true
                    fileId: file_01J9Z3…
                    fileName: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    downloadUrl: /api/tenant/transactions/txn_1789562400000_9f3a…/files/invoice-xml
                  invoicePdf:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/pdf
                    downloadUrl: null
                    reason: not_generated
                  sbdXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_stored
                  tddXml:
                    available: true
                    fileId: file_01J9Z5…
                    fileName: 6c1d2e3f-….xml
                    contentType: application/xml
                    downloadUrl: /api/tenant/transactions/txn_1789562400000_9f3a…/files/tdd-xml
                  mlsXml:
                    available: false
                    fileId: null
                    fileName: null
                    contentType: application/xml
                    downloadUrl: null
                    reason: not_received
                documentKind: invoice
                statusDetail:
                  documentKind: invoice
                  overall: accepted
                  processing:
                    platformStatus: delivered
                    apStatus: sent
                    source: platform+ap
                    createdAt: '2026-10-20T09:00:00.000Z'
                    failureStage: null
                  validation:
                    state: passed
                    engine: internal
                    at: '2026-10-20T08:59:59.000Z'
                    warnings: 0
                    error: null
                  delivery:
                    state: delivered
                    apStatus: sent
                    platformStatus: delivered
                    attempts: 1
                    lastAttemptAt: '2026-10-20T09:00:02.000Z'
                    completedAt: '2026-10-20T09:00:04.000Z'
                    failedAt: null
                    errorCode: null
                    error: null
                  mls:
                    role: received
                    state: accepted
                    code: AP
                    raw: received_ap
                    at: '2026-10-20T09:02:40.000Z'
                    mlsId: mls-3e1f…
                    errorCode: null
                    errorMessage: null
                    policy: ALWAYS_SEND
                  reporting:
                    state: reported
                    raw: sent
                    typeCode: S
                    at: '2026-10-20T09:05:30.000Z'
                    error: null
                    followUpStatus: null
                    followUpError: null
                    ftaResponse:
                      state: accepted
                      code: AP
                      at: '2026-10-20T09:05:30.000Z'
                      errorMessage: null
                status: delivered
                deliveryStatus: delivered
                reportingStatus: reported
                responseStatus: accepted
                journey:
                  - stage: validated
                    track: preparation
                    status: complete
                    at: '2026-10-20T09:00:00.000Z'
                    detail: Schema and PINT rules passed.
                  - stage: sentAs4
                    track: delivery
                    status: complete
                    at: '2026-10-20T09:00:02.000Z'
                    detail: Submitted to the Peppol access point via AS4.
                  - stage: deliveredToAp
                    track: delivery
                    status: complete
                    at: '2026-10-20T09:02:41.000Z'
                    detail: Delivered to the receiver's access point.
                  - stage: mlsReceived
                    track: delivery
                    status: complete
                    at: '2026-10-20T09:02:40.000Z'
                    detail: Positive message level status received from the receiver's access point.
                  - stage: tddReported
                    track: reporting
                    status: complete
                    at: '2026-10-20T09:05:30.000Z'
                    detail: Trading document disposition reported to the FTA.
                  - stage: tddMlsReceived
                    track: reporting
                    status: complete
                    at: '2026-10-20T09:05:30.000Z'
                    detail: Positive message level status received from the FTA for the tax report.
                apLog:
                  source: ap-core
                  apTransactionId: a3c5e7f9-…
                  apStatus: sent
                  as4MessageId: f0debc61-…@peppol.azdanapps.com
                  sbdhInstanceId: 6c1d2e3f-…
                  correlationId: cor_1789562400000_4d5e…
                  mlsStatus: received_ap
                  mlsType: ALWAYS_SEND
                  mlsReceivedAt: '2026-10-20T09:02:40.000Z'
                  mlsVerdict: accepted
                  attempts: 1
                  linkState: linked
                  failureReason: null
                  failureStage: null
                tdd:
                  typeCode: S
                  sendStatus: sent
                  mlsStatus: received_ap
                  mlsType: ALWAYS_SEND
                  mlsReceivedAt: '2026-10-20T09:05:30.000Z'
                  mlsVerdict: accepted
                  error: null
                  followUpStatus: null
                  followUpError: null
                  chain:
                    - apTransactionId: b7e2c1d0-…
                      typeCode: S
                      sendStatus: sent
                      mlsStatus: received_ap
                      mlsReceivedAt: '2026-10-20T09:05:30.000Z'
                      mlsErrorMessage: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions/{transactionId}/webhooks:
    get:
      operationId: getTenantTransactionsByTransactionIdWebhooks
      summary: List the webhook events and deliveries of a transaction
      description: >-
        The webhook side of one transaction of the tenant: its outbox events (one per target webhook, with queue status,
        attempts, last error and next dispatch time) and the delivery attempts made for them, newest first. AP-keyed
        events are found by the transaction's correlation, SBDH and AS4 ids too. Never includes payloads.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Events and attempts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transactionId:
                    type: string
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        eventId:
                          type: string
                        outboxEventId:
                          type: string
                        webhookId:
                          type: string
                        tenantId:
                          type: string
                        transactionId:
                          type: string
                        eventType:
                          type: string
                        status:
                          type: string
                        attempts:
                          type: integer
                        lastError:
                          type: string
                        nextDispatchAt:
                          type: string
                        createdAt:
                          type: string
                        deliveredAt:
                          type: 'null'
                        deadAt:
                          type: string
                  attempts:
                    type: array
                    items:
                      type: object
                      properties:
                        attemptId:
                          type: string
                        webhookId:
                          type: string
                        eventId:
                          type: string
                        eventType:
                          type: string
                        deliveryStatus:
                          type: string
                        outcome:
                          type: string
                        httpStatus:
                          type: integer
                        latencyMs:
                          type: integer
                        attempt:
                          type: integer
                        repeatCount:
                          type: integer
                        lastAttemptAt:
                          type: 'null'
                        urlHost:
                          type: string
                        errorCode:
                          type: 'null'
                        transactionId:
                          type: string
                        metadata:
                          type: object
                          properties: {}
                        createdAt:
                          type: string
                        queueStatus:
                          type: string
                        attemptsTotal:
                          type: integer
                        nextRetryAt:
                          type: 'null'
                        deadAt:
                          type: 'null'
              example:
                transactionId: txn_1789562400000_9f3a…
                events:
                  - eventId: evt_ap_9c1d2e3f4a5b6c7d
                    outboxEventId: evt_ap_9c1d2e3f4a5b6c7d:whk_1789562400000_k3j9a1
                    webhookId: whk_1789562400000_k3j9a1
                    tenantId: tnt_7Q2X…
                    transactionId: txn_1789562400000_9f3a…
                    eventType: ap.outbound.delivered
                    status: dead
                    attempts: 8
                    lastError: http_503
                    nextDispatchAt: '2026-10-20T11:02:45.000Z'
                    createdAt: '2026-10-20T09:02:45.000Z'
                    deliveredAt: null
                    deadAt: '2026-10-20T11:10:12.000Z'
                attempts:
                  - attemptId: att_1789562565000_4f2a
                    webhookId: whk_1789562400000_k3j9a1
                    eventId: evt_1789562560000_9c1d
                    eventType: transaction.delivered
                    deliveryStatus: delivered
                    outcome: success
                    httpStatus: 200
                    latencyMs: 184
                    attempt: 1
                    repeatCount: 1
                    lastAttemptAt: null
                    urlHost: erp.example.com
                    errorCode: null
                    transactionId: txn_1789562400000_9f3a…
                    metadata: {}
                    createdAt: '2026-10-20T09:02:45.000Z'
                    queueStatus: delivered
                    attemptsTotal: 1
                    nextRetryAt: null
                    deadAt: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions/{transactionId}/retry:
    post:
      operationId: postTenantTransactionsByTransactionIdRetry
      summary: Mark a failed transaction as queued for retry
      description: >-
        Bookkeeping only: for a platform transaction whose status is in the FAILED group, sets `status` to `queued`,
        increments `attempts`, stamps `lastAttemptAt` and sets `nextRetryAt` (30 s backoff doubling per attempt, capped
        at 30 min), and writes a `transaction.retry` audit row. It does not call AP Core or resend the document, and no
        worker in the Platform API reads `nextRetryAt`; when the transaction sweeper is enabled it moves a `queued`
        outbound row older than 5 minutes back to `failed` (`queued_timeout`). AP-only ids answer 404. Not narrowed by a
        `tenant_user`'s company scope. To actually resend, submit again through `POST /api/tenant/invoices/send` with
        the same `Idempotency-Key`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - name: transactionId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The row is now `queued`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  transactionId:
                    type: string
                  attempts:
                    type: integer
                  nextRetryAt:
                    type: string
              example:
                ok: true
                transactionId: txn_1789562400000_9f3a…
                attempts: 2
                nextRetryAt: '2026-10-24T10:01:00.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          description: The transaction is not in a failed state (`NOT_RETRYABLE`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      currentStatus:
                        type: string
              example:
                error:
                  code: NOT_RETRYABLE
                  currentStatus: delivered
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/reports/submission:
    get:
      operationId: getTenantReportsSubmission
      summary: Get the submission report
      description: >-
        Delivered / failed / pending counts, delivery rate and total amount, a per-day series (last 60 days with data),
        totals per document type and one row per document, computed from up to 400 of the tenant's most recent business
        documents (the transaction list with `view=documents`, AP-enriched). The status is MLS-aware: a positive MLS
        counts as delivered, a negative one as failed. Amounts are the `amount` captured at submit (0 when missing).
        `from` / `to` are calendar dates or ISO date-times; `to` is inclusive (the whole day). `subsidiaryId=all` means
        no company filter. tenant_user callers only see rows of their allowed companies / Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Reports
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The submission report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      submitted:
                        type: integer
                      delivered:
                        type: integer
                      failed:
                        type: integer
                      pending:
                        type: integer
                      deliveryRatePct:
                        type: integer
                      totalAmount:
                        type: number
                  series:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                        delivered:
                          type: integer
                        failed:
                          type: integer
                        pending:
                          type: integer
                  byType:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        count:
                          type: integer
                        amount:
                          type: number
                  rows:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        date:
                          type: string
                        type:
                          type: string
                        companyName:
                          type: string
                        counterparty:
                          type: string
                        amount:
                          type: integer
                        currency:
                          type: string
                        status:
                          type: string
                        mlsStatus:
                          type: string
                        tddStatus:
                          type: string
              example:
                stats:
                  submitted: 128
                  delivered: 121
                  failed: 3
                  pending: 4
                  deliveryRatePct: 95
                  totalAmount: 512340.5
                series:
                  - date: '2026-10-20'
                    delivered: 9
                    failed: 0
                    pending: 1
                byType:
                  - type: Invoice
                    count: 120
                    amount: 530100.5
                  - type: CreditNote
                    count: 8
                    amount: 17760
                rows:
                  - id: txn_1789562400000_9f3a…
                    date: '2026-10-20T09:00:00.000Z'
                    type: Invoice
                    companyName: Sample Trading
                    counterparty: 0235:1002****03
                    amount: 1050
                    currency: AED
                    status: delivered
                    mlsStatus: acknowledged
                    tddStatus: pending
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/reports/reconciliation:
    get:
      operationId: getTenantReportsReconciliation
      summary: Get the reconciliation report
      description: >-
        Four control stages (`source`, `platform`, `peppol`, `fta`) with counts and amounts, and the mismatches:
        `not_delivered` (failed, or pending for 24 h or more) and `missing_tdd` (delivered but no TDD filed — no
        `tdd_xml` file and no AP TDD fields). There is no ERP feed, so `source` mirrors `platform`. Computed from up to
        1,000 recent business documents. `matchedPct` is the share that passed every control. `from` / `to` are calendar
        dates or ISO date-times; `to` is inclusive (the whole day). `subsidiaryId=all` means no company filter.
        tenant_user callers only see rows of their allowed companies / Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Reports
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The reconciliation report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      matchedPct:
                        type: number
                      totalDocs:
                        type: integer
                      mismatchCount:
                        type: integer
                      varianceAmount:
                        type: integer
                  controls:
                    type: array
                    items:
                      type: object
                      properties:
                        stage:
                          type: string
                        count:
                          type: integer
                        amount:
                          type: number
                  mismatches:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        transactionId:
                          type: string
                        issue:
                          type: string
                        presence:
                          type: object
                          properties:
                            source:
                              type: boolean
                            platform:
                              type: boolean
                            peppol:
                              type: boolean
                            fta:
                              type: boolean
                        amount:
                          type: integer
                        ageHours:
                          type: integer
                        companyName:
                          type: string
                        detail:
                          type: string
              example:
                summary:
                  matchedPct: 93.8
                  totalDocs: 128
                  mismatchCount: 8
                  varianceAmount: 21450
                controls:
                  - stage: source
                    count: 128
                    amount: 512340.5
                  - stage: platform
                    count: 128
                    amount: 512340.5
                  - stage: peppol
                    count: 121
                    amount: 498010
                  - stage: fta
                    count: 120
                    amount: 496960
                mismatches:
                  - id: MISM-001
                    transactionId: txn_1789…_c4d1
                    issue: missing_tdd
                    presence:
                      source: true
                      platform: true
                      peppol: true
                      fta: false
                    amount: 1050
                    ageHours: 30
                    companyName: Sample Trading
                    detail: Delivered over Peppol but the tax data document was never filed with the FTA.
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/reports/exception-aging:
    get:
      operationId: getTenantReportsExceptionAging
      summary: Get the exception aging report
      description: >-
        How long exceptions have been open: failed business documents (MLS-aware) plus open / claimed quarantined
        inbound documents, bucketed by age (`0-24h`, `1-2d`, `2-7d`, `>7d`, always all four), grouped per company
        (oldest first) and listed oldest first. Quarantined documents carry no company or amount (amount 0, company "—")
        and are left out when a specific `subsidiaryId` is requested. Computed from up to 1,000 recent business
        documents and up to 500 quarantined documents. `from` / `to` are calendar dates or ISO date-times; `to` is
        inclusive (the whole day). `subsidiaryId=all` means no company filter. tenant_user callers only see rows of
        their allowed companies / Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Reports
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The exception aging report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      open:
                        type: integer
                      agedOver24h:
                        type: integer
                      agedOver7d:
                        type: integer
                      totalAmount:
                        type: integer
                  buckets:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        count:
                          type: integer
                        amount:
                          type: integer
                  byCompany:
                    type: array
                    items:
                      type: object
                      properties:
                        subsidiaryId:
                          type: string
                        companyName:
                          type: string
                        count:
                          type: integer
                        amount:
                          type: integer
                        oldestHours:
                          type: integer
                  rows:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        type:
                          type: string
                        date:
                          type: string
                        companyName:
                          type: string
                        counterparty:
                          type: string
                        amount:
                          type: integer
                        ageHours:
                          type: integer
                        reason:
                          type: string
              example:
                stats:
                  open: 5
                  agedOver24h: 3
                  agedOver7d: 1
                  totalAmount: 7350
                buckets:
                  - key: 0-24h
                    count: 2
                    amount: 2100
                  - key: 1-2d
                    count: 1
                    amount: 0
                  - key: 2-7d
                    count: 1
                    amount: 4200
                  - key: '>7d'
                    count: 1
                    amount: 1050
                byCompany:
                  - subsidiaryId: sub_1789…_a1b2
                    companyName: Sample Trading
                    count: 4
                    amount: 7350
                    oldestHours: 190
                rows:
                  - id: txn_1789…_e2f3
                    type: failed
                    date: '2026-10-16T12:00:00.000Z'
                    companyName: Sample Trading
                    counterparty: 0235:1002****03
                    amount: 1050
                    ageHours: 190
                    reason: Receiver endpoint not found in SMP (ap_core_response)
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/reports/tdd-status:
    get:
      operationId: getTenantReportsTddStatus
      summary: Get the TDD / FTA reporting status report
      description: >-
        For every delivered business document, where its Tax Data Document stands: `failed` (the TDD's own AP
        transaction failed or the FTA rejected it), `acked` (FTA MLS accepted), `pending` (TDD in flight), `filed` (a
        TDD exists but its AP row could not be linked) or `missing`. Rows are sorted worst first; `reportingLatencyMs`
        is TDD time minus delivery time. Pending rows are not counted in `stats`. Computed from up to 1,000 recent
        business documents. `from` / `to` are calendar dates or ISO date-times; `to` is inclusive (the whole day).
        `subsidiaryId=all` means no company filter. tenant_user callers only see rows of their allowed companies /
        Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Reports
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The TDD status report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      delivered:
                        type: integer
                      acked:
                        type: integer
                      filed:
                        type: integer
                      failed:
                        type: integer
                      missing:
                        type: integer
                      avgReportingLatencyMs:
                        type: integer
                  rows:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        date:
                          type: string
                        companyName:
                          type: string
                        counterparty:
                          type: string
                        amount:
                          type: integer
                        currency:
                          type: string
                        tddStatus:
                          type: string
                        tddFiledAt:
                          type: string
                        tddMlsReceivedAt:
                          type: string
                        reportingLatencyMs:
                          type: integer
                        error:
                          type: string
              example:
                stats:
                  delivered: 121
                  acked: 117
                  filed: 1
                  failed: 1
                  missing: 1
                  avgReportingLatencyMs: 172000
                rows:
                  - id: txn_1789…_a7b8
                    date: '2026-10-18T10:00:00.000Z'
                    companyName: Sample Trading
                    counterparty: 0235:1002****03
                    amount: 2100
                    currency: AED
                    tddStatus: failed
                    tddFiledAt: '2026-10-18T10:03:00.000Z'
                    tddMlsReceivedAt: '2026-10-18T10:04:10.000Z'
                    reportingLatencyMs: 240000
                    error: 'Rejected by FTA: invalid buyer TIN'
                  - id: txn_1789562400000_9f3a…
                    date: '2026-10-20T09:00:00.000Z'
                    companyName: Sample Trading
                    counterparty: 0235:1002****03
                    amount: 1050
                    currency: AED
                    tddStatus: acked
                    tddFiledAt: '2026-10-20T09:03:12.000Z'
                    tddMlsReceivedAt: '2026-10-20T09:05:30.000Z'
                    reportingLatencyMs: 170000
                    error: null
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/reports/vat-summary:
    get:
      operationId: getTenantReportsVatSummary
      summary: Get the VAT summary report
      description: >-
        VAT totals from the tax fields captured at submit time (`taxAmount`, `taxExclusiveAmount`, `taxBreakdown`); VAT
        is never estimated — `coverage` says how many documents carry tax data. Credit notes are subtracted. Broken down
        by month, by VAT category / rate (documents without a breakdown under `(unclassified)`) and by company. Computed
        from up to 1,000 recent business documents. `from` / `to` are calendar dates or ISO date-times; `to` is
        inclusive (the whole day). `subsidiaryId=all` means no company filter. tenant_user callers only see rows of
        their allowed companies / Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Reports
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The VAT summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  coverage:
                    type: object
                    properties:
                      totalDocs:
                        type: integer
                      docsWithVat:
                        type: integer
                      pct:
                        type: integer
                  totals:
                    type: object
                    properties:
                      taxableAmount:
                        type: integer
                      vatAmount:
                        type: integer
                  byMonth:
                    type: array
                    items:
                      type: object
                      properties:
                        month:
                          type: string
                        taxableAmount:
                          type: integer
                        vatAmount:
                          type: integer
                        docs:
                          type: integer
                  byCategory:
                    type: array
                    items:
                      type: object
                      properties:
                        category:
                          type: string
                        percent:
                          type: string
                        taxableAmount:
                          type: integer
                        vatAmount:
                          type: integer
                        docs:
                          type: integer
                  byCompany:
                    type: array
                    items:
                      type: object
                      properties:
                        subsidiaryId:
                          type: string
                        companyName:
                          type: string
                        taxableAmount:
                          type: integer
                        vatAmount:
                          type: integer
                        docs:
                          type: integer
              example:
                coverage:
                  totalDocs: 128
                  docsWithVat: 119
                  pct: 93
                totals:
                  taxableAmount: 470200
                  vatAmount: 23510
                byMonth:
                  - month: 2026-10
                    taxableAmount: 182000
                    vatAmount: 9100
                    docs: 46
                byCategory:
                  - category: S
                    percent: '5'
                    taxableAmount: 465200
                    vatAmount: 23260
                    docs: 115
                byCompany:
                  - subsidiaryId: sub_1789…_a1b2
                    companyName: Sample Trading
                    taxableAmount: 470200
                    vatAmount: 23510
                    docs: 119
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/compliance/detail:
    get:
      operationId: getTenantComplianceDetail
      summary: Get the compliance center detail
      description: >-
        Compliance score (0–100) from five dimensions — delivery success, validation pass rate, TDD reporting, SLA
        compliance (delivery within `SLA_DELIVERY_TARGET_MINUTES`, default 10; TDD within `SLA_TDD_TARGET_MINUTES` of
        delivery, default 15) and exception hygiene (penalties for open / aged failures and quarantined inbound
        documents) — plus an 8-week trend, counts, SLA percentiles, bottlenecks and a per-participant breakdown. Without
        `from`, the scan starts 56 days back; up to 1,000 business documents. `estimatedExposure` is `agedExceptions ×
        1000 + missingTdd × 100`. `from` / `to` are calendar dates or ISO date-times; `to` is inclusive (the whole day).
        `subsidiaryId=all` means no company filter. tenant_user callers only see rows of their allowed companies /
        Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Dashboard & compliance
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The compliance detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  overall:
                    type: integer
                  dimensions:
                    type: object
                    properties:
                      deliverySuccess:
                        type: integer
                      validationPassRate:
                        type: integer
                      tddReporting:
                        type: integer
                      slaCompliance:
                        type: integer
                      exceptionHygiene:
                        type: integer
                  trend:
                    type: array
                    items:
                      type: integer
                  agedExceptions:
                    type: integer
                  missingTdd:
                    type: integer
                  estimatedExposure:
                    type: integer
                  deliveryRatePct:
                    type: number
                  counts:
                    type: object
                    properties:
                      total:
                        type: integer
                      delivered:
                        type: integer
                      failed:
                        type: integer
                      pending:
                        type: integer
                      quarantined:
                        type: integer
                      openExceptions:
                        type: integer
                      agedExceptions:
                        type: integer
                      missingTdd:
                        type: integer
                  sla:
                    type: object
                    properties:
                      deliveryTargetMinutes:
                        type: integer
                      tddTargetMinutes:
                        type: integer
                      checks:
                        type: integer
                      passes:
                        type: integer
                      compliancePct:
                        type: integer
                      deliveryMeasured:
                        type: integer
                      deliveryP50Ms:
                        type: integer
                      deliveryP95Ms:
                        type: integer
                      tddMeasured:
                        type: integer
                      tddP50Ms:
                        type: integer
                      tddP95Ms:
                        type: integer
                  bottlenecks:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        count:
                          type: integer
                  participants:
                    type: array
                    items:
                      type: object
                      properties:
                        peppolId:
                          type: string
                        label:
                          type: string
                        subsidiaryId:
                          type: string
                        overall:
                          type: integer
                        dimensions:
                          type: object
                          properties:
                            deliverySuccess:
                              type: integer
                            validationPassRate:
                              type: integer
                            tddReporting:
                              type: integer
                            slaCompliance:
                              type: integer
                            exceptionHygiene:
                              type: integer
                        counts:
                          type: object
                          properties:
                            total:
                              type: integer
                            delivered:
                              type: integer
                            failed:
                              type: integer
                            pending:
                              type: integer
                            quarantined:
                              type: integer
                            openExceptions:
                              type: integer
                            agedExceptions:
                              type: integer
                            missingTdd:
                              type: integer
                        sla:
                          type: object
                          properties:
                            deliveryTargetMinutes:
                              type: integer
                            tddTargetMinutes:
                              type: integer
                            checks:
                              type: integer
                            passes:
                              type: integer
                            compliancePct:
                              type: integer
                            deliveryMeasured:
                              type: integer
                            deliveryP50Ms:
                              type: integer
                            deliveryP95Ms:
                              type: integer
                            tddMeasured:
                              type: integer
                            tddP50Ms:
                              type: integer
                            tddP95Ms:
                              type: integer
              example:
                overall: 92
                dimensions:
                  deliverySuccess: 98
                  validationPassRate: 100
                  tddReporting: 97
                  slaCompliance: 90
                  exceptionHygiene: 75
                trend:
                  - 88
                  - 90
                  - 91
                  - 91
                  - 93
                  - 92
                  - 94
                  - 93
                agedExceptions: 1
                missingTdd: 3
                estimatedExposure: 1300
                deliveryRatePct: 94.5
                counts:
                  total: 128
                  delivered: 121
                  failed: 3
                  pending: 4
                  quarantined: 1
                  openExceptions: 4
                  agedExceptions: 1
                  missingTdd: 3
                sla:
                  deliveryTargetMinutes: 10
                  tddTargetMinutes: 15
                  checks: 230
                  passes: 207
                  compliancePct: 90
                  deliveryMeasured: 112
                  deliveryP50Ms: 4100
                  deliveryP95Ms: 186000
                  tddMeasured: 118
                  tddP50Ms: 150000
                  tddP95Ms: 1260000
                bottlenecks:
                  - key: slowTdd
                    count: 14
                  - key: missingTdd
                    count: 3
                participants:
                  - peppolId: 0235:1004****59
                    label: Production TRN
                    subsidiaryId: sub_1789…_a1b2
                    overall: 92
                    dimensions:
                      deliverySuccess: 98
                      validationPassRate: 100
                      tddReporting: 97
                      slaCompliance: 90
                      exceptionHygiene: 85
                    counts:
                      total: 128
                      delivered: 121
                      failed: 3
                      pending: 4
                      quarantined: 0
                      openExceptions: 3
                      agedExceptions: 0
                      missingTdd: 3
                    sla:
                      deliveryTargetMinutes: 10
                      tddTargetMinutes: 15
                      checks: 230
                      passes: 207
                      compliancePct: 90
                      deliveryMeasured: 112
                      deliveryP50Ms: 4100
                      deliveryP95Ms: 186000
                      tddMeasured: 118
                      tddP50Ms: 150000
                      tddP95Ms: 1260000
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/compliance/score:
    get:
      operationId: getTenantComplianceScore
      summary: Get the compliance score
      description: >-
        The overall score and three of the dimensions of `GET /api/tenant/compliance/detail` (same computation, same
        query), for the dashboard ring. `from` / `to` are calendar dates or ISO date-times; `to` is inclusive (the whole
        day). `subsidiaryId=all` means no company filter. tenant_user callers only see rows of their allowed companies /
        Peppol identities.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Dashboard & compliance
      parameters:
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: subsidiaryId
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The compliance score.
          content:
            application/json:
              schema:
                type: object
                properties:
                  overall:
                    type: integer
                  deliverySuccess:
                    type: integer
                  validationPassRate:
                    type: integer
                  slaCompliance:
                    type: integer
              example:
                overall: 92
                deliverySuccess: 98
                validationPassRate: 100
                slaCompliance: 90
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/files:
    get:
      operationId: getTenantFiles
      summary: List the tenant's stored files
      description: >-
        Paged `transaction_files` rows of the tenant, newest first, in the tenant-safe projection. Filters:
        `transactionId` (exact match on the row's own `transactionId`) and `role`. A `tenant_user` sees files of allowed
        companies and files of (or generated for) transactions in their scope.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Documents & files
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 25
        - name: transactionId
          in: query
          required: false
          schema:
            type: string
        - name: role
          in: query
          required: false
          schema:
            type: string
            enum:
              - payload
              - mlr
              - mdn
              - envelope
              - attachment
        - name: fileType
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 40
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
      responses:
        '200':
          description: One page of files.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        fileId:
                          type: string
                        transactionId:
                          type: string
                        tenantId:
                          type: string
                        subsidiaryId:
                          type: string
                        direction:
                          type: string
                        environment:
                          type: string
                        transactionType:
                          type: string
                        fileType:
                          type: string
                        role:
                          type: string
                        originalName:
                          type: string
                        filename:
                          type: string
                        contentType:
                          type: string
                        sizeBytes:
                          type: integer
                        sha256:
                          type: string
                        source:
                          type: string
                        parentTransactionId:
                          type: string
                        apTddTransactionId:
                          type: string
                        apCoreMessageId:
                          type: string
                        sbdhInstanceId:
                          type: string
                        storageProvider:
                          type: string
                        status:
                          type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        encryption:
                          type: object
                          properties:
                            mode:
                              type: string
                        legacyLocalPathPending:
                          type: boolean
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - fileId: file_01J9Z5…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: taxdata
                    fileType: tdd_xml
                    role: tdd_xml
                    originalName: 6c1d2e3f-….xml
                    filename: 6c1d2e3f-….xml
                    contentType: application/xml
                    sizeBytes: 4210
                    sha256: a91e04…
                    source: ap_core_generated_tdd
                    parentTransactionId: txn_1789562400000_9f3a…
                    apTddTransactionId: b7e2c1d0-…
                    apCoreMessageId: 2a4b6c8d-…@peppol.azdanapps.com
                    sbdhInstanceId: 9d8c7b6a-…
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:03:12.000Z'
                    updatedAt: '2026-10-20T09:03:12.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
                  - fileId: file_01J9Z3…
                    transactionId: txn_1789562400000_9f3a…
                    tenantId: tnt_7Q2X…
                    subsidiaryId: sub_1789…_a1b2
                    direction: outbound
                    environment: production
                    transactionType: ''
                    fileType: payload
                    role: payload
                    originalName: txn_1789562400000_9f3a….xml
                    filename: txn_1789562400000_9f3a….xml
                    contentType: application/xml
                    sizeBytes: 18432
                    sha256: 3f7a9c…
                    source: ''
                    parentTransactionId: ''
                    apTddTransactionId: ''
                    apCoreMessageId: ''
                    sbdhInstanceId: ''
                    storageProvider: oci
                    status: uploaded
                    createdAt: '2026-10-20T09:00:01.000Z'
                    updatedAt: '2026-10-20T09:00:01.000Z'
                    encryption:
                      mode: NONE
                    legacyLocalPathPending: false
                page:
                  number: 1
                  size: 25
                  total: 2
                  totalPages: 1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks:
    get:
      operationId: getTenantWebhooks
      summary: List webhooks
      description: >-
        The tenant's webhook configurations. Signing secrets are never returned (`hasSigningSecret` says whether one is
        set); outbound authentication is shown redacted (`auth.type`, `configured`, a masked `display`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      responses:
        '200':
          description: The webhooks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        webhookId:
                          type: string
                        tenantId:
                          type: string
                        name:
                          type: string
                        url:
                          type: string
                        events:
                          type: array
                          items:
                            type: string
                        subsidiaryId:
                          type: 'null'
                        payloadVersion:
                          type: string
                        status:
                          type: string
                        signingSecretRotatedAt:
                          type: 'null'
                        previousSecretValidUntil:
                          type: 'null'
                        health:
                          type: object
                          properties:
                            consecutiveFailures:
                              type: integer
                            successRate24h:
                              type: integer
                            lastDeliveryAt:
                              type: string
                            lastSuccessAt:
                              type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        hasSigningSecret:
                          type: boolean
                        auth:
                          type: object
                          properties:
                            type:
                              type: string
                            configured:
                              type: boolean
                            display:
                              type: string
              example:
                items:
                  - _id: 671f3a…
                    webhookId: whk_1789562400000_k3j9a1
                    tenantId: tnt_7Q2X…
                    name: ERP production
                    url: https://erp.example.com/hooks/azdan
                    events:
                      - document.delivered
                      - document.failed
                      - mls.rejected
                    subsidiaryId: null
                    payloadVersion: '2'
                    status: active
                    signingSecretRotatedAt: null
                    previousSecretValidUntil: null
                    health:
                      consecutiveFailures: 0
                      successRate24h: 1
                      lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                      lastSuccessAt: '2026-10-20T09:02:45.000Z'
                    createdAt: '2026-09-02T10:00:00.000Z'
                    updatedAt: '2026-10-01T11:30:00.000Z'
                    hasSigningSecret: true
                    auth:
                      type: bearer
                      configured: true
                      display: Bearer ••••••••••••
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantWebhooks
      summary: Create a webhook
      description: >-
        Registers an HTTPS endpoint for event notifications. The URL is checked against the outbound URL policy — https
        only, no credentials, no private, loopback, link-local or metadata address, and the host must not resolve to one
        (400 with the rejection `code` and `hint`, e.g. `HTTPS_REQUIRED`, `PRIVATE_IP`, `DNS_PRIVATE`). `events` lists
        topics by their v2 or legacy name (see `GET …/webhooks/events`) and defaults to `["*"]`. `name` is a label;
        `subsidiaryId` scopes the webhook to one company of the tenant (null = all); `payloadVersion` chooses the
        envelope (`"2"`, the default for new webhooks, or `"1"` for receivers written against the original one). A
        signing secret is generated unless `generateSecret` is `false`; it is returned **once** in `revealedSecret` and
        only a hash plus an encrypted copy (used to sign deliveries) are stored. Optional outbound `auth` (bearer,
        basic, API key header, custom headers, OAuth2 client credentials, NetSuite OAuth 2.0) is validated and stored
        encrypted; responses only show it redacted. Audit-logged (`webhook.create`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    minLength: 1
                  minItems: 1
                  default:
                    - '*'
                generateSecret:
                  type: boolean
                name:
                  type: string
                  maxLength: 80
                subsidiaryId:
                  type:
                    - string
                    - 'null'
                  maxLength: 64
                payloadVersion:
                  type: string
                  enum:
                    - '1'
                    - '2'
                auth:
                  type: object
                  properties:
                    type:
                      type: string
                      enum:
                        - none
                        - bearer
                        - basic
                        - apiKey
                        - customHeaders
                        - oauth2ClientCredentials
                        - netsuiteOauth2
                    token:
                      type: string
                    username:
                      type: string
                    password:
                      type: string
                    headerName:
                      type: string
                    headerValue:
                      type: string
                    headers:
                      type: array
                      items:
                        type: object
                        properties:
                          name:
                            type: string
                          value:
                            type: string
                        required:
                          - name
                          - value
                    tokenUrl:
                      type: string
                      format: uri
                    clientId:
                      type: string
                    clientSecret:
                      type: string
                    scope:
                      type: string
                    audience:
                      type: string
                    authMethod:
                      type: string
                      enum:
                        - client_secret_basic
                        - client_secret_post
                    connectionId:
                      type: string
                  required:
                    - type
              required:
                - url
            example:
              name: ERP production
              url: https://erp.example.com/hooks/azdan
              events:
                - document.delivered
                - document.failed
                - mls.rejected
              subsidiaryId: null
              payloadVersion: '2'
              generateSecret: true
              auth:
                type: bearer
                token: …
      responses:
        '201':
          description: Created. The signing secret is returned only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  webhookId:
                    type: string
                  tenantId:
                    type: string
                  name:
                    type: string
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  subsidiaryId:
                    type: 'null'
                  payloadVersion:
                    type: string
                  status:
                    type: string
                  signingSecretRotatedAt:
                    type: 'null'
                  previousSecretValidUntil:
                    type: 'null'
                  health:
                    type: object
                    properties:
                      consecutiveFailures:
                        type: integer
                      successRate24h:
                        type: integer
                  createdAt:
                    type: string
                  updatedAt:
                    type: string
                  hasSigningSecret:
                    type: boolean
                  auth:
                    type: object
                    properties:
                      type:
                        type: string
                      configured:
                        type: boolean
                      display:
                        type: string
                  revealedSecret:
                    type: string
              example:
                _id: 671f3a…
                webhookId: whk_1789562400000_k3j9a1
                tenantId: tnt_7Q2X…
                name: ERP production
                url: https://erp.example.com/hooks/azdan
                events:
                  - document.delivered
                  - document.failed
                  - mls.rejected
                subsidiaryId: null
                payloadVersion: '2'
                status: active
                signingSecretRotatedAt: null
                previousSecretValidUntil: null
                health:
                  consecutiveFailures: 0
                  successRate24h: 1
                createdAt: '2026-09-02T10:00:00.000Z'
                updatedAt: '2026-10-01T11:30:00.000Z'
                hasSigningSecret: true
                auth:
                  type: bearer
                  configured: true
                  display: Bearer ••••••••••••
                revealedSecret: whs_AbCdEf…
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/deliveries:
    get:
      operationId: getTenantWebhooksDeliveries
      summary: List webhook delivery attempts across all webhooks
      description: >-
        Delivery attempts of the tenant's webhooks, newest first, with keyset pagination (`limit` 1–100, opaque `cursor`
        from `pagination.nextCursor`). Filters: `webhookId` (must belong to the tenant, else 404), `status` (`delivered`
        or `failed`; `pending`, `in_flight` and `dead` are accepted but return nothing — those states are not recorded
        as attempts), `eventType`, `dateFrom` / `dateTo`. Each row carries the `transactionId` the event belongs to and
        where the event stands in the queue now (`queueStatus`, `attemptsTotal`, `nextRetryAt`, `deadAt`). Header and
        body fields are not exposed; metadata is redacted.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            maxLength: 256
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - delivered
              - failed
              - pending
              - in_flight
              - dead
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: dateFrom
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: dateTo
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
      responses:
        '200':
          description: One page of delivery attempts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        attemptId:
                          type: string
                        webhookId:
                          type: string
                        eventId:
                          type: string
                        eventType:
                          type: string
                        deliveryStatus:
                          type: string
                        outcome:
                          type: string
                        httpStatus:
                          type: integer
                        latencyMs:
                          type: integer
                        attempt:
                          type: integer
                        repeatCount:
                          type: integer
                        lastAttemptAt:
                          type: 'null'
                        urlHost:
                          type: string
                        errorCode:
                          type: 'null'
                        transactionId:
                          type: string
                        metadata:
                          type: object
                          properties: {}
                        createdAt:
                          type: string
                        queueStatus:
                          type: string
                        attemptsTotal:
                          type: integer
                        nextRetryAt:
                          type: 'null'
                        deadAt:
                          type: 'null'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      nextCursor:
                        type: string
                      hasNext:
                        type: boolean
              example:
                items:
                  - attemptId: att_1789562565000_4f2a
                    webhookId: whk_1789562400000_k3j9a1
                    eventId: evt_1789562560000_9c1d
                    eventType: transaction.delivered
                    deliveryStatus: delivered
                    outcome: success
                    httpStatus: 200
                    latencyMs: 184
                    attempt: 1
                    repeatCount: 1
                    lastAttemptAt: null
                    urlHost: erp.example.com
                    errorCode: null
                    transactionId: txn_1789562400000_9f3a…
                    metadata: {}
                    createdAt: '2026-10-20T09:02:45.000Z'
                    queueStatus: delivered
                    attemptsTotal: 1
                    nextRetryAt: null
                    deadAt: null
                pagination:
                  limit: 25
                  nextCursor: eyJ0IjoiMjAyNi0xMC0yMFQwOTowMjo0NS4wMDBaIiwiYSI6ImF0dF8…
                  hasNext: true
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/events:
    get:
      operationId: getTenantWebhooksEvents
      summary: List the webhook event types
      description: >-
        The catalogue a webhook can subscribe to: each topic's payload-v2 name (`type`), the legacy v1 name it is also
        known by (`legacyType`, null for topics introduced with v2), its `group` (documents, transactions, mls, tdd,
        webhook) and a description. A subscription may use either name; `"*"` means every topic.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      responses:
        '200':
          description: The catalogue.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        legacyType:
                          type: string
                        group:
                          type: string
                        description:
                          type: string
              example:
                items:
                  - type: document.delivered
                    legacyType: ap.outbound.delivered
                    group: documents
                    description: The receiving access point confirmed delivery (AS4 receipt).
                  - type: mls.rejected
                    legacyType: null
                    group: mls
                    description: 'The receiver''s access point rejected the document: MLS status RE, with the reason.'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/dead-letters:
    get:
      operationId: getTenantWebhooksDeadLetters
      summary: List dead-lettered webhook events
      description: >-
        Events of the tenant's webhooks that exhausted their delivery attempts or failed permanently (`status=dead`, the
        default), or the ones still retrying (`status=failed`), newest first, one row per target webhook. Filters:
        `webhookId`, `eventType` (either name of a topic), `transactionId`, `from` / `to` (event creation time).
        Page-based (`page`, `pageSize` 1–200). Requeue one with `POST …/deliveries/{eventId}/retry` or all of a
        webhook's with `POST …/{webhookId}/replay`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: transactionId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - dead
              - failed
            default: dead
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 25
      responses:
        '200':
          description: One page of the dead-letter queue.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        eventId:
                          type: string
                        outboxEventId:
                          type: string
                        webhookId:
                          type: string
                        tenantId:
                          type: string
                        transactionId:
                          type: string
                        eventType:
                          type: string
                        status:
                          type: string
                        attempts:
                          type: integer
                        lastError:
                          type: string
                        nextDispatchAt:
                          type: string
                        createdAt:
                          type: string
                        deliveredAt:
                          type: 'null'
                        deadAt:
                          type: string
                  total:
                    type: integer
                  page:
                    type: integer
                  pageSize:
                    type: integer
              example:
                items:
                  - eventId: evt_ap_9c1d2e3f4a5b6c7d
                    outboxEventId: evt_ap_9c1d2e3f4a5b6c7d:whk_1789562400000_k3j9a1
                    webhookId: whk_1789562400000_k3j9a1
                    tenantId: tnt_7Q2X…
                    transactionId: txn_1789562400000_9f3a…
                    eventType: ap.outbound.delivered
                    status: dead
                    attempts: 8
                    lastError: http_503
                    nextDispatchAt: '2026-10-20T11:02:45.000Z'
                    createdAt: '2026-10-20T09:02:45.000Z'
                    deliveredAt: null
                    deadAt: '2026-10-20T11:10:12.000Z'
                total: 1
                page: 1
                pageSize: 25
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/deliveries/{eventId}/retry:
    post:
      operationId: postTenantWebhooksDeliveriesByEventIdRetry
      summary: Requeue one webhook event
      description: >-
        Puts one event of the tenant back to `pending` for immediate dispatch: every target webhook's copy of it, or
        only the copy for `webhookId` when given. `eventId` is the public event id (`X-Azdan-Event-Id`) or a target
        row's own id. Only rows in `failed` or `dead` are touched; attempt counters are kept.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                webhookId:
                  type: string
                  minLength: 1
                  maxLength: 128
              additionalProperties: false
            example:
              webhookId: whk_1789562400000_k3j9a1
      responses:
        '200':
          description: What was matched and requeued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  matched:
                    type: integer
                  requeued:
                    type: integer
                  eventIds:
                    type: array
                    items:
                      type: string
              example:
                matched: 1
                requeued: 1
                eventIds:
                  - evt_ap_9c1d2e3f4a5b6c7d:whk_1789562400000_k3j9a1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}/deliveries:
    get:
      operationId: getTenantWebhooksByWebhookIdDeliveries
      summary: List delivery attempts of one webhook
      description: >-
        Same as `GET /api/tenant/webhooks/deliveries` for one webhook; the path id wins over a `webhookId` query
        parameter. A webhook of another tenant answers 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - name: webhookId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            maxLength: 256
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - delivered
              - failed
              - pending
              - in_flight
              - dead
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 128
        - name: dateFrom
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: dateTo
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
      responses:
        '200':
          description: One page of delivery attempts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        attemptId:
                          type: string
                        webhookId:
                          type: string
                        eventId:
                          type: string
                        eventType:
                          type: string
                        deliveryStatus:
                          type: string
                        outcome:
                          type: string
                        httpStatus:
                          type: integer
                        latencyMs:
                          type: integer
                        attempt:
                          type: integer
                        repeatCount:
                          type: integer
                        lastAttemptAt:
                          type: 'null'
                        urlHost:
                          type: string
                        errorCode:
                          type: 'null'
                        transactionId:
                          type: string
                        metadata:
                          type: object
                          properties: {}
                        createdAt:
                          type: string
                        queueStatus:
                          type: string
                        attemptsTotal:
                          type: integer
                        nextRetryAt:
                          type: 'null'
                        deadAt:
                          type: 'null'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      nextCursor:
                        type: string
                      hasNext:
                        type: boolean
              example:
                items:
                  - attemptId: att_1789562565000_4f2a
                    webhookId: whk_1789562400000_k3j9a1
                    eventId: evt_1789562560000_9c1d
                    eventType: transaction.delivered
                    deliveryStatus: delivered
                    outcome: success
                    httpStatus: 200
                    latencyMs: 184
                    attempt: 1
                    repeatCount: 1
                    lastAttemptAt: null
                    urlHost: erp.example.com
                    errorCode: null
                    transactionId: txn_1789562400000_9f3a…
                    metadata: {}
                    createdAt: '2026-10-20T09:02:45.000Z'
                    queueStatus: delivered
                    attemptsTotal: 1
                    nextRetryAt: null
                    deadAt: null
                pagination:
                  limit: 25
                  nextCursor: eyJ0IjoiMjAyNi0xMC0yMFQwOTowMjo0NS4wMDBaIiwiYSI6ImF0dF8…
                  hasNext: true
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}:
    get:
      operationId: getTenantWebhooksByWebhookId
      summary: Get one webhook
      description: >-
        One webhook of the tenant in the same redacted shape as the list, with its outbox queue counts (`pending`,
        `in_flight`, `failed`, `delivered`, `dead`) and its ten most recent delivery attempts. A webhook of another
        tenant answers 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook:
                    type: object
                    properties:
                      _id:
                        type: string
                      webhookId:
                        type: string
                      tenantId:
                        type: string
                      name:
                        type: string
                      url:
                        type: string
                      events:
                        type: array
                        items:
                          type: string
                      subsidiaryId:
                        type: 'null'
                      payloadVersion:
                        type: string
                      status:
                        type: string
                      signingSecretRotatedAt:
                        type: 'null'
                      previousSecretValidUntil:
                        type: 'null'
                      health:
                        type: object
                        properties:
                          consecutiveFailures:
                            type: integer
                          successRate24h:
                            type: integer
                          lastDeliveryAt:
                            type: string
                          lastSuccessAt:
                            type: string
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
                      hasSigningSecret:
                        type: boolean
                      auth:
                        type: object
                        properties:
                          type:
                            type: string
                          configured:
                            type: boolean
                          display:
                            type: string
                  queue:
                    type: object
                    properties:
                      pending:
                        type: integer
                      in_flight:
                        type: integer
                      failed:
                        type: integer
                      delivered:
                        type: integer
                      dead:
                        type: integer
                  recentAttempts:
                    type: array
                    items:
                      type: object
                      properties:
                        attemptId:
                          type: string
                        webhookId:
                          type: string
                        eventId:
                          type: string
                        eventType:
                          type: string
                        deliveryStatus:
                          type: string
                        outcome:
                          type: string
                        httpStatus:
                          type: integer
                        latencyMs:
                          type: integer
                        attempt:
                          type: integer
                        repeatCount:
                          type: integer
                        lastAttemptAt:
                          type: 'null'
                        urlHost:
                          type: string
                        errorCode:
                          type: 'null'
                        transactionId:
                          type: string
                        metadata:
                          type: object
                          properties: {}
                        createdAt:
                          type: string
                        queueStatus:
                          type: string
                        attemptsTotal:
                          type: integer
                        nextRetryAt:
                          type: 'null'
                        deadAt:
                          type: 'null'
              example:
                webhook:
                  _id: 671f3a…
                  webhookId: whk_1789562400000_k3j9a1
                  tenantId: tnt_7Q2X…
                  name: ERP production
                  url: https://erp.example.com/hooks/azdan
                  events:
                    - document.delivered
                    - document.failed
                    - mls.rejected
                  subsidiaryId: null
                  payloadVersion: '2'
                  status: active
                  signingSecretRotatedAt: null
                  previousSecretValidUntil: null
                  health:
                    consecutiveFailures: 0
                    successRate24h: 1
                    lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                    lastSuccessAt: '2026-10-20T09:02:45.000Z'
                  createdAt: '2026-09-02T10:00:00.000Z'
                  updatedAt: '2026-10-01T11:30:00.000Z'
                  hasSigningSecret: true
                  auth:
                    type: bearer
                    configured: true
                    display: Bearer ••••••••••••
                queue:
                  pending: 0
                  in_flight: 0
                  failed: 1
                  delivered: 412
                  dead: 2
                recentAttempts:
                  - attemptId: att_1789562565000_4f2a
                    webhookId: whk_1789562400000_k3j9a1
                    eventId: evt_1789562560000_9c1d
                    eventType: transaction.delivered
                    deliveryStatus: delivered
                    outcome: success
                    httpStatus: 200
                    latencyMs: 184
                    attempt: 1
                    repeatCount: 1
                    lastAttemptAt: null
                    urlHost: erp.example.com
                    errorCode: null
                    transactionId: txn_1789562400000_9f3a…
                    metadata: {}
                    createdAt: '2026-10-20T09:02:45.000Z'
                    queueStatus: delivered
                    attemptsTotal: 1
                    nextRetryAt: null
                    deadAt: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    delete:
      operationId: deleteTenantWebhooksByWebhookId
      summary: Delete a webhook
      description: >-
        Permanently deletes the webhook configuration. Delivery history and outbox rows are kept. Audit-logged
        (`webhook.delete`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  webhookId:
                    type: string
              example:
                ok: true
                webhookId: whk_1789562400000_k3j9a1
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    patch:
      operationId: patchTenantWebhooksByWebhookId
      summary: Update a webhook's URL, events, status, name, company or payload version
      description: >-
        Changes any of `url` (re-checked against the outbound URL policy, DNS included), `events`, `status` (`active`,
        `paused` — a temporary hold — or `disabled` — switched off; both keep events queued and deliver nothing),
        `name`, `subsidiaryId` (one of the tenant's companies, or null for all) and `payloadVersion` (`"1"` or `"2"`);
        at least one is required and unknown fields are rejected. The signing secret is rotated through `POST
        …/rotate-secret`. Audit-logged (`webhook.update`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    minLength: 1
                  minItems: 1
                status:
                  type: string
                  enum:
                    - active
                    - paused
                    - disabled
                name:
                  type: string
                  maxLength: 80
                subsidiaryId:
                  type:
                    - string
                    - 'null'
                  maxLength: 64
                payloadVersion:
                  type: string
                  enum:
                    - '1'
                    - '2'
              additionalProperties: false
            example:
              status: paused
      responses:
        '200':
          description: The updated webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  webhookId:
                    type: string
                  tenantId:
                    type: string
                  name:
                    type: string
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  subsidiaryId:
                    type: 'null'
                  payloadVersion:
                    type: string
                  status:
                    type: string
                  signingSecretRotatedAt:
                    type: 'null'
                  previousSecretValidUntil:
                    type: 'null'
                  health:
                    type: object
                    properties:
                      consecutiveFailures:
                        type: integer
                      successRate24h:
                        type: integer
                      lastDeliveryAt:
                        type: string
                      lastSuccessAt:
                        type: string
                  createdAt:
                    type: string
                  updatedAt:
                    type: string
                  hasSigningSecret:
                    type: boolean
                  auth:
                    type: object
                    properties:
                      type:
                        type: string
                      configured:
                        type: boolean
                      display:
                        type: string
              example:
                _id: 671f3a…
                webhookId: whk_1789562400000_k3j9a1
                tenantId: tnt_7Q2X…
                name: ERP production
                url: https://erp.example.com/hooks/azdan
                events:
                  - document.delivered
                  - document.failed
                  - mls.rejected
                subsidiaryId: null
                payloadVersion: '2'
                status: paused
                signingSecretRotatedAt: null
                previousSecretValidUntil: null
                health:
                  consecutiveFailures: 0
                  successRate24h: 1
                  lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                  lastSuccessAt: '2026-10-20T09:02:45.000Z'
                createdAt: '2026-09-02T10:00:00.000Z'
                updatedAt: '2026-10-01T11:30:00.000Z'
                hasSigningSecret: true
                auth:
                  type: bearer
                  configured: true
                  display: Bearer ••••••••••••
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}/test:
    post:
      operationId: postTenantWebhooksByWebhookIdTest
      summary: Send a test event to a webhook
      description: >-
        Queues a `webhook.test` outbox event for this webhook, dispatched by the webhook worker like any other event;
        the result shows up in the delivery history. The webhook must be `active` (400 otherwise). Audit-logged
        (`webhook.test_requested`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The test event was queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  eventId:
                    type: string
                  requestId:
                    type: string
                  webhookId:
                    type: string
                  status:
                    type: string
              example:
                ok: true
                eventId: evt_test_1789562400000_8b7c…
                requestId: req_4f1e…
                webhookId: whk_1789562400000_k3j9a1
                status: queued
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}/replay:
    post:
      operationId: postTenantWebhooksByWebhookIdReplay
      summary: Replay failed or dead-lettered webhook events
      description: >-
        Puts this webhook's outbox events in status `failed` or `dead` back to `pending` for immediate dispatch, oldest
        first, up to `limit` (1–1000, default 100); `eventIds` narrows the selection. `dryRun: true` only reports what
        would be requeued. A webhook of another tenant answers 404.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                eventIds:
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 128
                  maxItems: 1000
                limit:
                  type: integer
                  minimum: 1
                  maximum: 1000
                  default: 100
                dryRun:
                  type: boolean
                  default: false
              additionalProperties: false
            example:
              eventIds:
                - evt_1789562560000_9c1d
              limit: 100
              dryRun: false
      responses:
        '200':
          description: What was matched and requeued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  matched:
                    type: integer
                  requeued:
                    type: integer
                  dryRun:
                    type: boolean
                  eventIds:
                    type: array
                    items:
                      type: string
              example:
                matched: 1
                requeued: 1
                dryRun: false
                eventIds:
                  - evt_1789562560000_9c1d
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}/auth:
    delete:
      operationId: deleteTenantWebhooksByWebhookIdAuth
      summary: Remove a webhook's outbound authentication
      description: |-
        Resets the webhook's outbound authentication to `none`. Audit-logged (`webhook.auth_clear`).

        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The updated webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  webhookId:
                    type: string
                  tenantId:
                    type: string
                  name:
                    type: string
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  subsidiaryId:
                    type: 'null'
                  payloadVersion:
                    type: string
                  status:
                    type: string
                  signingSecretRotatedAt:
                    type: 'null'
                  previousSecretValidUntil:
                    type: 'null'
                  health:
                    type: object
                    properties:
                      consecutiveFailures:
                        type: integer
                      successRate24h:
                        type: integer
                      lastDeliveryAt:
                        type: string
                      lastSuccessAt:
                        type: string
                  createdAt:
                    type: string
                  updatedAt:
                    type: string
                  hasSigningSecret:
                    type: boolean
                  auth:
                    type: object
                    properties:
                      type:
                        type: string
                      configured:
                        type: boolean
              example:
                _id: 671f3a…
                webhookId: whk_1789562400000_k3j9a1
                tenantId: tnt_7Q2X…
                name: ERP production
                url: https://erp.example.com/hooks/azdan
                events:
                  - document.delivered
                  - document.failed
                  - mls.rejected
                subsidiaryId: null
                payloadVersion: '2'
                status: active
                signingSecretRotatedAt: null
                previousSecretValidUntil: null
                health:
                  consecutiveFailures: 0
                  successRate24h: 1
                  lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                  lastSuccessAt: '2026-10-20T09:02:45.000Z'
                createdAt: '2026-09-02T10:00:00.000Z'
                updatedAt: '2026-10-01T11:30:00.000Z'
                hasSigningSecret: true
                auth:
                  type: none
                  configured: false
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    patch:
      operationId: patchTenantWebhooksByWebhookIdAuth
      summary: Set a webhook's outbound authentication
      description: >-
        Replaces how deliveries authenticate to the receiving endpoint (`none`, `bearer`, `basic`, `apiKey`,
        `customHeaders`, `oauth2ClientCredentials`, `netsuiteOauth2`). Credentials are validated and stored encrypted;
        the response shows them redacted. Audit-logged (`webhook.auth_update`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum:
                    - none
                    - bearer
                    - basic
                    - apiKey
                    - customHeaders
                    - oauth2ClientCredentials
                    - netsuiteOauth2
                token:
                  type: string
                username:
                  type: string
                password:
                  type: string
                headerName:
                  type: string
                headerValue:
                  type: string
                headers:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      value:
                        type: string
                    required:
                      - name
                      - value
                tokenUrl:
                  type: string
                  format: uri
                clientId:
                  type: string
                clientSecret:
                  type: string
                scope:
                  type: string
                audience:
                  type: string
                authMethod:
                  type: string
                  enum:
                    - client_secret_basic
                    - client_secret_post
                connectionId:
                  type: string
              required:
                - type
            example:
              type: oauth2ClientCredentials
              tokenUrl: https://auth.example.com/oauth2/token
              clientId: azdan-hooks
              clientSecret: …
              scope: hooks.write
              authMethod: client_secret_post
      responses:
        '200':
          description: The updated webhook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  webhookId:
                    type: string
                  tenantId:
                    type: string
                  name:
                    type: string
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  subsidiaryId:
                    type: 'null'
                  payloadVersion:
                    type: string
                  status:
                    type: string
                  signingSecretRotatedAt:
                    type: 'null'
                  previousSecretValidUntil:
                    type: 'null'
                  health:
                    type: object
                    properties:
                      consecutiveFailures:
                        type: integer
                      successRate24h:
                        type: integer
                      lastDeliveryAt:
                        type: string
                      lastSuccessAt:
                        type: string
                  createdAt:
                    type: string
                  updatedAt:
                    type: string
                  hasSigningSecret:
                    type: boolean
                  auth:
                    type: object
                    properties:
                      type:
                        type: string
                      configured:
                        type: boolean
                      display:
                        type: string
                      tokenUrl:
                        type: string
                      clientId:
                        type: string
                      scope:
                        type: string
                      audience:
                        type: 'null'
                      authMethod:
                        type: string
              example:
                _id: 671f3a…
                webhookId: whk_1789562400000_k3j9a1
                tenantId: tnt_7Q2X…
                name: ERP production
                url: https://erp.example.com/hooks/azdan
                events:
                  - document.delivered
                  - document.failed
                  - mls.rejected
                subsidiaryId: null
                payloadVersion: '2'
                status: active
                signingSecretRotatedAt: null
                previousSecretValidUntil: null
                health:
                  consecutiveFailures: 0
                  successRate24h: 1
                  lastDeliveryAt: '2026-10-20T09:02:45.000Z'
                  lastSuccessAt: '2026-10-20T09:02:45.000Z'
                createdAt: '2026-09-02T10:00:00.000Z'
                updatedAt: '2026-10-01T11:30:00.000Z'
                hasSigningSecret: true
                auth:
                  type: oauth2ClientCredentials
                  configured: true
                  display: OAuth2 (azdan-hooks)
                  tokenUrl: https://auth.example.com/oauth2/token
                  clientId: azdan-hooks
                  scope: hooks.write
                  audience: null
                  authMethod: client_secret_post
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/webhooks/{webhookId}/rotate-secret:
    post:
      operationId: postTenantWebhooksByWebhookIdRotateSecret
      summary: Rotate a webhook's signing secret
      description: >-
        Generates a new signing secret, returned **once** in `revealedSecret`. Rotation overlaps: until
        `previousSecretValidUntil` (24 hours, `WEBHOOK_SECRET_OVERLAP_HOURS`) every delivery carries two `v1=`
        signatures in `X-Azdan-Signature`, one per secret, so the receiver can switch without a gap; afterwards only the
        new secret signs. Only a hash and an encrypted copy are stored. Audit-logged (`webhook.rotate_secret`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Webhooks
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The new secret, shown only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhookId:
                    type: string
                  revealedSecret:
                    type: string
                  signingSecretRotatedAt:
                    type: string
                  previousSecretValidUntil:
                    type: string
              example:
                webhookId: whk_1789562400000_k3j9a1
                revealedSecret: whs_AbCdEf…
                signingSecretRotatedAt: '2026-10-24T10:00:00.000Z'
                previousSecretValidUntil: '2026-10-25T10:00:00.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/api-keys:
    get:
      operationId: getTenantApiKeys
      summary: List API keys
      description: >-
        The tenant's Integration API keys, active and revoked, without secrets (label, prefix, scopes, status, last
        use).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      responses:
        '200':
          description: The API keys.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        apiKeyId:
                          type: string
                        tenantId:
                          type: string
                        label:
                          type: string
                        prefix:
                          type: string
                        scopes:
                          type: array
                          items:
                            type: string
                        status:
                          type: string
                        createdBy:
                          type: string
                        createdAt:
                          type: string
                        lastUsedAt:
                          type: string
                  note:
                    type: string
              example:
                items:
                  - _id: 671f4b…
                    apiKeyId: ak_1789562400000_x7k2p9
                    tenantId: tnt_7Q2X…
                    label: NetSuite production
                    prefix: azd_live_AbCdEf
                    scopes:
                      - peppol:validate
                      - peppol:submit
                      - transactions:read
                    status: active
                    createdBy: user_1789…_5e6f
                    createdAt: '2026-10-01T07:45:00.000Z'
                    lastUsedAt: '2026-10-24T07:55:12.000Z'
                note: API keys are listed without secrets. Create a new key to reveal a secret once.
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantApiKeys
      summary: Create an API key
      description: >-
        Creates an Integration API key (`azd_live_…`, or `azd_test_…` with `live: false`). The full token is returned
        **once** in `revealedToken`; only its bcrypt hash is stored. `scopes` defaults to `["transactions:submit",
        "transactions:read"]`; scope names are not validated. Audit-logged (`apikey.create`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  minLength: 1
                  maxLength: 80
                scopes:
                  type: array
                  items:
                    type: string
                    minLength: 1
                live:
                  type: boolean
              required:
                - label
            example:
              label: NetSuite production
              scopes:
                - peppol:validate
                - peppol:submit
                - transactions:read
              live: true
      responses:
        '201':
          description: Created. The token is returned only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  _id:
                    type: string
                  apiKeyId:
                    type: string
                  tenantId:
                    type: string
                  label:
                    type: string
                  prefix:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  status:
                    type: string
                  createdBy:
                    type: string
                  createdAt:
                    type: string
                  revealedToken:
                    type: string
              example:
                _id: 671f4b…
                apiKeyId: ak_1789562400000_x7k2p9
                tenantId: tnt_7Q2X…
                label: NetSuite production
                prefix: azd_live_AbCdEf
                scopes:
                  - peppol:validate
                  - peppol:submit
                  - transactions:read
                status: active
                createdBy: user_1789…_5e6f
                createdAt: '2026-10-01T07:45:00.000Z'
                revealedToken: azd_live_AbCdEf_…
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/api-keys/{apiKeyId}/revoke:
    post:
      operationId: postTenantApiKeysByApiKeyIdRevoke
      summary: Revoke an API key
      description: >-
        Sets the key to `revoked`; it is refused from the next request on. Idempotent — revoking a revoked key succeeds
        and keeps the original `revokedAt`. A key of another tenant answers 404. Audit-logged (`apikey.revoke`).


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Users & API keys
      parameters:
        - name: apiKeyId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: The key is revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  apiKeyId:
                    type: string
                  status:
                    type: string
                  revokedAt:
                    type: string
              example:
                ok: true
                apiKeyId: ak_1789562400000_x7k2p9
                status: revoked
                revokedAt: '2026-10-24T10:00:00.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/audit-logs:
    get:
      operationId: getTenantAuditLogs
      summary: List the tenant's audit log
      description: >-
        Paged audit rows of the tenant, newest first. Filters: `actor` (actor type), `outcome`, `action` (exact),
        `resourceType`, `from` / `to` and `q` (action, actor label or resource id). A `tenant_user` only sees rows whose
        resource id is one of their allowed companies or participants.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 25
        - name: actor
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - api_key
              - system
              - super_admin
        - name: outcome
          in: query
          required: false
          schema:
            type: string
            enum:
              - success
              - denied
              - failure
        - name: action
          in: query
          required: false
          schema:
            type: string
        - name: resourceType
          in: query
          required: false
          schema:
            type: string
            enum:
              - tenant
              - participant
              - subsidiary
              - location
              - webhook
              - user
              - api_key
              - transaction
              - outbox_event
        - name: from
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: to
          in: query
          required: false
          schema:
            anyOf:
              - type: string
                format: date-time
              - type: string
                pattern: ^\d{4}-\d{2}-\d{2}
        - name: q
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: One page of audit rows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        auditId:
                          type: string
                        tenantId:
                          type: string
                        actor:
                          type: object
                          properties:
                            type:
                              type: string
                            id:
                              type: string
                            label:
                              type: string
                        action:
                          type: string
                        resource:
                          type: object
                          properties:
                            type:
                              type: string
                            id:
                              type: string
                        outcome:
                          type: string
                        createdAt:
                          type: string
                  page:
                    type: object
                    properties:
                      number:
                        type: integer
                      size:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
              example:
                items:
                  - _id: 671f8f…
                    auditId: aud_1789562400000_k2j3l4
                    tenantId: tnt_7Q2X…
                    actor:
                      type: user
                      id: user_1789…_5e6f
                      label: o***@example.com
                    action: apikey.create
                    resource:
                      type: api_key
                      id: ak_1789562400000_x7k2p9
                    outcome: success
                    createdAt: '2026-10-01T07:45:00.000Z'
                page:
                  number: 1
                  size: 25
                  total: 1
                  totalPages: 1
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/participants:
    get:
      operationId: getTenantParticipants
      summary: List the tenant's Peppol participants
      description: >-
        The tenant's Peppol identities (read-only; default first, then newest), each with its company (`subsidiary`:
        name, legal name, TRN, country, currency), a `displayName` and `canSend` — true when the participant is active,
        outbound-capable and has a C1 country code. A `tenant_user` only sees participants of allowed companies or
        allowed Peppol ids.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      responses:
        '200':
          description: The participants.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        participantId:
                          type: string
                        tenantId:
                          type: string
                        peppolId:
                          type: string
                        scheme:
                          type: string
                        value:
                          type: string
                        direction:
                          type: string
                        environment:
                          type: string
                        allowedSenders:
                          type: array
                          items: {}
                        endpointUrl:
                          type: string
                        label:
                          type: string
                        subsidiaryId:
                          type: string
                        locationId:
                          type: string
                        isDefault:
                          type: boolean
                        status:
                          type: string
                        smpPublishStatus:
                          type: string
                        c1CountryCode:
                          type: string
                        createdBy:
                          type: 'null'
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        subsidiary:
                          type: object
                          properties:
                            subsidiaryId:
                              type: string
                            name:
                              type: string
                            legalName:
                              type: string
                            country:
                              type: string
                            taxRegistrationNumber:
                              type: string
                            defaultCurrency:
                              type: string
                        displayName:
                          type: string
                        canSend:
                          type: boolean
              example:
                items:
                  - _id: 671f6d…
                    participantId: pp_3a9c1e…
                    tenantId: tnt_7Q2X…
                    peppolId: 0235:1004****59
                    scheme: '0235'
                    value: 1004****59
                    direction: both
                    environment: production
                    allowedSenders: []
                    endpointUrl: ''
                    label: Production TRN
                    subsidiaryId: sub_1789…_a1b2
                    locationId: ''
                    isDefault: true
                    status: active
                    smpPublishStatus: published
                    c1CountryCode: AE
                    createdBy: null
                    createdAt: '2026-09-01T08:10:00.000Z'
                    updatedAt: '2026-09-03T10:00:00.000Z'
                    subsidiary:
                      subsidiaryId: sub_1789…_a1b2
                      name: Sample Trading
                      legalName: Sample Trading LLC
                      country: AE
                      taxRegistrationNumber: 1004****59
                      defaultCurrency: AED
                    displayName: Sample Trading LLC
                    canSend: true
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantParticipants
      summary: 'Register a Peppol participant (closed: EmaraTax only)'
      description: >-
        No longer creates participants. Taxpayers are onboarded only from EmaraTax: the FTA verifies the company and the
        onboarding flow creates its `0235:<TIN>` participant (and publishes it in the SMP). This route answers 403
        `PEPPOL_ID_EMARATAX_ONLY` for every body; it stays mounted so existing callers get a clear answer. List
        participants with `GET /api/tenant/participants`. Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                peppolId:
                  type: string
                direction:
                  type: string
                environment:
                  type: string
                subsidiaryId:
                  type: string
            example:
              peppolId: 0235:1004****59
              direction: both
              environment: production
              subsidiaryId: sub_1789…_a1b2
      responses:
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          description: Participants are created only by EmaraTax onboarding.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: PEPPOL_ID_EMARATAX_ONLY
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/subsidiaries/{subsidiaryId}:
    patch:
      operationId: patchTenantSubsidiariesBySubsidiaryId
      summary: Update a company
      description: >-
        Updates the given fields of one of the caller's companies (another tenant's id answers 404). `country` and
        `defaultCurrency` are upper-cased; `sellerProfile`, when present, replaces the stored profile as a whole;
        `isDefault: true` clears the previous default. A duplicate `code` answers 409 `SUBSIDIARY_CODE_TAKEN`.
        Audit-logged (`subsidiary.update`). Requires the `tenant_admin` role; a `tenant_user` gets 403.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      parameters:
        - name: subsidiaryId
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                legalName:
                  type: string
                code:
                  type: string
                country:
                  type: string
                  maxLength: 2
                  minLength: 2
                taxRegistrationNumber:
                  type: string
                defaultCurrency:
                  type: string
                isDefault:
                  type: boolean
                sellerProfile:
                  type: object
                  properties:
                    legalRegistrationId:
                      type: string
                    legalRegistrationType:
                      type: string
                      enum:
                        - TL
                        - EID
                        - PAS
                        - CD
                    legalRegistrationAuthorityName:
                      type: string
                    address:
                      type: object
                      properties:
                        streetName:
                          type: string
                        cityName:
                          type: string
                        countrySubentity:
                          type: string
                        postalZone:
                          type: string
            example:
              legalName: Sample Trading LLC
              sellerProfile:
                legalRegistrationId: CN-12****7
                legalRegistrationType: TL
                legalRegistrationAuthorityName: Department of Economic Development
                address:
                  streetName: Sheikh Zayed Road
                  cityName: Dubai
                  countrySubentity: DXB
                  postalZone: '00000'
      responses:
        '200':
          description: The updated company.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  subsidiary:
                    type: object
                    properties:
                      _id:
                        type: string
                      subsidiaryId:
                        type: string
                      tenantId:
                        type: string
                      name:
                        type: string
                      legalName:
                        type: string
                      code:
                        type: string
                      country:
                        type: string
                      taxRegistrationNumber:
                        type: string
                      netsuiteSubsidiaryId:
                        type: string
                      defaultCurrency:
                        type: string
                      status:
                        type: string
                      isDefault:
                        type: boolean
                      sellerProfile:
                        type: object
                        properties:
                          legalRegistrationId:
                            type: string
                          legalRegistrationType:
                            type: string
                          legalRegistrationAuthorityName:
                            type: string
                          address:
                            type: object
                            properties:
                              streetName:
                                type: string
                              cityName:
                                type: string
                              countrySubentity:
                                type: string
                              postalZone:
                                type: string
                      deletedAt:
                        type: 'null'
                      createdBy:
                        type: 'null'
                      updatedBy:
                        type: 'null'
                      createdAt:
                        type: string
                      updatedAt:
                        type: string
              example:
                ok: true
                subsidiary:
                  _id: 671f2c…
                  subsidiaryId: sub_1789…_a1b2
                  tenantId: tnt_7Q2X…
                  name: Sample Trading
                  legalName: Sample Trading LLC
                  code: AE-HQ
                  country: AE
                  taxRegistrationNumber: 1004****59
                  netsuiteSubsidiaryId: ''
                  defaultCurrency: AED
                  status: active
                  isDefault: true
                  sellerProfile:
                    legalRegistrationId: CN-12****7
                    legalRegistrationType: TL
                    legalRegistrationAuthorityName: Department of Economic Development
                    address:
                      streetName: Sheikh Zayed Road
                      cityName: Dubai
                      countrySubentity: DXB
                      postalZone: '00000'
                  deletedAt: null
                  createdBy: null
                  updatedBy: null
                  createdAt: '2026-09-01T08:00:00.000Z'
                  updatedAt: '2026-10-20T09:15:00.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '409':
          description: The company code is already used (`SUBSIDIARY_CODE_TAKEN`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
              example:
                error:
                  code: SUBSIDIARY_CODE_TAKEN
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/branding:
    get:
      operationId: getTenantBranding
      summary: Get the tenant's logo facts
      description: >-
        Whether a company logo is set for the signed-in tenant, with its type, size and pixel dimensions — never the
        bytes. Open to every tenant role: the Tenant Console shows the logo to everyone. Pinned to the caller's tenant.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: 'The logo''s facts, or `logo: null` when none is set.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  logo:
                    type: object
                    properties:
                      contentType:
                        type: string
                      sizeBytes:
                        type: integer
                      width:
                        type: integer
                      height:
                        type: integer
                      updatedAt:
                        type: string
              example:
                logo:
                  contentType: image/png
                  sizeBytes: 18234
                  width: 320
                  height: 96
                  updatedAt: '2026-10-28T09:12:00.000Z'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/branding/logo:
    get:
      operationId: getTenantBrandingLogo
      summary: Download the tenant's logo image
      description: >-
        The logo bytes as `image/png` or `image/jpeg`, with `Cache-Control: private, max-age=300`; the Tenant Console
        appends `?v=<updatedAt>` to bust it after a change. 404 when no logo is set.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: The image.
          content:
            image/png:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    put:
      operationId: putTenantBrandingLogo
      summary: Upload or replace the tenant's logo
      description: >-
        Tenant administrators only, through `company.manage`. The body carries the file as base64 with its declared
        type. Rules: PNG or JPEG only — SVG is refused because it can carry scripts — at most 512 KB, between 16 and
        4096 pixels on each side, and the bytes must be the type declared. Stored on the tenant document
        (`tenants.branding.logo`). Audited as `tenant.branding.logo.update`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                contentType:
                  type: string
                  enum:
                    - image/png
                    - image/jpeg
                dataBase64:
                  type: string
                  minLength: 1
                  maxLength: 720000
              required:
                - contentType
                - dataBase64
              additionalProperties: false
            example:
              contentType: image/png
              dataBase64: iVBORw0KGgoAAAANSUhEUgAAAUAAAABg…
      responses:
        '200':
          description: The stored logo's facts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  logo:
                    type: object
                    properties:
                      contentType:
                        type: string
                      sizeBytes:
                        type: integer
                      width:
                        type: integer
                      height:
                        type: integer
                      updatedAt:
                        type: string
              example:
                logo:
                  contentType: image/png
                  sizeBytes: 18234
                  width: 320
                  height: 96
                  updatedAt: '2026-10-28T09:12:00.000Z'
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    delete:
      operationId: deleteTenantBrandingLogo
      summary: Remove the tenant's logo
      description: >-
        Tenant administrators only. The Tenant Console falls back to the AZDAN mark. Audited as
        `tenant.branding.logo.delete`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      responses:
        '200':
          description: Removed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  logo:
                    type: 'null'
              example:
                logo: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '404':
          $ref: '#/components/responses/ErrNotFound'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/onboarding/companies:
    get:
      operationId: getTenantOnboardingCompanies
      summary: Get company onboarding readiness
      description: >-
        One entry per company with its seller profile, its participants and a `readinessState`: `live` (active
        production participant published in the SMP), `production_pending` (active production participant not yet
        published), `test_ready` (active test participant) or `draft`. A `tenant_user` only sees allowed companies and
        participants.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Company & participants
      responses:
        '200':
          description: The companies and their readiness.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        subsidiaryId:
                          type: string
                        name:
                          type: string
                        legalName:
                          type: string
                        code:
                          type: string
                        country:
                          type: string
                        taxRegistrationNumber:
                          type: string
                        status:
                          type: string
                        isDefault:
                          type: boolean
                        sellerProfile:
                          type: object
                          properties:
                            legalRegistrationId:
                              type: string
                            legalRegistrationType:
                              type: string
                            legalRegistrationAuthorityName:
                              type: string
                            address:
                              type: object
                              properties:
                                streetName:
                                  type: string
                                cityName:
                                  type: string
                                countrySubentity:
                                  type: string
                                postalZone:
                                  type: string
                        readinessState:
                          type: string
                        peppolIds:
                          type: array
                          items:
                            type: string
                        participants:
                          type: array
                          items:
                            type: object
                            properties:
                              participantId:
                                type: string
                              peppolId:
                                type: string
                              environment:
                                type: string
                              direction:
                                type: string
                              status:
                                type: string
                              smpPublishStatus:
                                type: string
                              isDefault:
                                type: boolean
              example:
                items:
                  - subsidiaryId: sub_1789…_a1b2
                    name: Sample Trading
                    legalName: Sample Trading LLC
                    code: AE-HQ
                    country: AE
                    taxRegistrationNumber: 1004****59
                    status: active
                    isDefault: true
                    sellerProfile:
                      legalRegistrationId: CN-12****7
                      legalRegistrationType: TL
                      legalRegistrationAuthorityName: Department of Economic Development
                      address:
                        streetName: Sheikh Zayed Road
                        cityName: Dubai
                        countrySubentity: DXB
                        postalZone: '00000'
                    readinessState: live
                    peppolIds:
                      - 0235:1004****59
                    participants:
                      - participantId: pp_3a9c1e…
                        peppolId: 0235:1004****59
                        environment: production
                        direction: both
                        status: active
                        smpPublishStatus: published
                        isDefault: true
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/transactions/submit:
    post:
      operationId: postTenantTransactionsSubmit
      summary: Legacy mock submit (sends nothing)
      description: >-
        Legacy endpoint kept for an old portal page. It validates the body shape, rejects a `payloadBase64` longer than
        8 MiB characters (400), and answers 202 with a generated `txn_<timestamp>` id and `bridge: "mock"`. Nothing is
        stored, validated as Peppol, or sent to AP Core, and the returned id cannot be looked up. Use `POST
        /api/tenant/invoices/send` instead.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Transactions
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                documentType:
                  type: string
                receiver:
                  type: object
                  properties:
                    participantId:
                      type: string
                    name:
                      type: string
                  required:
                    - participantId
                payloadBase64:
                  type: string
                  minLength: 1
                correlationId:
                  type: string
              required:
                - documentType
                - receiver
                - payloadBase64
            example:
              documentType: invoice
              receiver:
                participantId: 0235:1002****03
                name: Sample Buyer FZ-LLC
              payloadBase64: PD94bWwgdmVyc2lvbj0iMS4wIj8+…
              correlationId: erp-INV-2026-000123
      responses:
        '202':
          description: Mock acknowledgement; nothing was sent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transactionId:
                    type: string
                  tenantId:
                    type: string
                  status:
                    type: string
                  documentType:
                    type: string
                  bridge:
                    type: string
              example:
                transactionId: txn_1789562400000
                tenantId: tnt_7Q2X…
                status: queued
                documentType: invoice
                bridge: mock
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      deprecated: true
      x-azdan-access: tenant
  /api/tenant/plan:
    get:
      operationId: getTenantPlan
      summary: Get the tenant's plan and usage
      description: >-
        The tenant's plan (name, limits — `null` = unlimited — and reference price), usage this calendar month in UAE
        time and any limit the usage is over. Actions beyond a limit are refused with 403 `PLAN_LIMIT_REACHED`
        (`details`: `limit`, `used`, `max`, `plan`). Plans are changed by AZDAN.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: Plan and usage.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenantId:
                    type: string
                  planCode:
                    type: string
                  plan:
                    type: object
                    properties:
                      code:
                        type: string
                      name:
                        type: string
                      description:
                        type: string
                      highlights:
                        type: array
                        items: {}
                      status:
                        type: string
                      isDefault:
                        type: boolean
                      sortOrder:
                        type: integer
                      price:
                        type: object
                        properties:
                          monthly:
                            type: 'null'
                          annual:
                            type: 'null'
                          currency:
                            type: string
                      limits:
                        type: object
                        properties:
                          documentsPerMonth:
                            type: integer
                          users:
                            type: integer
                          participants:
                            type: integer
                          apiKeys:
                            type: integer
                          webhooks:
                            type: integer
                      updatedAt:
                        type: string
                  usage:
                    type: object
                    properties:
                      documentsPerMonth:
                        type: integer
                      users:
                        type: integer
                      participants:
                        type: integer
                      apiKeys:
                        type: integer
                      webhooks:
                        type: integer
                  overLimits:
                    type: array
                    items: {}
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                      timeZone:
                        type: string
              example:
                tenantId: tnt_7Q2X…
                planCode: starter
                plan:
                  code: starter
                  name: Starter
                  description: For a single business starting with e-invoicing.
                  highlights: []
                  status: active
                  isDefault: true
                  sortOrder: 10
                  price:
                    monthly: null
                    annual: null
                    currency: AED
                  limits:
                    documentsPerMonth: 500
                    users: 3
                    participants: 1
                    apiKeys: 2
                    webhooks: 2
                  updatedAt: '2026-10-26T09:00:00.000Z'
                usage:
                  documentsPerMonth: 128
                  users: 2
                  participants: 1
                  apiKeys: 1
                  webhooks: 0
                overLimits: []
                period:
                  from: '2026-09-30T20:00:00.000Z'
                  to: '2026-10-31T20:00:00.000Z'
                  timeZone: Asia/Dubai
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/subscription:
    get:
      operationId: getTenantSubscription
      summary: Get the tenant's subscription and usage
      description: >-
        The subscription the Current Plan badge and Plan & usage page show: pricing model (`legacy` plan catalogue —
        with the plan summary under `legacy` —, `free` or `annual`), status, EmaraTax accounts and package, the period's
        credit pool (`credits`: carried over + included + package, pooled across the account's companies; unused credits
        roll over on renewal) and its usage (inbound + outbound documents counted once each when successfully
        processed), remaining documents, excess beyond the quota, the end / renewal date, the billing snapshot (ex VAT,
        VAT, incl. VAT, AED), quota warnings (80 / 90 / 100 %) and expiry warnings (30 / 7 / 0 days), whether new
        documents are processed right now, and `connections`: how many companies may be connected through EmaraTax (the
        subscription's EmaraTax accounts; the Free plan allows one; `unlimited` for a legacy tenant), how many are, and
        what is left. Changes are made by AZDAN — ask for one with `POST /api/tenant/subscription/requests`.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: The subscription.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenantId:
                    type: string
                  tenantName:
                    type: string
                  subscription:
                    type: object
                    properties:
                      pricingModel:
                        type: string
                      status:
                        type: string
                      catalogueVersion:
                        type: integer
                      accounts:
                        type: integer
                      seats:
                        type: integer
                      packageCapacity:
                        type: integer
                      package:
                        type: object
                        properties:
                          code:
                            type: string
                          capacity:
                            type: integer
                          annualFee:
                            type: integer
                          name:
                            type: object
                            properties:
                              en:
                                type: string
                              ar:
                                type: string
                      customQuote:
                        type: 'null'
                      annualDocuments:
                        type: integer
                      includedDocuments:
                        type: integer
                      credits:
                        type: object
                        properties:
                          carried:
                            type: integer
                          included:
                            type: integer
                          package:
                            type: integer
                          total:
                            type: integer
                      pricing:
                        type: object
                        properties:
                          currency:
                            type: string
                          accountFee:
                            type: integer
                          packageFee:
                            type: integer
                          subtotalExVat:
                            type: integer
                          vatRate:
                            type: number
                          vat:
                            type: number
                          totalIncVat:
                            type: number
                          monthlyEquivalentExVat:
                            type: number
                          monthlyEquivalentIncVat:
                            type: number
                      startsAt:
                        type: string
                      endsAt:
                        type: string
                      nextRenewalAt:
                        type: string
                      activatedAt:
                        type: string
                      cancelledAt:
                        type: 'null'
                      daysLeft:
                        type: integer
                      allowQuotaExceedance:
                        type: boolean
                      notes:
                        type: 'null'
                      updatedAt:
                        type: string
                      updatedBy:
                        type: string
                  planName:
                    type: string
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                      to:
                        type: string
                      kind:
                        type: string
                  usage:
                    type: object
                    properties:
                      periodKey:
                        type: string
                      used:
                        type: integer
                      pending:
                        type: integer
                      excess:
                        type: integer
                      quota:
                        type: integer
                      remaining:
                        type: integer
                      percent:
                        type: number
                      notified:
                        type: array
                        items: {}
                  breakdown:
                    type: object
                    properties:
                      inbound:
                        type: integer
                      outbound:
                        type: integer
                  warnings:
                    type: array
                    items: {}
                  processingAllowed:
                    type: boolean
                  legacy:
                    type: 'null'
              example:
                tenantId: tnt_7Q2X…
                tenantName: Acme Trading
                subscription:
                  pricingModel: annual
                  status: active
                  catalogueVersion: 2
                  accounts: 2
                  seats: 2
                  packageCapacity: 600
                  package:
                    code: docs-600
                    capacity: 600
                    annualFee: 200
                    name:
                      en: 600 additional documents a year
                      ar: 600 مستند إضافي سنويًا
                  customQuote: null
                  annualDocuments: 3000
                  includedDocuments: 2400
                  credits:
                    carried: 0
                    included: 2400
                    package: 600
                    total: 3000
                  pricing:
                    currency: AED
                    accountFee: 1896
                    packageFee: 200
                    subtotalExVat: 2096
                    vatRate: 0.05
                    vat: 104.8
                    totalIncVat: 2200.8
                    monthlyEquivalentExVat: 174.67
                    monthlyEquivalentIncVat: 183.4
                  startsAt: '2026-10-29T08:00:00.000Z'
                  endsAt: '2027-10-29T08:00:00.000Z'
                  nextRenewalAt: '2027-10-29T08:00:00.000Z'
                  activatedAt: '2026-10-29T08:00:00.000Z'
                  cancelledAt: null
                  daysLeft: 365
                  allowQuotaExceedance: false
                  notes: null
                  updatedAt: '2026-10-29T08:00:00.000Z'
                  updatedBy: user_1…
                planName: AZDAN E-Invoicing
                period:
                  from: '2026-10-29T08:00:00.000Z'
                  to: '2027-10-29T08:00:00.000Z'
                  kind: year
                usage:
                  periodKey: sub:2026-10-29T08:00:00.000Z
                  used: 128
                  pending: 2
                  excess: 0
                  quota: 3000
                  remaining: 2870
                  percent: 4.3
                  notified: []
                breakdown:
                  inbound: 40
                  outbound: 88
                warnings: []
                processingAllowed: true
                legacy: null
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/subscription/pricing:
    get:
      operationId: getTenantSubscriptionPricing
      summary: Get the AZDAN E-Invoicing price list
      description: >-
        The active pricing catalogue (`version`, `items` with bilingual names: Free plan, EmaraTax account, onboarding,
        the 32 document packages, custom quote) and the constants: account price (AED 948 a year, 1,200 credits
        included), the packages, the custom-quote threshold, the VAT rate and the free allowance (120 documents a year).
        Ex VAT; billing is annual; monthly figures are informational.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: The price list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  currency:
                    type: string
                  vatRate:
                    type: number
                  account:
                    type: object
                    properties:
                      annualFee:
                        type: integer
                      monthlyEquivalent:
                        type: integer
                      annualDocuments:
                        type: integer
                      maxAccounts:
                        type: integer
                  packages:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                        capacity:
                          type: integer
                        annualFee:
                          type: integer
                        monthlyEquivalent:
                          type: number
                  customQuoteAbove:
                    type: integer
                  free:
                    type: object
                    properties:
                      documents:
                        type: integer
                      annualFee:
                        type: integer
                  onboardingFee:
                    type: integer
                  version:
                    type: integer
                  items:
                    type: array
                    items: {}
              example:
                currency: AED
                vatRate: 0.05
                account:
                  annualFee: 948
                  monthlyEquivalent: 79
                  annualDocuments: 1200
                  maxAccounts: 1000
                packages:
                  - code: docs-600
                    capacity: 600
                    annualFee: 200
                    monthlyEquivalent: 16.67
                customQuoteAbove: 238800
                free:
                  documents: 120
                  annualFee: 0
                onboardingFee: 0
                version: 2
                items: []
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
  /api/tenant/subscription/requests:
    get:
      operationId: getTenantSubscriptionRequests
      summary: List the tenant's subscription requests
      description: >-
        Every request this account made for a subscription change (newest first) with its status (`pending`,
        `under_review`, `approved`, `completed`, `rejected`), what was asked for (`requested`: the target subscription),
        the price list's estimate, the comment, AZDAN's decision and reason, and the history; `open` says whether one is
        still being handled.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      responses:
        '200':
          description: The requests.
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      type: object
                      properties:
                        requestId:
                          type: string
                        tenantId:
                          type: string
                        type:
                          type: string
                        status:
                          type: string
                        requested:
                          type: object
                          properties:
                            model:
                              type: string
                            accounts:
                              type: integer
                            additionalAccounts:
                              type: integer
                            packageCapacity:
                              type: integer
                            catalogueKey:
                              type: 'null'
                            catalogueVersion:
                              type: integer
                        estimate:
                          type: object
                          properties:
                            model:
                              type: string
                            accounts:
                              type: integer
                            packageCapacity:
                              type: integer
                            annualDocuments:
                              type: integer
                            subtotalExVat:
                              type: integer
                            vat:
                              type: number
                            totalIncVat:
                              type: number
                            currency:
                              type: string
                        comment:
                          type: string
                        snapshot:
                          type: object
                          properties:
                            pricingModel:
                              type: string
                            accounts:
                              type: integer
                            packageCapacity:
                              type: integer
                            usage:
                              type: object
                              properties:
                                used:
                                  type: integer
                                pending:
                                  type: integer
                                quota:
                                  type: integer
                                percent:
                                  type: integer
                            connections:
                              type: object
                              properties:
                                connected:
                                  type: integer
                                limit:
                                  type: integer
                        requestedBy:
                          type: object
                          properties:
                            label:
                              type: string
                        createdAt:
                          type: string
                        updatedAt:
                          type: string
                        review:
                          type: 'null'
                        decision:
                          type: 'null'
                        applied:
                          type: 'null'
                        appliedAt:
                          type: 'null'
                        applyError:
                          type: 'null'
                        history:
                          type: array
                          items:
                            type: object
                            properties:
                              at:
                                type: string
                              status:
                                type: string
                              by:
                                type: string
                              note:
                                type: 'null'
                  open:
                    type: boolean
              example:
                requests:
                  - requestId: sreq_1761732000000_a1b2c3d4
                    tenantId: tnt_7Q2X…
                    type: accounts
                    status: pending
                    requested:
                      model: annual
                      accounts: 3
                      additionalAccounts: 1
                      packageCapacity: 600
                      catalogueKey: null
                      catalogueVersion: 2
                    estimate:
                      model: annual
                      accounts: 3
                      packageCapacity: 600
                      annualDocuments: 4200
                      subtotalExVat: 3044
                      vat: 152.2
                      totalIncVat: 3196.2
                      currency: AED
                    comment: A new subsidiary
                    snapshot:
                      pricingModel: annual
                      accounts: 2
                      packageCapacity: 600
                      usage:
                        used: 120
                        pending: 0
                        quota: 3000
                        percent: 4
                      connections:
                        connected: 2
                        limit: 2
                    requestedBy:
                      label: finance@acme.ae
                    createdAt: '2026-10-29T10:00:00.000Z'
                    updatedAt: '2026-10-29T10:00:00.000Z'
                    review: null
                    decision: null
                    applied: null
                    appliedAt: null
                    applyError: null
                    history:
                      - at: '2026-10-29T10:00:00.000Z'
                        status: pending
                        by: finance@acme.ae
                        note: null
                open: true
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
    post:
      operationId: postTenantSubscriptionRequests
      summary: Ask AZDAN for a subscription change
      description: >-
        Admin and Finance roles. `type`: `upgrade` (to an annual subscription of `accounts` EmaraTax accounts and an
        optional `packageCapacity`), `change` (another annual size, or `model: free` for the Free plan), `accounts`
        (`additionalAccounts` more EmaraTax accounts — more companies can then be connected), `package`
        (`packageCapacity`: one of the 32 packages of the price list). An optional `comment` (≤ 1,000 characters). The
        request is normalised to the target subscription and priced from the price list for information; nothing changes
        and nothing is charged until an AZDAN administrator approves and applies it. One open request per account at a
        time (409). The account's administrators are emailed when it is received and at each status change.


        **Access:** Tenant session: `tenant_admin` or `tenant_user` of the tenant. Data is always scoped to that tenant.
      tags:
        - Settings & integrations
      parameters:
        - $ref: '#/components/parameters/CsrfToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum:
                    - upgrade
                    - change
                    - accounts
                    - package
                model:
                  type: string
                  enum:
                    - free
                    - annual
                accounts:
                  type: integer
                  minimum: 1
                  maximum: 1000
                additionalAccounts:
                  type: integer
                  minimum: 1
                  maximum: 1000
                packageCapacity:
                  type: integer
                  minimum: 0
                comment:
                  type: string
                  maxLength: 1000
              required:
                - type
              additionalProperties: false
            example:
              type: accounts
              additionalAccounts: 1
              comment: A new subsidiary joins in November
      responses:
        '201':
          description: The request.
          content:
            application/json:
              schema:
                type: object
                properties:
                  requestId:
                    type: string
                  tenantId:
                    type: string
                  type:
                    type: string
                  status:
                    type: string
                  requested:
                    type: object
                    properties:
                      model:
                        type: string
                      accounts:
                        type: integer
                      additionalAccounts:
                        type: integer
                      packageCapacity:
                        type: integer
                      catalogueKey:
                        type: 'null'
                      catalogueVersion:
                        type: integer
                  estimate:
                    type: object
                    properties:
                      model:
                        type: string
                      accounts:
                        type: integer
                      packageCapacity:
                        type: integer
                      annualDocuments:
                        type: integer
                      subtotalExVat:
                        type: integer
                      vat:
                        type: number
                      totalIncVat:
                        type: number
                      currency:
                        type: string
                  comment:
                    type: string
                  createdAt:
                    type: string
                  history:
                    type: array
                    items:
                      type: object
                      properties:
                        at:
                          type: string
                        status:
                          type: string
                        by:
                          type: string
                        note:
                          type: 'null'
              example:
                requestId: sreq_1761732000000_a1b2c3d4
                tenantId: tnt_7Q2X…
                type: accounts
                status: pending
                requested:
                  model: annual
                  accounts: 3
                  additionalAccounts: 1
                  packageCapacity: 600
                  catalogueKey: null
                  catalogueVersion: 2
                estimate:
                  model: annual
                  accounts: 3
                  packageCapacity: 600
                  annualDocuments: 4200
                  subtotalExVat: 3044
                  vat: 152.2
                  totalIncVat: 3196.2
                  currency: AED
                comment: A new subsidiary joins in November
                createdAt: '2026-10-29T10:00:00.000Z'
                history:
                  - at: '2026-10-29T10:00:00.000Z'
                    status: pending
                    by: finance@acme.ae
                    note: null
        '400':
          $ref: '#/components/responses/ErrBadRequest'
        '401':
          $ref: '#/components/responses/ErrUnauthorized'
        '403':
          $ref: '#/components/responses/ErrForbidden'
        '409':
          $ref: '#/components/responses/ErrConflict'
        '500':
          $ref: '#/components/responses/ErrInternal'
      security:
        - BearerJWT: []
        - SessionCookie: []
      x-azdan-access: tenant
components:
  parameters:
    CsrfToken:
      name: X-CSRF-Token
      in: header
      required: false
      description: >-
        Required with cookie sessions on unsafe methods: the value of the `azdan_csrf` cookie. Not needed with
        `Authorization: Bearer`.
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 120
      description: >-
        Send Idempotency-Key as an HTTP header on submit. The key identifies ONE logical submission, scoped to the
        tenant: the same key always returns the original transaction (idempotent: true) and never sends twice, so retry
        a timeout or a 5xx with the SAME key and use a new key for a new document. 1 to 120 characters — a longer key is
        refused with HTTP 400 IDEMPOTENCY_KEY_INVALID before anything is stored, and the same limit applies to the
        legacy body field idempotencyKey and to the by-idempotency lookup. The header is authoritative; if the legacy
        body field is also sent the two must be equal (IDEMPOTENCY_KEY_CONFLICT).
      example: invoice-INV-2026-000123
    TransactionIdPath:
      name: transactionId
      in: path
      required: true
      schema:
        type: string
      description: Platform transaction id (`txn_*`) or AP transaction id.
      example: txn_1715690640123_a1b2c3d4
    CursorLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 50
      description: Maximum items to return (cursor mode).
    Cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
      description: Opaque cursor from a previous response's `nextCursor`.
    Sort:
      name: sort
      in: query
      required: false
      schema:
        type: string
        enum:
          - createdAt:desc
          - createdAt:asc
          - updatedAt:desc
        default: createdAt:desc
      description: Sort order for cursor pagination.
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
      description: Page number (page mode only).
    PageSize:
      name: pageSize
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 25
      description: Items per page (page mode only).
    FilterDirection:
      name: direction
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/Direction'
    FilterStatus:
      name: status
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/TransactionStatus'
    FilterEnvironment:
      name: environment
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/Environment'
    FilterDocumentType:
      name: documentType
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/DocumentType'
    FilterSenderPeppolId:
      name: senderPeppolId
      in: query
      required: false
      schema:
        type: string
      example: '0235:1000000001'
    FilterReceiverPeppolId:
      name: receiverPeppolId
      in: query
      required: false
      schema:
        type: string
      example: '0235:1000000002'
    FilterPeppolParticipantId:
      name: peppolParticipantId
      in: query
      required: false
      schema:
        type: string
      description: Match transactions where sender OR receiver matches.
    FilterSubsidiaryId:
      name: subsidiaryId
      in: query
      required: false
      schema:
        type: string
    FilterCorrelationId:
      name: correlationId
      in: query
      required: false
      schema:
        type: string
    FilterAs4MessageId:
      name: as4MessageId
      in: query
      required: false
      schema:
        type: string
    FilterApCoreMessageId:
      name: apCoreMessageId
      in: query
      required: false
      schema:
        type: string
    FilterSbdhInstanceId:
      name: sbdhInstanceId
      in: query
      required: false
      schema:
        type: string
    FilterIdempotencyKey:
      name: idempotencyKey
      in: query
      required: false
      schema:
        type: string
        maxLength: 120
    FilterDateFrom:
      name: dateFrom
      in: query
      required: false
      schema:
        type: string
        format: date-time
      description: Only transactions created on or after this date.
    FilterDateTo:
      name: dateTo
      in: query
      required: false
      schema:
        type: string
        format: date-time
      description: Only transactions created on or before this date.
    FilterMessageId:
      name: messageId
      in: query
      required: false
      schema:
        type: string
      description: Alias for apCoreMessageId filter.
    FilterSearch:
      name: search
      in: query
      required: false
      schema:
        type: string
      description: Free-text search across transactionId, correlationId, message IDs, and participant IDs.
    IncludeParam:
      name: include
      in: query
      required: false
      schema:
        type: string
        enum:
          - storage
      description: |
        Opt-in to include additional data in list items.
        - `storage`: Attach a lightweight storage summary (driver, fileType, sizeBytes) to each item.
          No S3 keys or filesystem paths are exposed.
  schemas:
    PlatformError:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable code, e.g. VALIDATION, NOT_FOUND, FORBIDDEN, or a domain code.
            message:
              type: string
              description: Human-readable message; safe to show.
            details:
              description: Optional structured context for the code.
            issues:
              type: array
              description: 'Present on VALIDATION errors: the zod issues (path, message, code).'
              items:
                type: object
                properties:
                  path:
                    type: array
                    items: {}
                  message:
                    type: string
                  code:
                    type: string
    Direction:
      type: string
      enum:
        - inbound
        - outbound
    TransactionStatus:
      type: string
      enum:
        - queued
        - sending
        - delivered
        - failed
        - received
        - quarantined
      x-azdan-notes: |
        queued → sending → delivered (happy path)
        queued → sending → failed (delivery error)
        received (inbound from network)
        quarantined (inbound held for review)
    Environment:
      type: string
      enum:
        - test
        - production
      description: >-
        The Peppol network a document is routed to. Set per request (environment, default "test"), stored on the
        transaction and echoed in every webhook payload. The API key prefix does not influence it; the sender
        participant must be registered in the environment named.
    SubmittableDocumentType:
      type: string
      enum:
        - invoice
        - creditNote
      description: |
        Document types accepted by validate and submit endpoints.
        - `invoice` — AE PINT Invoice
        - `creditNote` — AE PINT Credit Note
    DocumentType:
      type: string
      enum:
        - invoice
        - creditNote
        - tdd
      description: >-
        All Peppol document types that can appear in transaction responses and list filters.

        - `invoice` and `creditNote` are submittable via the API.

        - `tdd` (UAE Tax Data Document) is **read-only**: Tax Data Documents are generated automatically by AZDAN. Do
        not submit a TDD. Use the invoice transaction's TDD endpoint to inspect the generated TDD status and download it
        when available. Inspect it with GET /api/integrations/peppol/transactions/{transactionId}/tdd. Submitting `tdd`
        via validate or submit is rejected with HTTP 400 (invalid enum value).


        Use `SubmittableDocumentType` for request schemas; use `DocumentType` for response/filter schemas.
    WebhookEventType:
      type: string
      enum:
        - transaction.submitted
        - ap.outbound.created
        - ap.outbound.delivered
        - ap.outbound.failed
        - ap.inbound.received
        - ap.mls.updated
        - transaction.tdd_generated
        - mls.sent
        - mls.received
        - mls.accepted
        - mls.rejected
        - tdd.reported
        - transaction.status_changed
        - document.validated
        - document.rejected
        - webhook.test
      x-azdan-notes: >
        Legacy (v1) names, as stored and as a v1 delivery's `type`:

        transaction.submitted: Emitted immediately when POST /invoices succeeds (v2 name document.submitted).

        ap.outbound.created: AP Core created the outbound row (v2 name document.sent).

        ap.outbound.delivered: AS4 delivery confirmed (v2 name document.delivered).

        ap.outbound.failed: Delivery failed permanently (v2 name document.failed).

        ap.inbound.received: Inbound document received from network (v2 name document.received).

        ap.mls.updated: Message Level Status receipt update (v2 name mls.updated).

        transaction.tdd_generated: A UAE Tax Data Document was generated for a transaction (v2 name tdd.generated).

        mls.sent: This AP sent an MLS ApplicationResponse (linked to the originating transaction where known).

        mls.received: An MLS was received from a peer AP for one of the tenant's outbound transactions.

        Topics introduced with payload v2 (one name only):

        mls.accepted: The receiver's access point accepted or acknowledged the document (MLS AP / AB).

        mls.rejected: The receiver's access point rejected the document (MLS RE), with the reason.

        tdd.reported: The FTA's access point answered a Tax Data Document — outcome accepted or rejected, with the
        reason.

        transaction.status_changed: The platform transaction moved to another status (previous and new status in the
        payload).

        document.validated: A document submitted through this API passed validation and was recorded.

        document.rejected: A document submitted through this API failed validation and was not sent (validation issues
        in the payload; no transaction).

        webhook.test: A test event sent from the console.
    InputType:
      type: string
      enum:
        - json
        - xml
    ParticipantRef:
      type: object
      properties:
        participantId:
          type: string
          description: Peppol participant ID (scheme:id format)
          example: '0235:1000000001'
        name:
          type: string
          nullable: true
          example: Acme Trading LLC
    DocumentLink:
      type: object
      required:
        - available
        - fileName
        - contentType
        - downloadUrl
      description: |
        A single document reference inside the unified `documents` object. When
        `available` is false the file has not been generated/indexed yet:
        `fileName` and `downloadUrl` are null and consumers must treat it as
        not-yet-present.
      properties:
        available:
          type: boolean
          description: Whether the file exists and can be downloaded.
        fileName:
          type: string
          nullable: true
          example: INV-10001.xml
        contentType:
          type: string
          description: Expected MIME type (present even when unavailable).
          example: application/xml
        downloadUrl:
          type: string
          nullable: true
          description: |
            Authenticated Platform API download URL — NEVER a raw object-storage
            (OCI/S3) URL. Relative in API/portal responses; absolute in webhook
            payloads when `PUBLIC_API_BASE_URL` is configured. May resolve to a
            short-lived signed URL, so do not cache it.
          example: /api/integrations/peppol/transactions/txn_1715690640123_a1b2c3d4/files/invoice-xml
        reason:
          type: string
          nullable: true
          enum:
            - metadata_only
            - not_generated
            - not_in_object_storage
            - not_received
            - not_stored
            - not_stored_for_outbound
            - transaction_not_found
            - null
          description: >-
            Present only when available is false: why the document is absent. documents.<kind>.available on the
            transaction detail is computed from what is actually stored, and the download route resolves the same rows —
            the two never disagree. An unavailable entry carries reason: not_generated (PDF, TDD), metadata_only (MLS
            known from the access point without a stored file), not_received (no MLS), not_stored_for_outbound (an
            outbound submission is payload-only, the SBDH exists only inside the AS4 message) or not_stored. Optional
            documents answer 404 with the same reason in error.details; that is an expected state, not a failure.
            Privileged routes (raw payload, TDD signed download) answer 403 without the peppol:payload:read scope.
    TddMetadata:
      type: object
      properties:
        fileId:
          type: string
        transactionId:
          type: string
          description: The invoice / credit note transaction the TDD reports on.
        parentTransactionId:
          type: string
        apTddTransactionId:
          type: string
          description: Access-point transaction id of the TDD transport itself.
        status:
          type: string
          nullable: true
          description: Delivery status of the TDD transport (pending, sending, sent, failed, permanently_failed, …).
        documentTypeId:
          type: string
          nullable: true
        processId:
          type: string
          nullable: true
        generatedAt:
          type: string
          format: date-time
          nullable: true
        sha256:
          type: string
        sizeBytes:
          type: integer
        contentType:
          type: string
        source:
          type: string
          example: ap_core_generated_tdd
        sbdhInstanceId:
          type: string
          nullable: true
        apCoreMessageId:
          type: string
          nullable: true
        tddDownloadUrl:
          type: string
          description: Platform API route for the signed download (peppol:payload:read).
      description: >-
        Tax Data Documents are generated automatically by AZDAN. Do not submit a TDD. Use the invoice transaction's TDD
        endpoint to inspect the generated TDD status and download it when available.
    TransactionDocuments:
      type: object
      description: >-
        documents.<kind>.available on the transaction detail is computed from what is actually stored, and the download
        route resolves the same rows — the two never disagree. An unavailable entry carries reason: not_generated (PDF,
        TDD), metadata_only (MLS known from the access point without a stored file), not_received (no MLS),
        not_stored_for_outbound (an outbound submission is payload-only, the SBDH exists only inside the AS4 message) or
        not_stored. Optional documents answer 404 with the same reason in error.details; that is an expected state, not
        a failure. Privileged routes (raw payload, TDD signed download) answer 403 without the peppol:payload:read
        scope.
      required:
        - invoiceXml
        - invoicePdf
        - sbdXml
        - tddXml
        - mlsXml
      properties:
        invoiceXml:
          $ref: '#/components/schemas/DocumentLink'
        invoicePdf:
          $ref: '#/components/schemas/DocumentLink'
        sbdXml:
          $ref: '#/components/schemas/DocumentLink'
        tddXml:
          $ref: '#/components/schemas/DocumentLink'
        mlsXml:
          $ref: '#/components/schemas/DocumentLink'
        payloadXml:
          type: object
          description: >-
            The stored business payload, served as a short-lived signed object-storage URL (303). Same bytes as Invoice
            XML; a different route with a privileged scope. Integration surface only.
          properties:
            available:
              type: boolean
            privileged:
              type: boolean
              enum:
                - true
            requiredScope:
              type: string
              enum:
                - peppol:payload:read
            contentType:
              type: string
            downloadUrl:
              type: string
              nullable: true
            reason:
              type: string
              nullable: true
    Transaction:
      type: object
      properties:
        transactionId:
          type: string
          example: txn_1715690640123_a1b2c3d4
        direction:
          $ref: '#/components/schemas/Direction'
        status:
          $ref: '#/components/schemas/TransactionStatus'
        documentType:
          type: string
          nullable: true
        documentTypeId:
          type: string
          nullable: true
          description: Full Peppol document type URN.
        processId:
          type: string
          nullable: true
          example: urn:peppol:bis:billing
        sender:
          $ref: '#/components/schemas/ParticipantRef'
        receiver:
          $ref: '#/components/schemas/ParticipantRef'
        correlationId:
          type: string
          nullable: true
        apCoreMessageId:
          type: string
          nullable: true
          description: AP Core / AS4 message ID.
        as4MessageId:
          type: string
          nullable: true
          description: Alias of apCoreMessageId for convenience.
        sbdhInstanceId:
          type: string
          nullable: true
        environment:
          $ref: '#/components/schemas/Environment'
        sizeBytes:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        failedAt:
          type: string
          format: date-time
          nullable: true
        failureReason:
          type: string
          nullable: true
        idempotencyKey:
          type: string
          nullable: true
        documents:
          $ref: '#/components/schemas/TransactionDocuments'
    CursorPagination:
      type: object
      required:
        - mode
        - limit
        - hasNext
      properties:
        mode:
          type: string
          const: cursor
        limit:
          type: integer
        nextCursor:
          type: string
          nullable: true
          description: Opaque token for the next page. Pass as `cursor` param.
        prevCursor:
          type: string
          nullable: true
          description: Reserved for future bidirectional cursors.
        hasNext:
          type: boolean
    PagePagination:
      type: object
      required:
        - page
        - pageSize
        - total
        - totalPages
        - hasMore
      properties:
        page:
          type: integer
        pageSize:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
        hasMore:
          type: boolean
    CursorListResponse:
      type: object
      required:
        - items
        - pagination
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
        pagination:
          $ref: '#/components/schemas/CursorPagination'
    PageListResponse:
      type: object
      required:
        - transactions
        - pagination
      properties:
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
        pagination:
          $ref: '#/components/schemas/PagePagination'
    WebhookEvent:
      type: object
      properties:
        eventId:
          type: string
          example: evt_ap_a1b2c3d4e5f6g7h8
        type:
          $ref: '#/components/schemas/WebhookEventType'
        deliveryStatus:
          type: string
          enum:
            - pending
            - in_flight
            - delivered
            - failed
            - dead
        payload:
          type: object
          description: |
            Event-specific payload. All webhook payloads include the additive
            `documents` object when document lookup succeeds. For
            `transaction.tdd_generated`, the legacy `tddDownloadUrl` field is
            retained and `documents.tddXml` points at the same TDD through the
            unified secure document-download contract.
          properties:
            event:
              type: string
            transactionId:
              type: string
            status:
              type: string
            direction:
              type: string
            environment:
              type: string
            sender:
              type: string
              description: Sender Peppol ID.
            receiver:
              type: string
              description: Receiver Peppol ID.
            documentType:
              type: string
            processId:
              type: string
            correlationId:
              type: string
            apMessageId:
              type: string
            sbdhInstanceId:
              type: string
            tddDownloadUrl:
              type: string
              nullable: true
              description: Legacy TDD download URL retained for backward compatibility on `transaction.tdd_generated`.
              example: /api/integrations/peppol/transactions/txn_1715690640123_a1b2c3d4/tdd/download
            documents:
              $ref: '#/components/schemas/TransactionDocuments'
        createdAt:
          type: string
          format: date-time
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        attempts:
          type: integer
        lastError:
          type: string
          nullable: true
    EventsResponse:
      type: object
      required:
        - events
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEvent'
    ApEnrichment:
      type: object
      description: |
        AP Core link status. If `linked: true`, additional AP metadata is available.
        If `linkAmbiguous: true`, multiple AP records match and the system cannot auto-link.
      required:
        - linked
      properties:
        linked:
          type: boolean
          description: Whether this transaction is linked to an AP Core record.
        linkAmbiguous:
          type: boolean
          description: True if multiple AP candidates exist (no auto-link).
        candidateCount:
          type: integer
          description: Number of ambiguous AP candidates (only when linkAmbiguous).
        direction:
          $ref: '#/components/schemas/Direction'
        apTransactionId:
          type: string
          description: AP Core's internal transaction ID.
        status:
          type: string
          description: AP Core status (delivered, permanently_failed, sending, etc.)
        as4MessageId:
          type: string
          nullable: true
          description: AS4 transport message ID.
        sbdhInstanceId:
          type: string
          nullable: true
        messageId:
          type: string
          nullable: true
          description: Same as as4MessageId (legacy alias).
        documentType:
          type: string
          nullable: true
        processId:
          type: string
          nullable: true
        sender:
          type: string
          nullable: true
          description: Normalised sender participant ID (without URI prefix).
        receiver:
          type: string
          nullable: true
          description: Normalised receiver participant ID (without URI prefix).
        createdAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        errorDetails:
          type: string
          nullable: true
          description: AP error description (when status is permanently_failed).
        mlsStatus:
          type: string
          nullable: true
          enum:
            - pending
            - received_ap
            - received_ab
            - received_re
            - not_applicable
            - null
          description: >-
            MLS values are lower-case, exactly as stored. ap.mlsStatus / ap.mlsReceivedAt / ap.mlsId / ap.mlsType /
            ap.mlsErrorCode / ap.mlsErrorMessage are metadata on the enriched detail; documents.mlsXml is available only
            when an MLS XML file is actually stored — for outbound documents it is normally not. Outbound only.
          example: received_ap
        mlsReceivedAt:
          type: string
          format: date-time
          nullable: true
          description: When the MLS was received. Outbound only.
        mlsId:
          type: string
          nullable: true
          description: Identifier of the MLS document. Outbound only.
        mlsType:
          type: string
          nullable: true
          description: MLS document type recorded by the access point (either direction).
        mlsErrorCode:
          type: string
          nullable: true
          description: Error code carried by a negative MLS (received_re). Outbound only.
        mlsErrorMessage:
          type: string
          nullable: true
          description: Error message carried by a negative MLS (received_re). Outbound only.
        isTdd:
          type: boolean
          description: |
            True only when THIS access-point row is itself a UAE Tax Data Document
            transport. For an invoice or credit note it is always false — it does not
            say whether a TDD was generated for the document (use GET
            …/transactions/{transactionId}/tdd for that).
        tddStatus:
          type: string
          nullable: true
          description: |
            Status of this row when it is itself a TDD transport; always null for an
            invoice or credit note. The generated TDD's delivery status is tdd.status
            on GET …/transactions/{transactionId}/tdd.
    StorageInfo:
      type: object
      nullable: true
      description: |
        Primary file storage metadata. The `s3Key` field is included in
        authenticated detail responses but must not be logged or displayed
        in end-user UIs. Use the `/transactions/{id}/xml` endpoint
        to obtain a short-lived presigned download URL instead.
      properties:
        driver:
          type: string
          enum:
            - s3
            - local
        s3Key:
          type: string
          nullable: true
          description: |
            S3 object key (detail view only, not in list storage summary).
            **Do not expose to end-users or store in client-side logs.**
            Use the `/transactions/{id}/xml` endpoint for downloads.
          x-internal: true
        contentType:
          type: string
          example: application/xml
        fileType:
          type: string
          enum:
            - payload
            - sbdh
            - mlr
            - mdn
            - envelope
            - attachment
            - receipt
        sizeBytes:
          type: integer
        cleanupStatus:
          type: string
          enum:
            - clean
            - pending_cleanup
          description: Whether a legacy local path still exists alongside S3.
    FileInfo:
      type: object
      properties:
        name:
          type: string
          description: Original filename or generated name.
        contentType:
          type: string
          example: application/xml
        sizeBytes:
          type: integer
        storage:
          type: string
          enum:
            - s3
            - local
        s3Key:
          type: string
          nullable: true
          description: S3 object key (null for local-only files).
    PlatformMeta:
      type: object
      description: Platform-specific metadata for the transaction.
      properties:
        transactionId:
          type: string
        status:
          $ref: '#/components/schemas/TransactionStatus'
        correlationId:
          type: string
          nullable: true
        idempotencyKey:
          type: string
          nullable: true
        validation:
          type: object
          properties:
            engine:
              type: string
              nullable: true
        webhookSummary:
          type: object
          nullable: true
          properties:
            totalEvents:
              type: integer
            lastEventType:
              type: string
              nullable: true
            lastEventStatus:
              type: string
              nullable: true
            lastEventAt:
              type: string
              format: date-time
              nullable: true
    TransactionLinks:
      type: object
      description: HATEOAS links to related resources.
      properties:
        self:
          type: string
        events:
          type: string
        xml:
          type: string
    EnrichedTransactionDetail:
      type: object
      required:
        - transaction
        - platform
        - ap
        - storage
        - files
        - documents
        - links
      properties:
        transaction:
          $ref: '#/components/schemas/Transaction'
        platform:
          $ref: '#/components/schemas/PlatformMeta'
        ap:
          $ref: '#/components/schemas/ApEnrichment'
        storage:
          $ref: '#/components/schemas/StorageInfo'
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileInfo'
        documents:
          $ref: '#/components/schemas/TransactionDocuments'
        links:
          $ref: '#/components/schemas/TransactionLinks'
    ValidateRequest:
      type: object
      required:
        - inputType
        - environment
        - documentType
        - senderPeppolId
        - receiverPeppolId
      properties:
        inputType:
          $ref: '#/components/schemas/InputType'
        environment:
          $ref: '#/components/schemas/Environment'
        documentType:
          $ref: '#/components/schemas/SubmittableDocumentType'
        senderPeppolId:
          type: string
          description: Sender Peppol ID in scheme:id format.
          example: '0235:1000000001'
        receiverPeppolId:
          type: string
          description: Receiver Peppol ID in scheme:id format.
          example: '0235:1000000002'
        payload:
          type: object
          description: Invoice/credit note data (JSON mode only).
        xml:
          type: string
          description: Complete UBL XML document (XML mode only).
    SubmitRequest:
      type: object
      required:
        - inputType
        - environment
        - documentType
        - senderPeppolId
        - receiverPeppolId
      properties:
        inputType:
          $ref: '#/components/schemas/InputType'
        environment:
          $ref: '#/components/schemas/Environment'
        documentType:
          $ref: '#/components/schemas/SubmittableDocumentType'
        senderPeppolId:
          type: string
          example: '0235:1000000001'
        receiverPeppolId:
          type: string
          example: '0235:1000000002'
        idempotencyKey:
          type: string
          minLength: 1
          deprecated: true
          maxLength: 120
          description: >-
            Deprecated legacy alternative to the Idempotency-Key header. Prefer the HTTP header. If both are sent they
            must have the same value or the API returns IDEMPOTENCY_KEY_CONFLICT. Same 1 to 120 character limit as the
            header (IDEMPOTENCY_KEY_INVALID).
        payload:
          type: object
          description: Invoice/credit note data (JSON mode only).
        xml:
          type: string
          description: Complete UBL XML document (XML mode only).
    ValidateResponse:
      type: object
      properties:
        valid:
          type: boolean
        documentTypeId:
          type: string
        processId:
          type: string
        warnings:
          type: array
          items:
            type: object
        phormSkipped:
          type: boolean
          description: True if only local structural checks ran (Phorm unavailable).
        normalized:
          type: object
          description: Normalized sender/receiver and document metadata.
    SubmitResponse:
      type: object
      properties:
        transactionId:
          type: string
        status:
          $ref: '#/components/schemas/TransactionStatus'
        valid:
          type: boolean
        documentTypeId:
          type: string
        processId:
          type: string
        correlationId:
          type: string
        as4MessageId:
          type: string
          nullable: true
        sbdhInstanceId:
          type: string
          nullable: true
        idempotent:
          type: boolean
          description: True if this is a replay of a previous submission with the same idempotency key.
        warnings:
          type: array
          items:
            type: object
        validatorPhormSkipped:
          type: boolean
        documents:
          $ref: '#/components/schemas/TransactionDocuments'
        message:
          type: string
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code. See the ErrorCatalog schema for every value.
              example: PINT_VALIDATION_FAILED
            message:
              type: string
              description: Human-readable error description.
            details:
              type: array
              items:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  path:
                    type: string
  responses:
    ErrBadRequest:
      description: The request is invalid. `VALIDATION` errors list each failed field in `issues`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: VALIDATION
              message: Invalid request
              issues:
                - code: invalid_type
                  path:
                    - name
                  message: Required
    ErrUnauthorized:
      description: Missing, invalid or expired credentials, or the account or tenant is no longer active.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: UNAUTHORIZED
              message: Invalid or expired token
    ErrForbidden:
      description: >-
        Authenticated, but not allowed: wrong role, missing API-key scope, a missing or mismatched CSRF token on a
        cookie session, or MFA not yet set up (`MFA_ENROLLMENT_REQUIRED` — mandatory MFA).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: FORBIDDEN
              message: Forbidden
    ErrNotFound:
      description: The resource does not exist or does not belong to the caller's tenant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: NOT_FOUND
              message: Not found
    ErrConflict:
      description: The request conflicts with the current state of the resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: CONFLICT
              message: Conflict
    ErrUnprocessable:
      description: Understood but rejected by a business rule or by the access point.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: AP_CORE_REJECTED
              message: AP Core rejected submission
    ErrTooManyRequests:
      description: Rate limit exceeded. Retry after the time in the `RateLimit-Reset` / `Retry-After` header.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests
    ErrInternal:
      description: Unexpected server error. No internal detail is returned.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: INTERNAL
              message: Internal server error
    ErrBadGateway:
      description: An upstream service (access point, SMP, EmaraTax, object storage) failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: UPSTREAM
              message: Upstream failure
    ErrUnavailable:
      description: A required service or configuration is not available.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlatformError'
          example:
            error:
              code: SERVICE_UNAVAILABLE
              message: Service unavailable
    ValidationError:
      description: Request validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            pintFailed:
              summary: The document failed validation
              value:
                valid: false
                documentTypeId: urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:peppol:pint:billing-1@ae-1::2.1
                processId: urn:peppol:bis:billing
                error:
                  code: PINT_VALIDATION_FAILED
                  message: Document failed Peppol/PINT validation.
                  details:
                    - code: PINT_BR_AE_20
                      severity: fatal
                      message: dueDate (IBT-009) is required when the amount due for payment is greater than zero [ibr-127-ae]
                      xpath: /dueDate
    BadRequest:
      description: Invalid request parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            cursorMismatch:
              summary: Cursor filter mismatch
              value:
                error:
                  code: CURSOR_FILTER_MISMATCH
                  message: Cursor was generated with different filters. Start a new query without a cursor.
            invalidCursor:
              summary: Invalid cursor format
              value:
                error:
                  code: INVALID_CURSOR
                  message: Invalid cursor format
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: UNAUTHORIZED
              message: Invalid API key
    Forbidden:
      description: API key lacks required scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: FORBIDDEN
              message: 'Missing scope: transactions:read'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: NOT_FOUND
              message: Transaction not found
    UpstreamError:
      description: Access point rejected the submission.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            apRejected:
              summary: AP Core rejected submission
              value:
                error:
                  code: UPSTREAM
                  message: AP Core rejected submission
                  details:
                    transactionId: txn_1715690640123_a1b2c3d4
                    upstreamStatus: 400
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: AZDAN API Key
      description: >-
        Tenant integration API key from the Tenant Console (API Keys): `Authorization: Bearer azd_live_…` or
        `azd_test_…`. Not a JWT. Each operation lists the scopes it accepts in `x-azdan-scopes` (any one suffices).
    BearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Tenant user session token from `POST /api/auth/login` (or `POST /api/auth/mfa/verify` for accounts with MFA),
        sent as `Authorization: Bearer <token>`. Valid for 8 hours. Bearer callers do not need a CSRF token. Roles:
        `tenant_admin`, `tenant_user`.
    SessionCookie:
      type: apiKey
      in: cookie
      name: azdan_session
      description: >-
        The HTTP-only session cookie set by `POST /api/auth/login` (name configurable with `AUTH_COOKIE_NAME`, default
        `azdan_session`). Unsafe methods (POST/PUT/PATCH/DELETE) also need the `X-CSRF-Token` header equal to the
        readable `azdan_csrf` cookie.
