openapi: "3.1.0"
info:
  title: Qpher PQC API
  description: |
    Complete API reference for the Qpher Post-Quantum Cryptography platform.
    Covers key encapsulation (KEM), digital signatures, key management, and
    tenant administration — all protected by quantum-resistant algorithms.

    ## Algorithms

    | Algorithm | Type | Standard |
    |-----------|------|----------|
    | Kyber768 (ML-KEM-768) | KEM | FIPS 203 |
    | Dilithium3 (ML-DSA-65) | Signature | FIPS 204 |
    | X-Wing (X25519 + ML-KEM-768) | Hybrid KEM | IETF CFRG draft |
    | Composite-ML-DSA (ECDSA P-256 + ML-DSA-65) | Hybrid Signature | IETF LAMPS draft |

    ## Authentication

    Include your API key in the `X-API-Key` header on every request.
  version: "1.1.0"
  contact:
    name: Qpher Support
    url: https://qpher.ai/support
    email: support@qpher.ai
  license:
    name: Proprietary
    url: https://qpher.ai/terms

servers:
  - url: https://api.qpher.ai
    description: Production

security:
  - apiKey: []

tags:
  - name: KEM
    description: Key Encapsulation Mechanism — encrypt, decrypt, encapsulate, decapsulate, key wrap, and key unwrap
  - name: Signatures
    description: Digital signatures — sign, verify, sign-hash, and verify-hash
  - name: Key Management
    description: PQC key lifecycle — generate, rotate, list, retire, and query keys
  - name: Tenants
    description: Tenant management — create tenants and manage API keys

