# GENERATED — do not edit. Public subset of internal.yaml served at
# GET /openapi.yaml. Edit internal.yaml, then `go generate ./api/openapi`.
components:
    headers:
        RetryAfter:
            description: Seconds.
            schema:
                type: integer
        XRateLimitLimit:
            schema:
                type: integer
        XRateLimitRemaining:
            schema:
                type: integer
        XRateLimitReset:
            description: Unix seconds.
            schema:
                type: integer
        XRequestId:
            description: Per-request UUIDv7; log it for support.
            schema:
                format: uuid
                type: string
    parameters:
        Id:
            description: UUIDv7.
            in: path
            name: id
            required: true
            schema:
                format: uuid
                type: string
    responses:
        Internal:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: Unhandled failure; safe detail only.
        InvalidRequest:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: 'Malformed JSON, invalid query parameters, unparseable PEM, or unsupported policy. An undocumented member is NOT a cause: the decoder ignores what it does not recognize.'
        KeyForbidden:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: |
                Authenticated but not allowed — problem type `ip-not-allowed` (this
                API key restricts callers by source IP and the request's address is
                outside its allowlist) or `insufficient-scope` (the key's scopes do
                not cover this operation).
        NotFound:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: Unknown id, or the resource belongs to another tenant (existence is not revealed cross-tenant).
        RateLimitedOrQuota:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: |
                Rate limit exceeded (problem type `rate-limited`, Retry-After set),
                monthly quota exhausted on a blocking plan (`quota-exceeded`), or
                the monthly overage spend cap reached on a paid plan
                (`overage-cap-reached`). The `quota-exceeded` and
                `overage-cap-reached` problems carry the `resetAt` extension
                member: the RFC 3339 instant the monthly window rolls over (no
                Retry-After header — the wait can be weeks).
            headers:
                Retry-After:
                    $ref: '#/components/headers/RetryAfter'
        RequestTooLarge:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: Request body exceeds the 256 KiB cap (problem type `request-too-large`).
        Unauthorized:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: 'Missing, unknown or revoked credential — deliberately indistinguishable. Exception: a structurally valid key past its expiry answers problem type `key-expired`, since the caller has proven possession.'
        UpstreamUnavailable:
            content:
                application/problem+json:
                    schema:
                        $ref: '#/components/schemas/Problem'
            description: |
                Service temporarily unable to serve the request; Retry-After set.
                Problem type `upstream-unavailable` (a downstream dependency is
                down) or `overloaded` (platform backstop / admission control is
                shedding load — retry with backoff; per-tenant limits were NOT
                exceeded).
            headers:
                Retry-After:
                    $ref: '#/components/headers/RetryAfter'
    schemas:
        CertificateInfo:
            additionalProperties: false
            properties:
                issuer:
                    type: string
                notAfter:
                    format: date-time
                    type: string
                notBefore:
                    format: date-time
                    type: string
                serialNumber:
                    description: Colon-separated hex.
                    type: string
                sha256:
                    description: Lowercase hex.
                    type: string
                subject:
                    type: string
            required:
                - subject
                - issuer
                - serialNumber
                - notBefore
                - notAfter
                - sha256
            type: object
        Pem:
            description: PEM-encoded X.509 certificate.
            examples:
                - |-
                  -----BEGIN CERTIFICATE-----
                  …
            type: string
        Plan:
            description: '`PLAN_FREE_PUBLIC` is the personal-email free tier (50 validations/month, 30-day retention), assigned at signup with a public mail domain; it never appears in the public catalog. `PLAN_INTERNAL` is reserved (company use); it never appears in the public catalog and is not self-serve purchasable.'
            enum:
                - PLAN_FREE
                - PLAN_FREE_PUBLIC
                - PLAN_STARTER
                - PLAN_GROWTH
                - PLAN_SCALE
                - PLAN_INTERNAL
            type: string
        Policy:
            enum:
                - POLICY_EIDAS_QWAC_V1
                - POLICY_EIDAS_QSEALC_V1
            type: string
        Problem:
            additionalProperties: false
            description: |
                RFC 9457 problem document. `type` is `urn:quovalis:problem:<code>`.
                The catalog is extensible: new codes may appear in
                backwards-compatible releases, so treat unknown codes by their HTTP
                status. Codes the API emits (status — when):

                - `invalid-request` (400) — malformed JSON, invalid query
                  parameters, unparseable PEM, or unsupported policy. Never an
                  undocumented request member: those are ignored, not rejected.
                - `unauthorized` (401) — missing, unknown or revoked credential
                  (deliberately indistinguishable).
                - `key-expired` (401) — structurally valid API key past its expiry.
                - `ip-not-allowed` (403) — source IP outside the key's allowlist.
                - `insufficient-scope` (403) — key scopes do not cover the operation.
                - `not-found` (404) — unknown id, or the resource belongs to another
                  tenant.
                - `request-too-large` (413) — body exceeds the request size cap.
                - `rate-limited` (429) — per-tenant rate limit exceeded;
                  `Retry-After` header set.
                - `quota-exceeded` (429) — monthly quota exhausted on a plan that
                  blocks overage; `resetAt` member set.
                - `overage-cap-reached` (429) — monthly overage spend cap reached;
                  `resetAt` member set.
                - `internal` (500) — unhandled failure; safe detail only.
                - `upstream-unavailable` (503) — a downstream dependency is down;
                  `Retry-After` set.
                - `overloaded` (503) — platform backstop shedding load (not a
                  tenant limit); `Retry-After` set.

                Surface-specific codes (e.g. of the web console's own API) are
                documented on the operations that emit them.
            properties:
                detail:
                    type: string
                instance:
                    type: string
                requestId:
                    format: uuid
                    type: string
                resetAt:
                    description: 'Extension member (RFC 9457 §3.2), present on `quota-exceeded` and `overage-cap-reached`: the RFC 3339 instant the monthly window rolls over and calls are accepted again.'
                    format: date-time
                    type: string
                status:
                    type: integer
                title:
                    type: string
                type:
                    description: '`urn:quovalis:problem:<code>`; catalog above.'
                    examples:
                        - urn:quovalis:problem:quota-exceeded
                    type: string
            required:
                - type
                - title
                - status
            type: object
        ProviderList:
            additionalProperties: false
            properties:
                nextCursor:
                    description: Absent when there are no further pages.
                    type: string
                providers:
                    items:
                        additionalProperties: false
                        properties:
                            name:
                                type: string
                            services:
                                items:
                                    additionalProperties: false
                                    properties:
                                        certSha256:
                                            description: Lowercase hex.
                                            type: string
                                        name:
                                            type: string
                                        status:
                                            examples:
                                                - granted
                                            type: string
                                        statusStart:
                                            description: Absent when the trust list declares no status start.
                                            format: date-time
                                            type: string
                                        type:
                                            description: TL service type URI.
                                            type: string
                                    required:
                                        - name
                                        - type
                                        - status
                                        - certSha256
                                    type: object
                                type: array
                            territory:
                                type: string
                        required:
                            - name
                            - territory
                            - services
                        type: object
                    type: array
            required:
                - providers
            type: object
        Psd2Info:
            additionalProperties: false
            properties:
                authorizationNumber:
                    examples:
                        - PSDNL-DNB-33002587
                    type: string
                ncaId:
                    type: string
                ncaName:
                    type: string
                organizationIdentifier:
                    type: string
                roles:
                    items:
                        enum:
                            - PSD2_ROLE_PSP_AS
                            - PSD2_ROLE_PSP_PI
                            - PSD2_ROLE_PSP_AI
                            - PSD2_ROLE_PSP_IC
                        type: string
                    type: array
            required:
                - authorizationNumber
                - roles
                - ncaName
                - ncaId
                - organizationIdentifier
            type: object
        QualifiedInfo:
            additionalProperties: false
            properties:
                isQualified:
                    description: Whether the certificate maps to a granted CA/QC service and is qualified after evaluating the certificate's QcStatements and the service's trusted-list Qualifications (Sie) overrides.
                    type: boolean
                qcStatements:
                    items:
                        type: string
                    type: array
                qcType:
                    description: Absent when the certificate declares no known QC type.
                    enum:
                        - QC_TYPE_WEB
                        - QC_TYPE_ESEAL
                        - QC_TYPE_ESIGN
                    type: string
                trustServiceProvider:
                    additionalProperties: false
                    description: Absent when no trust-list anchor was mapped.
                    properties:
                        name:
                            type: string
                        serviceName:
                            type: string
                        serviceStatus:
                            examples:
                                - granted
                            type: string
                        territory:
                            type: string
                    required:
                        - name
                        - territory
                        - serviceName
                        - serviceStatus
                    type: object
            required:
                - isQualified
                - qcStatements
            type: object
        RevocationInfo:
            additionalProperties: false
            properties:
                cached:
                    type: boolean
                checkedAt:
                    format: date-time
                    type: string
                method:
                    description: Absent when the revocation status is unknown — the check ran but no OCSP/CRL evidence could be obtained (no endpoints declared, or none reachable within budget).
                    enum:
                        - REVOCATION_METHOD_OCSP
                        - REVOCATION_METHOD_CRL
                    type: string
                producedAt:
                    description: Absent when no evidence was obtained.
                    format: date-time
                    type: string
                reason:
                    description: Absent when the revocation source declared no reason.
                    type: string
                revokedAt:
                    description: Absent when the certificate is not revoked.
                    format: date-time
                    type: string
                source:
                    description: Absent when no evidence was obtained.
                    format: uri
                    type: string
                status:
                    enum:
                        - REVOCATION_STATUS_GOOD
                        - REVOCATION_STATUS_REVOKED
                        - REVOCATION_STATUS_UNKNOWN
                    type: string
            required:
                - status
                - checkedAt
                - cached
            type: object
        SubIndication:
            description: Absent — never null — when there is no sub-indication (a VALID verdict).
            enum:
                - SUB_INDICATION_EXPIRED
                - SUB_INDICATION_NOT_YET_VALID
                - SUB_INDICATION_REVOKED
                - SUB_INDICATION_CHAIN_FAILURE
                - SUB_INDICATION_NOT_QUALIFIED
                - SUB_INDICATION_WRONG_QC_TYPE
                - SUB_INDICATION_REVOCATION_UNAVAILABLE
                - SUB_INDICATION_TRUST_LIST_STALE
                - SUB_INDICATION_PARSE_ERROR
                - SUB_INDICATION_QUALIFICATION_UNDETERMINED
            type: string
        TrustListRef:
            additionalProperties: false
            description: The exact trust state used — reproducibility anchor.
            properties:
                sequenceNumber:
                    description: 64-bit integers are JSON strings on this API.
                    format: int64
                    type: string
                snapshotId:
                    format: uuid
                    type: string
                territory:
                    type: string
            required:
                - snapshotId
                - territory
                - sequenceNumber
            type: object
        TrustListStatus:
            additionalProperties: false
            properties:
                lotlSequenceNumber:
                    description: 64-bit integers are JSON strings on this API.
                    format: int64
                    type: string
                territories:
                    items:
                        additionalProperties: false
                        properties:
                            expectedSince:
                                description: When the LOTL first listed this territory; set only for neverIngested entries, so an operator can see how long the first ingest has been failing.
                                format: date-time
                                type: string
                            fetchedAt:
                                description: Absent when the territory has not been successfully fetched yet.
                                format: date-time
                                type: string
                            neverIngested:
                                description: True when the LOTL points to this territory (it is expected) but no verified snapshot has ever been stored — its first ingest has never succeeded. Such a synthetic entry omits snapshotId and fetchedAt, and carries sequenceNumber "0".
                                type: boolean
                            nextUpdate:
                                description: Absent when the trust list declares no NextUpdate.
                                format: date-time
                                type: string
                            sequenceNumber:
                                description: 64-bit integers are JSON strings on this API.
                                format: int64
                                type: string
                            signatureOk:
                                type: boolean
                            snapshotId:
                                format: uuid
                                type: string
                            stale:
                                type: boolean
                            territory:
                                type: string
                        required:
                            - territory
                            - sequenceNumber
                            - signatureOk
                            - stale
                            - neverIngested
                        type: object
                    type: array
            required:
                - lotlSequenceNumber
                - territories
            type: object
        Usage:
            properties:
                consumed:
                    format: int64
                    type: string
                overage:
                    format: int64
                    type: string
                period:
                    examples:
                        - 2026-07
                    type: string
                plan:
                    $ref: '#/components/schemas/Plan'
                quota:
                    description: 64-bit integers are JSON strings on this API. "0" means no fixed quota.
                    format: int64
                    type: string
                rateLimit:
                    additionalProperties: false
                    properties:
                        burst:
                            type: integer
                        requestsPerSecond:
                            type: integer
                    required:
                        - requestsPerSecond
                        - burst
                    type: object
                remaining:
                    format: int64
                    type: string
            required:
                - plan
                - period
                - quota
                - consumed
                - remaining
                - overage
                - rateLimit
            type: object
        ValidationRequest:
            additionalProperties: false
            description: Max request size 256 KiB.
            properties:
                certificate:
                    $ref: '#/components/schemas/Pem'
                    description: Leaf certificate.
                chain:
                    description: Untrusted intermediates; TL-derived and AIA-known intermediates are also used.
                    items:
                        $ref: '#/components/schemas/Pem'
                    maxItems: 10
                    type: array
                options:
                    additionalProperties: false
                    properties:
                        checkRevocation:
                            default: true
                            type: boolean
                    type: object
                policy:
                    $ref: '#/components/schemas/Policy'
                    description: Required. An omitted or unrecognized value is rejected with `400 invalid-request`; the decoder itself accepts any string, so this is a server-side check rather than a schema one.
            required:
                - certificate
                - policy
            type: object
        ValidationResult:
            additionalProperties: false
            description: |
                The canonical result document. The validator's internal
                ValidationResult message is the single source of truth: it is
                persisted as internal ProtoJSON for storage and debugging, and
                rendered to this external wire form by exactly one mapper on every
                read — so served results are always derived from the stored message
                and cannot drift semantically, but stored and served bytes are no
                longer identical, by design. On non-VALID verdicts every determinable
                field is still populated. Fields that do not apply are absent keys,
                never null.
            properties:
                certificate:
                    $ref: '#/components/schemas/CertificateInfo'
                id:
                    format: uuid
                    type: string
                policy:
                    $ref: '#/components/schemas/Policy'
                psd2:
                    $ref: '#/components/schemas/Psd2Info'
                    description: Absent when no PSD2 QCStatement is present.
                qualified:
                    $ref: '#/components/schemas/QualifiedInfo'
                revocation:
                    $ref: '#/components/schemas/RevocationInfo'
                    description: Absent when checkRevocation=false, and when no chain could be built (there is no issuer to verify OCSP/CRL evidence against).
                subIndication:
                    $ref: '#/components/schemas/SubIndication'
                    description: Absent on a VALID verdict.
                trustList:
                    $ref: '#/components/schemas/TrustListRef'
                    description: Absent when no trust anchor was found — the chain did not build to any EU trust-list anchor (e.g. sub-indication SUB_INDICATION_CHAIN_FAILURE).
                validatedAt:
                    format: date-time
                    type: string
                verdict:
                    $ref: '#/components/schemas/Verdict'
            required:
                - id
                - verdict
                - policy
                - validatedAt
                - certificate
                - qualified
            type: object
        Verdict:
            enum:
                - VERDICT_VALID
                - VERDICT_INVALID
                - VERDICT_INDETERMINATE
            type: string
    securitySchemes:
        apiKey:
            description: |
                Tenant API key `qv_<prefix>_<secret>`; only its SHA-256 is stored
                server-side.
            scheme: bearer
            type: http
info:
    contact:
        name: API support
    description: |
        Multi-tenant eIDAS/QWAC trust validation by Quovalis. Validation outcomes are never
        HTTP errors: an INVALID or INDETERMINATE certificate is a 200 with the
        verdict in the body.

        Compatibility: within a major version the API evolves additively — new
        endpoints, new optional fields, new problem `type` codes and new values
        in string enums may appear in backwards-compatible releases. Clients
        MUST tolerate unknown fields, unknown enum values and unknown problem
        types (fall back to the HTTP status code).

        Response schemas are closed (`additionalProperties: false`, or
        `unevaluatedProperties: false` where composed), which documents this
        version exactly — it is not a promise that no member will ever be added.
        A strict validator generated from a pinned copy of this document will
        therefore reject a later release's additions, which is the same event the
        paragraph above tells clients to tolerate. Pin the document and the API
        version together, or relax the closure in the generated validator.
    title: Quovalis API
    version: 1.0.0
openapi: 3.1.0
paths:
    /v1/trustlist/providers:
        get:
            operationId: listTrustListProviders
            parameters:
                - description: ISO 3166 territory filter; omitted means all territories.
                  in: query
                  name: territory
                  required: false
                  schema:
                    examples:
                        - NL
                    type: string
                - in: query
                  name: cursor
                  required: false
                  schema:
                    type: string
                - in: query
                  name: limit
                  required: false
                  schema:
                    default: 50
                    maximum: 200
                    minimum: 1
                    type: integer
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/ProviderList'
                    description: Providers from the current snapshot.
                    headers:
                        X-Request-Id:
                            $ref: '#/components/headers/XRequestId'
                "400":
                    $ref: '#/components/responses/InvalidRequest'
                "401":
                    $ref: '#/components/responses/Unauthorized'
                "403":
                    $ref: '#/components/responses/KeyForbidden'
                "429":
                    $ref: '#/components/responses/RateLimitedOrQuota'
                "500":
                    $ref: '#/components/responses/Internal'
                "503":
                    $ref: '#/components/responses/UpstreamUnavailable'
            summary: List trust service providers and their CA/QC services
            tags:
                - trustlist
    /v1/trustlist/status:
        get:
            operationId: getTrustListStatus
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/TrustListStatus'
                    description: Ingestion status.
                    headers:
                        X-Request-Id:
                            $ref: '#/components/headers/XRequestId'
                "401":
                    $ref: '#/components/responses/Unauthorized'
                "403":
                    $ref: '#/components/responses/KeyForbidden'
                "429":
                    $ref: '#/components/responses/RateLimitedOrQuota'
                "500":
                    $ref: '#/components/responses/Internal'
                "503":
                    $ref: '#/components/responses/UpstreamUnavailable'
            summary: Trust-list ingestion health per territory
            tags:
                - trustlist
    /v1/usage:
        get:
            operationId: getUsage
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                allOf:
                                    - $ref: '#/components/schemas/Usage'
                                unevaluatedProperties: false
                    description: Usage snapshot.
                    headers:
                        X-Request-Id:
                            $ref: '#/components/headers/XRequestId'
                "401":
                    $ref: '#/components/responses/Unauthorized'
                "403":
                    $ref: '#/components/responses/KeyForbidden'
                "429":
                    $ref: '#/components/responses/RateLimitedOrQuota'
                "500":
                    $ref: '#/components/responses/Internal'
                "503":
                    $ref: '#/components/responses/UpstreamUnavailable'
            summary: Current-period usage and plan limits for the calling tenant
            tags:
                - usage
    /v1/validations:
        post:
            description: |
                Billable operation `validation`. Intentionally not idempotent — every
                call produces a new validation. Free-tier tenants over their monthly
                quota receive `429 quota-exceeded`; paid tiers proceed with metered
                overage (visible in GET /v1/usage) up to your configurable monthly
                overage spend cap — once the cap is reached, further calls receive
                `429 overage-cap-reached` until the cap is raised or the period
                rolls over.
            operationId: createValidation
            requestBody:
                content:
                    application/json:
                        schema:
                            $ref: '#/components/schemas/ValidationRequest'
                required: true
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/ValidationResult'
                    description: Validation computed (verdict may be VALID, INVALID or INDETERMINATE).
                    headers:
                        X-RateLimit-Limit:
                            $ref: '#/components/headers/XRateLimitLimit'
                        X-RateLimit-Remaining:
                            $ref: '#/components/headers/XRateLimitRemaining'
                        X-RateLimit-Reset:
                            $ref: '#/components/headers/XRateLimitReset'
                        X-Request-Id:
                            $ref: '#/components/headers/XRequestId'
                "400":
                    $ref: '#/components/responses/InvalidRequest'
                "401":
                    $ref: '#/components/responses/Unauthorized'
                "403":
                    $ref: '#/components/responses/KeyForbidden'
                "413":
                    $ref: '#/components/responses/RequestTooLarge'
                "429":
                    $ref: '#/components/responses/RateLimitedOrQuota'
                "500":
                    $ref: '#/components/responses/Internal'
                "503":
                    $ref: '#/components/responses/UpstreamUnavailable'
            summary: Validate one certificate under a named policy
            tags:
                - validations
    /v1/validations/{id}:
        get:
            description: Returns the stored result document, re-rendered from the stored message on every read — never recomputed. The stored and served encodings differ by design; the document is the same either way.
            operationId: getValidation
            parameters:
                - $ref: '#/components/parameters/Id'
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/ValidationResult'
                    description: Stored validation result.
                    headers:
                        X-Request-Id:
                            $ref: '#/components/headers/XRequestId'
                "401":
                    $ref: '#/components/responses/Unauthorized'
                "403":
                    $ref: '#/components/responses/KeyForbidden'
                "404":
                    $ref: '#/components/responses/NotFound'
                "429":
                    $ref: '#/components/responses/RateLimitedOrQuota'
                "500":
                    $ref: '#/components/responses/Internal'
                "503":
                    $ref: '#/components/responses/UpstreamUnavailable'
            summary: Fetch a stored validation result
            tags:
                - validations
security:
    - apiKey: []
servers:
    - description: Production API host.
      url: https://api.quovalis.eu
tags:
    - name: validations
    - name: trustlist
    - name: usage
