openapi: 3.1.0
info:
  title: Certis API
  version: 0.4.0
  description: |
    Certis is a verifiable-certificate-as-a-service platform built on Cloudflare Workers.
    Issue tamper-proof digital certificates backed by W3C Verifiable Credentials 2.0,
    verify them publicly, and embed badges anywhere.

    ## Authentication

    Authenticated endpoints require a Bearer token in the `Authorization` header:
    ```
    Authorization: Bearer certis_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```
    API keys are created during signup or via the API Keys endpoints. Each key carries
    a set of permissions that control which resources it can access.

    ## Response Format

    All JSON responses follow a consistent envelope:

    - **Success:** `{ "success": true, "data": { ... } }`
    - **Paginated:** `{ "success": true, "data": [...], "pagination": { "page", "limit", "total", "total_pages" } }`
    - **Error:** `{ "success": false, "error": { "code": "...", "message": "..." } }`

    ## Rate Limits

    | Context | Limit | Window |
    |---|---|---|
    | Authenticated API (per key) | 100 requests | 1 minute |
    | Public endpoints (per IP) | 30 requests | 1 minute |

    ## Plan Limits

    | Plan | Certificates/month | Templates |
    |---|---|---|
    | Free | 20 | 3 |
    | Pro | 500 | Unlimited |
    | Business | 2,000 | Unlimited |
    | Enterprise | Unlimited | Unlimited |
  contact:
    name: Certis Support
    url: https://certis-api.sergio-22d.workers.dev
  license:
    name: MIT

servers:
  - url: https://certis-api.sergio-22d.workers.dev
    description: Production

tags:
  - name: Health
    description: Service health check
  - name: Signup
    description: Organization registration and domain verification
  - name: Organizations
    description: Manage the authenticated organization
  - name: API Keys
    description: Create, list, and revoke API keys
  - name: Templates
    description: Certificate template management
  - name: Certificates
    description: Issue, list, and revoke certificates
  - name: Audit Logs
    description: Organization audit trail
  - name: Verification
    description: Public certificate verification and PDF download
  - name: Badges
    description: Embeddable badge images and snippets
  - name: Credentials (VC 2.0)
    description: W3C Verifiable Credentials 2.0 endpoints

security: []

