# Generated from openapi.json — do not edit manually.
openapi: "3.0.3"
info:
  title: Verified.Tools API
  version: "1.0.0"
  contact:
    name: Verified.Tools Support
    url: "https://verified.tools/support"
    email: "support@verified.tools"
  license:
    name: Proprietary
    url: "https://verified.tools/terms"
  description: |
      ## Overview
      
      The Verified.Tools REST API provides programmatic access to document certification, verification, and audit features.
      
      Base URL: `https://api.verified.tools/api/v1`
      
      ## eIDAS Disclosure
      
      This API constitutes a Simple Electronic Signature (SES) platform under eIDAS Regulation (EU) 910/2014. All verification outputs are labelled "Platform-Verified Integrity Check" and do not constitute a Qualified Electronic Signature (QES) or Advanced Electronic Signature (AdES) as defined under eIDAS. QTSP/AdES integration is planned for a future release. Until that integration is live, relying parties should treat verification results as integrity evidence rather than legally binding electronic signatures under eIDAS Article 25.
      
      ## Authentication
      
      The API supports two authentication methods:
      
      1. **Bearer JWT** — Short-lived access token (1-hour TTL) issued at login. Pass in `Authorization: Bearer <token>` header.
      2. **API Key** — Long-lived key with the `vt_` prefix. Manage keys via the `/api-keys` endpoints. Pass in `Authorization: Bearer vt_<key>` header.
      
      Certain endpoints require **Step-up MFA** — the JWT must include a fresh `mfa_verified_at` claim (within 5 minutes). Obtain a step-up token via `POST /auth/step-up`.
      
      ## Rate Limiting
      
      All endpoints are rate-limited at the Cloudflare WAF layer. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. A `429 Too Many Requests` response is returned when the limit is exceeded.
      
      ## Errors
      
      All errors follow the envelope:
      
      ```json
      {
        "error": {
          "code": "ERROR_CODE",
          "message": "Human-readable description",
          "details": {}
        }
      }
      ```
      
      ## Pagination
      
      List endpoints use cursor-based pagination. The response includes a `next_cursor` field. Pass `?cursor=<value>` to retrieve the next page.
      
      ## Idempotency
      
      Document upload supports the `Idempotency-Key` header for safe retries. Pass a UUID v4 in the header; duplicate requests with the same key return the cached response.
servers:
-   url: "https://api.verified.tools/api/v1"
  description: Production
-   url: "https://api-staging.verified.tools/api/v1"
  description: Staging
