openapi: 3.1.0
info:
  # Do not change the title, if the title changes, the import paths will be broken
  title: Api
  version: 0.1.0
  description: |
    ## RouxFinOfc — Official Letter Generation API

    Generate, review, approve, and deliver professional company letters via AI.

    ### Authentication

    **Tenant endpoints** (`/v1/*`) require an `X-API-Key` header. Obtain your key from the
    Settings page in the RouxFinOfc web app or ask your administrator.

    **Admin endpoints** (`/admin/*`) require an `X-Superadmin-Key` header. Contact your
    RouxFinOfc instance administrator.

    Click **Authorize** above to enter your key — it will be sent automatically with every
    request you make from this page.

    ### Base URL

    All paths below are relative to `/api`. If you are running locally the full URL is
    `http://localhost:{PORT}/api`.

servers:
  - url: /api
    description: Current server (relative path — works in any environment)

tags:
  - name: health
    description: Server health check
  - name: letters
    description: |
      Letter generation and lifecycle management.
      Generate AI-drafted letters, submit them for approval, then send to recipients.
  - name: templates
    description: |
      Reusable sender profiles and letter defaults.
      Save your sender details once and reuse them across all letter generations.
  - name: tenant
    description: Current tenant information and settings
  - name: billing
    description: Subscription plans, Stripe checkout, and monthly usage
  - name: dashboard
    description: Aggregated statistics for the current tenant
  - name: webhooks
    description: |
      Webhook configuration and delivery history.
      Register a default callback URL for your account, view delivery attempt history, and
      manually retry failed deliveries. Webhooks are dispatched when batch jobs complete or fail.
  - name: contract-signing
    description: |
      Contract signing pipeline — attach signing parties and national-ID verification to a
      contract letter PDF.

      National-ID images are processed **entirely in memory** and are **never persisted** to the
      database, logs, or object storage. Only a SHA-256 content hash is stored so future audits
      can confirm that an ID was presented without reconstructing the image.
  - name: admin
    description: |
      Superadmin operations — tenant management, API key issuance, license keys.
      Requires `X-Superadmin-Key` header.

# No global security — each operation declares its own requirement
security: []