paths:
  # ──────────────────────────── Health ────────────────────────────
  /health:
    get:
      operationId: getHealth
      summary: Health check
      description: Returns the service health status, version, and current timestamp.
      tags: [Health]
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                required: [status, version, timestamp]
                properties:
                  status:
                    type: string
                    enum: [healthy]
                  version:
                    type: string
                    example: "0.3.0"
                  timestamp:
                    type: string
                    format: date-time
                    example: "2025-07-14T12:00:00.000Z"

  # ──────────────────────────── Signup (public) ────────────────────────────
  /v1/signup:
    post:
      operationId: signup
      summary: Register a new organization
      description: |
        Creates an organization, generates the first API key (shown once), starts domain
        verification, and provisions an Ed25519 signing key for VC 2.0 proofs.
      tags: [Signup]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignupRequest"
            example:
              name: Acme Corp
              contact_email: hello@acme.com
              domain: acme.com
              website: https://acme.com
              description: Leading provider of security certifications
      responses:
        "201":
          description: Organization created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-SignupResult"
              example:
                success: true
                data:
                  organization:
                    id: "550e8400-e29b-41d4-a716-446655440000"
                    name: Acme Corp
                    slug: acme-corp
                    domain: acme.com
                    domain_verified: false
                    plan: free
                    contact_email: hello@acme.com
                    created_at: "2025-07-14T12:00:00.000Z"
                  api_key:
                    id: "660e8400-e29b-41d4-a716-446655440001"
                    key: "certis_live_abc123def456ghi789jkl012mno345pq"
                    key_prefix: "certis_live_abc1"
                    name: Default API Key
                    warning: "Store this key securely. It will not be shown again."
                  plan:
                    name: free
                    certificates_total: 20
                    templates: 3
                  domain_verification:
                    token: "certis-verify-abc123"
                    host: "_certis.acme.com"
                    deadline: "2025-07-21T12:00:00.000Z"
                  signing_key:
                    id: "770e8400-e29b-41d4-a716-446655440002"
                    public_key: "z6Mkf5rGMoatrSj1f..."
                    algorithm: Ed25519
        "400":
          $ref: "#/components/responses/ValidationError"
        "409":
          $ref: "#/components/responses/ConflictError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  # ──────────────────────────── Signup (authenticated) ────────────────────────────
  /v1/contact:
    post:
      operationId: createContactRequest
      summary: Request plan upgrade
      description: Sends a contact request for a plan upgrade or sales inquiry.
      tags: [Signup]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactRequest"
            example:
              contact_name: Jane Smith
              contact_email: jane@acme.com
              message: We'd like to upgrade to the Pro plan.
      responses:
        "201":
          description: Contact request created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-ContactResult"
              example:
                success: true
                data:
                  id: "880e8400-e29b-41d4-a716-446655440003"
                  status: pending
                  message: Contact request submitted successfully
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "409":
          $ref: "#/components/responses/ConflictError"

  /v1/domain/initiate:
    post:
      operationId: initiateDomainVerification
      summary: Start domain verification
      description: |
        Generates a DNS TXT record that must be added to the organization's domain.
        The record must be set before calling `/v1/domain/verify`.
      tags: [Signup]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Domain to verify ownership of
                  example: acme.com
      responses:
        "200":
          description: Domain verification initiated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-DomainInitiateResult"
              example:
                success: true
                data:
                  domain: acme.com
                  verification_record: "TXT _certis.acme.com certis-verify-abc123"
                  instructions: "Add the TXT record to your DNS, then call POST /v1/domain/verify."
                  token: "certis-verify-abc123"
                  host: "_certis.acme.com"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  /v1/domain/verify:
    post:
      operationId: verifyDomain
      summary: Verify domain ownership
      description: |
        Checks if the required DNS TXT record is present and verifies domain ownership.
        Call this after adding the record from `/v1/domain/initiate`.
      tags: [Signup]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:write"]
      responses:
        "200":
          description: Domain verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-DomainVerifyResult"
              example:
                success: true
                data:
                  verified: true
                  domain: acme.com
                  domain_verified_at: "2025-07-14T13:00:00.000Z"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  # ──────────────────────────── Organizations ────────────────────────────
  /v1/organizations/me:
    get:
      operationId: getCurrentOrganization
      summary: Get current organization
      description: Returns the organization associated with the authenticated API key.
      tags: [Organizations]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:read"]
      responses:
        "200":
          description: Organization details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-OrganizationResponse"
              example:
                success: true
                data:
                  id: "550e8400-e29b-41d4-a716-446655440000"
                  name: Acme Corp
                  slug: acme-corp
                  description: Leading provider of security certifications
                  website: https://acme.com
                  logo_url: null
                  contact_email: hello@acme.com
                  verified: true
                  domain: acme.com
                  domain_verified: true
                  plan: free
                  usage:
                    certificates_this_month: 5
                    month: "2025-07"
                  created_at: "2025-07-01T00:00:00.000Z"
                  updated_at: "2025-07-14T12:00:00.000Z"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
    patch:
      operationId: updateOrganization
      summary: Update organization
      description: Updates the authenticated organization. At least one field must be provided.
      tags: [Organizations]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOrganizationRequest"
            example:
              name: Acme Corporation
              website: https://www.acme.com
      responses:
        "200":
          description: Organization updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-OrganizationResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  # ──────────────────────────── API Keys ────────────────────────────
  /v1/api-keys:
    post:
      operationId: createApiKey
      summary: Create API key
      description: |
        Creates a new API key. The full key is returned only once in the response.
        Defaults to `certificates:write` and `certificates:read` permissions if none are specified.
      tags: [API Keys]
      security:
        - bearerAuth: []
      x-required-permissions: ["api_keys:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiKeyRequest"
            example:
              name: CI/CD Pipeline
              permissions: ["certificates:write", "certificates:read", "templates:read"]
              expires_at: "2026-01-01T00:00:00.000Z"
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-ApiKeyCreatedResponse"
              example:
                success: true
                data:
                  id: "990e8400-e29b-41d4-a716-446655440004"
                  name: CI/CD Pipeline
                  key: "certis_live_xyz789abc012def345ghi678jkl901mn"
                  key_prefix: "certis_live_xyz7"
                  permissions: ["certificates:write", "certificates:read", "templates:read"]
                  expires_at: "2026-01-01T00:00:00.000Z"
                  created_at: "2025-07-14T12:00:00.000Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
    get:
      operationId: listApiKeys
      summary: List API keys
      description: Returns a paginated list of API keys for the authenticated organization.
      tags: [API Keys]
      security:
        - bearerAuth: []
      x-required-permissions: ["api_keys:read"]
      parameters:
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/LimitParam"
      responses:
        "200":
          description: Paginated list of API keys
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedResponse-ApiKeyListItem"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  /v1/api-keys/{id}:
    delete:
      operationId: revokeApiKey
      summary: Revoke API key
      description: Permanently deactivates the specified API key.
      tags: [API Keys]
      security:
        - bearerAuth: []
      x-required-permissions: ["api_keys:write"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-MessageOnly"
              example:
                success: true
                data:
                  message: API key revoked
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"

  # ──────────────────────────── Templates ────────────────────────────
  /v1/templates:
    post:
      operationId: createTemplate
      summary: Create template
      description: Creates a new certificate template with optional style configuration for badge rendering.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTemplateRequest"
            example:
              name: ISO 27001 Compliance
              category: security
              description: Certifies compliance with ISO 27001 information security standard.
              default_validity_days: 365
              criteria:
                standard: ISO 27001
                scope: Information Security Management
              style_config:
                primary_color: "#1a73e8"
                shape: shield
                border_style: solid
      responses:
        "201":
          description: Template created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-TemplateResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "403.plan":
          description: Template limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                success: false
                error:
                  code: PLAN_LIMIT_REACHED
                  message: Template limit reached for your plan
    get:
      operationId: listTemplates
      summary: List templates
      description: Returns a paginated list of templates. Optionally filter by category or active status.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:read"]
      parameters:
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/LimitParam"
        - name: category
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/TemplateCategory"
        - name: is_active
          in: query
          required: false
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter by active status
      responses:
        "200":
          description: Paginated list of templates
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedResponse-TemplateResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  /v1/templates/{id}:
    get:
      operationId: getTemplate
      summary: Get template
      description: Returns a single template by ID.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:read"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Template details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-TemplateResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
    patch:
      operationId: updateTemplate
      summary: Update template
      description: Updates a template. At least one field must be provided.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:write"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTemplateRequest"
      responses:
        "200":
          description: Template updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-TemplateResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
    delete:
      operationId: deactivateTemplate
      summary: Deactivate template
      description: Soft-deletes the template by setting `is_active` to `false`.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:write"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Template deactivated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-MessageOnly"
              example:
                success: true
                data:
                  message: Template deactivated
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"

  /v1/templates/{id}/logo:
    post:
      operationId: uploadTemplateLogo
      summary: Upload template logo
      description: |
        Upload a logo image for the template. The image is stored in R2 and
        displayed on the verification page and badge. Accepts multipart/form-data
        (field: `logo`) or raw binary with appropriate Content-Type header.
      tags: [Templates]
      security:
        - bearerAuth: []
      x-required-permissions: ["templates:write"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [logo]
              properties:
                logo:
                  type: string
                  format: binary
                  description: "Image file (PNG, JPEG, WebP, or SVG). Max 2 MB."
          image/png:
            schema:
              type: string
              format: binary
          image/jpeg:
            schema:
              type: string
              format: binary
          image/webp:
            schema:
              type: string
              format: binary
          image/svg+xml:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Logo uploaded
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    const: true
                  data:
                    type: object
                    required: [badge_image_url]
                    properties:
                      badge_image_url:
                        type: string
                        format: uri
              example:
                success: true
                data:
                  badge_image_url: "https://certis-api.sergio-22d.workers.dev/assets/logos/org-id/template-id-abc123.png"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"

  /assets/{key}:
    get:
      operationId: getAsset
      summary: Serve a static asset
      description: Serves a file from R2 storage (logos, images). Cached with immutable headers.
      tags: [Badges]
      security: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Asset key (e.g. logos/org-id/template-id.png)
      responses:
        "200":
          description: Asset file
          content:
            image/*:
              schema:
                type: string
                format: binary
        "404":
          description: Asset not found

  # ──────────────────────────── Certificates ────────────────────────────
  /v1/certificates:
    post:
      operationId: issueCertificate
      summary: Issue certificate
      description: |
        Issues a new certificate for a recipient. The certificate is signed with a
        DataIntegrityProof (Ed25519) and can be resolved as a W3C VC 2.0 credential.
        The recipient receives a claim token to accept the certificate.
      tags: [Certificates]
      security:
        - bearerAuth: []
      x-required-permissions: ["certificates:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IssueCertificateRequest"
            example:
              template_id: "550e8400-e29b-41d4-a716-446655440000"
              recipient_name: SecureCorp
              recipient_email: security@securecorp.com
              recipient_website: https://securecorp.com
              evidences:
                - type: report
                  title: Audit Report 2025
                  description: Annual security audit findings
                  url: https://securecorp.com/audit-2025.pdf
              metadata:
                audit_firm: Big4 Auditors
                scope: Cloud Infrastructure
              validity_days: 365
      responses:
        "201":
          description: Certificate issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-CertificateResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "403.plan":
          description: Certificate limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                success: false
                error:
                  code: PLAN_LIMIT_REACHED
                  message: Monthly certificate limit reached for your plan
    get:
      operationId: listCertificates
      summary: List certificates
      description: Returns a paginated list of certificates. Optionally filter by status, template, or recipient.
      tags: [Certificates]
      security:
        - bearerAuth: []
      x-required-permissions: ["certificates:read"]
      parameters:
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/LimitParam"
        - name: status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/CertificateStatus"
        - name: template_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by template ID
        - name: recipient_slug
          in: query
          required: false
          schema:
            type: string
            maxLength: 100
          description: Filter by recipient slug
      responses:
        "200":
          description: Paginated list of certificates
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedResponse-CertificateResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  /v1/certificates/{id}:
    get:
      operationId: getCertificate
      summary: Get certificate
      description: Returns a single certificate by ID.
      tags: [Certificates]
      security:
        - bearerAuth: []
      x-required-permissions: ["certificates:read"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Certificate details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-CertificateResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"

  /v1/certificates/{id}/revoke:
    post:
      operationId: revokeCertificate
      summary: Revoke certificate
      description: Permanently revokes a certificate with a stated reason.
      tags: [Certificates]
      security:
        - bearerAuth: []
      x-required-permissions: ["certificates:write"]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RevokeCertificateRequest"
            example:
              reason: Certificate holder no longer meets compliance requirements.
      responses:
        "200":
          description: Certificate revoked
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    const: true
                  data:
                    type: object
                    required: [id, status, revoked_at, revocation_reason]
                    properties:
                      id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        enum: [revoked]
                      revoked_at:
                        type: string
                        format: date-time
                      revocation_reason:
                        type: string
              example:
                success: true
                data:
                  id: "550e8400-e29b-41d4-a716-446655440000"
                  status: revoked
                  revoked_at: "2025-07-14T15:00:00.000Z"
                  revocation_reason: Certificate holder no longer meets compliance requirements.
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"

  # ──────────────────────────── Audit Logs ────────────────────────────
  /v1/audit-logs:
    get:
      operationId: listAuditLogs
      summary: List audit logs
      description: Returns a paginated list of audit log entries for the authenticated organization.
      tags: [Audit Logs]
      security:
        - bearerAuth: []
      x-required-permissions: ["organization:read"]
      parameters:
        - $ref: "#/components/parameters/PageParam"
        - $ref: "#/components/parameters/LimitParam"
        - name: certificate_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter by certificate ID
        - name: action
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/AuditAction"
          description: Filter by audit action
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Start date filter (inclusive)
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: End date filter (inclusive)
      responses:
        "200":
          description: Paginated list of audit log entries
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedResponse-AuditLogItem"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"

  # ──────────────────────────── Verification (public) ────────────────────────────
  /v1/verify/{code}:
    get:
      operationId: verifyCertificate
      summary: Verify a certificate
      description: |
        Public endpoint that verifies a certificate by its verification code.
        Supports content negotiation:
        - `Accept: application/json` returns a JSON verification result.
        - `Accept: text/html` returns an HTML verification page.
      tags: [Verification]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
          example: ACME2025-001
      responses:
        "200":
          description: Verification result (JSON or HTML depending on Accept header)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse-VerificationResult"
            text/html:
              schema:
                type: string
                description: HTML verification page
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/verify/{code}/pdf:
    get:
      operationId: downloadCertificatePdf
      summary: Download certificate PDF
      description: Returns a PDF rendition of the verified certificate.
      tags: [Verification]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
          example: ACME2025-001
      responses:
        "200":
          description: Certificate PDF file
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/claim/{code}:
    get:
      operationId: getClaimPage
      summary: Recipient claim page
      description: Returns the HTML acknowledgement flow for the recipient named on a record.
      tags: [Verification]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Public certificate verification code
      responses:
        "200":
          description: HTML claim page or non-enumerating unavailable state
          content:
            text/html:
              schema:
                type: string
        "429":
          $ref: "#/components/responses/RateLimitError"
    post:
      operationId: verifyClaimCode
      summary: Acknowledge a record
      description: Verifies the six-digit code sent to the recipient mailbox and records recipient acknowledgement.
      tags: [Verification]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Public certificate verification code
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  pattern: "^[0-9]{6}$"
                  description: Six-digit one-time claim code
      responses:
        "200":
          description: HTML claim result page
          content:
            text/html:
              schema:
                type: string
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/claim/{code}/request-otp:
    post:
      operationId: requestClaimCode
      summary: Send a recipient claim code
      description: Sends a six-digit one-time code to the recipient mailbox recorded at issuance.
      tags: [Verification]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Public certificate verification code
      responses:
        "200":
          description: HTML claim page showing the next step or a non-enumerating unavailable state
          content:
            text/html:
              schema:
                type: string
        "429":
          $ref: "#/components/responses/RateLimitError"

  # ──────────────────────────── Badges (public) ────────────────────────────
  /v1/badge/{code}.svg:
    get:
      operationId: getBadgeSvg
      summary: Badge SVG
      description: Returns an SVG badge image for a certificate.
      tags: [Badges]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
          example: ACME2025-001
        - name: variant
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeVariant"
        - name: theme
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeTheme"
      responses:
        "200":
          description: SVG badge image
          content:
            image/svg+xml:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/badge/{code}/snippet:
    get:
      operationId: getBadgeSnippet
      summary: HTML embed snippet
      description: Returns an HTML snippet for embedding the badge on a webpage.
      tags: [Badges]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
        - name: variant
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeVariant"
        - name: theme
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeTheme"
      responses:
        "200":
          description: HTML embed snippet
          content:
            text/html:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/badge/{code}/markdown:
    get:
      operationId: getBadgeMarkdown
      summary: Markdown badge
      description: Returns a Markdown string for embedding the badge in documentation.
      tags: [Badges]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
        - name: variant
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeVariant"
      responses:
        "200":
          description: Markdown badge string
          content:
            text/plain:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/badge/{code}/embed:
    get:
      operationId: getBadgeEmbed
      summary: Badge iframe embed page
      description: Returns an HTML page suitable for embedding via iframe.
      tags: [Badges]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
        - name: variant
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeVariant"
        - name: theme
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BadgeTheme"
      responses:
        "200":
          description: HTML embed page for iframe
          content:
            text/html:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"

  /v1/embed.js:
    get:
      operationId: getEmbedScript
      summary: Embed widget script
      description: Returns the JavaScript widget that auto-renders Certis badges on a page.
      tags: [Badges]
      security: []
      responses:
        "200":
          description: JavaScript widget
          content:
            application/javascript:
              schema:
                type: string

  # ──────────────────────────── Credentials / VC 2.0 ────────────────────────────
  /v1/credentials/{code}:
    get:
      operationId: getVerifiableCredential
      summary: Get W3C VC 2.0 credential
      description: Returns the certificate as a W3C Verifiable Credential 2.0 with DataIntegrityProof.
      tags: [Credentials (VC 2.0)]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
          example: ACME2025-001
      responses:
        "200":
          description: Verifiable Credential
          content:
            application/vc+ld+json:
              schema:
                $ref: "#/components/schemas/VerifiableCredential"
        "404":
          $ref: "#/components/responses/NotFoundError"

  /v1/credentials/{code}/status:
    get:
      operationId: getCredentialStatus
      summary: Credential status
      description: Returns the current status of a verifiable credential following the Certis status scheme.
      tags: [Credentials (VC 2.0)]
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Certificate verification code
          example: ACME2025-001
      responses:
        "200":
          description: Credential status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CredentialStatusResponse"
              example:
                "@context":
                  - https://www.w3.org/ns/credentials/v2
                type: CertisCredentialStatus2025
                id: https://certis-api.sergio-22d.workers.dev/v1/credentials/ACME2025-001/status
                status: active
                statusPurpose: revocation
                valid: true
                issued_at: "2025-07-14T12:00:00.000Z"
                expires_at: "2026-07-14T12:00:00.000Z"
                checked_at: "2025-07-14T15:00:00.000Z"
        "404":
          $ref: "#/components/responses/NotFoundError"

  /orgs/{slug}/keys:
    get:
      operationId: getOrgDIDDocument
      summary: Organization DID Document
      description: |
        Returns a DID Document containing the organization's public verification keys
        as Multikey objects. Used by verifiers to resolve the `verificationMethod` in VC proofs.
      tags: [Credentials (VC 2.0)]
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          description: Organization slug
          example: acme-corp
      responses:
        "200":
          description: DID Document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DIDDocument"
              example:
                "@context":
                  - https://www.w3.org/ns/did/v1
                  - https://w3id.org/security/multikey/v1
                id: https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys
                verificationMethod:
                  - "@context": https://w3id.org/security/multikey/v1
                    id: https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys/key-1
                    type: Multikey
                    controller: https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys
                    publicKeyMultibase: "z6Mkf5rGMoatrSj1f..."
                    status: active
                    created: "2025-07-01T00:00:00.000Z"
                assertionMethod:
                  - https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys/key-1
        "404":
          $ref: "#/components/responses/NotFoundError"

  /orgs/{slug}/keys/{keyId}:
    get:
      operationId: getOrgPublicKey
      summary: Get public key
      description: Returns a single Multikey object for the specified key ID.
      tags: [Credentials (VC 2.0)]
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          description: Organization slug
          example: acme-corp
        - name: keyId
          in: path
          required: true
          schema:
            type: string
          description: Key identifier
          example: key-1
      responses:
        "200":
          description: Multikey object
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MultikeyObject"
              example:
                "@context": https://w3id.org/security/multikey/v1
                id: https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys/key-1
                type: Multikey
                controller: https://certis-api.sergio-22d.workers.dev/orgs/acme-corp/keys
                publicKeyMultibase: "z6Mkf5rGMoatrSj1f..."
                status: active
                created: "2025-07-01T00:00:00.000Z"
        "404":
          $ref: "#/components/responses/NotFoundError"

# ════════════════════════════════════════════════════════════════════
#  COMPONENTS
# ════════════════════════════════════════════════════════════════════
components:
  # ──────────────── Security Schemes ────────────────
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "certis_live_..."
      description: |
        API key prefixed with `certis_live_`. Obtain one via the signup endpoint
        or the API Keys management endpoints.

  # ──────────────── Reusable Parameters ────────────────
  parameters:
    PageParam:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
      description: Page number (1-indexed)
    LimitParam:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
      description: Items per page (max 100)
    ResourceId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID

  # ──────────────── Reusable Responses ────────────────
  responses:
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Invalid input
              details:
                - field: name
                  message: String must contain at least 2 character(s)
    UnauthorizedError:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: UNAUTHORIZED
              message: Missing or invalid API key
    ForbiddenError:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: FORBIDDEN
              message: Missing required permission
    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Resource not found
    ConflictError:
      description: Duplicate or conflicting resource
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: CONFLICT
              message: A resource with that identifier already exists
    RateLimitError:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: RATE_LIMITED
              message: Too many requests. Try again later.

  # ──────────────── Schemas ────────────────
  schemas:
    # ── Enums ──
    Permission:
      type: string
      enum:
        - certificates:read
        - certificates:write
        - templates:read
        - templates:write
        - organization:read
        - organization:write
        - api_keys:read
        - api_keys:write

    TemplateCategory:
      type: string
      enum: [security, compliance, quality, audit, training, other]

    CertificateStatus:
      type: string
      enum: [active, expired, revoked]

    AuditAction:
      type: string
      enum:
        - certificate.issued
        - certificate.revoked
        - certificate.expired
        - certificate.verified
        - template.created
        - template.updated
        - template.deactivated
        - api_key.created
        - api_key.revoked
        - organization.updated
        - signing_key.created
        - signing_key.rotated

    ActorType:
      type: string
      enum: [api_key, system, admin]

    BadgeVariant:
      type: string
      enum: [full, compact, mini, square]
      default: full

    BadgeTheme:
      type: string
      enum: [light, dark]
      default: light

    BadgeShape:
      type: string
      enum: [shield, circle, hexagon, square]

    BorderStyle:
      type: string
      enum: [solid, dashed, gradient]

    ClaimStatus:
      type: string
      enum: [pending_claim, claimed]

    # ── Shared Structures ──
    Pagination:
      type: object
      required: [page, limit, total, total_pages]
      properties:
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total:
          type: integer
          example: 42
        total_pages:
          type: integer
          example: 3

    ErrorDetail:
      type: object
      required: [field, message]
      properties:
        field:
          type: string
          example: name
        message:
          type: string
          example: String must contain at least 2 character(s)

    ErrorBody:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          example: VALIDATION_ERROR
        message:
          type: string
          example: Invalid input
        details:
          type: array
          items:
            $ref: "#/components/schemas/ErrorDetail"

    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: "#/components/schemas/ErrorBody"

    StyleConfig:
      type: object
      properties:
        primary_color:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          example: "#1a73e8"
        secondary_color:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          example: "#e8f0fe"
        shape:
          $ref: "#/components/schemas/BadgeShape"
        border_style:
          $ref: "#/components/schemas/BorderStyle"
        font_family:
          type: string
          minLength: 1
          maxLength: 100
          example: Inter

    Evidence:
      type: object
      required: [type, title]
      properties:
        type:
          type: string
          enum: [report, url, file, text]
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 1000
        url:
          type: string
          format: uri
          maxLength: 2048
        hash:
          type: string
          maxLength: 256
          description: Content hash for integrity verification
        content_hash:
          type: string
          readOnly: true
          description: "SHA-256 hash of the URL content at issuance time (auto-computed by Certis)"
          example: "sha256:e3b0c44298fc1c149afbf4c8996fb924..."
        content_hash_at:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp when the content hash was captured

    # ── Request Bodies ──
    SignupRequest:
      type: object
      required: [name, contact_email, domain]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 200
          description: Organization name
        contact_email:
          type: string
          format: email
          maxLength: 320
        domain:
          type: string
          minLength: 3
          maxLength: 253
          description: Primary domain for the organization
        website:
          type: string
          format: uri
          maxLength: 2048
        description:
          type: string
          maxLength: 1000

    ContactRequest:
      type: object
      required: [contact_name, contact_email]
      properties:
        contact_name:
          type: string
          minLength: 2
          maxLength: 100
        contact_email:
          type: string
          format: email
          maxLength: 320
        message:
          type: string
          maxLength: 2000

    UpdateOrganizationRequest:
      type: object
      description: At least one field must be provided.
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 200
        description:
          type: string
          maxLength: 1000
        website:
          type: string
          format: uri
          maxLength: 2048
        contact_email:
          type: string
          format: email
          maxLength: 320

    CreateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/Permission"
          minItems: 1
          default: ["certificates:write", "certificates:read"]
        expires_at:
          type: string
          format: date-time
          description: Optional expiration date

    CreateTemplateRequest:
      type: object
      required: [name, category]
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 200
        category:
          $ref: "#/components/schemas/TemplateCategory"
        description:
          type: string
          maxLength: 2000
        criteria:
          type: object
          additionalProperties: true
          description: Arbitrary criteria object
        default_validity_days:
          type: integer
          minimum: 1
          maximum: 3650
          default: 365
        style_config:
          $ref: "#/components/schemas/StyleConfig"

    UpdateTemplateRequest:
      type: object
      description: At least one field must be provided.
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 200
        category:
          $ref: "#/components/schemas/TemplateCategory"
        description:
          type: string
          maxLength: 2000
        criteria:
          type: object
          additionalProperties: true
        default_validity_days:
          type: integer
          minimum: 1
          maximum: 3650
        style_config:
          $ref: "#/components/schemas/StyleConfig"

    IssueCertificateRequest:
      type: object
      required: [template_id, recipient_name, recipient_email]
      properties:
        template_id:
          type: string
          format: uuid
        recipient_name:
          type: string
          minLength: 3
          maxLength: 200
        recipient_email:
          type: string
          format: email
          maxLength: 320
        recipient_website:
          type: string
          format: uri
          maxLength: 2048
        evidences:
          type: array
          items:
            $ref: "#/components/schemas/Evidence"
          maxItems: 20
          default: []
        metadata:
          type: object
          additionalProperties: true
          default: {}
          description: Arbitrary metadata key-value pairs
        validity_days:
          type: integer
          minimum: 1
          maximum: 3650

    RevokeCertificateRequest:
      type: object
      required: [reason]
      properties:
        reason:
          type: string
          minLength: 10
          maxLength: 500
          description: Reason for revocation

    # ── Response Models ──
    OrganizationResponse:
      type: object
      required:
        - id
        - name
        - slug
        - contact_email
        - verified
        - domain_verified
        - plan
        - usage
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        website:
          type: string
          format: uri
          nullable: true
        logo_url:
          type: string
          format: uri
          nullable: true
        contact_email:
          type: string
          format: email
        verified:
          type: boolean
        domain:
          type: string
          nullable: true
        domain_verified:
          type: boolean
        plan:
          type: string
          enum: [free, pro, business, enterprise]
        usage:
          type: object
          required: [certificates_this_month, month]
          properties:
            certificates_this_month:
              type: integer
            month:
              type: string
              example: "2025-07"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ApiKeyCreatedResponse:
      type: object
      required: [id, name, key, key_prefix, permissions, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key:
          type: string
          description: Full API key (shown only once)
        key_prefix:
          type: string
          description: First characters for identification
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/Permission"
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    ApiKeyListItem:
      type: object
      required: [id, name, key_prefix, permissions, is_active, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
        permissions:
          type: array
          items:
            $ref: "#/components/schemas/Permission"
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time

    TemplateResponse:
      type: object
      required:
        - id
        - organization_id
        - name
        - category
        - default_validity_days
        - is_active
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        criteria:
          type: object
          additionalProperties: true
          nullable: true
        category:
          $ref: "#/components/schemas/TemplateCategory"
        default_validity_days:
          type: integer
        badge_image_url:
          type: string
          format: uri
          nullable: true
        style_config:
          $ref: "#/components/schemas/StyleConfig"
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CertificateResponse:
      type: object
      required:
        - id
        - template_id
        - recipient_name
        - recipient_slug
        - recipient_email
        - verification_code
        - integrity_hash
        - has_vc_proof
        - status
        - evidences
        - metadata
        - issued_at
        - expires_at
        - claim_status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        template_id:
          type: string
          format: uuid
        issuer:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
        issuer_org_id:
          type: string
          format: uuid
          nullable: true
        recipient_name:
          type: string
        recipient_slug:
          type: string
        recipient_website:
          type: string
          format: uri
          nullable: true
        recipient_email:
          type: string
          format: email
        verification_code:
          type: string
        verification_url:
          type: string
          format: uri
        integrity_hash:
          type: string
        has_vc_proof:
          type: boolean
        status:
          $ref: "#/components/schemas/CertificateStatus"
        evidences:
          type: array
          items:
            $ref: "#/components/schemas/Evidence"
        metadata:
          type: object
          additionalProperties: true
        issued_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
          nullable: true
        revocation_reason:
          type: string
          nullable: true
        claim_status:
          $ref: "#/components/schemas/ClaimStatus"
        claim_token:
          type: string
          nullable: true
        claimed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AuditLogItem:
      type: object
      required:
        - id
        - organization_id
        - action
        - actor_type
        - actor_id
        - details
        - created_at
      properties:
        id:
          type: string
          format: uuid
        certificate_id:
          type: string
          format: uuid
          nullable: true
        organization_id:
          type: string
          format: uuid
        action:
          $ref: "#/components/schemas/AuditAction"
        actor_type:
          $ref: "#/components/schemas/ActorType"
        actor_id:
          type: string
        details:
          type: object
          additionalProperties: true
        ip_address:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    VerificationResult:
      type: object
      required: [status, valid, presentation_status, checks, certificate, issuer, recipient, template, vc_proof, verified_at]
      properties:
        status:
          $ref: "#/components/schemas/CertificateStatus"
        valid:
          type: boolean
          description: Backward-compatible lifecycle, integrity, and attached-proof validity. Use presentation_status for the aggregate public trust decision.
        presentation_status:
          type: string
          enum: [verified, unverified, expired, revoked]
          description: Aggregate public status derived from lifecycle, recipient claim, issuer domain, record integrity, and digital signature checks.
        checks:
          type: object
          required: [lifecycle_active, recipient_claimed, issuer_domain_verified, integrity_valid, signature_present, signature_valid]
          properties:
            lifecycle_active:
              type: boolean
            recipient_claimed:
              type: boolean
            issuer_domain_verified:
              type: boolean
            integrity_valid:
              type: boolean
            signature_present:
              type: boolean
            signature_valid:
              type: boolean
        certificate:
          type: object
          required: [id, verification_code, issued_at, expires_at, integrity_valid]
          properties:
            id:
              type: string
              format: uuid
            verification_code:
              type: string
            issued_at:
              type: string
              format: date-time
            expires_at:
              type: string
              format: date-time
            integrity_valid:
              type: boolean
        issuer:
          type: object
          required: [name, slug, verified]
          properties:
            name:
              type: string
            slug:
              type: string
            website:
              type: string
              format: uri
              nullable: true
            logo_url:
              type: string
              format: uri
              nullable: true
            verified:
              type: boolean
        recipient:
          type: object
          required: [name, slug]
          properties:
            name:
              type: string
            slug:
              type: string
            website:
              type: string
              format: uri
              nullable: true
        template:
          type: object
          required: [name, category]
          properties:
            name:
              type: string
            description:
              type: string
              nullable: true
            category:
              $ref: "#/components/schemas/TemplateCategory"
            criteria:
              type: object
              additionalProperties: true
              nullable: true
        vc_proof:
          type: object
          required: [signed, signature_valid]
          properties:
            signed:
              type: boolean
            signature_valid:
              type: boolean
            cryptosuite:
              type: string
              example: eddsa-jcs-2022
            verification_method:
              type: string
              format: uri
        verifiable_credential:
          $ref: "#/components/schemas/VerifiableCredential"
        verified_at:
          type: string
          format: date-time

    # ── W3C Verifiable Credentials 2.0 ──
    DataIntegrityProof:
      type: object
      required: [type, cryptosuite, created, verificationMethod, proofPurpose, proofValue]
      properties:
        type:
          type: string
          const: DataIntegrityProof
        cryptosuite:
          type: string
          const: eddsa-jcs-2022
        created:
          type: string
          format: date-time
        verificationMethod:
          type: string
          format: uri
          description: URL to the public key used for signing
        proofPurpose:
          type: string
          const: assertionMethod
        proofValue:
          type: string
          description: Multibase-encoded Ed25519 signature

    VerifiableCredential:
      type: object
      required: ["@context", type, id, issuer, validFrom, validUntil, credentialSubject, credentialStatus, proof]
      properties:
        "@context":
          type: array
          items:
            type: string
          example:
            - https://www.w3.org/ns/credentials/v2
            - https://w3id.org/security/data-integrity/v2
        type:
          type: array
          items:
            type: string
          example: [VerifiableCredential, CertisCredential]
        id:
          type: string
          format: uri
        issuer:
          type: object
          required: [id, name]
          properties:
            id:
              type: string
              format: uri
            name:
              type: string
            url:
              type: string
              format: uri
        validFrom:
          type: string
          format: date-time
        validUntil:
          type: string
          format: date-time
        credentialSubject:
          type: object
          required: [id, type, name, achievement]
          properties:
            id:
              type: string
              format: uri
            type:
              type: string
            name:
              type: string
            email:
              type: string
              format: email
            achievement:
              type: object
              required: [type, name]
              properties:
                type:
                  type: string
                name:
                  type: string
                description:
                  type: string
                  nullable: true
                criteria:
                  type: object
                  properties:
                    narrative:
                      type: string
                category:
                  $ref: "#/components/schemas/TemplateCategory"
        credentialStatus:
          type: object
          required: [type, id]
          properties:
            type:
              type: string
              const: CertisCredentialStatus2025
            id:
              type: string
              format: uri
        evidence:
          type: array
          items:
            type: object
            required: [type, name]
            properties:
              type:
                type: string
              name:
                type: string
              description:
                type: string
              id:
                type: string
                format: uri
        proof:
          $ref: "#/components/schemas/DataIntegrityProof"

    CredentialStatusResponse:
      type: object
      required: ["@context", type, id, status, statusPurpose, valid, issued_at, expires_at, checked_at]
      properties:
        "@context":
          type: array
          items:
            type: string
        type:
          type: string
          const: CertisCredentialStatus2025
        id:
          type: string
          format: uri
        status:
          $ref: "#/components/schemas/CertificateStatus"
        statusPurpose:
          type: string
          example: revocation
        valid:
          type: boolean
        issued_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
          nullable: true
        revocation_reason:
          type: string
          nullable: true
        checked_at:
          type: string
          format: date-time

    MultikeyObject:
      type: object
      required: ["@context", id, type, controller, publicKeyMultibase, status, created]
      properties:
        "@context":
          type: string
          const: https://w3id.org/security/multikey/v1
        id:
          type: string
          format: uri
        type:
          type: string
          const: Multikey
        controller:
          type: string
          format: uri
        publicKeyMultibase:
          type: string
          description: Multibase-encoded (z-base58btc) Ed25519 public key
        status:
          type: string
          enum: [active, rotated, revoked]
        created:
          type: string
          format: date-time
        rotated:
          type: string
          format: date-time
          nullable: true

    DIDDocument:
      type: object
      required: ["@context", id, verificationMethod, assertionMethod]
      properties:
        "@context":
          type: array
          items:
            type: string
          example:
            - https://www.w3.org/ns/did/v1
            - https://w3id.org/security/multikey/v1
        id:
          type: string
          format: uri
        verificationMethod:
          type: array
          items:
            $ref: "#/components/schemas/MultikeyObject"
        assertionMethod:
          type: array
          items:
            type: string
            format: uri
          description: Array of key IDs referenced in verificationMethod

    # ── Generic Wrappers (used in $ref for typed responses) ──
    SuccessResponse-SignupResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [organization, api_key, plan, domain_verification, signing_key]
          properties:
            organization:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                slug:
                  type: string
                domain:
                  type: string
                domain_verified:
                  type: boolean
                plan:
                  type: string
                contact_email:
                  type: string
                  format: email
                created_at:
                  type: string
                  format: date-time
            api_key:
              type: object
              required: [id, key, key_prefix, name, warning]
              properties:
                id:
                  type: string
                  format: uuid
                key:
                  type: string
                  description: Full API key (shown only once)
                key_prefix:
                  type: string
                name:
                  type: string
                warning:
                  type: string
            plan:
              type: object
              required: [name, certificates_total, templates]
              properties:
                name:
                  type: string
                  enum: [free, pro, business, enterprise]
                certificates_total:
                  type: integer
                templates:
                  type: integer
            domain_verification:
              type: object
              required: [token, host, deadline]
              properties:
                token:
                  type: string
                host:
                  type: string
                deadline:
                  type: string
                  format: date-time
            signing_key:
              type: object
              required: [id, public_key, algorithm]
              properties:
                id:
                  type: string
                  format: uuid
                public_key:
                  type: string
                  description: Multibase-encoded Ed25519 public key
                algorithm:
                  type: string
                  const: Ed25519

    SuccessResponse-ContactResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [id, status, message]
          properties:
            id:
              type: string
              format: uuid
            status:
              type: string
              enum: [pending]
            message:
              type: string

    SuccessResponse-DomainInitiateResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [domain, verification_record, instructions, token, host]
          properties:
            domain:
              type: string
            verification_record:
              type: string
            instructions:
              type: string
            token:
              type: string
            host:
              type: string

    SuccessResponse-DomainVerifyResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [verified]
          properties:
            verified:
              type: boolean
            domain:
              type: string
            domain_verified_at:
              type: string
              format: date-time

    SuccessResponse-OrganizationResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/OrganizationResponse"

    SuccessResponse-TemplateResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/TemplateResponse"

    SuccessResponse-CertificateResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/CertificateResponse"

    SuccessResponse-VerificationResult:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/VerificationResult"

    SuccessResponse-ApiKeyCreatedResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/ApiKeyCreatedResponse"

    SuccessResponse-MessageOnly:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [message]
          properties:
            message:
              type: string

    PaginatedResponse-ApiKeyListItem:
      type: object
      required: [success, data, pagination]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/ApiKeyListItem"
        pagination:
          $ref: "#/components/schemas/Pagination"

    PaginatedResponse-TemplateResponse:
      type: object
      required: [success, data, pagination]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/TemplateResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"

    PaginatedResponse-CertificateResponse:
      type: object
      required: [success, data, pagination]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/CertificateResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"

    PaginatedResponse-AuditLogItem:
      type: object
      required: [success, data, pagination]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: "#/components/schemas/AuditLogItem"
        pagination:
          $ref: "#/components/schemas/Pagination"