security:
-   BearerJWT: []
-   ApiKey: []
components:
  securitySchemes:
    BearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Short-lived JWT access token (1-hour TTL). Obtain via POST /auth/login or POST /auth/register.
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: "Long-lived API key with `vt_` prefix. Create and manage via the /api-keys endpoints. Requires step-up MFA to create."
  schemas:
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: VALIDATION_ERROR
            message:
              type: string
              description: Human-readable description of the error.
              example: Request validation failed
            details:
              type: object
              additionalProperties: true
              description: Optional structured details (e.g. per-field validation errors).
    RegisterRequest:
      type: object
      required:
      - name
      - slug
      - domain
      - country_code
      - business_id_type
      - business_id_value
      - admin_email
      - admin_password
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 200
          example: Acme Engineering Pty Ltd
        slug:
          type: string
          minLength: 2
          maxLength: 63
          pattern: "^[a-z0-9-]+$"
          example: acme-engineering
        domain:
          type: string
          minLength: 4
          maxLength: 253
          example: acme.com.au
        country_code:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 country code.
          example: AU
        business_id_type:
          type: string
          minLength: 2
          maxLength: 20
          example: ABN
        business_id_value:
          type: string
          minLength: 4
          maxLength: 50
          example: "51824753556"
        admin_email:
          type: string
          format: email
          example: "admin@acme.com.au"
        admin_password:
          type: string
          minLength: 12
          format: password
          description: "Minimum 12 characters with at least one uppercase letter, one digit, and one special character."
    RegisterResponse:
      type: object
      properties:
        company_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        status:
          type: string
          example: PendingDnsVerification
        dns_txt_record:
          type: object
          properties:
            host:
              type: string
              example: _verified-tools
            value:
              type: string
              example: verified-tools-verify=abc123...
            ttl:
              type: integer
              example: 300
        jwt:
          type: string
          description: Initial access JWT.
        expires_at:
          type: integer
          description: Unix epoch seconds when JWT expires.
        message:
          type: string
    LoginRequest:
      type: object
      required:
      - email
      - password
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password
    LoginResponse:
      type: object
      properties:
        access_token:
          type: string
        refresh_token:
          type: string
        expires_in:
          type: integer
          example: 3600
        mfa_required:
          type: boolean
        token_type:
          type: string
          example: Bearer
    RefreshRequest:
      type: object
      required:
      - refresh_token
      properties:
        refresh_token:
          type: string
    RefreshResponse:
      type: object
      properties:
        access_token:
          type: string
        refresh_token:
          type: string
        expires_in:
          type: integer
          example: 3600
    StepUpRequest:
      type: object
      required:
      - code
      properties:
        code:
          type: string
          description: "6-digit TOTP code from authenticator app."
          example: "123456"
          minLength: 6
          maxLength: 6
    StepUpResponse:
      type: object
      properties:
        access_token:
          type: string
          description: New JWT with mfa_verified_at claim.
        expires_in:
          type: integer
          example: 3600
    VerifyDnsResponse:
      type: object
      properties:
        verified:
          type: boolean
        message:
          type: string
    DocumentSummary:
      type: object
      properties:
        document_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - Processing
          - Active
          - Revoked
          - Superseded
          - Failed
        original_filename:
          type: string
        content_type:
          type: string
        file_size_bytes:
          type: integer
        sha256_hash:
          type: string
        verification_url:
          type: string
          format: uri
        hmac_verification_id:
          type: string
        verifier_name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DocumentDetail:
      allOf:
      -         $ref: "#/components/schemas/DocumentSummary"
      -         type: object
        properties:
          download_url:
            type: string
            format: uri
            description: Time-limited signed R2 URL (1-hour TTL). Only present for Active documents.
          download_url_expires_at:
            type: string
            format: date-time
          timestamp_token:
            type: object
            properties:
              tsa:
                type: string
              serial:
                type: string
              timestamp:
                type: string
                format: date-time
    DocumentUploadResponse:
      type: object
      properties:
        document_id:
          type: string
          format: uuid
        hmac_verification_id:
          type: string
        status:
          type: string
          example: Processing
        verification_url:
          type: string
          format: uri
        message:
          type: string
    DocumentListResponse:
      type: object
      properties:
        documents:
          type: array
          items:
            $ref: "#/components/schemas/DocumentSummary"
        next_cursor:
          type: string
          nullable: true
          description: Opaque cursor for the next page. null if no more pages.
        total_count:
          type: integer
          description: Approximate total count.
    VerificationUrlResponse:
      type: object
      properties:
        verification_url:
          type: string
          format: uri
        hmac_verification_id:
          type: string
    RevokeRequest:
      type: object
      required:
      - reason
      properties:
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: Human-readable justification for revocation. Stored in audit log.
          example: Document contained an error
    RevokeResponse:
      type: object
      properties:
        document_id:
          type: string
          format: uuid
        status:
          type: string
          example: Revoked
        relationship_id:
          type: string
          format: uuid
        revoked_at:
          type: string
          format: date-time
    SupersedeRequest:
      type: object
      required:
      - new_document_id
      properties:
        new_document_id:
          type: string
          format: uuid
          description: UUID of the new (superseding) document. Must be Active and in the same company.
    SupersedeResponse:
      type: object
      properties:
        old_document_id:
          type: string
          format: uuid
        new_document_id:
          type: string
          format: uuid
        old_status:
          type: string
          example: Superseded
        relationship_id:
          type: string
          format: uuid
    AmendRequest:
      type: object
      required:
      - new_document_id
      properties:
        new_document_id:
          type: string
          format: uuid
          description: UUID of the new document that amends the old one.
    AmendResponse:
      type: object
      properties:
        old_document_id:
          type: string
          format: uuid
        new_document_id:
          type: string
          format: uuid
        relationship_id:
          type: string
          format: uuid
        relationship_type:
          type: string
          example: Amends
    ReferRequest:
      type: object
      required:
      - target_document_id
      properties:
        target_document_id:
          type: string
          format: uuid
          description: UUID of the document being referred to.
    ReferResponse:
      type: object
      properties:
        source_document_id:
          type: string
          format: uuid
        target_document_id:
          type: string
          format: uuid
        relationship_id:
          type: string
          format: uuid
        relationship_type:
          type: string
          example: Refers
    RelationshipsResponse:
      type: object
      properties:
        relationships:
          type: array
          items:
            type: object
            properties:
              relationship_id:
                type: string
                format: uuid
              relationship_type:
                type: string
                enum:
                - Supersedes
                - Amends
                - Refers
                - Revokes
              source_document_id:
                type: string
                format: uuid
              target_document_id:
                type: string
                format: uuid
              created_at:
                type: string
                format: date-time
    AuditEvent:
      type: object
      properties:
        event_id:
          type: string
          format: uuid
        company_id:
          type: string
          format: uuid
        actor_pseudonym:
          type: string
          description: HMAC pseudonym of the actor (GDPR pseudonymisation). Never a raw email address.
        action:
          type: string
          example: document.upload
          description: Event type / action code.
        resource_type:
          type: string
          example: Document
        resource_id:
          type: string
          format: uuid
        prev_hash:
          type: string
          nullable: true
          description: SHA-256 of previous event hash for chain verification.
        event_hash:
          type: string
          description: SHA-256 tamper-evident seal of this event.
        metadata:
          type: object
          additionalProperties: true
          nullable: true
        created_at:
          type: string
          format: date-time
    AuditListResponse:
      type: object
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/AuditEvent"
        next_cursor:
          type: string
          nullable: true
    PublicVerificationResult:
      type: object
      description: Result of a public document verification lookup.
      properties:
        verified:
          type: boolean
        company_name:
          type: string
        company_domain:
          type: string
        document_filename:
          type: string
        verification_level:
          type: integer
          minimum: 1
          maximum: 4
        verified_at:
          type: string
          format: date-time
        timestamp_token:
          type: object
          properties:
            tsa:
              type: string
            serial:
              type: string
            timestamp:
              type: string
              format: date-time
        eidas_disclosure:
          type: string
          example: Platform-Verified Integrity Check — not a Qualified Electronic Signature under eIDAS Regulation (EU) 910/2014.
    WebhookEndpoint:
      type: object
      properties:
        endpoint_id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
            enum:
            - document.processed
            - document.failed
            - document.revoked
            - document.superseded
            - document.amended
            - relationship.created
            - verification.viewed
            - billing.subscription_updated
            - billing.payment_failed
        is_active:
          type: boolean
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    WebhookRegisterRequest:
      type: object
      required:
      - url
      - events
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint URL to receive webhook deliveries. HTTP is rejected.
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum:
            - document.processed
            - document.failed
            - document.revoked
            - document.superseded
            - document.amended
            - relationship.created
            - verification.viewed
            - billing.subscription_updated
            - billing.payment_failed
        description:
          type: string
          maxLength: 255
          nullable: true
    WebhookRegisterResponse:
      type: object
      properties:
        endpoint_id:
          type: string
          format: uuid
        secret:
          type: string
          description: Base64url signing secret. Shown ONCE — store securely. Used to verify X-Verified-Tools-Signature header on deliveries.
    WebhookDelivery:
      type: object
      properties:
        delivery_id:
          type: string
          format: uuid
        endpoint_id:
          type: string
          format: uuid
        event_type:
          type: string
        status:
          type: string
          enum:
          - Pending
          - Delivered
          - Failed
          - Exhausted
        attempt_count:
          type: integer
        last_response_status:
          type: integer
          nullable: true
        next_retry_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    BillingPlan:
      type: object
      properties:
        id:
          type: string
          example: price_starter
        name:
          type: string
          example: Starter
        price_aud_cents:
          type: integer
          example: 4900
        document_limit:
          type: integer
          example: 50
        features:
          type: array
          items:
            type: string
    BillingSetupRequest:
      type: object
      required:
      - price_id
      properties:
        price_id:
          type: string
          description: Stripe Price ID of the selected plan.
          example: price_starter
    BillingSetupResponse:
      type: object
      properties:
        checkout_url:
          type: string
          format: uri
          description: Stripe-hosted checkout URL. Redirect the user to this URL.
    BillingPortalResponse:
      type: object
      properties:
        portal_url:
          type: string
          format: uri
          description: "Stripe Customer Portal URL. Short-lived, single-use."
    ApiKeySummary:
      type: object
      properties:
        key_id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          example: vt_abc123
        scopes:
          type: array
          items:
            type: string
        key_type:
          type: string
          enum:
          - Standard
          - MCP
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    ApiKeyCreateRequest:
      type: object
      required:
      - name
      - scopes
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: CI/CD Pipeline
        scopes:
          type: array
          minItems: 1
          items:
            type: string
            enum:
            - "documents:read"
            - "documents:write"
            - "audit:read"
            - "webhooks:read"
            - "webhooks:write"
            - "verify:read"
        key_type:
          type: string
          enum:
          - Standard
          - MCP
          default: Standard
    ApiKeyCreateResponse:
      type: object
      properties:
        key_id:
          type: string
          format: uuid
        api_key:
          type: string
          description: Full plaintext API key (shown ONCE). Store securely.
        key_prefix:
          type: string
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        key_type:
          type: string
    AddressPrediction:
      type: object
      properties:
        place_id:
          type: string
        description:
          type: string
        main_text:
          type: string
        secondary_text:
          type: string
    AddressAutocompleteResponse:
      type: object
      properties:
        predictions:
          type: array
          items:
            $ref: "#/components/schemas/AddressPrediction"
    AddressValidateRequest:
      type: object
      required:
      - address
      properties:
        address:
          type: object
          description: Address lines for validation via Google Address Validation API.
          properties:
            regionCode:
              type: string
            addressLines:
              type: array
              items:
                type: string
    AddressValidateResponse:
      type: object
      properties:
        result:
          type: object
          properties:
            verdict:
              type: object
              properties:
                inputGranularity:
                  type: string
                validationGranularity:
                  type: string
                geocodeGranularity:
                  type: string
                addressComplete:
                  type: boolean
            address:
              type: object
              additionalProperties: true
    AddressConfirmRequest:
      type: object
      required:
      - confirmed_address
      properties:
        confirmed_address:
          type: string
          description: The user-confirmed formatted address string.
    AddressConfirmResponse:
      type: object
      properties:
        verification_level:
          type: integer
          example: 3
        confirmed_at:
          type: string
          format: date-time
    PhoneSendRequest:
      type: object
      required:
      - phone
      properties:
        phone:
          type: string
          description: E.164 format phone number.
          example: +61412345678
    PhoneSendResponse:
      type: object
      properties:
        message:
          type: string
          example: OTP sent
        expires_in:
          type: integer
          example: 300
    PhoneConfirmRequest:
      type: object
      required:
      - phone
      - code
      properties:
        phone:
          type: string
          description: E.164 format phone number.
          example: +61412345678
        code:
          type: string
          description: "6-digit OTP."
          example: "123456"
          minLength: 6
          maxLength: 6
    PhoneConfirmResponse:
      type: object
      properties:
        verification_level:
          type: integer
          example: 2
        verified_at:
          type: string
          format: date-time
    BankStartResponse:
      type: object
      properties:
        client_secret:
          type: string
          description: Stripe Financial Connections client_secret for Stripe.js. Short-lived (15 min).
    BankConfirmRequest:
      type: object
      required:
      - financial_connections_account_id
      properties:
        financial_connections_account_id:
          type: string
          description: Stripe Financial Connections account ID returned by Stripe.js after user links account.
          example: fca_abc123
    BankConfirmResponse:
      type: object
      properties:
        verification_level:
          type: integer
          example: 4
        verified_at:
          type: string
          format: date-time
    TeamInvitation:
      type: object
      properties:
        invitation_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role_type:
          type: string
          enum:
          - Admin
          - Certifier
        expires_at:
          type: string
          format: date-time
        used_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    TeamInviteRequest:
      type: object
      required:
      - email
      - role_type
      properties:
        email:
          type: string
          format: email
        role_type:
          type: string
          enum:
          - Admin
          - Certifier
    TeamInviteResponse:
      type: object
      properties:
        invitation_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        expires_at:
          type: string
          format: date-time
    TeamMember:
      type: object
      properties:
        user_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role_type:
          type: string
          enum:
          - Admin
          - Certifier
        mfa_enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
    TeamMembersResponse:
      type: object
      properties:
        members:
          type: array
          items:
            $ref: "#/components/schemas/TeamMember"
  parameters:
    DocumentId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Document UUID.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        format: uuid
      description: Optional UUID v4 for idempotent retries. Duplicate requests with the same key return the cached 202 response.
    CursorParam:
      name: cursor
      in: query
      required: false
      schema:
        type: string
      description: Opaque pagination cursor from a previous response's next_cursor field.
  responses:
    Unauthorized:
      description: Authentication required or token invalid/expired.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: UNAUTHORIZED
              message: Missing or invalid authentication token
    Forbidden:
      description: Insufficient permissions or step-up MFA required.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: FORBIDDEN
              message: Step-up MFA required for this operation
    NotFound:
      description: Resource not found or not accessible to this account.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
    Conflict:
      description: "Resource conflict (e.g. duplicate, invalid state transition)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests. Please retry after the specified interval.
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: INTERNAL_ERROR
              message: An unexpected error occurred
    ValidationError:
      description: Request validation failed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: VALIDATION_ERROR
              message: Request validation failed
              details:
                email:
                - Invalid email address
