openapi: 3.0.3
info:
  title: Zyntem Internal Operations API
  version: 1.0.0
  description: |
    Internal-only API surface: admin/ops endpoints, customer portal self-service (auth via portal session, not API keys), and external webhook receivers (Stripe → us, Italy SDI → us). NOT for ISV consumption. Auth is mixed; each endpoint documents what it expects.

    ## Authentication
    All endpoints (except `/health`, `/version`, and `/v1/accounts`) require a
    Bearer token API key in the `Authorization` header.

    Keys are prefixed:
    - `zyn_test_` — test mode (sandbox tax authority endpoints)
    - `zyn_live_` — live mode (production tax authority endpoints)

    The key prefix determines endpoint routing — test keys always reach sandbox,
    live keys always reach production, regardless of deployment environment.

    ## Idempotency
    `POST /v1/transactions` accepts an `Idempotency-Key` header. Duplicate
    submissions with the same key return the original response.

    ## Instant Receipts
    Transaction creation always returns `201 Created` with the fiscal ID and
    receipt data populated immediately. The prepare phase runs synchronously
    with no network calls — POS systems are never blocked by tax authority
    latency. Submission to the tax authority happens automatically in the
    background, following each country's regulatory timeline.

    ## Rate Limiting
    All authenticated endpoints return rate limit headers:
    - `X-RateLimit-Limit` — requests allowed per window
    - `X-RateLimit-Remaining` — requests remaining in current window
    - `X-RateLimit-Reset` — UTC epoch seconds when the window resets

    When the limit is exceeded the API returns `429 Too Many Requests` with a
    `Retry-After` header (seconds until the next window).

    ## Pagination
    List endpoints accept `limit` (1–100, default 20) and `offset` (≥ 0) query
    parameters. Responses include `data` (array), `total`, `limit`, and `offset`.

    ## Webhook Events
    Supported event types:
    - `transaction.fiscalized` — fiscalization completed (immediate or after retry)
    - `transaction.failed` — permanent fiscalization failure after retries exhausted
    - `certificate.expiring` — certificate expires within 30 days
    - `certificate.expired` — certificate has expired

    Payloads are signed with HMAC-SHA256 via the `X-Fiscalization-Signature` header.

    ## Deployment Modes

    Zyntem supports two deployment modes. Most endpoints work in both modes.
    Exceptions are noted in each endpoint's description.

    | Mode | Description | Endpoint availability |
    |------|-------------|----------------------|
    | **Embedded-Cloud** | ISV backend calls the Zyntem hosted API | All endpoints except `POST /management/v1/tokens` |
    | **Embedded-Local** | ISV embeds the engine library in their POS | All endpoints available locally. `POST /management/v1/tokens` for offline token provisioning. `POST /v1/admin/process-pending` triggered by ISV (no Cloud Scheduler). Webhooks and retry queue managed locally by the engine. |
    | **HardwareDevice** | Requires a certified hardware device (TSE, control unit, FDM) for fiscal signing | Same as Embedded-Local. The hardware device handles signing and tamper-proof storage. A per-location pricing floor (€10/location) applies to cover hardware provisioning costs. |

    Endpoints tagged **Cloud-only** are only relevant for Embedded-Cloud deployments.
    Endpoints tagged **Embedded-only** are only relevant for Embedded-Local deployments.
    Most endpoints work identically in both modes.
  contact:
    name: Zyntem Engineering
    email: engineering@zyntem.com
servers:
  - url: https://api.zyntem.dev
    description: Staging
  - url: http://localhost:8080
    description: Local server (Embedded-Local HTTP mode)
security:
  - BearerAuth: []
tags:
  - name: Account
    description: Account self-service (status, GDPR data export, consent records)
  - name: Inbound Webhooks
    description: Inbound webhooks from third parties (Stripe billing, Italy SDI)
  - name: Retry Queue
    description: Fiscalization retry queue
  - name: Subscriptions
    description: Subscription management and volume pricing (portal only)
  - name: API Keys
    description: API key self-service management
  - name: Onboarding
    description: Account onboarding status and checklist
  - name: Usage
    description: Usage metering and tier status
  - name: Fleet
    description: Agent telemetry and fleet-wide metrics
  - name: Admin
    description: Batch processing and operational endpoints
  - name: Batch
    description: Background batch processing schedules