paths:
  # ── KEM ────────────────────────────────────────────────────────────────

  /api/v1/kem/encrypt:
    post:
      operationId: kemEncrypt
      summary: Encrypt data using KEM-DEM
      description: |
        Encrypt plaintext using the hybrid KEM-DEM scheme:
        KEM encapsulate -> HKDF-SHA256 -> AES-256-GCM.
        The shared secret is ephemeral and never stored.
        Supports Kyber768 (default) and X-Wing hybrid algorithms.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plaintext, key_version]
              properties:
                plaintext:
                  type: string
                  format: byte
                  description: Base64-encoded plaintext (max 1 MB)
                  example: SGVsbG8sIFF1YW50dW0gV29ybGQh
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (must be active)
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
                mode:
                  type: string
                  enum: [standard, deterministic]
                  default: standard
                  description: Encryption mode. Deterministic requires salt.
                  example: standard
                salt:
                  type: string
                  format: byte
                  description: Base64-encoded salt (required if mode=deterministic)
                  example: c2FsdHZhbHVlMTIzNDU2Nzg=
      responses:
        "200":
          description: Encryption successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemEncryptResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kem/decrypt:
    post:
      operationId: kemDecrypt
      summary: Decrypt data using KEM-DEM
      description: |
        Decrypt ciphertext that was encrypted using the KEM-DEM scheme.
        Supports active and retired key versions.
        Supports Kyber768 (default) and X-Wing hybrid algorithms.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ciphertext, key_version]
              properties:
                ciphertext:
                  type: string
                  format: byte
                  description: Base64-encoded ciphertext from encrypt
                  example: eyJrZW1fY2lwaGVydGV4dCI6Ii4uLiIsIm5vbmNlIjoiLi4uIiwiY2lwaGVydGV4dCI6Ii4uLiJ9
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (active or retired)
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
      responses:
        "200":
          description: Decryption successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemDecryptResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kem/encapsulate:
    post:
      operationId: kemEncapsulate
      summary: KEM encapsulate (raw shared secret)
      description: |
        Perform a raw KEM encapsulation to produce a shared secret and KEM ciphertext.
        The caller receives both the shared secret and the KEM ciphertext.
        Use this when you want to derive your own encryption keys from the shared secret.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key_version]
              properties:
                key_version:
                  type: integer
                  minimum: 1
                  description: Active key version to use
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
      responses:
        "200":
          description: Encapsulation successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemEncapsulateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kem/decapsulate:
    post:
      operationId: kemDecapsulate
      summary: KEM decapsulate (recover shared secret)
      description: |
        Perform a raw KEM decapsulation to recover the shared secret from KEM ciphertext.
        The private key never leaves the server — decapsulation happens server-side.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kem_ciphertext, key_version]
              properties:
                kem_ciphertext:
                  type: string
                  format: byte
                  description: Base64-encoded KEM ciphertext from encapsulate
                  example: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v
                key_version:
                  type: integer
                  minimum: 1
                  description: Key version (active or retired)
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
      responses:
        "200":
          description: Decapsulation successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemDecapsulateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kem/key/wrap:
    post:
      operationId: kemKeyWrap
      summary: Wrap a symmetric key using KEM-DEM
      description: |
        Wrap (encrypt) a symmetric key using the KEM-DEM scheme. The symmetric key
        must be 16, 24, 32, 48, or 64 bytes. Use this to protect AES keys, HMAC keys,
        or other symmetric material with post-quantum security.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [symmetric_key, key_version]
              properties:
                symmetric_key:
                  type: string
                  format: byte
                  description: Base64-encoded symmetric key to wrap (16/24/32/48/64 bytes)
                  example: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
                key_version:
                  type: integer
                  minimum: 1
                  description: Active key version to use for wrapping
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
      responses:
        "200":
          description: Key wrapped successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemKeyWrapResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kem/key/unwrap:
    post:
      operationId: kemKeyUnwrap
      summary: Unwrap a symmetric key using KEM-DEM
      description: |
        Unwrap (decrypt) a symmetric key that was previously wrapped using kemKeyWrap.
        The private key never leaves the server — unwrapping happens server-side.
      tags: [KEM]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wrapped_key, key_version]
              properties:
                wrapped_key:
                  type: string
                  format: byte
                  description: Base64-encoded wrapped key from key/wrap
                  example: eyJrZW1fY2lwaGVydGV4dCI6Ii4uLiIsIm5vbmNlIjoiLi4uIiwiY2lwaGVydGV4dCI6Ii4uLiJ9
                key_version:
                  type: integer
                  minimum: 1
                  description: Key version used for wrapping (active or retired)
                  example: 1
                algorithm:
                  type: string
                  enum: [Kyber768, X-Wing]
                  description: KEM algorithm. Default is Kyber768.
                  example: Kyber768
      responses:
        "200":
          description: Key unwrapped successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KemKeyUnwrapResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ── Signatures ─────────────────────────────────────────────────────────

  /api/v1/signature/sign:
    post:
      operationId: signatureSign
      summary: Sign a message
      description: |
        Create a digital signature for the provided message using
        Dilithium3 (ML-DSA-65) or Composite-ML-DSA. Requires an active key version.
      tags: [Signatures]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message, key_version]
              properties:
                message:
                  type: string
                  format: byte
                  description: Base64-encoded message to sign
                  example: SGVsbG8sIFF1YW50dW0gV29ybGQh
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (must be active)
                  example: 1
                algorithm:
                  type: string
                  enum: [Dilithium3, Composite-ML-DSA]
                  description: Signature algorithm. Default is Dilithium3.
                  example: Dilithium3
      responses:
        "200":
          description: Signing successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SigSignResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/signature/verify:
    post:
      operationId: signatureVerify
      summary: Verify a signature
      description: |
        Verify a digital signature against the original message.
        Supports active and retired key versions.
        Supports Dilithium3 (default) and Composite-ML-DSA algorithms.
      tags: [Signatures]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message, signature, key_version]
              properties:
                message:
                  type: string
                  format: byte
                  description: Base64-encoded original message
                  example: SGVsbG8sIFF1YW50dW0gV29ybGQh
                signature:
                  type: string
                  format: byte
                  description: Base64-encoded signature to verify
                  example: TUlJRHNpZ25hdHVyZV9leGFtcGxlX2Jhc2U2NF9wbGFjZWhvbGRlcl92YWx1ZQ==
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (active or retired)
                  example: 1
                algorithm:
                  type: string
                  enum: [Dilithium3, Composite-ML-DSA]
                  description: Signature algorithm. Default is Dilithium3.
                  example: Dilithium3
      responses:
        "200":
          description: Verification complete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SigVerifyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/signature/sign-hash:
    post:
      operationId: signatureSignHash
      summary: Sign a pre-computed hash
      description: |
        Sign a pre-computed hash digest instead of the full message.
        Useful for large files or streaming scenarios where you compute
        the hash client-side. Note: Dilithium internally re-hashes the input,
        so sign-hash output differs from sign(hash(message)).
      tags: [Signatures]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hash, hash_algorithm, key_version]
              properties:
                hash:
                  type: string
                  format: byte
                  description: Base64-encoded hash digest
                  example: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
                hash_algorithm:
                  type: string
                  enum: [SHA-256, SHA-384, SHA-512]
                  description: Hash algorithm used to compute the digest
                  example: SHA-256
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (must be active)
                  example: 1
                algorithm:
                  type: string
                  enum: [Dilithium3, Composite-ML-DSA]
                  description: Signature algorithm. Default is Dilithium3.
                  example: Dilithium3
      responses:
        "200":
          description: Hash signing successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SigSignHashResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/signature/verify-hash:
    post:
      operationId: signatureVerifyHash
      summary: Verify a hash-based signature
      description: |
        Verify a signature that was created using sign-hash against the
        same pre-computed hash digest. The hash algorithm and length must match.
      tags: [Signatures]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hash, hash_algorithm, signature, key_version]
              properties:
                hash:
                  type: string
                  format: byte
                  description: Base64-encoded hash digest
                  example: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
                hash_algorithm:
                  type: string
                  enum: [SHA-256, SHA-384, SHA-512]
                  description: Hash algorithm used to compute the digest
                  example: SHA-256
                signature:
                  type: string
                  format: byte
                  description: Base64-encoded signature from sign-hash
                  example: TUlJRHNpZ25hdHVyZV9leGFtcGxlX2Jhc2U2NF9wbGFjZWhvbGRlcl92YWx1ZQ==
                key_version:
                  type: integer
                  minimum: 1
                  description: PQC key version (active or retired)
                  example: 1
                algorithm:
                  type: string
                  enum: [Dilithium3, Composite-ML-DSA]
                  description: Signature algorithm. Default is Dilithium3.
                  example: Dilithium3
      responses:
        "200":
          description: Hash verification complete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SigVerifyHashResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ── Key Management ─────────────────────────────────────────────────────

  /api/v1/kms/keys/generate:
    post:
      operationId: generateKey
      summary: Generate a new PQC key pair
      description: |
        Generate a new PQC key pair for the specified algorithm.
        The key starts in "active" status at version 1 (or next available version).
        Private keys are stored encrypted and never returned.
      tags: [Key Management]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [algorithm]
              properties:
                algorithm:
                  type: string
                  enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
                  description: PQC algorithm for key generation
                  example: Kyber768
      responses:
        "201":
          description: Key generated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys/rotate:
    post:
      operationId: rotateKey
      summary: Rotate an active key
      description: |
        Create a new active key version and retire the current active key.
        The old key transitions to "retired" (decrypt/verify only).
      tags: [Key Management]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [algorithm]
              properties:
                algorithm:
                  type: string
                  enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
                  description: Algorithm of the key to rotate
                  example: Kyber768
      responses:
        "201":
          description: Key rotated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsRotateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys:
    get:
      operationId: listKeys
      summary: List all PQC keys
      description: List all PQC keys for the current tenant, optionally filtered by algorithm or status.
      tags: [Key Management]
      parameters:
        - name: algorithm
          in: query
          schema:
            type: string
            enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
          description: Filter by algorithm (optional)
          example: Kyber768
        - name: status
          in: query
          schema:
            type: string
            enum: [active, retired, archived]
          description: Filter by key status (optional)
      responses:
        "200":
          description: Keys listed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys/active:
    get:
      operationId: getActiveKey
      summary: Get the active key for an algorithm
      description: Returns the currently active key version for the specified algorithm.
      tags: [Key Management]
      parameters:
        - name: algorithm
          in: query
          required: true
          schema:
            type: string
            enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
          example: Kyber768
      responses:
        "200":
          description: Active key info
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys/retire:
    post:
      operationId: retireKey
      summary: Retire a key version
      description: |
        Transition a key from "active" to "retired".
        Retired keys can only be used for decrypt/verify, not encrypt/sign.
      tags: [Key Management]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [algorithm, key_version]
              properties:
                algorithm:
                  type: string
                  enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
                  description: Algorithm of the key to retire
                  example: Kyber768
                key_version:
                  type: integer
                  minimum: 1
                  description: Key version to retire
                  example: 1
      responses:
        "200":
          description: Key retired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys/archive:
    post:
      operationId: archiveKey
      summary: Archive a retired key
      description: |
        Transition a key from "retired" to "archived" and securely delete
        the private key material. Archived keys cannot perform any operations.
      tags: [Key Management]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [algorithm, key_version]
              properties:
                algorithm:
                  type: string
                  enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
                  description: Algorithm of the key to archive
                  example: Kyber768
                key_version:
                  type: integer
                  minimum: 1
                  description: Key version to archive
                  example: 1
      responses:
        "200":
          description: Key archived
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/kms/keys/{key_version}:
    get:
      operationId: getKeyByVersion
      summary: Get key metadata by version
      description: Returns the key metadata for a specific version and algorithm.
      tags: [Key Management]
      parameters:
        - name: key_version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
          description: Key version number
          example: 1
        - name: algorithm
          in: query
          required: true
          schema:
            type: string
            enum: [Kyber768, Dilithium3, X-Wing, Composite-ML-DSA]
          description: PQC algorithm
          example: Kyber768
      responses:
        "200":
          description: Key metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KmsKeyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ── Tenants ────────────────────────────────────────────────────────────

  /api/v1/tenants:
    post:
      operationId: createTenant
      summary: Create a new tenant
      description: |
        Create a new tenant with the specified name and plan.
        Returns the tenant details. To obtain an API key, call
        POST /api/v1/tenants/{tenant_id}/api-keys after creation.
      tags: [Tenants]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: Tenant display name
                  example: Acme Corp
                plan:
                  type: string
                  enum: [free, starter, growth, pro, enterprise]
                  default: free
                  description: Pricing plan tier (defaults to "free" if omitted)
                  example: starter
      responses:
        "201":
          description: Tenant created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          $ref: "#/components/responses/Conflict"

  /api/v1/tenants/{tenant_id}:
    get:
      operationId: getTenant
      summary: Get tenant details
      description: Returns tenant details. Does not include the API key.
      tags: [Tenants]
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Tenant ID
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        "200":
          description: Tenant details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/tenants/{tenant_id}/api-keys:
    post:
      operationId: createApiKey
      summary: Create a new API key
      description: |
        Create a new API key for the tenant. The plaintext key is returned
        only once in the response — store it immediately.
        Key quota is enforced per plan tier (ADR-0005).
      tags: [Tenants]
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Tenant ID
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      operationId: listApiKeys
      summary: List API keys for a tenant
      description: Returns all API keys (metadata only, no plaintext) for the tenant.
      tags: [Tenants]
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Tenant ID
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        "200":
          description: API key list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/tenants/{tenant_id}/api-keys/rotate:
    post:
      operationId: rotateApiKey
      summary: Rotate tenant API key
      description: |
        Create a new API key and revoke the old one.
        The new key is shown only once in the response.
        There is a brief concurrent window where both keys work.
      tags: [Tenants]
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Tenant ID
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        "200":
          description: API key rotated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyRotateResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/tenants/{tenant_id}/api-keys/{version}:
    delete:
      operationId: revokeApiKey
      summary: Revoke a specific API key
      description: Revoke an API key by its version number. The key immediately stops working.
      tags: [Tenants]
      parameters:
        - name: tenant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Tenant ID
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
          description: API key version to revoke
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyInfoResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key obtained when creating a tenant. Include in every request.

  schemas:
    # ── KEM Schemas ──────────────────────────────────────────────────────

    KemEncryptResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            ciphertext:
              type: string
              format: byte
              description: Base64-encoded ciphertext
              example: eyJrZW1fY2lwaGVydGV4dCI6Ii4uLiIsIm5vbmNlIjoiLi4uIiwiY2lwaGVydGV4dCI6Ii4uLiJ9
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KemDecryptResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            plaintext:
              type: string
              format: byte
              description: Base64-encoded decrypted plaintext
              example: SGVsbG8sIFF1YW50dW0gV29ybGQh
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KemEncapsulateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            kem_ciphertext:
              type: string
              format: byte
              description: Base64-encoded KEM ciphertext
              example: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v
            shared_secret:
              type: string
              format: byte
              description: Base64-encoded 32-byte shared secret
              example: dGhpcyBpcyBhIDMyLWJ5dGUgc2hhcmVkIHNlY3JldCE=
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KemDecapsulateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            shared_secret:
              type: string
              format: byte
              description: Base64-encoded 32-byte shared secret
              example: dGhpcyBpcyBhIDMyLWJ5dGUgc2hhcmVkIHNlY3JldCE=
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KemKeyWrapResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            wrapped_key:
              type: string
              format: byte
              description: Base64-encoded wrapped key (KEM-DEM ciphertext)
              example: eyJrZW1fY2lwaGVydGV4dCI6Ii4uLiIsIm5vbmNlIjoiLi4uIiwiY2lwaGVydGV4dCI6Ii4uLiJ9
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
            wrapping_method:
              type: string
              description: Wrapping method used
              example: KEM-DEM
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KemKeyUnwrapResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            symmetric_key:
              type: string
              format: byte
              description: Base64-encoded recovered symmetric key
              example: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    # ── Signature Schemas ────────────────────────────────────────────────

    SigSignResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            signature:
              type: string
              format: byte
              description: Base64-encoded signature (Dilithium3 / ML-DSA-65 is 3,309 bytes raw per FIPS 204)
              example: TUlJRHNpZ25hdHVyZV9leGFtcGxlX2Jhc2U2NF9wbGFjZWhvbGRlcl92YWx1ZQ==
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Dilithium3
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    SigVerifyResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            valid:
              type: boolean
              description: Whether the signature is valid
              example: true
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Dilithium3
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    SigSignHashResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            signature:
              type: string
              format: byte
              description: Base64-encoded signature over the hash
              example: TUlJRHNpZ25hdHVyZV9leGFtcGxlX2Jhc2U2NF9wbGFjZWhvbGRlcl92YWx1ZQ==
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Dilithium3
            hash_algorithm:
              type: string
              example: SHA-256
            signature_type:
              type: string
              description: Always "detached" for hash-based signatures
              example: detached
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    SigVerifyHashResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            valid:
              type: boolean
              description: Whether the hash signature is valid
              example: true
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Dilithium3
            hash_algorithm:
              type: string
              example: SHA-256
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    # ── KMS Schemas ──────────────────────────────────────────────────────

    KmsKeyResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            key_version:
              type: integer
              example: 1
            algorithm:
              type: string
              example: Kyber768
            status:
              type: string
              enum: [active, retired, archived]
              example: active
            public_key:
              type: string
              format: byte
              description: Base64-encoded public key
              example: TUlJQm9qQU5CZ3B1YmxpY19rZXlfZXhhbXBsZV9iYXNlNjRfcGxhY2Vob2xkZXI=
            created_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KmsRotateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            public_key:
              type: string
              format: byte
              description: Base64-encoded public key of the new active key
              example: TUlJQm9qQU5CZ3B1YmxpY19rZXlfZXhhbXBsZV9iYXNlNjRfcGxhY2Vob2xkZXI=
            key_version:
              type: integer
              description: Version number of the new active key
              example: 2
            algorithm:
              type: string
              example: Kyber768
            old_key_version:
              type: integer
              description: Version number of the previously active key (now retired)
              example: 1
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    KmsKeyListResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            keys:
              type: array
              items:
                type: object
                properties:
                  key_version:
                    type: integer
                    example: 1
                  algorithm:
                    type: string
                    example: Kyber768
                  status:
                    type: string
                    example: active
                  public_key:
                    type: string
                    format: byte
                    example: TUlJQm9qQU5CZ3B1YmxpY19rZXlfZXhhbXBsZV9iYXNlNjRfcGxhY2Vob2xkZXI=
                  created_at:
                    type: string
                    format: date-time
                    example: "2026-01-15T10:30:00.000Z"
            total:
              type: integer
              description: Total number of keys returned
              example: 2
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    # ── Tenant Schemas ───────────────────────────────────────────────────

    TenantCreateResponse:
      type: object
      description: |
        Tenant details. To obtain an API key, call POST /api/v1/tenants/{tenant_id}/api-keys after creation.
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
              example: 550e8400-e29b-41d4-a716-446655440000
            name:
              type: string
              example: Acme Corp
            plan:
              type: string
              example: starter
            status:
              type: string
              example: active
            created_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
            updated_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    TenantResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
              example: 550e8400-e29b-41d4-a716-446655440000
            name:
              type: string
              example: Acme Corp
            plan:
              type: string
              example: starter
            status:
              type: string
              example: active
            created_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
            updated_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    ApiKeyRotateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            new_key:
              type: object
              description: The newly created API key (shown ONCE — store securely)
              properties:
                api_key:
                  type: string
                  description: Plaintext API key — copy now, never shown again
                  example: qph_live_xyz789abc012def345
                key_last4:
                  type: string
                  description: Last 4 characters of the key for display
                  example: f345
                version:
                  type: integer
                  description: Version number of the new key
                  example: 2
                status:
                  type: string
                  description: Key status (always "active" for new keys)
                  example: active
                created_at:
                  type: string
                  format: date-time
                  example: "2026-01-15T12:00:00.000Z"
            revoked_version:
              type: ["integer", "null"]
              description: Version number of the revoked key (null if no previous key)
              example: 1
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890

    ApiKeyCreateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            api_key:
              type: string
              description: Plaintext API key — copy now, never shown again
              example: qph_live_abc123def456ghi789
            key_last4:
              type: string
              description: Last 4 characters of the key for display
              example: h789
            version:
              type: integer
              description: Key version number
              example: 1
            status:
              type: string
              description: Key status (always "active" for new keys)
              example: active
            created_at:
              type: string
              format: date-time
              example: "2026-01-15T10:30:00.000Z"
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

    ApiKeyListResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            api_keys:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  tenant_id:
                    type: string
                    format: uuid
                  key_last4:
                    type: string
                    example: h789
                  version:
                    type: integer
                    example: 1
                  status:
                    type: string
                    enum: [active, revoked]
                    example: active
                  created_at:
                    type: string
                    format: date-time
                  revoked_at:
                    type: ["string", "null"]
                    format: date-time
            total:
              type: integer
              example: 2
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890

    ApiKeyInfoResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            tenant_id:
              type: string
              format: uuid
            key_last4:
              type: string
              example: h789
            version:
              type: integer
              example: 1
            status:
              type: string
              enum: [active, revoked]
              example: revoked
            created_at:
              type: string
              format: date-time
            revoked_at:
              type: ["string", "null"]
              format: date-time
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890

    # ── Shared Schemas ───────────────────────────────────────────────────

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            error_code:
              type: string
              pattern: "^ERR_[A-Z_]+_\\d{3}$"
              description: Structured error code
              example: ERR_AUTH_001
            message:
              type: string
              description: Human-readable error message
              example: Missing or invalid API key
        request_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        timestamp:
          type: string
          format: date-time
          example: "2026-01-15T10:30:00.000Z"

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Operation not allowed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: Resource conflict
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