tags:
-   name: Auth
  description: "Authentication, registration, and MFA operations."
-   name: Team
  description: Team invitation and member management.
-   name: Documents
  description: "Document upload, retrieval, and lifecycle management."
-   name: Relationships
  description: "Document relationship operations: revoke, supersede, amend, refer."
-   name: Audit
  description: Tamper-evident audit log query and export.
-   name: Verification
  description: Public document verification endpoint.
-   name: Webhooks
  description: "Webhook endpoint registration, testing, and delivery history."
-   name: Billing
  description: Subscription plan listing and Stripe checkout/portal.
-   name: API Keys
  description: Programmatic API key management.
-   name: Address
  description: Address autocomplete and validation (Google Maps Platform).
-   name: Phone Verify
  description: Level 2 phone verification via SMS OTP.
-   name: Bank Verify
  description: Level 4 bank account verification via Stripe Financial Connections.
paths:
  /auth/register:
    post:
      tags:
      - Auth
      summary: Register a new company
      description: "Creates a company and admin user, starts a 14-day trial, and returns DNS TXT verification instructions. The company enters `PendingDnsVerification` status until DNS is confirmed."
      operationId: registerCompany
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        201:
          description: Company and admin user created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegisterResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        409:
          description: "Email, slug, or domain already exists."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                email_exists:
                  value:
                    error:
                      code: EMAIL_EXISTS
                      message: An account with this email already exists
                slug_exists:
                  value:
                    error:
                      code: SLUG_EXISTS
                      message: This company slug is already taken
                domain_exists:
                  value:
                    error:
                      code: DOMAIN_EXISTS
                      message: This domain is already registered by an active company
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /auth/login:
    post:
      tags:
      - Auth
      summary: Authenticate with email and password
      description: "Validates credentials and returns a short-lived access JWT (1 hour) and a long-lived refresh token (30 days). If MFA is required, `mfa_required: true` is returned and the caller must POST to `/auth/step-up` with a TOTP code."
      operationId: loginUser
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        200:
          description: Authentication successful.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          description: Invalid credentials.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: INVALID_CREDENTIALS
                  message: Invalid email or password
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /auth/refresh:
    post:
      tags:
      - Auth
      summary: Refresh access token
      description: Exchanges a valid refresh token for a new access token and refresh token. The old refresh token is immediately invalidated (rotation).
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshRequest"
      responses:
        200:
          description: New tokens issued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RefreshResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          description: "Refresh token invalid, expired, or already rotated."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /auth/verify-dns:
    post:
      tags:
      - Auth
      summary: Trigger DNS TXT verification check
      description: "Manually triggers a DNS-over-HTTPS lookup to verify the company's domain TXT record. Rate limited to 5 calls/hour per company. On success, advances the company to `verification_level >= 1`."
      operationId: verifyDns
      security:
      -         BearerJWT: []
      responses:
        200:
          description: DNS check completed (verified or not).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyDnsResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /auth/step-up:
    post:
      tags:
      - Auth
      summary: Verify TOTP for step-up MFA
      description: "Accepts a 6-digit TOTP code from the user's authenticator app. Returns a new JWT with a fresh `mfa_verified_at` claim valid for 5 minutes. Required before sensitive operations (document upload, revocation, supersede, API key creation, billing changes)."
      operationId: stepUpMfa
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StepUpRequest"
      responses:
        200:
          description: TOTP verified. New JWT with mfa_verified_at claim issued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StepUpResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          description: TOTP code invalid or expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: INVALID_TOTP
                  message: Invalid or expired TOTP code
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /team/invitations:
    post:
      tags:
      - Team
      summary: Invite a team member
      description: "Sends an invitation email with a 32-byte crypto-random token (48-hour expiry, single-use) to the specified email address. Admin role required."
      operationId: inviteTeamMember
      security:
      -         BearerJWT: []
      -         ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TeamInviteRequest"
      responses:
        201:
          description: Invitation created and email queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamInviteResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        409:
          description: A pending invitation for this email already exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /team/members:
    get:
      tags:
      - Team
      summary: List team members
      description: Returns all active users in the authenticated company. Admin role required.
      operationId: listTeamMembers
      security:
      -         BearerJWT: []
      -         ApiKey: []
      responses:
        200:
          description: Team members listed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamMembersResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /documents:
    get:
      tags:
      - Documents
      summary: List documents
      description: Returns a cursor-paginated list of documents for the authenticated company. Page size is 50.
      operationId: listDocuments
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: status
        in: query
        schema:
          type: string
          enum:
          - Processing
          - Active
          - Revoked
          - Superseded
          - Failed
        description: Filter by document status.
      -         name: uploaded_after
        in: query
        schema:
          type: string
          format: date-time
        description: ISO 8601 date-time; return documents uploaded after this time.
      -         $ref: "#/components/parameters/CursorParam"
      responses:
        200:
          description: Documents listed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentListResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
    post:
      tags:
      - Documents
      summary: Upload a document
      description: "Uploads a document for verification processing. Accepts `multipart/form-data` with a `file` field. Supports idempotent retries via `Idempotency-Key` header. Returns `202 Accepted` immediately; processing is asynchronous."
      operationId: uploadDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
              - file
              properties:
                file:
                  type: string
                  format: binary
                  description: "Document file. Max 50 MB. Allowed types: PDF, PNG, JPEG, DOC, DOCX."
                filename:
                  type: string
                  description: Optional override filename. Falls back to Content-Disposition.
      responses:
        202:
          description: Upload accepted and queued for processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentUploadResponse"
        400:
          description: "Invalid file, MIME type not allowed, or Gateway scan failed."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        401:
          $ref: "#/components/responses/Unauthorized"
        402:
          description: Trial limit reached (document count or date).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: TRIAL_LIMIT_REACHED
                  message: Trial document limit exceeded
        403:
          $ref: "#/components/responses/Forbidden"
        409:
          description: Duplicate file (SHA-256 already uploaded by this company).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: DUPLICATE_FILE
                  message: A document with this file hash already exists
        422:
          description: File size exceeds 50 MB or MIME type not allowed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}":
    get:
      tags:
      - Documents
      summary: Get document details
      description: "Returns full document metadata. For Active documents, includes a time-limited (1-hour) signed R2 download URL."
      operationId: getDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      responses:
        200:
          description: Document details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentDetail"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
      - Documents
      summary: Delete a document
      description: Permanently deletes a document record and its R2 object. Only Processing or Failed documents can be deleted; Active/Revoked/Superseded documents must be revoked first. Requires step-up MFA.
      operationId: deleteDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      responses:
        200:
          description: Document deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: Document cannot be deleted in its current status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/verification-url":
    get:
      tags:
      - Documents
      summary: Get verification URL
      description: "Returns the public verification URL and HMAC verification ID for the document. The URL format is `https://verified.tools/verify/{hmacId}`."
      operationId: getVerificationUrl
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      responses:
        200:
          description: Verification URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificationUrlResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/revoke":
    post:
      tags:
      - Relationships
      summary: Revoke a document
      description: "Permanently revokes an Active document (terminal state). Creates an audit-trail `Revokes` relationship record. Requires step-up MFA. KV verification cache is invalidated immediately."
      operationId: revokeDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RevokeRequest"
      responses:
        200:
          description: Document revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RevokeResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: Document is not in Active status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/supersede":
    post:
      tags:
      - Relationships
      summary: Supersede a document
      description: "Records that a new document supersedes this document. The old document transitions from `Active` to `Superseded`. Both documents must be Active and belong to the same company. Requires step-up MFA."
      operationId: supersedeDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SupersedeRequest"
      responses:
        200:
          description: Supersession recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupersedeResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: One or both documents are not Active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/amend":
    post:
      tags:
      - Relationships
      summary: Amend a document
      description: "Records that a new document amends this document. Unlike supersession, amendment does not change either document's status — both remain Active. Requires step-up MFA."
      operationId: amendDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AmendRequest"
      responses:
        200:
          description: Amendment relationship recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AmendResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: One or both documents are not Active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/refer":
    post:
      tags:
      - Relationships
      summary: Create a reference between documents
      description: Records that this document refers to a target document. Both documents remain Active. Requires step-up MFA.
      operationId: referDocument
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReferRequest"
      responses:
        200:
          description: Reference relationship recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReferResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: One or both documents are not Active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/documents/{id}/relationships":
    get:
      tags:
      - Relationships
      summary: List document relationships
      description: Returns all relationship records (as source or target) for the specified document.
      operationId: listDocumentRelationships
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         $ref: "#/components/parameters/DocumentId"
      responses:
        200:
          description: Relationships listed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelationshipsResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /audit:
    get:
      tags:
      - Audit
      summary: Query audit log
      description: "Returns a paginated, filtered view of AuditEvent records for the authenticated company. Includes `prev_hash` and `event_hash` for client-side chain verification."
      operationId: queryAuditLog
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: date_from
        in: query
        schema:
          type: string
          format: date-time
        description: ISO 8601 — return events at or after this timestamp.
      -         name: date_to
        in: query
        schema:
          type: string
          format: date-time
        description: ISO 8601 — return events strictly before this timestamp.
      -         name: event_type
        in: query
        schema:
          type: string
        description: "Filter by action/event type (e.g. `document.upload`)."
      -         name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
      -         $ref: "#/components/parameters/CursorParam"
      responses:
        200:
          description: Audit events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuditListResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /audit/export.json:
    get:
      tags:
      - Audit
      summary: Export audit log as JSONL
      description: "Streams a JSONL (newline-delimited JSON) export of AuditEvent records. The hash chain is verified before returning; the final line includes `{\"_chain_verification\": {\"valid\": true|false, \"brokenAt\": N|null}}`."
      operationId: exportAuditJson
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: start
        in: query
        schema:
          type: string
          format: date-time
        description: Inclusive lower bound (created_ts).
      -         name: end
        in: query
        schema:
          type: string
          format: date-time
        description: Exclusive upper bound (created_ts).
      -         name: action
        in: query
        schema:
          type: string
        description: Exact-match filter on action/event type.
      -         name: resource_type
        in: query
        schema:
          type: string
        description: Exact-match filter on resource_type.
      -         name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1000
      responses:
        200:
          description: JSONL stream of audit events.
          headers:
            Content-Disposition:
              schema:
                type: string
              example: "attachment; filename=\"audit-{company_id}-2025-01-01.jsonl\""
          content:
            application/x-ndjson:
              schema:
                type: string
                format: binary
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /audit/export.pdf:
    get:
      tags:
      - Audit
      summary: Export audit log as PDF
      description: Returns a formatted PDF report of audit events for the authenticated company.
      operationId: exportAuditPdf
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: start
        in: query
        schema:
          type: string
          format: date-time
      -         name: end
        in: query
        schema:
          type: string
          format: date-time
      responses:
        200:
          description: PDF audit report.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/verify/{hmacId}":
    get:
      tags:
      - Verification
      summary: Verify a document (public)
      description: |
          Public endpoint — no authentication required. Verifies that a document was certified by the named company. Returns HTML by default; pass `Accept: application/json` for JSON. Rate limited to 100 requests/min per IP.
          
          **eIDAS Disclosure:** Results are labelled "Platform-Verified Integrity Check" and do not constitute a Qualified Electronic Signature under eIDAS Regulation (EU) 910/2014.
      operationId: verifyDocument
      security: []
      parameters:
      -         name: hmacId
        in: path
        required: true
        schema:
          type: string
          pattern: "^[A-Za-z0-9_-]{43}$"
        description: HMAC-SHA256 verification ID (43 base64url chars).
      responses:
        200:
          description: Document found and verified.
          content:
            text/html:
              schema:
                type: string
            application/json:
              schema:
                $ref: "#/components/schemas/PublicVerificationResult"
        404:
          description: "Document not found, revoked, or HMAC ID invalid."
          content:
            text/html:
              schema:
                type: string
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /webhooks:
    get:
      tags:
      - Webhooks
      summary: List webhook endpoints
      description: Returns all webhook endpoints (active and inactive) for the authenticated company. The signing secret is never included in list responses.
      operationId: listWebhooks
      security:
      -         BearerJWT: []
      -         ApiKey: []
      responses:
        200:
          description: Webhook endpoints listed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoints:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
    post:
      tags:
      - Webhooks
      summary: Register a webhook endpoint
      description: "Creates a new webhook endpoint. The signing secret is returned ONCE in the response — store it securely. Use the secret to verify the `X-Verified-Tools-Signature: sha256=<HMAC-SHA256(body, secret)>` header on incoming deliveries."
      operationId: registerWebhook
      security:
      -         BearerJWT: []
      -         ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookRegisterRequest"
      responses:
        201:
          description: Webhook endpoint created. Secret returned once.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookRegisterResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/webhooks/{id}":
    delete:
      tags:
      - Webhooks
      summary: Delete (deactivate) a webhook endpoint
      description: "Soft-deletes the webhook endpoint by setting `is_active = false`. Historical delivery records are retained for audit purposes."
      operationId: deleteWebhook
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Webhook endpoint UUID.
      responses:
        200:
          description: Endpoint deactivated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deactivated:
                    type: boolean
                    example: true
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: Endpoint already inactive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/webhooks/{id}/test":
    post:
      tags:
      - Webhooks
      summary: Send a test delivery
      description: Sends a test webhook delivery to the endpoint with a synthetic event payload. Useful for verifying endpoint connectivity and signature validation.
      operationId: testWebhook
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        200:
          description: Test delivery attempted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivery_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                    - Delivered
                    - Failed
                  response_status:
                    type: integer
                    nullable: true
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/webhooks/{id}/deliveries":
    get:
      tags:
      - Webhooks
      summary: List delivery history
      description: "Returns recent delivery attempts for the specified webhook endpoint, including status and retry schedule."
      operationId: listWebhookDeliveries
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      -         name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
      -         $ref: "#/components/parameters/CursorParam"
      responses:
        200:
          description: Delivery history.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deliveries:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
                  next_cursor:
                    type: string
                    nullable: true
        401:
          $ref: "#/components/responses/Unauthorized"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /billing/plans:
    get:
      tags:
      - Billing
      summary: List available subscription plans
      description: Returns available subscription plans. No authentication required — plan information is public.
      operationId: listBillingPlans
      security: []
      responses:
        200:
          description: Plans listed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  plans:
                    type: array
                    items:
                      $ref: "#/components/schemas/BillingPlan"
        500:
          $ref: "#/components/responses/InternalError"
  /billing/setup:
    post:
      tags:
      - Billing
      summary: Create Stripe Checkout Session
      description: Creates a Stripe Checkout Session for subscription setup and returns the checkout URL. Requires step-up MFA. Admin role required.
      operationId: billingSetup
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BillingSetupRequest"
      responses:
        200:
          description: Checkout session created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingSetupResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /billing/portal:
    get:
      tags:
      - Billing
      summary: Create Stripe Customer Portal Session
      description: Returns a Stripe Customer Portal URL for self-serve subscription management. Requires step-up MFA. Admin role required.
      operationId: billingPortal
      security:
      -         BearerJWT: []
      responses:
        200:
          description: Portal session created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingPortalResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        402:
          description: No Stripe customer ID — billing not yet set up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /api-keys:
    get:
      tags:
      - API Keys
      summary: List API keys
      description: "Returns all API key summaries for the company. Never returns key hashes or plaintext keys — only prefix, scopes, and metadata."
      operationId: listApiKeys
      security:
      -         BearerJWT: []
      parameters:
      -         name: include_revoked
        in: query
        schema:
          type: boolean
          default: true
      -         name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
      -         name: offset
        in: query
        schema:
          type: integer
          minimum: 0
          default: 0
      responses:
        200:
          description: API keys listed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKeySummary"
                  total:
                    type: integer
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
    post:
      tags:
      - API Keys
      summary: Create an API key
      description: Creates a new API key. The full key is returned ONCE in the response — store it securely. Requires step-up MFA.
      operationId: createApiKey
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApiKeyCreateRequest"
      responses:
        201:
          description: API key created. Full key returned once.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreateResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/api-keys/{id}":
    delete:
      tags:
      - API Keys
      summary: Revoke an API key
      description: Permanently revokes the API key. Cannot be undone. Requires step-up MFA.
      operationId: revokeApiKey
      security:
      -         BearerJWT: []
      parameters:
      -         name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: API key UUID.
      responses:
        200:
          description: API key revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                    example: true
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: API key already revoked.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  "/api-keys/{id}/rotate":
    post:
      tags:
      - API Keys
      summary: Rotate an API key
      description: "Atomically creates a new key (inheriting name, scopes, and key_type) and revokes the old key. New plaintext key returned once. Requires step-up MFA."
      operationId: rotateApiKey
      security:
      -         BearerJWT: []
      parameters:
      -         name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        201:
          description: New key created and old key revoked. New full key returned once.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreateResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        404:
          $ref: "#/components/responses/NotFound"
        409:
          description: API key already revoked (cannot rotate).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /address/autocomplete:
    get:
      tags:
      - Address
      summary: Address autocomplete
      description: "Proxies partial address strings to Google Places Autocomplete and returns filtered predictions. Pass a stable `session_token` UUID per autocomplete session for optimal Google billing."
      operationId: addressAutocomplete
      security:
      -         BearerJWT: []
      -         ApiKey: []
      parameters:
      -         name: input
        in: query
        required: true
        schema:
          type: string
          minLength: 3
          maxLength: 500
        description: Partial address string (3–500 chars).
      -         name: session_token
        in: query
        required: true
        schema:
          type: string
          format: uuid
        description: UUID v4 session token for Google Places billing grouping. Generate once per session.
      responses:
        200:
          description: Up to 5 address predictions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddressAutocompleteResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /address/validate:
    post:
      tags:
      - Address
      summary: Validate an address
      description: Validates an address via Google Address Validation API and returns geocoding and address component verdicts.
      operationId: validateAddress
      security:
      -         BearerJWT: []
      -         ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddressValidateRequest"
      responses:
        200:
          description: Address validation result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddressValidateResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /verify/address/confirm:
    post:
      tags:
      - Address
      summary: Confirm address for Level 3 verification
      description: "Records the user-confirmed postal address and advances the company to `verification_level = 3` (postal address verified)."
      operationId: confirmAddress
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddressConfirmRequest"
      responses:
        200:
          description: Address confirmed; verification level updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AddressConfirmResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /verify/phone/send:
    post:
      tags:
      - Phone Verify
      summary: Send phone verification OTP
      description: Sends a 6-digit OTP to the provided E.164 phone number via SMS. Rate limited to 3 sends per phone number per 24-hour window. OTP expires in 5 minutes.
      operationId: sendPhoneOtp
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PhoneSendRequest"
      responses:
        200:
          description: OTP sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneSendResponse"
        400:
          $ref: "#/components/responses/ValidationError"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /verify/phone/confirm:
    post:
      tags:
      - Phone Verify
      summary: Confirm phone OTP
      description: "Validates the 6-digit OTP and advances the company to `verification_level = 2` (phone verified)."
      operationId: confirmPhoneOtp
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PhoneConfirmRequest"
      responses:
        200:
          description: Phone verified; verification level updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneConfirmResponse"
        400:
          description: Invalid or expired OTP.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: INVALID_OTP
                  message: OTP is invalid or expired
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /verify/bank/start:
    post:
      tags:
      - Bank Verify
      summary: Start bank account verification
      description: "Creates a Stripe Financial Connections session and returns the `client_secret` for Stripe.js. The session is anchored to the company's Stripe customer. Session expires in 15 minutes."
      operationId: startBankVerify
      security:
      -         BearerJWT: []
      responses:
        200:
          description: Stripe Financial Connections client_secret returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BankStartResponse"
        401:
          $ref: "#/components/responses/Unauthorized"
        402:
          description: Company has no Stripe customer ID — billing setup required first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          $ref: "#/components/responses/NotFound"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"
  /verify/bank/confirm:
    post:
      tags:
      - Bank Verify
      summary: Confirm bank account verification
      description: "Validates the Stripe Financial Connections account ID obtained from Stripe.js and advances the company to `verification_level = 4` (bank account verified)."
      operationId: confirmBankVerify
      security:
      -         BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BankConfirmRequest"
      responses:
        200:
          description: Bank account verified; verification level updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BankConfirmResponse"
        400:
          description: Financial Connections account ID invalid or session expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        401:
          $ref: "#/components/responses/Unauthorized"
        429:
          $ref: "#/components/responses/TooManyRequests"
        500:
          $ref: "#/components/responses/InternalError"