paths:
  /healthz:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check
      description: 'Returns `{ "status": "ok" }` when the server is running.'
      security: []
      responses:
        "200":
          description: Server is healthy and the encryption key is configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                status: ok
                encryption_key: ok
        "503":
          description: Server is running but the encryption key is missing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                status: ok
                encryption_key: missing

  # ── Organisation / Auth ───────────────────────────────────────────────────

  /v1/org/register:
    post:
      operationId: registerOrg
      tags: [tenant]
      summary: Create a new organisation
      description: |
        Create a new tenant and admin account in one step. Starts a 14-day free trial.

        **Required:** `agreedToTerms: true` must be set. This confirms the registrant has
        accepted the Terms & Conditions and understands that generated content is
        AI-assisted and must be reviewed before use. Requests without it are rejected
        with HTTP 400.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [org_name, name, email, password, agreedToTerms]
              properties:
                org_name:
                  type: string
                  example: Acme Holdings Ltd.
                name:
                  type: string
                  example: Jane Smith
                email:
                  type: string
                  format: email
                  example: jane@acme.com
                password:
                  type: string
                  minLength: 8
                  example: "s3cur3P@ssw0rd"
                agreedToTerms:
                  type: boolean
                  enum: [true]
                  description: >
                    Must be `true`. Confirms acceptance of Terms & Conditions and
                    the AI-content disclaimer.
                  example: true
      responses:
        "201":
          description: Organisation and admin account created
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  user:
                    type: object
                  tenant:
                    type: object
                  api_key:
                    type: string
        "400":
          description: "`agreedToTerms` is missing or not `true`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/org/retention:
    get:
      operationId: getOrgRetention
      tags: [tenant]
      summary: Get retention settings
      description: Returns the current letter retention configuration and live usage summary. Admin only.
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Current retention settings and usage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgRetentionSettings"
    patch:
      operationId: updateOrgRetention
      tags: [tenant]
      summary: Update retention settings
      description: |
        Update letter retention settings. Enabling retention requires an enterprise or licensed plan.
        Admin only.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrgRetentionPatchInput"
      responses:
        "200":
          description: Updated retention settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgRetentionPatchResult"
        "403":
          description: Retention is an enterprise-only feature
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Invalid field values
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/org/accept-invite:
    post:
      operationId: acceptInvite
      tags: [tenant]
      summary: Accept a member invitation
      description: |
        Activate an invited account by setting a password.

        **Required:** `agreedToTerms: true` must be set. This confirms the new member
        accepts the Terms & Conditions and the AI-content disclaimer.
        Requests without it are rejected with HTTP 400.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invite_token, password, agreedToTerms]
              properties:
                invite_token:
                  type: string
                  example: "abc123def456..."
                password:
                  type: string
                  minLength: 8
                  example: "s3cur3P@ssw0rd"
                agreedToTerms:
                  type: boolean
                  enum: [true]
                  description: >
                    Must be `true`. Confirms acceptance of Terms & Conditions and
                    the AI-content disclaimer.
                  example: true
      responses:
        "200":
          description: Account activated and session token returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  user:
                    type: object
                  message:
                    type: string
        "400":
          description: "`agreedToTerms` is missing or not `true`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Invalid invite token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Invite token has expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Kacaroo AI Guide ──────────────────────────────────────────────────────

  /v1/kacaroo/message:
    post:
      operationId: kacarooMessage
      tags: [kacaroo]
      summary: Continue a Kacaroo coaching session
      description: |
        Starts or continues a tenant-scoped Kacaroo coaching session. The server
        persists the authoritative stage and payload; client history and stage
        values cannot advance the workflow. Sessions expire after 24 hours.

        Snake_case fields are the public contract. The existing mobile client
        may use the documented camelCase compatibility fields.
      security:
        - OrgJwtAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KacarooMessageRequest"
      responses:
        "200":
          description: Updated server-authoritative coaching state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KacarooMessageResponse"
        "403":
          description: Kacaroo is unavailable on the tenant plan or the session belongs to another user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Requested session has expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: A required user message or workflow prerequisite is missing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/kacaroo/send:
    post:
      operationId: kacarooSend
      tags: [kacaroo]
      summary: Send an approved letter after Kacaroo confirmation
      description: |
        Sends a letter only after the authenticated user has completed the
        server-authorized Kacaroo send stage. The same tenant ownership,
        approval, recipient, CC/BCC, suspicious-domain, tracking, and
        retention protections as normal email delivery apply.
      security:
        - OrgJwtAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KacarooSendRequest"
      responses:
        "200":
          description: Letter email was sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KacarooSendResponse"
        "403":
          description: The session or letter is not owned by the authenticated user, or the tenant lacks Kacaroo access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Session or letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The Kacaroo workflow has not authorized sending this letter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: The letter cannot be delivered or email recipient validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Letters ───────────────────────────────────────────────────────────────

  /v1/letters/generate:
    post:
      operationId: generateLetter
      tags: [letters]
      summary: Generate a letter
      description: |
        Generate a professionally formatted letter using AI. Provide structured sender /
        recipient details and a plain-language `summary` of what the letter should say.
        The AI expands the summary into a complete, formal letter body.

        Returns the letter object synchronously (status `generated`) including `html` and
        `pdf_base64` fields.

        **Plan limits** — Basic tenants may generate up to 10 letters per calendar month.
        A `429` response means the monthly limit has been reached.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LetterInput"
            example:
              sender:
                company_name: Acme Holdings Ltd
                contact_name: Sarah Mitchell
                email: sarah@acmeholdings.com
                phone: "+1-555-0100"
                address:
                  line1: 200 Park Avenue, Suite 4000
                  city: New York
                  postal_code: "10166"
                  country: US
              recipient:
                name: John Doe
                email: john.doe@email.com
                phone: "+1-555-0200"
                address:
                  line1: 45 West 34th Street, Apt 4B
                  city: New York
                  postal_code: "10001"
                  country: US
              letter:
                type: tenant_rent_payment_request
                format: expanded
                subject: "Notice: Outstanding Rent Payment — January 2026"
                summary: >
                  Notify tenant John Doe that his rent of $2,400 for Unit 4B is overdue
                  by 15 days. Request payment within 5 business days to avoid late fees.
                  This is his first late payment.
                language: en
                place_of_issue: "New York, NY"
              options:
                return_html: true
                return_pdf: true
                return_docx: false
                signature_mode: typed
      responses:
        "202":
          description: Letter generated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Monthly plan limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Monthly letter limit reached (10 letters on the Basic plan). Please upgrade your plan."

  /v1/letters/batch:
    post:
      operationId: batchGenerateLetters
      tags: [letters]
      summary: Batch generate letters (async)
      description: |
        Queue letters for up to **50 recipients** in a single call. Returns immediately with a
        `batch_job_id` and `202 Accepted`; generation runs in the background.

        Poll `GET /v1/letters/batch/{batchJobId}` to track progress and retrieve per-letter results.

        The `sender` and `letter` config are shared across all recipients; per-recipient
        `custom_fields` are merged on top of any letter-level `custom_fields` before being
        injected into the AI prompt.

        Plan limits are enforced **in aggregate** before the job is created — if the batch would
        push the tenant over the monthly cap a `429` is returned immediately without queuing any letters.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchLetterInput"
            example:
              sender:
                company_name: Greenfield Properties Inc
                contact_name: Mary Okonkwo
                email: mary@greenfield.com
                address:
                  line1: 14 Riverside Drive
                  city: Lagos
                  country: NG
              letter:
                type: rent_increase_notice
                format: expanded
                subject: Rent Increase Notice — Effective March 1, 2026
                summary: >
                  Inform each tenant of a 5% rent increase effective March 1 2026.
                  Be polite and reference their specific unit using the custom_fields provided.
                language: en
              options:
                return_html: true
                return_pdf: true
                signature_mode: typed
              recipients:
                - name: Alice Johnson
                  email: alice@email.com
                  address:
                    line1: 14 Riverside Drive, Unit 1A
                    city: Lagos
                    country: NG
                  custom_fields:
                    unit: "1A"
                    current_rent: "₦180,000"
                    new_rent: "₦189,000"
                - name: Bob Smith
                  email: bob@email.com
                  address:
                    line1: 14 Riverside Drive, Unit 2C
                    city: Lagos
                    country: NG
                  custom_fields:
                    unit: "2C"
                    current_rent: "₦210,000"
                    new_rent: "₦220,500"
      responses:
        "202":
          description: Job accepted — poll GET /v1/letters/batch/{batchJobId} for progress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJobAccepted"
        "422":
          description: Validation error (e.g. empty recipients array or batch too large)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Aggregate batch would exceed monthly plan limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Batch of 5 would exceed monthly limit (3 remaining on the Basic plan)."

  /v1/letters/batch/{batchJobId}:
    get:
      operationId: getBatchJob
      tags: [letters]
      summary: Get batch job status
      description: |
        Poll the status of a background batch generation job created by
        `POST /v1/letters/batch`.

        While the job is running, `status` is `processing` and `letters` contains
        results for recipients processed so far. When `status` is `completed` or
        `failed`, all recipients have been processed.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: batchJobId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Batch job status and per-letter results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJobStatus"
        "404":
          description: Batch job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters:
    get:
      operationId: listLetters
      tags: [letters]
      summary: List letters
      description: |
        Returns a paginated list of all letters for the current tenant.
        Supports filtering by `status` and `type`.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: page
          description: Page number (1-based)
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: limit
          description: Results per page (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - in: query
          name: status
          description: Filter by letter status
          schema:
            type: string
            enum:
              [
                draft,
                processing,
                generated,
                pending_approval,
                approved,
                rejected,
                sent,
                pending_caller_review,
                revising,
              ]
        - in: query
          name: type
          description: Filter by letter type (e.g. `tenant_rent_payment_request`)
          schema:
            type: string
      responses:
        "200":
          description: Paginated letter list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LetterListResponse"

  /v1/letters/{letterId}:
    get:
      operationId: getLetter
      tags: [letters]
      summary: Get a letter
      description: Returns a single letter including its full HTML, PDF, and approval event history.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Letter detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/submit:
    post:
      operationId: submitLetter
      tags: [letters]
      summary: Submit for approval
      description: |
        Move a `generated` (or `rejected`) letter into the `pending_approval` state.
        The in-app approval queue and mobile push notifications are triggered.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Letter is now pending approval
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Letter is not in a submittable state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/approve:
    post:
      operationId: approveLetter
      tags: [letters]
      summary: Approve a letter
      description: |
        Approve a `pending_approval` letter. If the recipient has an email address, the
        letter is automatically delivered and status advances to `sent`. Otherwise it
        remains `approved` and can be sent later via `/send-email`.

        **Required:** `disclaimerAcknowledged: true` must be included in the request body.
        This field confirms the caller has reviewed the letter and understands the platform
        accepts no liability for AI-generated content. Requests without it are rejected
        with HTTP 400.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovalInput"
            example:
              disclaimerAcknowledged: true
              comment: Reviewed and approved. Please send to the recipient.
      responses:
        "200":
          description: Letter approved (and delivered if email available)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "400":
          description: >
            `disclaimerAcknowledged` is missing or not `true`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Letter is not in an approvable state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/reject:
    post:
      operationId: rejectLetter
      tags: [letters]
      summary: Reject a letter
      description: |
        Reject a `pending_approval` letter with a mandatory comment explaining the reason.
        The letter moves to `rejected` status and can be re-submitted after corrections.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RejectionInput"
            example:
              comment: >
                The payment amount mentioned is incorrect — should be $2,400 not $2,000.
                Please regenerate with the correct figure.
      responses:
        "200":
          description: Letter rejected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Letter is not in a rejectable state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/review-response:
    post:
      operationId: reviewResponseLetter
      tags: [letters]
      summary: Respond to a caller review
      description: |
        Approve or request corrections on a letter that is pending caller review (`pending_caller_review`).
        - `approve`: triggers email delivery to the recipient and sets status to `sent`.
        - `revise`: re-generates the letter with the provided corrections and returns the revised content in the response (status stays `pending_caller_review`).
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewResponseInput"
            examples:
              approve:
                summary: Approve and send
                value:
                  action: approve
              revise:
                summary: Request corrections
                value:
                  action: revise
                  corrections: "Change the outstanding balance to $3,200. Use a firmer tone."
      responses:
        "200":
          description: Review response accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found or not owned by this tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Letter is not in pending_caller_review status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error (missing action, invalid action, or revise without corrections)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/send-email:
    post:
      operationId: sendLetterEmail
      tags: [letters]
      summary: Send letter via email
      description: |
        Manually send an `approved` letter to the recipient via Resend email.
        The letter must have an `html` body and the recipient must have an email address.
        Status advances to `sent` on success.

        Note: Approval auto-sends when an email is present — use this endpoint only
        if auto-send failed or was not triggered.

        If the recipient's email domain looks suspicious (unknown country-code TLD), the
        server returns `422 SUSPICIOUS_EMAIL` with a `warning` and optional `suggestion`.
        Re-send with `confirm_suspicious_email: true` to override and deliver anyway.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendLetterEmailBody"
      responses:
        "200":
          description: Email delivered and status set to sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Letter must be approved, have HTML content, and a recipient email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/letters/{letterId}/retry-pdf:
    post:
      operationId: retryPdf
      tags: [letters]
      summary: Retry PDF generation
      description: |
        Re-trigger PDF generation for a letter whose PDF failed or is missing.
        The letter must be in a `generated` or later status. Returns the updated letter
        with the regenerated `pdf_base64` field.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: PDF regenerated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Letter is not in a valid state for PDF regeneration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Contract Signing ──────────────────────────────────────────────────────

  /v1/letters/{letterId}/signatories:
    post:
      operationId: addSignatories
      tags: [contract-signing]
      summary: Add signatories to a contract letter
      description: |
        Attach one or more signing parties to a `contract` letter and optionally include a
        national-ID image for each party.

        The endpoint reads the existing letter PDF, appends a **Signatories** page listing
        all parties, and — for parties that provided an ID — appends a bordered
        **Identity Verification** appendix page containing the image (centred, watermarked
        "IDENTITY VERIFICATION COPY").

        **PII Safety** (GDPR Art. 5(1)(e) – data minimisation):
        National-ID images are used **only** to compute a SHA-256 content hash.  They are
        **never** embedded into the saved PDF, written to the database, sent to object
        storage, or included in any log.  Each image buffer is overwritten with zeros and
        dereferenced immediately after hashing — before PDF assembly begins — so raw
        biometric data is absent from memory during all subsequent operations.
        Only the SHA-256 hash is persisted as an audit token.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [parties]
              properties:
                parties:
                  type: string
                  description: |
                    JSON-encoded array of signatory party objects.
                    Each object must have `name`, `role`, `email`, `signing_date` (ISO 8601),
                    and `jurisdiction` fields.
                  example: '[{"name":"Alice Smith","role":"Buyer","email":"alice@example.com","signing_date":"2026-07-29","jurisdiction":"New York, US"},{"name":"Bob Jones","role":"Seller","email":"bob@example.com","signing_date":"2026-07-29","jurisdiction":"New York, US"}]'
                id_image_0:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 0 (JPEG or PNG, ≤ 5 MB). This file is processed in memory and never persisted."
                id_image_1:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 1. This file is processed in memory and never persisted."
                id_image_2:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 2. This file is processed in memory and never persisted."
                id_image_3:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 3. This file is processed in memory and never persisted."
                id_image_4:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 4. This file is processed in memory and never persisted."
                id_image_5:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 5. This file is processed in memory and never persisted."
                id_image_6:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 6. This file is processed in memory and never persisted."
                id_image_7:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 7. This file is processed in memory and never persisted."
                id_image_8:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 8. This file is processed in memory and never persisted."
                id_image_9:
                  type: string
                  format: binary
                  x-pii-ephemeral: true
                  description: "National-ID image for party 9. This file is processed in memory and never persisted."
      responses:
        "200":
          description: Signatories attached — returns the updated letter with refreshed PDF and signatories array
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Letter"
        "400":
          description: Letter is not of type `contract`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Letter belongs to a different tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: An ID image exceeds the 5 MB per-file limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Validation error (missing `parties` field, invalid JSON, invalid party data, or bad image magic bytes)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    get:
      operationId: getSignatories
      tags: [contract-signing]
      summary: Get signatories for a letter
      description: |
        Returns the stored signatory party metadata (names, roles, SHA-256 ID hashes) for a
        letter without re-fetching the full letter object.

        Note: national-ID images are never stored — only their SHA-256 hashes.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: letterId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Array of signatory records (empty array if no signatories have been added)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Signatory"
        "403":
          description: Letter belongs to a different tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Letter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Templates ─────────────────────────────────────────────────────────────

  /v1/templates:
    get:
      operationId: listTemplates
      tags: [templates]
      summary: List templates
      description: Returns all letter templates saved for the current tenant.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Array of templates
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Template"

    post:
      operationId: createTemplate
      tags: [templates]
      summary: Create a template
      description: |
        Save a reusable sender profile and letter defaults. On the Compose page, tenants
        can load a template to pre-fill sender details and letter type in one click.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateInput"
            example:
              name: Standard Rent Notice — Acme Properties
              description: Default template for rent-related notices sent from Acme Holdings
              sender:
                company_name: Acme Holdings Ltd
                contact_name: Sarah Mitchell
                email: sarah@acmeholdings.com
                phone: "+1-555-0100"
                address:
                  line1: 200 Park Avenue, Suite 4000
                  city: New York
                  postal_code: "10166"
                  country: US
              letter_defaults:
                type: tenant_rent_payment_request
                format: expanded
                language: en
                place_of_issue: "New York, NY"
      responses:
        "201":
          description: Template created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Template"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/templates/{templateId}:
    put:
      operationId: updateTemplate
      tags: [templates]
      summary: Update a template
      description: Update an existing letter template. All fields are optional — only provided fields are updated.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: templateId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateUpdate"
            example:
              name: Updated Rent Notice Template
              letter_defaults:
                language: fr
      responses:
        "200":
          description: Template updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Template"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      operationId: deleteTemplate
      tags: [templates]
      summary: Delete a template
      description: Permanently delete a letter template. Existing letters generated from this template are not affected.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: templateId
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Template deleted
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Tenant ────────────────────────────────────────────────────────────────

  /v1/tenant/me:
    get:
      operationId: getTenantMe
      tags: [tenant]
      summary: Get current tenant
      description: Returns information about the authenticated tenant including plan, masked API key, and creation date.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Tenant information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantInfo"
              example:
                id: tenant_acme_001
                name: Acme Holdings Ltd
                plan: licensed
                trial_active: false
                trial_ends_at: "2026-08-26T00:00:00Z"
                api_key_masked: "omk_acme_demo••••••••••••••••••••"
                created_at: "2026-01-15T09:00:00Z"

  # ── License Activation ────────────────────────────────────────────────────

  /v1/license/activate:
    post:
      operationId: activateLicense
      tags: [tenant]
      summary: Activate a license key
      description: |
        Activate a `lic_…` license key for the calling tenant. On success, the tenant's plan
        is immediately upgraded to `licensed` (unlimited letters, no monthly cap).
        The key can only be activated by one tenant. Re-activating with the same key by the
        same tenant is idempotent and returns success.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseActivateInput"
            example:
              license_key: lic_4a9f2c8d1b3e6f0a2c4d7e8f9b1c2d3e4f5a6b7c
      responses:
        "200":
          description: License activated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseActivateResult"
              example:
                success: true
                plan: licensed
                message: License activated. Unlimited plan is now active.
        "400":
          description: Invalid, already-activated, or revoked license key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Billing ───────────────────────────────────────────────────────────────

  /v1/billing/plans:
    get:
      operationId: listBillingPlans
      tags: [billing]
      summary: List billing plans
      description: Returns all available subscription plans with limits and pricing.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Array of billing plans
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/BillingPlan"

  /v1/billing/checkout:
    post:
      operationId: createBillingCheckout
      tags: [billing]
      summary: Create checkout session
      description: |
        Creates a Stripe Checkout session for the specified plan. Redirect the user to
        the returned `checkout_url` to complete payment.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckoutInput"
            example:
              plan_id: pro
              success_url: https://app.rouxfinofc.com/billing?upgraded=true
              cancel_url: https://app.rouxfinofc.com/billing
      responses:
        "200":
          description: Stripe checkout URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutResult"

  /v1/billing/portal:
    post:
      operationId: getBillingPortal
      tags: [billing]
      summary: Get billing portal URL
      description: Returns a Stripe Billing Portal URL for the current tenant to manage their subscription, payment method, and invoices.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Stripe portal URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortalResult"

  /v1/billing/usage:
    get:
      operationId: getBillingUsage
      tags: [billing]
      summary: Get billing usage
      description: |
        Returns letter generation usage for the active quota window. Active
        free trials use their whole-trial document allowance; other plans use
        the current calendar month.
        `letters_limit` is `-1` for unlimited plans (Enterprise, Licensed).
        `percentage_used` is `0` for unlimited plans.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Current period usage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingUsage"
              example:
                plan: pro
                trial_active: true
                trial_ends_at: "2026-08-26T00:00:00Z"
                letters_used: 2
                letters_limit: 5
                trial_document_limit: 5
                trial_documents_remaining: 3
                period_start: "2026-08-12T00:00:00Z"
                period_end: "2026-08-26T00:00:00Z"
                percentage_used: 40

  /v1/billing/subscription:
    get:
      operationId: getBillingSubscription
      tags: [billing]
      summary: Get subscription details
      description: |
        Returns the active Stripe subscription for the tenant (status, current billing period end,
        scheduled cancellation date, trial end). Returns `null` for the `subscription` field when
        the tenant is on the Basic plan and has no Stripe subscription.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Subscription details (or null for Basic-plan tenants)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingSubscription"
              example:
                subscription:
                  status: active
                  current_period_end: "2026-09-05T00:00:00Z"
                  cancel_at: null
                  trial_end: null

  # ── Dashboard ─────────────────────────────────────────────────────────────

  /v1/dashboard/stats:
    get:
      operationId: getDashboardStats
      tags: [dashboard]
      summary: Get dashboard statistics
      description: Returns aggregated letter counts, breakdowns by type and language, and recent activity for the current tenant.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Dashboard statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardStats"

  # ── Webhooks ──────────────────────────────────────────────────────────────

  /v1/webhooks/config:
    get:
      operationId: getWebhookConfig
      tags: [webhooks]
      summary: Get webhook configuration
      description: Returns the tenant's default callback URL. This URL is used for all batch jobs that don't include a per-request `callback_url`.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Current webhook configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookConfig"
              example:
                callback_url: "https://your-server.com/webhooks/rouxfinofc"

    put:
      operationId: updateWebhookConfig
      tags: [webhooks]
      summary: Set webhook configuration
      description: |
        Set or clear the tenant's default callback URL.

        When a batch job finishes, the server dispatches a signed `POST` to this URL containing
        the job result. Pass `null` to clear the URL and disable automatic callbacks.

        **Signature verification** — every request includes an `X-RouxFinOfc-Signature` header:
        ```
        X-RouxFinOfc-Signature: sha256=<hmac-sha256 of raw JSON body>
        ```
        The HMAC key is the first 12 characters of your API key. Verify it to authenticate the request.

        **Retry policy** — if the initial delivery fails (non-2xx or timeout ≤ 10 s), the server
        retries up to 4 more times with exponential back-off: 5 s, 25 s, 125 s, 625 s. After all
        retries are exhausted the delivery is marked `failed` but the batch job record is unaffected.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookConfigInput"
            example:
              callback_url: "https://your-server.com/webhooks/rouxfinofc"
      responses:
        "200":
          description: Configuration updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookConfig"
        "422":
          description: Invalid URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/webhooks/deliveries:
    get:
      operationId: listWebhookDeliveries
      tags: [webhooks]
      summary: List webhook delivery history
      description: |
        Returns a paginated list of every webhook delivery attempt for the current tenant,
        newest first. Includes both successful and failed attempts.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Paginated delivery history
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryListResponse"

  /v1/webhooks/deliveries/{deliveryId}/retry:
    post:
      operationId: retryWebhookDelivery
      tags: [webhooks]
      summary: Retry a failed webhook delivery
      description: |
        Manually dispatch a single delivery attempt for a previously failed webhook delivery.
        Only deliveries with `outcome: failed` can be retried. The retry is dispatched
        asynchronously — check the delivery history a few seconds later to see the new attempt.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: deliveryId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Retry dispatched
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                required:
                  - message
        "404":
          description: Delivery not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Delivery already succeeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Admin ─────────────────────────────────────────────────────────────────

  /admin/tenants:
    get:
      operationId: listAdminTenants
      tags: [admin]
      summary: List all tenants
      description: Returns all tenants in the system with letter and API key counts. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      responses:
        "200":
          description: Array of tenants
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdminTenant"
        "403":
          description: Invalid or missing superadmin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createAdminTenant
      tags: [admin]
      summary: Create a tenant
      description: Provision a new tenant workspace. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantInput"
            example:
              name: Greenfield Properties Inc
              plan: pro
      responses:
        "201":
          description: Tenant created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminTenant"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tenants/{tenantId}/stats:
    get:
      operationId: getAdminTenantStats
      tags: [admin]
      summary: Get tenant stats
      description: Returns usage statistics for a specific tenant. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: tenantId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Tenant statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminTenantStats"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tenants/{tenantId}/cost-by-model:
    get:
      operationId: getAdminTenantCostByModel
      tags: [admin]
      summary: Get per-model cost breakdown for a tenant
      description: Returns AI generation cost grouped by model for the current calendar month. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: tenantId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Array of per-model cost rows sorted by cost descending
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantCostByModelItem"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tenant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tenants/{tenantId}/keys:
    get:
      operationId: listAdminApiKeys
      tags: [admin]
      summary: List API keys for a tenant
      description: Returns all API keys (active and revoked) for a tenant. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: tenantId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Array of API keys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdminApiKey"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createAdminApiKey
      tags: [admin]
      summary: Create API key for a tenant
      description: |
        Creates a new API key for a tenant. The full key is returned **only once** — store
        it immediately. Only the prefix is stored in the database.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: tenantId
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApiKeyInput"
            example:
              name: Production API Key
              description: Main key for the website integration
      responses:
        "201":
          description: API key created — full key returned only once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminApiKeyCreated"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tenants/{tenantId}/keys/{keyId}:
    delete:
      operationId: deleteAdminApiKey
      tags: [admin]
      summary: Revoke an API key
      description: Permanently deletes an API key. Any requests using this key will immediately receive 401. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: tenantId
          required: true
          schema:
            type: string
        - in: path
          name: keyId
          required: true
          schema:
            type: string
      responses:
        "204":
          description: API key revoked
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Document Categories (public, org-auth) ────────────────────────────────

  /v1/document-categories:
    get:
      operationId: listDocumentCategories
      tags: [letters]
      summary: List active document categories
      description: |
        Returns active platform-level document categories with name, description,
        and required-fields schema. System prompts are not exposed to tenants.
        Pass a `categoryId` in `POST /v1/letters/generate` to apply a category's
        prompt override.
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Array of active document categories
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DocumentCategory"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Admin Document Categories ──────────────────────────────────────────────

  /admin/document-categories:
    get:
      operationId: listAdminDocumentCategories
      tags: [admin]
      summary: List all document categories (admin)
      description: Returns all document categories including inactive ones and system prompt text.
      security:
        - AdminKeyAuth: []
      responses:
        "200":
          description: Array of document categories
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdminDocumentCategory"
    post:
      operationId: createAdminDocumentCategory
      tags: [admin]
      summary: Create a document category
      security:
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: "Legal Notice"
                description:
                  type: ["string", "null"]
                system_prompt:
                  type: ["string", "null"]
                required_fields:
                  type: ["object", "null"]
                output_sections:
                  type: ["array", "null"]
                  items:
                    type: string
                sort_order:
                  type: integer
                  default: 0
                active:
                  type: boolean
                  default: true
              required:
                - name
      responses:
        "201":
          description: Document category created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminDocumentCategory"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/document-categories/{id}:
    get:
      operationId: getAdminDocumentCategory
      tags: [admin]
      summary: Get a document category (admin)
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Document category record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminDocumentCategory"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      operationId: updateAdminDocumentCategory
      tags: [admin]
      summary: Update a document category
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: ["string", "null"]
                system_prompt:
                  type: ["string", "null"]
                required_fields:
                  type: ["object", "null"]
                output_sections:
                  type: ["array", "null"]
                  items:
                    type: string
                sort_order:
                  type: integer
                active:
                  type: boolean
      responses:
        "200":
          description: Updated document category
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminDocumentCategory"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      operationId: deleteAdminDocumentCategory
      tags: [admin]
      summary: Delete a document category
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/document-categories/{id}/preview-prompt:
    get:
      operationId: previewAdminDocumentCategoryPrompt
      tags: [admin]
      summary: Preview effective system prompt for a category
      description: Returns the merged effective system prompt as the letter generator would construct it, with a sample jurisdiction note placeholder.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Prompt preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  effective_system_prompt:
                    type: string
                  category_system_prompt:
                    type: ["string", "null"]
                  required_fields:
                    type: ["object", "null"]
                  output_sections:
                    type: ["array", "null"]
                    items:
                      type: string
                required:
                  - id
                  - name
                  - effective_system_prompt
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Admin License Keys ────────────────────────────────────────────────────

  /admin/license-keys:
    get:
      operationId: listAdminLicenseKeys
      tags: [admin]
      summary: List all license keys
      description: Returns all issued license keys with masked prefix, activation status, and which tenant activated each. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      responses:
        "200":
          description: Array of license keys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LicenseKey"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      operationId: createAdminLicenseKey
      tags: [admin]
      summary: Generate a new license key
      description: |
        Generates a new unactivated `lic_…` license key. The **full key is returned only once**
        in this response — store it immediately. Subsequent requests only return the prefix.
        Requires superadmin key.
      security:
        - AdminKeyAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LicenseKeyInput"
            example:
              note: Acme Corp pilot license
      responses:
        "201":
          description: License key created — full key returned only once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseKeyCreated"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/letters/batch:
    post:
      operationId: adminBatchGenerateLetters
      tags: [admin]
      summary: Cross-tenant batch letter generation
      description: |
        Generate letters across multiple tenant accounts in one call. Each item in `batches[]`
        targets a specific tenant identified by `tenant_id`.

        Plan limits are enforced **per-tenant**. A limit hit on one tenant returns an inline error
        for that tenant's result and does **not** abort other tenants in the same request.

        Capped at **200 total letters** per call across all tenants.
      security:
        - AdminKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminBatchInput"
            example:
              batches:
                - tenant_id: tenant_acme_001
                  sender:
                    company_name: Acme Holdings Ltd
                    contact_name: Sarah Mitchell
                    email: sarah@acmeholdings.com
                    address:
                      line1: 200 Park Avenue
                      city: New York
                      country: US
                  letter:
                    type: formal_notice
                    subject: Annual Policy Update
                    summary: Inform the recipient of the annual policy changes.
                  recipients:
                    - name: Jane Roe
                      email: jane@example.com
                      address:
                        line1: 45 West 34th Street
                        city: New York
                        country: US
      responses:
        "202":
          description: Cross-tenant batch complete — per-tenant results with inline errors for plan-limit hits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminBatchResult"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden — missing or invalid superadmin key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/license-keys/{keyId}:
    delete:
      operationId: deleteAdminLicenseKey
      tags: [admin]
      summary: Revoke a license key
      description: |
        Revokes a license key so it can no longer be activated. Does **not** downgrade the
        tenant that already activated it — their plan stays `licensed`. Requires superadmin key.
      security:
        - AdminKeyAuth: []
      parameters:
        - in: path
          name: keyId
          required: true
          schema:
            type: string
      responses:
        "204":
          description: License key revoked
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: License key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Tenant API key. Obtain from the Settings page in the RouxFinOfc web app or ask your
        administrator to issue one via `POST /admin/tenants/{tenantId}/keys`.
        Format: `omk_<64 hex chars>`
    AdminKeyAuth:
      type: apiKey
      in: header
      name: X-Superadmin-Key
      description: |
        Superadmin API key. Set as the `SUPERADMIN_API_KEY` secret on the server.
        Required for all `/admin/*` endpoints.
    OrgJwtAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Authenticated organization-user session token.

  schemas:
    KacarooMessageRequest:
      type: object
      description: |
        Request body for a Kacaroo turn. `session_id`, `message`, `stage`, and
        `letter_id` are the public snake_case fields. `sessionId`, `userMessage`,
        `partialPayload`, and `letterId` are accepted for mobile compatibility.
        The server uses its persisted state instead of client history or stage
        to authorize a transition.
      properties:
        session_id:
          type: string
          description: Existing session ID. Omit to start a new session.
        message:
          type: string
          description: User's next message to Kacaroo.
        stage:
          type: string
          enum:
            [
              brief,
              recommend,
              confirm_recommendation,
              collect_fields,
              confirm_fields,
              generate,
              confirm_letter,
              offer_send,
              handoff_or_done,
            ]
          description: Accepted for compatibility; persisted server state is authoritative.
        letter_id:
          type: string
          description: Generated letter ID, accepted only when the server-authorized stage is `generate`.
        sessionId:
          type: string
          deprecated: true
          description: CamelCase compatibility alias for `session_id`.
        userMessage:
          type: string
          deprecated: true
          description: CamelCase compatibility alias for `message`.
        letterId:
          type: string
          deprecated: true
          description: CamelCase compatibility alias for `letter_id`.
        history:
          type: array
          deprecated: true
          description: Accepted for mobile compatibility but not used to decide state transitions.
          items:
            $ref: "#/components/schemas/KacarooTranscriptMessage"
        partialPayload:
          type: object
          deprecated: true
          additionalProperties: true
          description: Mobile compatibility payload. Only server-authorized values are retained.

    KacarooTranscriptMessage:
      type: object
      properties:
        role:
          type: string
          enum: [user, assistant]
        content:
          type: string
      required: [role, content]

    KacarooRecommendation:
      type: object
      properties:
        letter_type:
          type: string
        template_id:
          type: ["string", "null"]
        format:
          type: ["string", "null"]
        language:
          type: ["string", "null"]
        reason:
          type: ["string", "null"]
      required: [letter_type, template_id, format, language, reason]

    KacarooMessageResponse:
      type: object
      description: |
        Server-authoritative Kacaroo state. Snake_case fields are the public
        response contract; camelCase properties remain for the mobile client.
      properties:
        session_id:
          type: string
        reply:
          type: string
        stage:
          type: string
          enum:
            [
              brief,
              recommend,
              confirm_recommendation,
              collect_fields,
              confirm_fields,
              generate,
              confirm_letter,
              offer_send,
              handoff_or_done,
            ]
        recommendation:
          oneOf:
            - $ref: "#/components/schemas/KacarooRecommendation"
            - type: "null"
        fields:
          type: object
          additionalProperties: true
        letter_id:
          type: string
        letter_subject:
          type: string
        sessionId:
          type: string
          deprecated: true
        message:
          type: string
          deprecated: true
        nextStage:
          type: string
          deprecated: true
        updatedPayload:
          type: object
          deprecated: true
          additionalProperties: true
        action:
          type: ["string", "null"]
          deprecated: true
      required:
        [
          session_id,
          reply,
          stage,
          sessionId,
          message,
          nextStage,
          updatedPayload,
          action,
        ]

    KacarooSendRequest:
      type: object
      description: |
        `session_id` and `letter_id` are the public request fields. `sessionId`
        and `letterId` are accepted for mobile compatibility.
      properties:
        session_id:
          type: string
        letter_id:
          type: string
        cc:
          type: array
          items:
            type: string
            format: email
        bcc:
          type: array
          items:
            type: string
            format: email
        sessionId:
          type: string
          deprecated: true
        letterId:
          type: string
          deprecated: true
      anyOf:
        - required: [session_id, letter_id]
        - required: [sessionId, letterId]

    KacarooSendResponse:
      type: object
      properties:
        ok:
          type: boolean
        letter_id:
          type: string
        session_id:
          type: string
        recipient_email:
          type: string
          format: email
        resend_message_id:
          type: ["string", "null"]
        sent_at:
          type: string
          format: date-time
      required:
        [ok, letter_id, session_id, recipient_email, resend_message_id, sent_at]

    HealthStatus:
      type: object
      properties:
        status:
          type: string
          example: ok
        encryption_key:
          type: string
          enum: [ok, missing]
          description: >
            Whether the document encryption key is configured.
            `ok` means the key is present; `missing` means it is absent
            (the server will also return HTTP 503 in that case).
          example: ok
      required:
        - status
        - encryption_key

    Address:
      type: object
      properties:
        line1:
          type: string
          example: 200 Park Avenue, Suite 4000
        line2:
          type: ["string", "null"]
          example: null
        city:
          type: string
          example: New York
        state:
          type: ["string", "null"]
          example: NY
        postal_code:
          type: ["string", "null"]
          example: "10166"
        country:
          type: string
          example: US
      required:
        - line1
        - city
        - country

    SenderInfo:
      type: object
      properties:
        company_name:
          type: string
          example: Acme Holdings Ltd
        contact_name:
          type: string
          example: Sarah Mitchell
        phone:
          type: ["string", "null"]
          example: "+1-555-0100"
        email:
          type: string
          example: sarah@acmeholdings.com
        address:
          $ref: "#/components/schemas/Address"
      required:
        - company_name
        - contact_name
        - email
        - address

    RecipientInfo:
      type: object
      properties:
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: john.doe@email.com
        phone:
          type: ["string", "null"]
          example: "+1-555-0200"
        address:
          $ref: "#/components/schemas/Address"
      required:
        - name
        - email
        - address

    LetterRequest:
      type: object
      properties:
        type:
          type: string
          description: Category of letter. Determines tone, structure, and legal framing.
          enum:
            - official_correspondence
            - internal_memo
            - formal_notice
            - legal_notice
            - contract_termination
            - policy_update
            - tenant_rent_payment_request
            - rent_increase_notice
            - vacate_notice
            - eviction_notice
            - lease_renewal_offer
            - employment_offer
            - termination_letter
            - warning_letter
            - performance_review_summary
            - payment_reminder
            - invoice_cover_letter
            - refund_confirmation
            - custom_template
            - contract
            - business_plan
            - proposal
            - report
            - company_profile
            - policy_document
            - invoice
            - receipt
            - meeting_agenda
            - meeting_minutes
            - presentation
            - operational_document
            - research_paper
            - university_document
          example: tenant_rent_payment_request
        format:
          type: string
          description: Structural style of the letter body.
          enum: [expanded, brief, legal, notice, email_style, tenant_formal]
          default: expanded
          example: expanded
        subject:
          type: string
          maxLength: 500
          example: "Notice: Outstanding Rent Payment — January 2026"
        summary:
          type: string
          description: Plain-language description of what the letter should say. The AI expands this into a full professional letter body.
          maxLength: 1000
          example: >
            Notify tenant John Doe that his rent of $2,400 for Unit 4B is overdue by 15 days.
            Request payment within 5 business days to avoid late fees.
        reason:
          type: ["string", "null"]
          example: null
        description:
          type: ["string", "null"]
          maxLength: 2000
          example: null
        language:
          type: string
          description: BCP-47 language code for the letter body (e.g. en, fr, es, sw, ar)
          default: en
          example: en
        place_of_issue:
          type: ["string", "null"]
          example: "New York, NY"
        custom_fields:
          type: ["object", "null"]
          description: Arbitrary key-value pairs injected into the AI prompt for per-letter customization.
          additionalProperties:
            type: string
          example:
            unit_number: 4B
            outstanding_amount: "$2,400"
      required:
        - type
        - subject
        - summary

    LetterOptions:
      type: object
      properties:
        return_html:
          type: boolean
          default: true
          description: Include rendered HTML in the response
        return_pdf:
          type: boolean
          default: true
          description: Include base64-encoded PDF in the response
        return_docx:
          type: boolean
          default: false
          description: Include base64-encoded DOCX in the response
        print_friendly:
          type: boolean
          default: true
          description: Use black-and-white print-friendly styling
        include_company_logo:
          type: boolean
          default: false
        signature_mode:
          type: string
          enum: [typed, blank]
          default: typed
          description: "`typed` renders the contact name as a signature; `blank` leaves an empty line for physical signing"
        require_review:
          type: boolean
          default: false
          description: Force a caller review round even when a matching template exists

    LetterInput:
      type: object
      properties:
        template_id:
          type: string
          description: |
            Optional ID of a saved template (from `GET /v1/templates`). When supplied, the
            template's `sender` and `letter_defaults` fields are merged in as defaults; any
            fields provided directly in `sender` or `letter` override the template values.
            The template must belong to the same tenant, or a 404 is returned.
          example: "tpl_abc123"
        category_id:
          type: string
          description: |
            Optional ID of a document category (from `GET /v1/document-categories`).
            When supplied, the category's system prompt override is prepended to the generation
            prompt and its `required_fields` schema is merged into the generation context alongside
            jurisdiction rules. Missing output sections (as defined by the category's `output_sections`)
            are returned as `validation_warnings` in the response. Falls back to the default prompt
            when omitted (backward compatible).
          example: "cat_legal_notice_001"
        sender:
          $ref: "#/components/schemas/SenderInfo"
        recipient:
          $ref: "#/components/schemas/RecipientInfo"
        letter:
          $ref: "#/components/schemas/LetterRequest"
        options:
          $ref: "#/components/schemas/LetterOptions"
      required:
        - sender
        - recipient
        - letter

    ApprovalEvent:
      type: object
      properties:
        id:
          type: string
        action:
          type: string
          enum: [submitted, approved, rejected]
        comment:
          type: ["string", "null"]
        created_at:
          type: string
          format: date-time
      required:
        - id
        - action
        - created_at

    ReviewResponseInput:
      type: object
      properties:
        action:
          type: string
          enum: [approve, revise]
          description: "`approve` sends the letter to the recipient. `revise` submits corrections and returns a new version for review."
        corrections:
          type: ["string", "null"]
          description: Required when action is `revise`. Plain-language description of what to change.
      required:
        - action

    Signatory:
      type: object
      description: |
        A signing party on a contract letter.
        National-ID images are **never** stored — only the SHA-256 hash of the image is persisted
        for audit purposes (GDPR Art. 5(1)(e) data minimisation).
      properties:
        name:
          type: string
          description: Full legal name of the signing party
          example: Alice Smith
        role:
          type: string
          description: Role of the party in the contract (e.g. Buyer, Seller, Witness)
          example: Buyer
        email:
          type: string
          format: email
          description: Email address of the signing party
          example: alice@example.com
        signing_date:
          type: string
          format: date
          description: Date on which the party signs (ISO 8601)
          example: "2026-07-29"
        jurisdiction:
          type: string
          description: Legal jurisdiction governing the party's signing obligations
          example: "New York, US"
        id_image_hash:
          type: ["string", "null"]
          description: |
            SHA-256 hex digest of the national-ID image provided at signing time.
            Present when an ID image was submitted; null otherwise.
            The original image is never stored — this hash is the only persisted evidence
            that an identity document was presented.
          example: "a3f9b2c1d4e5f678901234567890abcdef1234567890abcdef1234567890abcd"
      required:
        - name
        - role
        - email
        - signing_date
        - jurisdiction

    Letter:
      type: object
      properties:
        id:
          type: string
          example: a3f9b2c1d4e5f678901234567890abcd
        template_id:
          type: ["string", "null"]
          description: ID of the template used when generating this letter, or null if no template was supplied.
          example: "tpl_abc123"
        status:
          type: string
          enum:
            [
              draft,
              processing,
              generated,
              pending_approval,
              approved,
              rejected,
              sent,
              pending_caller_review,
              revising,
            ]
          example: generated
        subject:
          type: string
          example: "Notice: Outstanding Rent Payment — January 2026"
        type:
          type: string
          example: tenant_rent_payment_request
        format:
          type: string
          example: expanded
        language:
          type: string
          example: en
        sender:
          oneOf:
            - $ref: "#/components/schemas/SenderInfo"
            - type: "null"
          description: Null when content has been purged by the retention policy
        recipient:
          oneOf:
            - $ref: "#/components/schemas/RecipientInfo"
            - type: "null"
          description: Null when content has been purged by the retention policy
        html:
          type: ["string", "null"]
          description: Full rendered HTML of the letter (present when status is generated or later)
        pdf_base64:
          type: ["string", "null"]
          description: Base64-encoded PDF of the letter
        docx_base64:
          type: ["string", "null"]
          description: Base64-encoded DOCX of the letter (only if `return_docx` was true)
        approval_events:
          type: array
          items:
            $ref: "#/components/schemas/ApprovalEvent"
        email_sent_at:
          type: ["string", "null"]
          format: date-time
        generated_at:
          type: ["string", "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        requires_review:
          type: boolean
          description: True when the letter requires caller review before it can be sent
        review_instructions:
          type: ["string", "null"]
          description: Instructions for the caller on how to respond to the review
        revision_number:
          type: integer
          description: Number of revisions that have been applied to this letter
        content_purged:
          type: boolean
          description: True when letter content (html, pdf, sender, recipient) has been purged per the retention policy
        signatories:
          type: ["array", "null"]
          description: |
            Signing parties attached to this contract letter.
            Present after `POST /v1/letters/{letterId}/signatories` has been called;
            null or absent on letters that have not yet been signed.
          items:
            $ref: "#/components/schemas/Signatory"
      required:
        - id
        - status
        - subject
        - type
        - format
        - language
        - sender
        - recipient
        - approval_events
        - created_at

    LetterListItem:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum:
            [
              draft,
              processing,
              generated,
              pending_approval,
              approved,
              rejected,
              sent,
              pending_caller_review,
              revising,
            ]
        subject:
          type: string
        type:
          type: string
        format:
          type: string
        language:
          type: string
        recipient_name:
          type: string
        created_at:
          type: string
          format: date-time
        generated_at:
          type: ["string", "null"]
          format: date-time
      required:
        - id
        - status
        - subject
        - type
        - format
        - language
        - recipient_name
        - created_at

    LetterListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/LetterListItem"
        total:
          type: integer
          example: 47
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total_pages:
          type: integer
          example: 3
      required:
        - items
        - total
        - page
        - limit
        - total_pages

    ApprovalInput:
      type: object
      required:
        - disclaimerAcknowledged
      properties:
        disclaimerAcknowledged:
          type: boolean
          enum: [true]
          description: >
            Must be `true`. Confirms that the caller has reviewed the letter and
            acknowledges that the platform accepts no liability for the accuracy,
            completeness, or legal standing of AI-generated content.
          example: true
        comment:
          type: ["string", "null"]
          example: Reviewed and approved. Please send to the recipient.
        confirm_suspicious_email:
          type: ["boolean", "null"]
          description: >
            When `true`, overrides a `SUSPICIOUS_EMAIL` warning returned by a
            previous approve attempt and sends the letter to the flagged recipient
            anyway. Omit (or set to `false`) on initial requests.
          example: true
        cc:
          type: array
          items:
            type: string
            format: email
          description: Optional CC email addresses to include when delivering the approved letter.
          example: ["manager@example.com"]
        bcc:
          type: array
          items:
            type: string
            format: email
          description: Optional BCC email addresses to include when delivering the approved letter.
          example: ["compliance@example.com"]

    SendLetterEmailBody:
      type: object
      properties:
        confirm_suspicious_email:
          type: ["boolean", "null"]
          description: >
            When `true`, overrides a `SUSPICIOUS_EMAIL` warning returned by a
            previous send-email attempt and sends the letter to the flagged
            recipient anyway. Omit (or set to `false`) on initial requests.
          example: true
        cc:
          type: array
          items:
            type: string
            format: email
          description: Optional CC email addresses to include when sending the letter.
          example: ["manager@example.com"]
        bcc:
          type: array
          items:
            type: string
            format: email
          description: Optional BCC email addresses to include when sending the letter.
          example: ["compliance@example.com"]

    RejectionInput:
      type: object
      properties:
        comment:
          type: string
          example: >
            The payment amount is incorrect — should be $2,400 not $2,000.
            Please regenerate with the correct figure.
      required:
        - comment

    TemplateDefaults:
      type: object
      properties:
        type:
          type: ["string", "null"]
          example: tenant_rent_payment_request
        format:
          type: ["string", "null"]
          example: expanded
        language:
          type: ["string", "null"]
          example: en
        place_of_issue:
          type: ["string", "null"]
          example: "New York, NY"
        custom_fields:
          type: ["object", "null"]
          additionalProperties:
            type: string
        sender_address_position:
          type: ["string", "null"]
          enum: [top_left, top_right, bottom_left, bottom_right, null]
          example: top_left
        recipient_address_position:
          type: ["string", "null"]
          enum: [top_left, top_right, bottom_left, bottom_right, null]
          example: top_left
        contract_subtype:
          type: ["string", "null"]
          description: Optional sub-type for letter types that support further classification (e.g. contract, legal_notice, eviction_notice, employment_offer, termination_letter, formal_notice).
          example: real_estate_property_management

    Template:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: ["string", "null"]
        sender:
          $ref: "#/components/schemas/SenderInfo"
        letter_defaults:
          $ref: "#/components/schemas/TemplateDefaults"
        pdf_template_url:
          type: ["string", "null"]
          description: Signed GET URL (valid 1 hour) for the attached letterhead PDF, or null if none.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - name
        - sender
        - letter_defaults
        - created_at
        - updated_at

    TemplateInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: ["string", "null"]
        sender:
          $ref: "#/components/schemas/SenderInfo"
        letter_defaults:
          $ref: "#/components/schemas/TemplateDefaults"
      required:
        - name
        - sender
        - letter_defaults

    TemplateUpdate:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: ["string", "null"]
        sender:
          $ref: "#/components/schemas/SenderInfo"
        letter_defaults:
          $ref: "#/components/schemas/TemplateDefaults"

    TenantInfo:
      type: object
      properties:
        id:
          type: string
          example: tenant_acme_001
        name:
          type: string
          example: Acme Holdings Ltd
        plan:
          type: string
          enum: [free, pro, enterprise, licensed]
          example: pro
        trial_active:
          type: boolean
          description: True if the tenant is currently within their 30-day free trial.
          example: false
        trial_ends_at:
          type: ["string", "null"]
          format: date-time
          description: ISO 8601 timestamp when the trial expires. Null if trial has already ended.
          example: "2026-08-26T00:00:00Z"
        api_key_masked:
          type: string
          example: "omk_acme_demo••••••••••••••••••••"
        created_at:
          type: string
          format: date-time
          example: "2026-01-15T09:00:00Z"
        is_rouxfin:
          type: boolean
          description: True when the authenticated tenant is the canonical RouxFin internal tenant.
          example: false
      required:
        - id
        - name
        - plan
        - trial_active
        - trial_ends_at
        - api_key_masked
        - created_at

    BillingPlan:
      type: object
      properties:
        id:
          type: string
          example: pro
        name:
          type: string
          example: Pro
        description:
          type: string
        letter_limit:
          type: integer
          description: Monthly letter generation limit. `-1` means unlimited.
          example: 100
        price_monthly:
          type: number
          example: 29
        features:
          type: array
          items:
            type: string
      required:
        - id
        - name
        - description
        - letter_limit
        - price_monthly
        - features

    CheckoutInput:
      type: object
      properties:
        plan_id:
          type: string
          enum: [free, pro, enterprise]
          example: free
        success_url:
          type: string
          example: https://app.rouxfinofc.com/billing?upgraded=true
        cancel_url:
          type: string
          example: https://app.rouxfinofc.com/billing
      required:
        - plan_id

    CheckoutResult:
      type: object
      properties:
        checkout_url:
          type: string
          description: Redirect the user to this URL to complete payment
      required:
        - checkout_url

    PortalResult:
      type: object
      properties:
        portal_url:
          type: string
          description: Stripe Billing Portal URL for the current tenant
      required:
        - portal_url

    BillingUsage:
      type: object
      properties:
        plan:
          type: string
          example: pro
        plan_display_name:
          type: string
          description: User-facing plan name. "Basic" for the free tier; matches the plan ID for all other tiers.
          example: Basic
        trial_active:
          type: boolean
          description: True if the tenant is currently within their free trial.
          example: false
        trial_ends_at:
          type: ["string", "null"]
          format: date-time
          description: ISO 8601 timestamp when the trial expires. Null if no active trial.
          example: null
        letters_used:
          type: integer
          example: 34
        letters_limit:
          type: integer
          description: Active trial allowance or monthly plan limit. `-1` for unlimited Licensed or Enterprise plans.
          example: 100
        trial_document_limit:
          type: ["integer", "null"]
          description: Platform-wide whole-trial document allowance while the free trial is active. Null outside an active trial.
          example: 5
        trial_documents_remaining:
          type: ["integer", "null"]
          description: Documents remaining in the active free-trial allowance. Null outside an active trial.
          example: 3
        period_start:
          type: string
          format: date-time
        period_end:
          type: string
          format: date-time
        percentage_used:
          type: number
          example: 34.0
        topups_this_month:
          type: integer
          description: Number of top-up packs purchased this billing month.
          example: 0
        topup_price_cents:
          type: ["integer", "null"]
          description: Price of one top-up pack in cents. Null if no top-up config for the plan.
          example: null
        topup_quantity:
          type: integer
          description: Letters per top-up pack for this plan.
          example: 10
        topups_total_cents:
          type: ["integer", "null"]
          description: Total cost of top-up packs purchased this month in cents. Null if no top-up config.
          example: null
      required:
        - plan
        - plan_display_name
        - trial_active
        - trial_ends_at
        - letters_used
        - letters_limit
        - trial_document_limit
        - trial_documents_remaining
        - period_start
        - period_end
        - percentage_used
        - topups_this_month
        - topup_price_cents
        - topup_quantity
        - topups_total_cents

    OrgRetentionSettings:
      type: object
      properties:
        retention_enabled:
          type: boolean
        retention_days:
          type: ["integer", "null"]
        retention_max_size_mb:
          type: ["integer", "null"]
        usage:
          type: object
          properties:
            retained_letter_count:
              type: integer
            total_size_mb:
              type: number
          required:
            - retained_letter_count
            - total_size_mb
      required:
        - retention_enabled
        - retention_days
        - retention_max_size_mb
        - usage

    OrgRetentionPatchInput:
      type: object
      properties:
        retention_enabled:
          type: boolean
        retention_days:
          type: integer
          minimum: 1
          maximum: 3650
        retention_max_size_mb:
          type: integer
          minimum: 1

    OrgRetentionPatchResult:
      type: object
      properties:
        retention_enabled:
          type: boolean
        retention_days:
          type: ["integer", "null"]
        retention_max_size_mb:
          type: ["integer", "null"]
      required:
        - retention_enabled
        - retention_days
        - retention_max_size_mb

    BillingSubscriptionDetails:
      type: object
      description: Active Stripe subscription fields.
      properties:
        status:
          type: string
          enum:
            - active
            - trialing
            - past_due
            - unpaid
            - canceled
            - incomplete
            - incomplete_expired
            - paused
          example: active
        current_period_end:
          type: ["string", "null"]
          format: date-time
          description: End of the current billing period (next renewal date). Null when Stripe does not provide a period-end timestamp.
          example: "2026-09-05T00:00:00Z"
        cancel_at:
          type: ["string", "null"]
          format: date-time
          description: If the subscription is scheduled to cancel, the timestamp when it will cancel. Null otherwise.
          example: null
        trial_end:
          type: ["string", "null"]
          format: date-time
          description: Timestamp when the trial ends. Null if not trialing.
          example: null
      required:
        - status
        - current_period_end
        - cancel_at
        - trial_end

    BillingSubscription:
      type: object
      description: Subscription details response.
      properties:
        subscription:
          oneOf:
            - $ref: "#/components/schemas/BillingSubscriptionDetails"
            - type: "null"
          description: Active Stripe subscription details, or null if the tenant is on the Basic plan with no subscription.
      required:
        - subscription

    LicenseActivateInput:
      type: object
      properties:
        license_key:
          type: string
          description: The full license key starting with `lic_`
          example: lic_4a9f2c8d1b3e6f0a2c4d7e8f9b1c2d3e4f5a6b7c
      required:
        - license_key

    LicenseActivateResult:
      type: object
      properties:
        success:
          type: boolean
          example: true
        plan:
          type: string
          example: licensed
        message:
          type: string
          example: License activated. Unlimited plan is now active.
      required:
        - success
        - plan
        - message

    LicenseKeyInput:
      type: object
      properties:
        note:
          type: ["string", "null"]
          description: Optional label to identify which customer this key is for
          example: Acme Corp pilot license

    LicenseKey:
      type: object
      properties:
        id:
          type: string
          example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
        key_prefix:
          type: string
          description: First 10 characters of the key (e.g. `lic_4a9f2c`)
          example: lic_4a9f2c
        note:
          type: ["string", "null"]
          example: Acme Corp pilot license
        is_active:
          type: boolean
          example: true
        tenant_id:
          type: ["string", "null"]
          description: Tenant that activated this key, or null if unused
          example: null
        redeemed_at:
          type: ["string", "null"]
          format: date-time
          example: null
        created_at:
          type: string
          format: date-time
      required:
        - id
        - key_prefix
        - note
        - is_active
        - tenant_id
        - redeemed_at
        - created_at

    LicenseKeyCreated:
      type: object
      properties:
        id:
          type: string
          example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
        key:
          type: string
          description: Full license key — shown only once. Store it immediately.
          example: lic_4a9f2c8d1b3e6f0a2c4d7e8f9b1c2d3e4f5a6b7c
        key_prefix:
          type: string
          example: lic_4a9f2c
        note:
          type: ["string", "null"]
          example: Acme Corp pilot license
        is_active:
          type: boolean
          example: true
        tenant_id:
          type: ["string", "null"]
          example: null
        created_at:
          type: string
          format: date-time
      required:
        - id
        - key
        - key_prefix
        - note
        - is_active
        - tenant_id
        - created_at

    LetterTypeCount:
      type: object
      properties:
        type:
          type: string
        count:
          type: integer
      required:
        - type
        - count

    RecentActivity:
      type: object
      properties:
        id:
          type: string
        subject:
          type: string
        status:
          type: string
        type:
          type: string
        created_at:
          type: string
          format: date-time
      required:
        - id
        - subject
        - status
        - type
        - created_at

    DashboardStats:
      type: object
      properties:
        total_letters:
          type: integer
        letters_this_month:
          type: integer
        pending_approval:
          type: integer
        approved:
          type: integer
        rejected:
          type: integer
        sent:
          type: integer
        by_type:
          type: array
          items:
            $ref: "#/components/schemas/LetterTypeCount"
        by_language:
          type: array
          items:
            $ref: "#/components/schemas/LetterTypeCount"
        recent_activity:
          type: array
          items:
            $ref: "#/components/schemas/RecentActivity"
      required:
        - total_letters
        - letters_this_month
        - pending_approval
        - approved
        - rejected
        - sent
        - by_type
        - by_language
        - recent_activity

    AdminTenant:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        plan:
          type: string
          enum: [free, pro, enterprise]
        letter_count:
          type: integer
        api_key_count:
          type: integer
        created_at:
          type: string
          format: date-time
        letters_this_month:
          type: integer
          description: Number of letters generated this calendar month
        cost_this_month_usd:
          type: string
          description: AI generation cost for metered letters this month (6 decimal places)
        revenue_this_month_usd:
          type: string
          description: Revenue from letters this month at the configured chargeback rate (6 decimal places)
        margin_percent:
          type: number
          nullable: true
          description: Gross margin percentage for this month, or null if no revenue
      required:
        - id
        - name
        - plan
        - letter_count
        - api_key_count
        - created_at
        - letters_this_month
        - cost_this_month_usd
        - revenue_this_month_usd

    TenantCostByModelItem:
      type: object
      properties:
        model:
          type: string
          description: LLM model name used for generation
        letter_count:
          type: integer
          description: Number of letters generated with this model this month
        cost_usd:
          type: string
          description: Total cost for this model this month (6 decimal places)
        prompt_tokens:
          type: integer
          description: Total prompt tokens consumed by this model this month
        completion_tokens:
          type: integer
          description: Total completion tokens consumed by this model this month
      required:
        - model
        - letter_count
        - cost_usd
        - prompt_tokens
        - completion_tokens

    AdminTenantStats:
      type: object
      properties:
        tenant_id:
          type: string
        tenant_name:
          type: string
        total_letters:
          type: integer
        letters_this_month:
          type: integer
        by_status:
          type: array
          items:
            $ref: "#/components/schemas/LetterTypeCount"
        by_type:
          type: array
          items:
            $ref: "#/components/schemas/LetterTypeCount"
      required:
        - tenant_id
        - tenant_name
        - total_letters
        - letters_this_month
        - by_status
        - by_type

    TenantInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          example: Greenfield Properties Inc
        plan:
          type: string
          enum: [free, pro, enterprise]
          default: free
          example: pro
      required:
        - name

    AdminApiKey:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        key_prefix:
          type: string
          description: First 12 characters of the key for identification
          example: omk_acme_demo
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: ["string", "null"]
          format: date-time
      required:
        - id
        - name
        - key_prefix
        - is_active
        - created_at

    AdminApiKeyCreated:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        key:
          type: string
          description: Full API key — shown only once, store it immediately
          example: omk_4a9f2c8d1b3e5f7a9c0d2e4f6a8b0c1d2e4f6a8b0c1d2e4f6a8b0c1d2e4f6a8b
        key_prefix:
          type: string
          example: omk_4a9f2c
        created_at:
          type: string
          format: date-time
      required:
        - id
        - name
        - key
        - key_prefix
        - created_at

    ApiKeyInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          example: Production API Key
        description:
          type: ["string", "null"]
          example: Main key for the website integration
      required:
        - name

    BatchRecipient:
      type: object
      description: A single recipient in a batch letter generation request. The `custom_fields` are merged on top of any letter-level `custom_fields` before being passed to the AI prompt.
      properties:
        name:
          type: string
          example: Alice Johnson
        email:
          type: string
          format: email
          example: alice@email.com
        phone:
          type: ["string", "null"]
          example: null
        address:
          $ref: "#/components/schemas/Address"
        custom_fields:
          type: ["object", "null"]
          description: Per-recipient key-value pairs merged with letter-level custom_fields before AI generation.
          additionalProperties:
            type: string
          example:
            unit: "1A"
            current_rent: "₦180,000"
            new_rent: "₦189,000"
      required:
        - name
        - email
        - address

    BatchLetterInput:
      type: object
      properties:
        template_id:
          type: string
          description: |
            Optional ID of a saved template (from `GET /v1/templates`). When supplied, the
            template's `sender` and `letter_defaults` fields are merged in as defaults for all
            recipients in this batch. Request-level `sender` and `letter` fields override the
            template. The template must belong to the same tenant, or a 404 is returned.
          example: "tpl_abc123"
        callback_url:
          type: ["string", "null"]
          format: uri
          description: |
            Optional URL to POST a signed webhook when the batch job reaches `completed` or `failed`.
            Overrides the tenant-level default webhook URL (`PUT /v1/webhooks/config`) if both are set.
            The request includes an `X-RouxFinOfc-Signature: sha256=<hmac>` header for verification.
          example: "https://your-server.com/webhooks/rouxfinofc"
        sender:
          $ref: "#/components/schemas/SenderInfo"
        letter:
          $ref: "#/components/schemas/LetterRequest"
        options:
          $ref: "#/components/schemas/LetterOptions"
        recipients:
          type: array
          description: List of recipients (1–50). One letter is generated per recipient.
          minItems: 1
          maxItems: 50
          items:
            $ref: "#/components/schemas/BatchRecipient"
      required:
        - sender
        - letter
        - recipients

    BatchJobAccepted:
      type: object
      description: Returned immediately when a batch job is accepted. Poll GET /v1/letters/batch/{batchJobId} for progress.
      properties:
        batch_job_id:
          type: string
          description: Unique ID of the background batch job
          example: 3f1e9a2b4c8d5e6f
        template_id:
          type: ["string", "null"]
          description: ID of the template used for this batch, or null if no template was supplied.
          example: "tpl_abc123"
        status:
          type: string
          enum: [pending]
          example: pending
        total:
          type: integer
          description: Number of recipients queued
          example: 5
        message:
          type: string
          example: "Batch job queued. Poll GET /v1/letters/batch/3f1e9a2b4c8d5e6f for progress."
      required:
        - batch_job_id
        - status
        - total
        - message

    BatchJobStatus:
      type: object
      description: Current state of a background batch generation job.
      properties:
        batch_job_id:
          type: string
          example: 3f1e9a2b4c8d5e6f
        status:
          type: string
          enum: [pending, processing, completed, failed]
          example: processing
        total:
          type: integer
          description: Total number of recipients in this job
          example: 5
        succeeded:
          type: integer
          description: Number of letters successfully generated so far
          example: 3
        failed:
          type: integer
          description: Number of letters that failed generation so far
          example: 0
        letters:
          type: array
          description: Per-recipient results for processed recipients (in input order)
          items:
            $ref: "#/components/schemas/BatchLetterResultItem"
        created_at:
          type: string
          format: date-time
        completed_at:
          type: ["string", "null"]
          format: date-time
      required:
        - batch_job_id
        - status
        - total
        - succeeded
        - failed
        - letters
        - created_at

    BatchLetterResultItem:
      type: object
      description: Result for a single recipient in a batch generation call.
      properties:
        id:
          type: string
          description: Letter ID (present even on failure — the record is created in draft state)
          example: abc123def456
        status:
          type: string
          enum: [generated, failed]
          example: generated
        recipient_name:
          type: string
          example: Alice Johnson
        error:
          type: ["string", "null"]
          description: Error message when status is failed
          example: null
      required:
        - id
        - status
        - recipient_name

    BatchLetterResult:
      type: object
      properties:
        total:
          type: integer
          description: Number of recipients in the request
          example: 2
        succeeded:
          type: integer
          description: Number of letters successfully generated
          example: 2
        failed:
          type: integer
          description: Number of letters that failed generation
          example: 0
        letters:
          type: array
          description: Per-recipient results in the same order as the input recipients array
          items:
            $ref: "#/components/schemas/BatchLetterResultItem"
      required:
        - total
        - succeeded
        - failed
        - letters

    AdminBatchItem:
      type: object
      description: A single tenant batch within an admin cross-tenant batch request.
      properties:
        tenant_id:
          type: string
          description: The tenant to generate letters for
          example: tenant_acme_001
        sender:
          $ref: "#/components/schemas/SenderInfo"
        letter:
          $ref: "#/components/schemas/LetterRequest"
        options:
          $ref: "#/components/schemas/LetterOptions"
        recipients:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/BatchRecipient"
      required:
        - tenant_id
        - sender
        - letter
        - recipients

    AdminBatchInput:
      type: object
      properties:
        batches:
          type: array
          description: One entry per tenant. Total letters across all batches must not exceed 200.
          minItems: 1
          items:
            $ref: "#/components/schemas/AdminBatchItem"
      required:
        - batches

    AdminBatchTenantResult:
      type: object
      description: Per-tenant result in a cross-tenant batch generation response.
      properties:
        tenant_id:
          type: string
          example: tenant_acme_001
        succeeded:
          type: integer
          example: 1
        failed:
          type: integer
          example: 0
        letters:
          type: array
          items:
            $ref: "#/components/schemas/BatchLetterResultItem"
        error:
          type: ["string", "null"]
          description: Set when the entire tenant batch was rejected (e.g. plan limit hit) before any letter was generated
          example: null
      required:
        - tenant_id
        - succeeded
        - failed

    AdminBatchResult:
      type: object
      properties:
        total_tenants:
          type: integer
          description: Number of tenant batches in the request
          example: 2
        total_letters:
          type: integer
          description: Total letters generated (succeeded + failed) across all tenants
          example: 3
        results:
          type: array
          items:
            $ref: "#/components/schemas/AdminBatchTenantResult"
      required:
        - total_tenants
        - total_letters
        - results

    WebhookConfig:
      type: object
      description: The tenant's default webhook callback URL.
      properties:
        callback_url:
          type: ["string", "null"]
          format: uri
          example: "https://your-server.com/webhooks/rouxfinofc"
      required:
        - callback_url

    WebhookConfigInput:
      type: object
      properties:
        callback_url:
          type: ["string", "null"]
          format: uri
          description: URL to receive webhook callbacks. Pass `null` to clear.
          example: "https://your-server.com/webhooks/rouxfinofc"
      required:
        - callback_url

    WebhookDelivery:
      type: object
      description: A single webhook delivery attempt.
      properties:
        id:
          type: string
          example: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
        batch_job_id:
          type: string
          example: 3f1e9a2b4c8d5e6f
        callback_url:
          type: string
          format: uri
          example: "https://your-server.com/webhooks/rouxfinofc"
        attempt:
          type: integer
          description: Delivery attempt number (1 = initial, 2–5 = retries)
          example: 1
        http_status:
          type: ["integer", "null"]
          description: HTTP status code returned by the remote server, or null on timeout/network error
          example: 200
        response_snippet:
          type: ["string", "null"]
          description: First 500 characters of the remote response body
          example: '{"ok":true}'
        outcome:
          type: string
          enum: [success, failed]
          example: success
        created_at:
          type: string
          format: date-time
      required:
        - id
        - batch_job_id
        - callback_url
        - attempt
        - http_status
        - response_snippet
        - outcome
        - created_at

    WebhookDeliveryListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        total:
          type: integer
          example: 12
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total_pages:
          type: integer
          example: 1
      required:
        - items
        - total
        - page
        - limit
        - total_pages

    WebhookPayload:
      type: object
      description: |
        JSON body sent to your callback URL when a batch job finishes.
        Verify authenticity by checking `X-RouxFinOfc-Signature: sha256=<hmac>` — HMAC-SHA256
        of the raw JSON body, keyed with the first 12 characters of your API key.
      properties:
        event:
          type: string
          enum: [batch.completed, batch.failed]
          example: batch.completed
        batch_job_id:
          type: string
          example: 3f1e9a2b4c8d5e6f
        tenant_id:
          type: string
          example: tenant_acme_001
        status:
          type: string
          enum: [completed, failed]
          example: completed
        total:
          type: integer
          example: 5
        succeeded:
          type: integer
          example: 5
        failed:
          type: integer
          example: 0
        completed_at:
          type: string
          format: date-time
        letters:
          type: array
          items:
            $ref: "#/components/schemas/BatchLetterResultItem"
      required:
        - event
        - batch_job_id
        - tenant_id
        - status
        - total
        - succeeded
        - failed
        - completed_at
        - letters

    DocumentCategory:
      type: object
      description: A platform-level document category that provides a system-prompt override and field schema for letter generation.
      properties:
        id:
          type: string
          example: "cat_legal_notice_001"
        name:
          type: string
          example: "Legal Notice"
        description:
          type: ["string", "null"]
          example: "Formal legal communications asserting rights or obligations."
        required_fields:
          type: ["object", "null"]
          description: |
            JSON schema describing additional input fields required for this category.
            Shape: `{ properties: { [key]: { type, label, placeholder?, required? } } }`
          example:
            properties:
              claim_amount:
                type: string
                label: "Claim Amount"
                placeholder: "$5,000"
                required: true
        output_sections:
          type: ["array", "null"]
          description: Expected section names in the generated output. Missing sections are returned as validation_warnings.
          items:
            type: string
          example: ["greeting", "body", "demand", "closing"]
        sort_order:
          type: integer
          example: 0
      required:
        - id
        - name
        - sort_order

    AdminDocumentCategory:
      type: object
      description: Full document category record including system prompt (admin-only).
      allOf:
        - $ref: "#/components/schemas/DocumentCategory"
      properties:
        system_prompt:
          type: ["string", "null"]
          description: System prompt fragment prepended to the base generator prompt.
          example: "When writing legal notices, use formal legal language and cite relevant statutes where applicable."
        active:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time
      required:
        - active
        - created_at

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: Invalid or inactive API key
        details:
          type: ["object", "null"]
      required:
        - error