paths:
  /v1/account/status:
    get:
      tags:
        - Account
      summary: Get account entitlements and merchant usage
      description: Returns the authenticated account's entitlements (tier, features), merchant count, and enforcement state. Uses the API key from the Authorization header to identify the account — no query parameter needed.
      operationId: getAccountStatus
      responses:
        '200':
          description: Account status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountStatusResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/agents/{agent_id}/telemetry:
    post:
      tags:
        - Fleet
      summary: Ingest agent telemetry
      description: Embedded-only. Accepts a batch of telemetry data points from an on-premise agent. Each data point includes transaction counts, error rates, storage utilization, and adapter version info. Max 100 data points per request. Anomaly detection runs asynchronously after ingestion.
      operationId: ingestTelemetry
      parameters:
        - name: agent_id
          in: path
          description: Agent identifier (1–255 characters).
          required: true
          schema:
            type: string
            maxLength: 255
            minLength: 1
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestTelemetryRequest'
        required: true
      responses:
        '202':
          description: Telemetry accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestTelemetryResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/fleet/metrics:
    get:
      tags:
        - Fleet
      summary: Get fleet-wide metrics
      description: Returns aggregated telemetry metrics across all agents for an account. Includes transaction totals, error rates, storage usage, and time-series data.
      operationId: getFleetMetrics
      parameters:
        - name: account_id
          in: query
          description: Account ID (must match the authenticated account).
          required: true
          schema:
            type: string
            format: uuid
        - name: range
          in: query
          description: |-
            Time range for metrics aggregation. One of: `1h`, `6h`, `24h`,
            `7d`, `30d`. Defaults to `24h`.
          required: false
          schema:
            type: string
          example: 24h
      responses:
        '200':
          description: Fleet metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FleetMetricsResponse'
        '400':
          description: Invalid range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Cannot access metrics for another account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/api-keys:
    get:
      tags:
        - ApiKeys
      summary: List API keys
      description: List all API keys for the authenticated account. Key hashes are never returned — only metadata (id, prefix, name, environment, permissions, …).
      operationId: listApiKeys
      responses:
        '200':
          description: API keys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
    post:
      tags:
        - ApiKeys
      summary: Create a new API key
      description: Create a new API key for the authenticated account. If `permissions` is omitted, creates a root key (only allowed when authenticated with a root key). If `permissions` is provided, creates a restricted key. Keys can only create keys with equal or lesser permissions (no privilege escalation). The plaintext key is returned **once** — it cannot be retrieved again.
      operationId: createApiKey
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
        required: true
      responses:
        '201':
          description: API key created (plaintext returned once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Privilege escalation or insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/api-keys/{id}:
    delete:
      tags:
        - ApiKeys
      summary: Revoke an API key
      description: Revoke an API key. The key stops working immediately. The key used to authenticate this request cannot be revoked here — use a different key.
      operationId: revokeApiKey
      parameters:
        - name: id
          in: path
          description: API key id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: API key revoked
        '400':
          description: Cannot revoke the calling key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: API key not found or already revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/api-keys/{id}/rotate:
    post:
      tags:
        - ApiKeys
      summary: Rotate an API key
      description: 'Rotate an API key: revokes the old key and creates a new one with the same environment. Returns the new plaintext key **once** — it cannot be retrieved again. The key used to authenticate this request cannot be rotated here — use a different key.'
      operationId: rotateApiKey
      parameters:
        - name: id
          in: path
          description: API key id to rotate
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Key rotated; old key revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateApiKeyResponse'
        '400':
          description: Cannot rotate the calling key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: API key not found or already revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/admin/batch-stats:
    get:
      tags:
        - Admin
      summary: Get batch processing statistics
      description: Returns statistics from all registered batch processors for the last 24 hours.
      operationId: getBatchStats
      responses:
        '200':
          description: Per-processor statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchStatsResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/admin/process-pending:
    post:
      tags:
        - Admin
      summary: Run all batch processors
      description: 'Runs all registered batch processors (e.g., Spain LROE, future offline fiscalization protocols). Each processor runs sequentially and returns its own result summary. Cloud: invoked by Cloud Scheduler on a daily cadence. Embedded-Local: ISV triggers this endpoint directly (via cron, systemd timer, or application logic) since Cloud Scheduler is not available.'
      operationId: processPending
      responses:
        '200':
          description: Batch processors completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchProcessResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/batch/schedule:
    get:
      tags:
        - Batch
      summary: Get upcoming batch run schedules
      description: Returns the schedule of upcoming batch processing runs across all registered batch systems. ISVs can use this to inform merchants when their pending_batch transactions will be submitted to the tax authority.
      operationId: getBatchSchedule
      responses:
        '200':
          description: List of batch schedules
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchScheduleResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/account/consents:
    get:
      tags:
        - Account
      summary: List consent records
      description: Returns every consent record for the authenticated account, including revoked ones. Use the `active` flag to filter for currently in-force consents.
      operationId: listConsents
      responses:
        '200':
          description: Consent records
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConsentResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
    post:
      tags:
        - Account
      summary: Grant a consent
      description: Grants a new consent of the given type. Any prior active consent for the same `consent_type` is automatically revoked. Source IP and User-Agent are captured on the record for audit purposes.
      operationId: grantConsent
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantConsentRequest'
        required: true
      responses:
        '201':
          description: Consent granted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsentResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/account/consents/{consent_type}:
    delete:
      tags:
        - Account
      summary: Revoke a consent
      description: Revokes the currently active consent of the given type. Returns 404 if no active grant exists.
      operationId: revokeConsent
      parameters:
        - name: consent_type
          in: path
          description: |-
            Consent type to revoke. One of `terms_of_service`, `privacy_policy`,
            `marketing_emails`.
          required: true
          schema:
            type: string
          example: marketing_emails
        - name: consent_type
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Consent revoked
        '400':
          description: Unknown consent_type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No active consent of this type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - BearerAuth: []
  /v1/account:
    delete:
      tags:
        - GDPR
      summary: Request account deletion (Art. 17)
      description: Initiates GDPR Right-to-Erasure for the authenticated account. Synchronously revokes all API keys and cancels the Stripe subscription, then sends a confirmation email and returns 202 with the grace-period metadata. Anonymization itself runs after the grace window via the admin batch job.
      operationId: deleteAccount
      responses:
        '202':
          description: Deletion request accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '409':
          description: Account is already anonymized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/account/export:
    get:
      tags:
        - GDPR
      summary: Download account data (Art. 15)
      description: Returns a JSON archive of every piece of personal data held for the authenticated account. Rate-limited to one request per 24 hours per account. API key hashes are never included — only metadata.
      operationId: exportAccountData
      responses:
        '200':
          description: 'JSON data archive (Content-Disposition: attachment)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountExport'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '429':
          description: Rate-limited (one export per 24h per account)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/admin/process-deletions:
    post:
      tags:
        - GDPR
      summary: Anonymize accounts past the grace window
      description: Admin-only batch endpoint. Iterates accounts whose deletion grace period has elapsed and anonymizes their PII. Intended to be invoked by an internal scheduler — in production, network ACLs gate this route in addition to bearer auth.
      operationId: processDeletions
      parameters:
        - name: grace_hours
          in: query
          description: |-
            Grace period in hours before an account's anonymization runs.
            Defaults to `DEFAULT_GRACE_HOURS` (72).
          required: false
          schema:
            type: integer
            format: int32
            minimum: 0
          example: 72
      responses:
        '200':
          description: Batch result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProcessDeletionsResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /internal/account-lookup:
    get:
      tags:
        - Internal
      summary: Exchange Keycloak email for account + session API key
      description: Internal-only. The dashboard backend hits this after authenticating a user with Keycloak, swapping the user's email for the matching account UUID and a freshly minted test-environment API key labelled `dashboard-session`. The endpoint must be network-restricted in production — there is no caller authentication beyond the email.
      operationId: internalAccountLookup
      parameters:
        - name: email
          in: query
          description: Email address attached to the Keycloak session.
          required: true
          schema:
            type: string
          example: merchant@example.com
      responses:
        '200':
          description: Account found and session key minted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountLookupResponse'
        '400':
          description: Email is missing or empty
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No active account found for email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /internal/auth-status:
    get:
      tags:
        - Internal
      summary: Verify the dashboard auth integration is wired
      description: Returns 200 unconditionally so the dashboard can confirm the API is reachable through whichever network path serves the internal endpoints. Does not check Keycloak connectivity itself.
      operationId: internalAuthStatus
      responses:
        '200':
          description: Auth integration is reachable
  /v1/onboarding/status:
    get:
      tags:
        - Onboarding
      summary: Get onboarding checklist status
      description: Returns the onboarding step completion status for the authenticated account. Steps vary by deployment mode (hosted vs on-premise).
      operationId: getOnboardingStatus
      responses:
        '200':
          description: Onboarding status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatusResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-3'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-3'
      security:
        - BearerAuth: []
  /v1/retry-queue:
    get:
      tags:
        - RetryQueue
      summary: List retry queue entries
      description: Returns the configured retry queue entries for the authenticated account. Filter by `status` to inspect entries currently pending, in flight, or in dead-letter.
      operationId: listRetryEntries
      parameters:
        - name: status
          in: query
          description: |-
            Filter by status. One of: `pending`, `processing`, `completed`,
            `failed`, `dead`.
          required: false
          schema:
            type: string
        - name: limit
          in: query
          description: Page size (default 20, max 100).
          required: false
          schema:
            type: integer
            format: int64
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Zero-based offset (max 10000).
          required: false
          schema:
            type: integer
            format: int64
            maximum: 10000
            minimum: 0
      responses:
        '200':
          description: Retry queue list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryQueueListResponse'
        '400':
          description: Invalid status / pagination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
    post:
      tags:
        - RetryQueue
      summary: Enqueue a transaction for retry processing
      description: Enqueues a previously-submitted transaction for asynchronous retry. The retry worker re-attempts authority submission with exponential backoff up to `max_attempts` before transitioning the entry to `dead`.
      operationId: enqueueRetry
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnqueueRequest'
        required: true
      responses:
        '201':
          description: Retry queue entry created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryQueueEntry'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/retry-queue/{id}:
    get:
      tags:
        - RetryQueue
      summary: Get a retry queue entry
      operationId: getRetryEntry
      parameters:
        - name: id
          in: path
          description: Retry queue entry id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Retry queue entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryQueueEntry'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: Retry queue entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/webhooks/stripe:
    post:
      tags:
        - Inbound Webhooks
      summary: Stripe webhook receiver
      description: 'Receives and processes Stripe webhook events. Verifies the HMAC-SHA256 signature using the `Stripe-Signature` header. Duplicate events are ignored (idempotent via `stripe_events` table). Supported event types: `invoice.payment_succeeded` reactivates suspended accounts, `invoice.payment_failed` and `customer.subscription.deleted` suspend accounts, `customer.subscription.updated` logs subscription status changes.'
      operationId: handleStripeWebhook
      parameters:
        - name: Stripe-Signature
          in: header
          description: 'Stripe webhook signature for payload verification. Format: `t=<timestamp>,v1=<signature>`.'
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeWebhookEvent'
        required: true
      responses:
        '200':
          description: Webhook processed (or duplicate ignored)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeWebhookAck'
        '400':
          description: Invalid signature or malformed payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: Payload exceeded size limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/subscriptions:
    get:
      tags:
        - Subscriptions
      summary: Get the current subscription
      description: Returns the active subscription for the authenticated account.
      operationId: getSubscription
      responses:
        '200':
          description: Subscription state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: No subscription found for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
    post:
      tags:
        - Subscriptions
      summary: Create a Stripe-backed subscription
      description: Provisions a Stripe customer + subscription for the authenticated account using the supplied payment method, then persists the local subscription row. Pricing follows the graduated per-location tiers; the response includes the tier name and monthly euro-cents total.
      operationId: createSubscription
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
        required: true
      responses:
        '201':
          description: Subscription created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '409':
          description: Account already has an active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
    patch:
      tags:
        - Subscriptions
      summary: Update subscription location count
      description: Changes the billed location count on the existing subscription. Stripe handles proration automatically.
      operationId: updateSubscription
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSubscriptionRequest'
        required: true
      responses:
        '200':
          description: Subscription updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '403':
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: No subscription found for this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/admin/usage/aggregate:
    post:
      tags:
        - Admin
      summary: Trigger daily usage aggregation
      description: Admin endpoint. Manually triggers the daily usage aggregation job. If `date` is not provided, defaults to yesterday. Aggregates transaction counts, reports to Stripe, and evaluates tier upgrades.
      operationId: triggerUsageAggregation
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriggerAggregationRequest'
        required: true
      responses:
        '200':
          description: Aggregation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageJobResult'
        '400':
          description: Invalid date format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
      security:
        - BearerAuth: []
  /v1/usage:
    get:
      tags:
        - Usage
      summary: List daily usage records
      description: Returns one row per day of metered usage for the authenticated account, optionally filtered by date range.
      operationId: listUsage
      parameters:
        - name: start_date
          in: query
          description: Filter records on or after this date (YYYY-MM-DD).
          required: false
          schema:
            type: string
          example: '2026-01-01'
        - name: end_date
          in: query
          description: Filter records on or before this date (YYYY-MM-DD).
          required: false
          schema:
            type: string
          example: '2026-01-31'
        - name: limit
          in: query
          description: Page size (default 20, max 100).
          required: false
          schema:
            type: integer
            format: int64
            maximum: 100
            minimum: 1
        - name: offset
          in: query
          description: Zero-based offset (max 10000).
          required: false
          schema:
            type: integer
            format: int64
            maximum: 10000
            minimum: 0
      responses:
        '200':
          description: Paginated list of usage records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageListResponse'
        '400':
          description: Invalid pagination or date parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/usage/tier:
    get:
      tags:
        - Usage
      summary: Get current tier status and recommendation
      description: Returns the rate-limit tier the account is currently on and the tier its rolling-average daily transaction count would put it in.
      operationId: getTierStatus
      responses:
        '200':
          description: Tier status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TierStatusResponse'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse-2'
  /v1/webhooks/sdi:
    post:
      tags:
        - Inbound Webhooks
      summary: Receive SDI async notification
      description: 'Callback endpoint for Italy''s Sistema di Interscambio (SDI). SDI posts XML notifications here after processing a submitted FatturaPA invoice. Notification types: RC (RicevutaConsegna) — delivery receipt, invoice accepted; NS (NotificaScarto) — rejection, invoice has validation errors; MC (NotificaMancataConsegna) — delivery failed, recipient unreachable. This endpoint is called by SDI infrastructure, not by API consumers.'
      operationId: handleSDINotification
      requestBody:
        description: SDI notification XML payload (RicevutaConsegna, NotificaScarto, or NotificaMancataConsegna).
        content:
          application/xml:
            schema:
              type: string
        required: true
      responses:
        '200':
          description: Notification processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SdiNotificationAck'
        '400':
          description: Invalid or unparseable notification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Transaction not found for the SDI identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: Payload exceeded size limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API key prefixed with `zyn_test_` (sandbox) or `zyn_live_` (production).
  schemas:
    AccountStatusResponse:
      type: object
      description: |-
        Response body for `GET /v1/account/status`.

        Returns the authenticated account's entitlements (tier, enabled
        features), aggregated merchant usage, and enforcement state. The
        dashboard, billing portal, and on-premise agents all consume this to
        decide whether the account can register new merchants.
      required:
        - account_id
        - tier
        - features
        - merchants_used
        - merchants_max
        - is_valid
        - grace_period_active
        - enforcement_active
        - country_list
      properties:
        account_id:
          type: string
          format: uuid
          description: Account identifier (UUID).
          example: 660e8400-e29b-41d4-a716-446655440001
        country_list:
          type: array
          items:
            type: string
          description: ISO 3166-1 alpha-2 codes for countries with active merchants.
          example:
            - ES
            - IT
            - FR
        enforcement_active:
          type: boolean
          description: '`true` when enforcement is blocking new merchant registrations.'
        features:
          type: array
          items:
            type: string
          description: |-
            Product capabilities enabled for this account. Possible values:
            `fiscalization`, `digital_receipts`, `invoicing`,
            `compliance_reporting`.
        grace_period_active:
          type: boolean
          description: '`true` when a grace period is active for limit/payment violations.'
        is_valid:
          type: boolean
          description: |-
            `true` when the account is in good standing (active or in grace
            period).
        merchants_max:
          type: integer
          format: int32
          description: |-
            Maximum allowed merchants for the current tier. `0` denotes a
            volume-priced or unlimited tier.
        merchants_used:
          type: integer
          format: int64
          description: |-
            Current number of active merchants (unique VAT IDs across all
            deployment modes).
        tier:
          type: string
          description: |-
            Pricing tier. One of: `free`, `starter`, `growth`, `scale`,
            `enterprise`.
          example: growth
    ErrorResponse:
      type: object
      description: Generic error envelope for the internal spec.
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
    TelemetryDataPoint:
      type: object
      description: Telemetry data point from an agent heartbeat.
      properties:
        adapter_versions:
          type: object
          description: 'Per-country adapter version map (e.g., `{ "spain": "1.2.0" }`).'
          additionalProperties:
            type: string
          propertyNames:
            type: string
        avg_response_time_ms:
          type: number
          format: double
          description: Average response time (ms) for the most recent window.
          example: 45.2
        error_count_since_last:
          type: integer
          format: int64
          description: Errors observed since the previous heartbeat.
          example: 2
        error_rate:
          type: number
          format: double
          description: Error rate percentage observed by the agent.
          example: 0.3
        storage_available_bytes:
          type: integer
          format: int64
          description: Local storage available to the agent (bytes).
          example: 4294967296
        storage_used_bytes:
          type: integer
          format: int64
          description: Local storage in use by the agent (bytes).
          example: 1073741824
        transaction_count_since_last:
          type: integer
          format: int64
          description: Transactions processed since the previous heartbeat.
          example: 47
        transaction_count_total:
          type: integer
          format: int64
          description: Lifetime transaction count for this agent.
          example: 12340
        uptime_seconds:
          type: integer
          format: int64
          description: Process uptime since last restart, in seconds.
          example: 86400
          minimum: 0
    IngestTelemetryRequest:
      type: object
      description: |-
        Batch telemetry ingestion request body for
        `POST /v1/agents/{agent_id}/telemetry`.
      required:
        - data_points
      properties:
        data_points:
          type: array
          items:
            $ref: '#/components/schemas/TelemetryDataPoint'
          description: Up to 100 telemetry data points captured by the agent.
    IngestTelemetryResponse:
      type: object
      description: Acknowledgement payload returned after a telemetry batch is queued.
      required:
        - accepted
        - timestamp
      properties:
        accepted:
          type: integer
          description: Number of data points accepted into the telemetry pipeline.
          minimum: 0
        timestamp:
          type: string
          format: date-time
          description: UTC timestamp the batch was accepted.
    FleetMetricsBlock:
      type: object
      description: Per-window aggregation block returned inside `FleetMetricsResponse.metrics`.
      required:
        - total_transactions
        - total_errors
        - avg_error_rate
        - total_storage_bytes
        - agent_count
        - reporting_agents
      properties:
        agent_count:
          type: integer
          format: int64
          description: Total agents associated with the account.
        avg_error_rate:
          type: number
          format: double
          description: Average error rate across reporting agents (percentage).
        reporting_agents:
          type: integer
          format: int64
          description: Agents that reported telemetry inside the window.
        total_errors:
          type: integer
          format: int64
          description: Total errors across all reporting agents.
        total_storage_bytes:
          type: integer
          format: int64
          description: Aggregate storage used across reporting agents (bytes).
        total_transactions:
          type: integer
          format: int64
          description: Total transactions across all reporting agents.
    FleetMetricsResponse:
      type: object
      description: Response body for `GET /v1/fleet/metrics`.
      required:
        - account_id
        - range
        - metrics
        - time_series
      properties:
        account_id:
          type: string
          format: uuid
          description: Account whose fleet was queried.
        metrics:
          $ref: '#/components/schemas/FleetMetricsBlock'
          description: Aggregated metrics for the requested window.
        range:
          type: string
          description: Echoed `range` parameter.
          example: 24h
        time_series:
          type: array
          items: {}
          description: |-
            Time-series buckets matching the requested window. Each entry is a
            free-form object whose shape is documented alongside the Grafana
            dashboards.
    ApiKeyResponse:
      type: object
      description: |-
        API key metadata returned by list / rotate operations. The plaintext key
        itself is never returned here — only `key_prefix`.
      required:
        - id
        - key_prefix
        - environment
        - key_type
        - created_at
        - revoked
      properties:
        created_at:
          type: string
          description: RFC 3339 creation timestamp.
        environment:
          type: string
          description: '`test` or `live`.'
        id:
          type: string
        key_prefix:
          type: string
          description: First few characters of the key, safe to display in dashboards.
          example: zyn_live_abc
        key_type:
          type: string
          description: '`root` or `restricted`.'
          example: restricted
        last_used_at:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        permissions:
          type: object
        revoked:
          type: boolean
    ErrorResponse-2:
      type: object
      description: |-
        Generic error envelope returned by validation/auth/not-found responses.

        Built by `ApiError::into_response` directly as JSON; the struct exists
        purely so utoipa can register the schema referenced by `responses(...)`.
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
    CreateApiKeyRequest:
      type: object
      description: Request body for creating a new API key.
      required:
        - environment
      properties:
        environment:
          type: string
          description: '`test` or `live`.'
          example: live
        name:
          type:
            - string
            - 'null'
          description: Optional human-readable name for the key.
        permissions:
          type: object
          description: |-
            Optional per-resource permissions map. If omitted, creates a root key
            (requires root key auth). If provided, creates a restricted key.
            Keys: resource names (`transactions`, `locations`, `webhooks`,
            `api_keys`, `account`, `tokens`); values: `none` / `read` / `write`.
    CreateApiKeyResponse:
      type: object
      description: |-
        Response returned the **single time** a plaintext API key is shown.
        Subsequent reads only return the prefix and metadata.
      required:
        - id
        - key
        - key_prefix
        - environment
        - key_type
        - created_at
      properties:
        created_at:
          type: string
        environment:
          type: string
        id:
          type: string
        key:
          type: string
          description: Plaintext key — store it now, it cannot be retrieved later.
          example: zyn_live_abc...xyz
        key_prefix:
          type: string
        key_type:
          type: string
          description: '`root` or `restricted`.'
        name:
          type:
            - string
            - 'null'
        permissions:
          type: object
    RotateApiKeyResponse:
      type: object
      description: |-
        Response from rotating an API key: the new plaintext key plus the id
        of the just-revoked old key.
      required:
        - new_key
        - revoked_key_id
      properties:
        new_key:
          $ref: '#/components/schemas/CreateApiKeyResponse'
        revoked_key_id:
          type: string
    BatchStatsResponse:
      type: object
      description: Response body for `GET /v1/admin/batch-stats`.
      required:
        - since
        - processors
      properties:
        processors:
          description: |-
            Per-processor statistics. Shape is processor-specific; consumers
            should treat it as a free-form map.
        since:
          type: string
          format: date-time
          description: RFC 3339 timestamp marking the start of the 24h window.
    ProcessResult:
      type: object
      description: |-
        Process result from a batch processor run.

        One entry per registered processor (`spain/lroe`, future
        `portugal/saft`, etc.) inside `BatchProcessResponse.results`.
      required:
        - processor
        - batches_created
        - batches_retried
        - accepted
        - rejected
        - failed
        - failure_rate
        - alert_triggered
        - errors_count
        - duration_ms
      properties:
        accepted:
          type: integer
          format: int32
          description: Transactions whose batch was accepted by the tax authority.
        alert_triggered:
          type: boolean
          description: '`true` when the run breached the alerting threshold (10%).'
        batches_created:
          type: integer
          format: int32
          description: Number of new batches created from pending transactions.
        batches_retried:
          type: integer
          format: int32
          description: Number of existing batches retried during this run.
        duration_ms:
          type: integer
          format: int64
          description: Wall-clock duration of the run, in milliseconds.
        errors_count:
          type: integer
          format: int32
          description: Number of error strings recorded for the run.
        failed:
          type: integer
          format: int32
          description: Transactions whose batch failed (network/build errors).
        failure_rate:
          type: number
          format: double
          description: Failure ratio (`failed / total`) for the run, in `[0, 1]`.
        processor:
          type: string
          description: Processor identifier (e.g., `spain/lroe`).
          example: spain/lroe
        rejected:
          type: integer
          format: int32
          description: Transactions whose batch was rejected by the tax authority.
    BatchProcessResponse:
      type: object
      description: Response body for `POST /v1/admin/process-pending`.
      required:
        - status
        - processors
        - results
        - duration_ms
      properties:
        duration_ms:
          type: integer
          format: int64
          description: Wall-clock duration of the entire run, in milliseconds.
        processors:
          type: integer
          description: Total processors invoked during the run.
          minimum: 0
        results:
          type: array
          items:
            $ref: '#/components/schemas/ProcessResult'
          description: Per-processor result summary.
        status:
          type: string
          description: Aggregate run status (`complete`, `partial`).
          example: complete
    Schedule:
      type: object
      description: Schedule for a batch processor.
      required:
        - system
        - next_run
        - frequency
        - pending_count
      properties:
        frequency:
          type: string
          description: Run frequency (`hourly`, `daily`, etc.).
          example: hourly
        next_run:
          type: string
          format: date-time
          description: RFC 3339 timestamp of the next scheduled run.
        pending_count:
          type: integer
          format: int32
          description: Number of transactions waiting to be batched on the next run.
        system:
          type: string
          description: Processor identifier (e.g., `spain/lroe`, `portugal/saft`).
          example: spain/lroe
    BatchScheduleResponse:
      type: object
      description: Response body for `GET /v1/batch/schedule`.
      required:
        - schedules
      properties:
        schedules:
          type: array
          items:
            $ref: '#/components/schemas/Schedule'
          description: Per-processor schedule entries.
    ConsentResponse:
      type: object
      description: Consent record as returned by the consents endpoints.
      required:
        - id
        - consent_type
        - version
        - granted_at
        - active
      properties:
        active:
          type: boolean
          description: '`true` while the consent is still in force (no `revoked_at`).'
        consent_type:
          type: string
          description: |-
            Consent type. One of `terms_of_service`, `privacy_policy`,
            `marketing_emails`.
        granted_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp the consent was granted.
        id:
          type: string
          format: uuid
          description: Stable identifier for the consent record (UUID).
        ip_address:
          type:
            - string
            - 'null'
          description: First-hop IP from the granting request, captured for audit.
        revoked_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |-
            RFC 3339 timestamp the consent was revoked, if any. Absent while
            the consent is still active.
        user_agent:
          type:
            - string
            - 'null'
          description: User-Agent header from the granting request, captured for audit.
        version:
          type: string
          description: Version identifier the user agreed to.
    GrantConsentRequest:
      type: object
      description: |-
        Request body for `POST /v1/account/consents`.

        Granting a new consent of the same `consent_type` automatically revokes
        any prior active grant, so callers do not need to issue a DELETE before
        upgrading to a new version.
      required:
        - consent_type
        - version
      properties:
        consent_type:
          type: string
          description: One of `terms_of_service`, `privacy_policy`, `marketing_emails`.
          example: terms_of_service
        version:
          type: string
          description: |-
            Version identifier the user agreed to (e.g., `2026-03-01`).
            Maximum 50 characters; control characters are rejected.
          example: '2026-03-01'
          maxLength: 50
    DeleteAccountResponse:
      type: object
      description: Response body for `DELETE /v1/account` (Art. 17 deletion request).
      required:
        - account_id
        - status
        - deletion_requested_at
        - grace_period_hours
        - keys_revoked
        - stripe_subscription_cancelled
      properties:
        account_id:
          type: string
        deletion_requested_at:
          type: string
          description: RFC 3339 timestamp the deletion was first requested (idempotent).
        grace_period_hours:
          type: integer
          format: int32
          description: Grace window before irreversible anonymization runs.
          minimum: 0
        keys_revoked:
          type: integer
          format: int64
          description: Number of API keys revoked synchronously.
          minimum: 0
        status:
          type: string
          description: Always `pending_deletion` for a successful request.
          example: pending_deletion
        stripe_subscription_cancelled:
          type: boolean
    ApiKeyExport:
      type: object
      description: API key metadata — hashes deliberately excluded.
      required:
        - id
        - key_prefix
        - environment
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
        environment:
          type: string
        id:
          type: string
          format: uuid
        key_prefix:
          type: string
        last_used_at:
          type:
            - string
            - 'null'
          format: date-time
        name:
          type:
            - string
            - 'null'
        permissions:
          type: object
        revoked_at:
          type:
            - string
            - 'null'
          format: date-time
    ExportMeta:
      type: object
      description: Metadata stamp on every Art. 15 export.
      required:
        - generated_at
        - schema_version
        - legal_basis
        - truncated
      properties:
        generated_at:
          type: string
          format: date-time
        legal_basis:
          type: string
        schema_version:
          type: string
        truncated:
          type: boolean
          description: Whether any collection hit its cap; if true, the export is partial.
    AccountExport:
      type: object
      description: |-
        Full Art. 15 (Right of Access) JSON archive. Embedded record types are
        kept opaque (`Object`) here — see the per-resource schemas in transactions
        / locations / webhooks / subscriptions for the full shapes.
      required:
        - meta
        - account
        - api_keys
        - locations
        - transactions
        - webhooks
        - audit_events
        - agent_telemetry
      properties:
        account:
          type: object
          description: Full account detail row. See the accounts resource for the schema.
        agent_telemetry:
          type: array
          items:
            type: object
          description: Raw on-premise agent telemetry rows.
        api_keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyExport'
        audit_events:
          type: array
          items:
            type: object
          description: Status-change audit events scoped to this account.
        locations:
          type: array
          items:
            type: object
          description: Locations registered on the account. See the locations resource.
        meta:
          $ref: '#/components/schemas/ExportMeta'
        subscription:
          type: object
          description: Active subscription, if any. See the subscriptions resource.
        transactions:
          type: array
          items:
            type: object
          description: Transactions submitted by the account. See the transactions resource.
        webhooks:
          type: array
          items:
            type: object
          description: Configured webhook endpoints. See the webhooks resource.
    ProcessDeletionsResponse:
      type: object
      description: Response body for `POST /v1/admin/process-deletions`.
      required:
        - processed
        - anonymized_account_ids
      properties:
        anonymized_account_ids:
          type: array
          items:
            type: string
          description: IDs of the accounts anonymized in this run.
        processed:
          type: integer
          description: Number of accounts anonymized in this run.
          minimum: 0
    AccountLookupResponse:
      type: object
      description: |-
        Response body for `GET /internal/account-lookup`.

        Returns the account UUID and a freshly minted **test-environment**
        API key the dashboard can use to call the rest of the API on the
        user's behalf. The key is registered in `api_keys` with a
        `dashboard-session` label so it can be rotated/revoked separately
        from regular keys.
      required:
        - account_id
        - api_key
      properties:
        account_id:
          type: string
          format: uuid
          description: Account identifier (UUID).
        api_key:
          type: string
          description: Plaintext API key — **returned once**, cannot be retrieved later.
    OnboardingStepResponse:
      type: object
      description: One row of the onboarding checklist.
      required:
        - key
        - completed
      properties:
        completed:
          type: boolean
          description: '`true` if the user has completed this step.'
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |-
            RFC 3339 timestamp the step was first marked complete. Absent when
            `completed = false`.
          example: '2026-03-01T10:00:00Z'
        key:
          type: string
          description: |-
            Stable identifier for the step. Step keys vary by deployment mode —
            see `OnboardingStatusResponse` for the per-mode lists.
          example: create_account
    OnboardingStatusResponse:
      type: object
      description: |-
        Response body for `GET /v1/onboarding/status`.

        `steps` is an ordered list — render it top-to-bottom in the dashboard.
        The set of step keys depends on `deployment_mode`:

        * `hosted` → `create_account`, `get_test_api_key`, `read_quickstart`,
          `create_location`, `first_test_transaction`, `explore_dashboard`,
          `create_live_api_key`.
        * `on_premise` → `purchase_license`, `download_agent`, `verify_agent`,
          `create_location`, `first_test_transaction`, `configure_country`,
          `first_live_transaction`.
      required:
        - deployment_mode
        - steps
      properties:
        deployment_mode:
          type: string
          description: |-
            Deployment mode for the authenticated account. One of: `hosted`,
            `on_premise`.
          example: hosted
        steps:
          type: array
          items:
            $ref: '#/components/schemas/OnboardingStepResponse'
          description: Ordered checklist for the deployment mode.
    ErrorResponse-3:
      type: object
      description: |-
        Generic error envelope for the internal spec.

        The handler returns errors as `ApiError`, which serialises directly to
        JSON; this struct exists purely so utoipa can register the schema
        referenced by `responses(...)`.
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
    RetryQueueEntry:
      type: object
      description: A retry queue entry.
      required:
        - id
        - transaction_id
        - account_id
        - status
        - attempt
        - max_attempts
        - next_retry_at
        - created_at
        - updated_at
      properties:
        account_id:
          type: string
          format: uuid
        attempt:
          type: integer
          format: int32
          description: Number of retry attempts made so far.
          example: 0
        created_at:
          type: string
          format: date-time
        id:
          type: string
          format: uuid
        last_error:
          type:
            - string
            - 'null'
        max_attempts:
          type: integer
          format: int32
          description: Maximum allowed retry attempts before transitioning to `dead`.
          example: 50
        next_retry_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of the next scheduled retry.
        status:
          type: string
          description: 'One of: `pending`, `processing`, `completed`, `failed`, `dead`.'
          example: pending
        transaction_id:
          type: string
          format: uuid
        updated_at:
          type: string
          format: date-time
        webhook_status:
          type:
            - string
            - 'null'
        webhook_url:
          type:
            - string
            - 'null'
          format: uri
          description: Optional HTTPS callback URL for delivery notifications.
    RetryQueueListResponse:
      type: object
      description: |-
        Paginated retry-queue list. Mirrors the generic `PaginatedResponse<T>`
        wire shape but uses a concrete element type so utoipa can register a
        schema.
      required:
        - data
        - total
        - limit
        - offset
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/RetryQueueEntry'
        limit:
          type: integer
          format: int64
        offset:
          type: integer
          format: int64
        total:
          type: integer
          format: int64
    EnqueueRequest:
      type: object
      description: Request body for enqueueing a transaction for retry processing.
      required:
        - transaction_id
      properties:
        max_attempts:
          type:
            - integer
            - 'null'
          format: int32
          description: Maximum retry attempts (1-50, default 50).
          example: 50
          maximum: 50
          minimum: 1
        transaction_id:
          type: string
          format: uuid
          description: Transaction id to enqueue.
        webhook_url:
          type:
            - string
            - 'null'
          format: uri
          description: Optional HTTPS callback URL for delivery notifications.
          example: https://example.com/zyntem-hook
    StripeWebhookEventData:
      type: object
      description: '`data` block inside a Stripe webhook event.'
      required:
        - object
      properties:
        object:
          description: Free-form Stripe object (Invoice, Subscription, etc.).
    StripeWebhookEvent:
      type: object
      description: |-
        Stripe webhook envelope. Mirrors the `Event` object documented at
        <https://stripe.com/docs/api/events/object> — only the fields we
        dispatch on are described here; the `data.object` payload is opaque
        and event-specific.
      required:
        - id
        - type
        - data
      properties:
        data:
          $ref: '#/components/schemas/StripeWebhookEventData'
          description: '`data.object` payload — opaque, depends on `type`.'
        id:
          type: string
          description: Stripe event identifier.
          example: evt_1NXyZ2KZ4bN9bPmJX
        type:
          type: string
          description: |-
            Event type, e.g. `invoice.payment_succeeded`,
            `invoice.payment_failed`, `customer.subscription.deleted`,
            `customer.subscription.updated`.
          example: invoice.payment_succeeded
    StripeWebhookAck:
      type: object
      description: |-
        Acknowledgement payload returned to Stripe after a successful or
        duplicate webhook delivery.
      required:
        - status
      properties:
        event_id:
          type:
            - string
            - 'null'
          description: Echo of the event id when newly processed (omitted on duplicates).
        status:
          type: string
          description: '`processed`, `duplicate`, or other status string.'
          example: processed
    SubscriptionResponse:
      type: object
      description: Subscription state returned by create / update / get.
      required:
        - id
        - account_id
        - stripe_customer_id
        - subscription_id
        - status
        - location_count
        - merchant_count
        - billing_interval
        - current_tier
        - monthly_amount_cents
        - created_at
      properties:
        account_id:
          type: string
          description: Owning account id.
        billing_interval:
          type: string
          description: '`month` or `year`.'
        created_at:
          type: string
          description: Subscription creation timestamp (RFC 3339).
        current_tier:
          type: string
          description: 'Current tier name: `starter`, `growth`, `scale`, `enterprise`.'
        id:
          type: string
          description: Stripe subscription id (mirrors `subscription_id`).
        location_count:
          type: integer
          format: int32
        merchant_count:
          type: integer
          format: int32
          description: Backward-compatible alias for `location_count`.
        monthly_amount_cents:
          type: integer
          format: int64
          description: Monthly cost in euro cents (graduated tiers, before country floors).
        status:
          type: string
          description: Stripe subscription status (`active`, `past_due`, `incomplete`, …).
        stripe_customer_id:
          type: string
        subscription_id:
          type: string
    CreateSubscriptionRequest:
      type: object
      description: Request body for creating a Stripe-backed subscription.
      required:
        - company_name
        - billing_email
        - payment_method_id
      properties:
        billing_email:
          type: string
          description: Billing contact email; receives invoices and dunning notices.
          example: billing@acme.example
        billing_interval:
          type: string
          description: '`month` (default) or `year`.'
          example: month
        company_name:
          type: string
          description: Company / merchant display name shown on Stripe receipts.
          example: Acme Coffee SL
        location_count:
          type:
            - integer
            - 'null'
          format: int32
          description: Preferred field name. Falls back to `merchant_count` for backward compat.
          example: 5
          minimum: 1
        merchant_count:
          type:
            - integer
            - 'null'
          format: int32
          description: 'Deprecated: use `location_count`.'
          deprecated: true
        payment_method_id:
          type: string
          description: Stripe `payment_method` id collected client-side via Stripe Elements.
          example: pm_1Nabcd...
    UpdateSubscriptionRequest:
      type: object
      description: Request body for updating an existing subscription's location count.
      properties:
        location_count:
          type:
            - integer
            - 'null'
          format: int32
          description: Preferred field name. Falls back to `merchant_count` for backward compat.
          example: 12
          minimum: 1
        merchant_count:
          type:
            - integer
            - 'null'
          format: int32
          description: 'Deprecated: use `location_count`.'
          deprecated: true
    TriggerAggregationRequest:
      type: object
      properties:
        date:
          type:
            - string
            - 'null'
          description: Date to aggregate (YYYY-MM-DD). Defaults to yesterday (UTC).
          example: '2026-01-31'
    UsageJobResult:
      type: object
      description: Result of the daily usage aggregation job.
      required:
        - date
        - accounts_processed
        - total_transactions
        - stripe_reported
        - stripe_failed
        - tier_upgrades
        - duration_ms
      properties:
        accounts_processed:
          type: integer
          format: int32
          description: Number of accounts whose usage was rolled up.
          minimum: 0
        date:
          type: string
          description: Date the aggregation covered (YYYY-MM-DD, UTC).
          example: '2026-01-31'
        duration_ms:
          type: integer
          format: int64
          description: Wall-clock duration of the job, in milliseconds.
          minimum: 0
        stripe_failed:
          type: integer
          format: int32
          description: Aggregations that failed to report to Stripe.
          minimum: 0
        stripe_reported:
          type: integer
          format: int32
          description: Aggregations that were successfully reported to Stripe.
          minimum: 0
        tier_upgrades:
          type: integer
          format: int32
          description: Accounts that crossed a tier threshold during the run.
          minimum: 0
        total_transactions:
          type: integer
          format: int64
          description: Total transactions counted across all accounts in the window.
    UsageRecord:
      type: object
      description: One day of metered usage for an account.
      required:
        - id
        - account_id
        - date
        - transaction_count
        - deployment_mode
        - agent_count
        - reported_to_stripe
        - created_at
      properties:
        account_id:
          type: string
        agent_count:
          type: integer
          format: int32
          description: Distinct agents that reported transactions during the window.
        created_at:
          type: string
          description: Time the aggregation row was written (RFC 3339).
        date:
          type: string
          description: Calendar day the metering window covers (YYYY-MM-DD, UTC).
          example: '2026-01-31'
        deployment_mode:
          type: string
          description: |-
            Deployment mode the account is provisioned under (`embedded-local`,
            `central-cloud`, or `hybrid`). Drives which billing dimensions apply.
        id:
          type: string
        reported_to_stripe:
          type: boolean
          description: Whether this row has already been pushed to Stripe metered billing.
        transaction_count:
          type: integer
          format: int64
          description: Number of fiscalized transactions counted in the window.
    UsageListResponse:
      type: object
      description: Paginated list of usage records.
      required:
        - data
        - total
        - limit
        - offset
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/UsageRecord'
        limit:
          type: integer
          format: int64
          description: Maximum items returned in this response.
        offset:
          type: integer
          format: int64
          description: Zero-based offset of the first item in this response.
        total:
          type: integer
          format: int64
          description: Total number of items matching the query.
    TierStatusResponse:
      type: object
      description: Current rate-limit tier and recommendation based on rolling usage.
      required:
        - account_id
        - current_tier
        - rolling_avg_daily_transactions
        - recommended_tier
        - would_upgrade
      properties:
        account_id:
          type: string
        current_tier:
          type: string
          description: |-
            Tier currently enforced for the account (`sandbox`, `starter`, `growth`,
            `enterprise`).
          example: starter
        recommended_tier:
          type: string
          description: |-
            Tier the rolling average would justify. May be the same as
            `current_tier`.
          example: growth
        rolling_avg_daily_transactions:
          type: number
          format: double
          description: |-
            Rolling average daily transaction count used to compute
            `recommended_tier`.
        would_upgrade:
          type: boolean
          description: '`true` if `recommended_tier` differs from `current_tier`.'
    SdiNotificationAck:
      type: object
      description: Acknowledgement payload returned to SDI after a successful notification.
      required:
        - status
        - transaction_id
        - notification_type
        - new_status
      properties:
        new_status:
          type: string
          description: |-
            Internal status the matching transaction was promoted to. One of
            `delivered`, `rejected`, `delivery_error`.
        notification_type:
          type: string
          description: |-
            SDI notification root element. One of `RicevutaConsegna`,
            `NotificaScarto`, `NotificaMancataConsegna`.
        status:
          type: string
          description: Always `accepted` on the success path.
          example: accepted
        transaction_id:
          type: string
          format: uuid
          description: Internal Zyntem transaction id matched against `IdentificativoSdI`.
