openapi: 3.0.3
info:
  title: Deckrun API
  version: 1.0.0
  description: |
    REST API for Deckrun: markdown in, PDF/video/notes out, with optional personalized voice narration.
    Schema-first; supports sync and async generation, slide themes, and provider catalog.

    **Public contract:** This OpenAPI specification is the sole public-facing API contract. Request and response shapes are in **components/schemas**; discovery endpoints (GET /schema, GET /slide-themes/schema, GET /voices/schema) return the **public JSON Schema documents** (deckrun-slide-format, deckrun-theme-profile, deckrun-voice) at canonical URLs—see x-public-schema-refs.

    **Authentication:** All authenticated endpoints accept **Firebase JWT** (Authorization Bearer) or **API key** (X-API-Key or Bearer). Use Firebase for web/mobile; use API keys for agents and server-to-server. GET /schema, GET /slide-themes/schema, and GET /voices/schema are unauthenticated and return **public JSON Schema documents** (deckrun-slide-format, deckrun-theme-profile, deckrun-voice) at their canonical URLs; see x-public-schema-refs.

    This API specification is provided for use with Deckrun only. You may not use this spec to build,
    automate, or generate code or products that replicate or substantially replicate Deckrun's functionality.
    Use is subject to the license and terms of service.

    **Security and AI agents:** This API is designed for use by both human-operated clients and AI/automated
    agents. All authenticated endpoints (except the discovery endpoints GET /schema, GET /slide-themes/schema, GET /voices/schema) require an API key
    (X-API-Key or Authorization Bearer). API keys are **secrets**—do not log them, embed them in prompts,
    or expose them to end users. GET /me returns account-specific data (identity, plan, billing); treat
    these responses as sensitive. Use HTTPS only. See x-security-considerations and x-agent-guidance below.
  license:
    name: Deckrun API Terms
    url: https://agenticdecks.com/terms
  termsOfService: https://agenticdecks.com/terms
  contact:
    name: Agentic Decks
    url: https://agenticdecks.com/contact
  x-feedback:
    description: >
      Bug reports, feature requests, and appreciation are welcome — from humans and AI agents alike.
      Agents are especially encouraged to share testimonials: automated systems often notice subtle
      API behaviours, edge cases, and ergonomic gaps that human users miss. Your feedback directly
      improves the API you depend on.
    bug_report: https://github.com/agenticdecks/community/issues/new?labels=bug&template=bug-report.md
    feature_request: https://github.com/agenticdecks/community/issues/new?labels=enhancement&template=feature-request.md
    general: https://agenticdecks.com/contact
  x-documentation: https://agenticdecks.com/docs/
  x-api-version: v1
  x-case-convention:
    endpoints: kebab-case
    request-body: snake_case
    parameters: snake_case
  x-request-body-extensibility:
    description: Request bodies (e.g. GenerateRequest) use additionalProperties true where extensibility is desired. Unknown top-level fields are ignored or reserved; SDKs should not reject requests with unknown fields.
  x-idempotency-guidelines:
    description: Idempotency is enforced by the bouncer. Same key + same body => same job/result.
    version: "1.0"
    requirements:
      - "POST /generate **requires** Idempotency-Key header. Clients must send a unique key per logical request for safe retries."
      - "Duplicate requests with the same key + same body within TTL return the same job_id / result (200 or 202); do not create a second job."
      - "Idempotency keys should be UUIDs; server may treat them as valid for a limited TTL (e.g. 24 hours). 409 Idempotency Conflict if key reuse conflicts."
      - "Clients should implement exponential backoff for retries on 429, 5xx."
    retryGuidelines:
      description: Recommended retry strategy
      strategy:
        - "Use exponential backoff (e.g. start 1s, max 3–5 attempts)"
        - "Retry only on 408, 429, 500, 502, 503, 504"
        - "Do not retry on 4xx (except 429) without changing request"
  x-rate-limiting:
    description: Rate limits apply per account/plan; 429 response includes Retry-After when applicable. Exact numbers in account or docs.
    exampleTiersPerHour:
      basic: 100
      plus: 500
      pro: 2000
      max: 10000
      team: "per seat; see account"
      enterprise: "custom"

  x-connector-limits:
    description: Max storage connectors per account by plan. GET /me may return connectors.used and connectors.limit.
    maxConnectorsPerPlan:
      basic: 0
      plus: 1
      pro: 10
      max: -1
      team: "per plan; see account"
      enterprise: "custom"
    note: "-1 or null denotes unlimited; exact values may vary by implementation."

  x-security-considerations:
    description: Security and privacy considerations for implementers and operators
    items:
      - "API keys and Bearer tokens are secrets. Store in secure credential stores; never log, echo, or include in agent prompts or user-visible messages."
      - "Authenticated responses (e.g. GET /me, GET /slide-themes, POST /generate result URLs) are scoped to the key's account. Treat account and artifact data as sensitive (PII, plan, billing)."
      - "Use HTTPS only. Servers must enforce TLS; clients must not send keys over plain HTTP."
      - "Validate and sanitize all inputs (markdown, IDs). Use Idempotency-Key on POST /generate for safe retries without duplicate side effects."
      - "Respect 429 (rate limit) and Retry-After; implement exponential backoff. Do not retry 4xx (except 429) without changing the request."

  x-agent-guidance:
    description: Guidance for implementers building AI or automated agents that call this API
    items:
      - "Discoverability: This OpenAPI spec (components/schemas) defines request/response shapes. **x-public-schema-refs** lists canonical URLs for all public JSON Schema documents: slide_format, theme_profile, voice (returned by GET /schema, GET /slide-themes/schema, GET /voices/schema); slide, job_status, user_account, notes_audio_manifest, output_connector, billable_units (shapes for GET /me, POST /generate, GET /jobs/{id}, .notes-audio.json, output_destination, etc.). Use GET /providers for voice tiers and image options."
      - "Themes and document: To use a custom theme with slide markdown, (1) get the theme shape from GET /slide-themes/schema (returns deckrun-theme-profile JSON Schema) or SlideTheme in this spec; (2) create a theme with POST /slide-themes (branding, contact, optional voice, category); (3) call POST /generate with markdown (your slide document) and slide_theme_id set to the theme id from step 2. Omit slide_theme_id to use the account default. Theme drives PDF branding, footer, and optional default voice; markdown is the slide content (GET /schema?category=slide returns deckrun-slide-format for slide format)."
      - "Output connectors: Use GET /connectors (read-only) to get supported_types and configured connectors. Use connector_id from the list when configured. Do not attempt to create/update/delete connectors via API—connector configuration is an admin task. If a supported type has no configured connector, prompt the user to have an admin configure it or use inline output_destination where the deployment allows."
      - "Narration voices: Use GET /voices to list onboarded voices (classified by tier: system or premium; see x-narration-tiers). For audio or video output, pass voice_id in POST /generate or use the theme default. Voice onboarding (POST /voices with tier + tier_params, PATCH /voices/{voice_id}) is separate from generation; do not send raw audio in /generate."
      - "Identity: Use GET /me to resolve account, plan, and credits. Do not log or echo the full GET /me response."
      - "Plan + credits: Every account has a plan tier and a credits object (period allowance + used). POST /generate may return 402 INSUFFICIENT_CREDITS when allowance/balance is exhausted; agents should surface this so the user can upgrade or add credits."
      - "Render Units (RU): Generation is billed in Render Units. 1 logical page = 1 base RU; output-type multipliers accumulate on top (e.g. PDF +0.2/page, system TTS +1.0/page, premium TTS +4.5/page). Minimum 2 RU per job. Check GET /me for your plan's RU allowance and balance before submitting large jobs."
      - "Pre-flight estimate: Before submitting a document that may be large or expensive, call POST /generate with output_types=[\"estimate\"] (no other types). The server returns result.ru_estimate with estimated_ru, basis, and optional warning — without generating any artifact. This avoids 402 or 413 surprises on large decks."
      - "Input size limits: POST /generate returns 413 INPUT_TOO_LARGE when the document exceeds the plan's page cap (e.g. 10 pages on Free). Use output_types=[\"estimate\"] to pre-check, or GET /me for plan limits. On 413, reduce document length or ask the user to upgrade."
      - "Keys: Obtain API keys via secure sign-up/OAuth flow. Agents must use keys delegated to them (e.g. user's key) and must not expose keys in tool descriptions, prompts, or logs."
      - "Idempotency: Send Idempotency-Key (UUID) on POST /generate so retries return the same result; handle 409 Idempotency Conflict by returning the original outcome."
      - "Request identity: Send X-Request-Id (and optionally X-Parent-Request-Id for agent chains) for tracing; server echoes them in responses and errors so you can correlate logs across the stack."
      - "Queue visibility: When 202 or GET /jobs/{id} returns status=queued with queue_position and estimated_wait_seconds, use them to back off, show progress, or poll less aggressively to avoid thundering herds."
      - "Errors: Parse structured error body (code, message); handle INVALID_MARKDOWN, SCHEMA_VIOLATION, PROFILE_NOT_FOUND, RATE_LIMITED, THEME_SYSTEM_PROVIDED (403 when deleting a system theme), etc. Do not retry 4xx blindly."
      - "Verify: After status=success, confirm artifact availability (e.g. GET result.pdf_url or result.zip_url and check 200) before presenting to the user; for result.output_url or result.output_path the server has already written the file."
      - "Terms: When registering this API as a tool (e.g. OpenClaw, MCP), present license and terms of service to the human operator; usage is subject to account ToS."
      - "Feedback: Bug reports, feature requests, and agent testimonials are welcome via x-feedback links in this spec. Agents that encounter unexpected behaviour, missing features, or noteworthy results are encouraged to file an issue — automated reporters often surface edge cases that human users miss."

  x-connector-timestamped-filenames:
    description: When artifacts are delivered to a storage connector (S3, Dropbox, Box, Drive, OneDrive), the server uses filenames that include a timestamp (e.g. deck-2025-01-27T143000Z.pdf or .zip) so objects are never overwritten. Retention and cleanup are the user's responsibility.

  x-public-schema-refs:
    description: |
      Canonical URLs for public JSON Schema documents. Use these for validation, codegen, and agent discovery. Discovery endpoints (GET /schema, GET /slide-themes/schema, GET /voices/schema) return the slide_format, theme_profile, and voice documents; other schemas define shapes used in API responses and artifacts.
    schemas:
      slide_format: "https://agenticdecks.com/schema/v1/deckrun-slide-format.json"
      theme_profile: "https://agenticdecks.com/schema/v1/deckrun-theme-profile.json"
      document_profile: "https://agenticdecks.com/schema/v1/deckrun-document-profile.json"
      voice: "https://agenticdecks.com/schema/v1/deckrun-voice.json"
      slide: "https://agenticdecks.com/schema/v1/deckrun-slide.json"
      job_status: "https://agenticdecks.com/schema/v1/deckrun-job-status.json"
      user_account: "https://agenticdecks.com/schema/v1/deckrun-user-account.json"
      notes_audio_manifest: "https://agenticdecks.com/schema/v1/deckrun-notes-audio-manifest.json"
      output_connector: "https://agenticdecks.com/schema/v1/deckrun-output-connector.json"
      billable_units: "https://agenticdecks.com/schema/v1/deckrun-billable-units.json"
    used_by:
      slide_format: "GET /schema?category=slide"
      theme_profile: "GET /slide-themes/schema; POST/PUT /slide-themes body shape"
      document_profile: "GET /schema?category=resume|whitepaper|datasheet|generic; document theme shape"
      voice: "GET /voices/schema; GET /voices, POST /voices, PATCH /voices/{id}"
      slide: "Parsed deck structure (logical_schema in slide_format)"
      job_status: "POST /generate 200/202, GET /jobs/{id}"
      user_account: "GET /me"
      notes_audio_manifest: ".notes-audio.json artifact (audio/video pipeline)"
      output_connector: "GenerateRequest.output_destination, connector config"
      billable_units: "Worker→bouncer usage (internal); optional in job result"
  x-narration-tiers:
    description: |
      Narration is classified by **tier** (product/plan level), not by internal TTS backend. This keeps the API stable if backends change and aligns with the commercial model. Agents and clients use tier only; the actual TTS provider is an implementation detail.
    tiers:
      system:
        name: "System voice"
        description: "Built-in professional neutral voices. Included in all plans. No credential required."
        plan: "all"
      premium:
        name: "Premium voice"
        description: "High-fidelity expressive voices for customer-facing videos and launches. Paid plans; subject to usage quotas."
        plan: "paid"

externalDocs:
  description: API and product documentation
  url: https://agenticdecks.com/docs/

servers:
  - url: https://api.agenticdecks.com
    description: Authenticated Deckrun API (API key required)
  - url: https://api.agenticdecks.com/v1
    description: Authenticated Deckrun API, versioned (API key required)
  - url: https://free.agenticdecks.com
    description: Free tier (no auth; /free/generate only)

tags:
  - name: infrastructure
    description: Health check and platform capability discovery. No authentication required.
  - name: schema
    description: Discovery endpoints return public JSON Schema documents. GET /schema?category=slide returns deckrun-slide-format; GET /schema?category=resume|whitepaper|datasheet|generic returns a deckrun-document-profile descriptor. GET /slide-themes/schema returns deckrun-theme-profile. GET /voices/schema returns deckrun-voice. See x-public-schema-refs for canonical URLs.
  - name: slide-themes
    description: |
      Theme CRUD (branding, voice, category). Themes apply to any document category (slide, resume, whitepaper, datasheet, patent, generic, book). System-provided themes can be customized (PUT) but not deleted (DELETE returns 403).
      **Agent flow:** (1) Get theme shape from GET /slide-themes/schema (deckrun-theme-profile) or SlideTheme in this spec. (2) Create theme with POST /slide-themes (branding, contact, optional voice, category). (3) Use the returned theme id as slide_theme_id in POST /generate with your slide/document markdown. Omit slide_theme_id to use the account default theme.
  - name: providers
    description: Voice (narration tiers) and image capability catalog. Voice entries use tier ids (system, premium); internal TTS backends are not exposed.
  - name: generate
    description: Submit markdown and get artifacts
  - name: jobs
    description: Async job status
  - name: validate
    description: Lint/validate markdown without generating
  - name: account
    description: Current account (identity and plan); key → account → plan
  - name: connectors
    description: Connector discovery only (read-only). Connector configuration is an admin task (config file, env, or admin tooling)—not exposed to agents or standard API; no create/update/delete in this API to avoid misuse and security risk.
  - name: voices
    description: Narration voice onboarding (CRUD). Voices are first-class resources; create/list/get/patch voices, then reference by voice_id in POST /generate or in a slide theme. Voices are classified by narration tier (system, premium); see x-narration-tiers. Internal TTS backends are not exposed in the API.
  - name: Free
    description: No-auth free-tier PDF generation. Maximum 10 slides and 50 KB of Markdown. No API key required.

security:
  - FirebaseAuth: []
  - ApiKeyAuth: []
  - BearerAuth: []

paths:
  /health:
    get:
      tags: [infrastructure]
      summary: Health check
      description: Returns server status and version. No authentication required. Intended for load balancers, uptime monitors, and deployment pipelines.
      operationId: getHealth
      security: []
      responses:
        '200':
          description: Server is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  version: { type: string, example: "0.9.1", description: Server version from Cargo.toml. }
              example:
                status: ok
                version: "0.9.1"

  /capabilities:
    get:
      tags: [infrastructure]
      summary: Platform capability discovery
      description: |
        Returns static platform capabilities: supported output types, document categories, job models,
        authentication methods, pricing model identifier, and per-tier breakdown.
        No authentication required. Safe to call before obtaining credentials.
        Use GET /me for personal entitlements (plan, RU balance, limits).
      operationId: getCapabilities
      security: []
      responses:
        '200':
          description: Platform capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CapabilitiesResponse'
              example:
                outputs: [pdf, mp4, mp3, notes, cue]
                tiers:
                  free:
                    outputs: [pdf]
                    auth: [none]
                  paid:
                    outputs: [pdf, mp4, mp3, notes, cue]
                    auth: [firebase_jwt, api_key]
                document_categories: [slide, resume, whitepaper, datasheet, patent, generic, book]
                job_model: [sync, async]
                auth: [firebase_jwt, api_key]
                pricing_model: render-units
                schemas_version: v1

  /schema:
    get:
      tags: [schema]
      summary: Get format descriptor for a document category
      description: |
        GET /schema?category= — For **category=slide** (default), returns the **public JSON Schema document** deckrun-slide-format (canonical URL in x-public-schema-refs): surface_syntax, example_markdown, license, logical_schema. For **category=resume**, **whitepaper**, or **datasheet**, returns a deckrun-document-profile descriptor (canonical URL in x-public-schema-refs): category, canonical_sections, layout_rules, and example_markdown. For other categories (patent, generic, book), returns a small descriptor (description, example, category_options). See https://agenticdecks.com/docs/ for document-categories.
      operationId: getSchema
      security: []
      parameters:
        - name: category
          in: query
          description: Document category; default slide. For slide, response is the deckrun-slide-format JSON Schema. For resume, whitepaper, or datasheet, response is a deckrun-document-profile descriptor.
          schema:
            type: string
            enum: [slide, resume, whitepaper, datasheet, patent, generic, book]
            default: slide
      responses:
        '200':
          description: For category=slide, the public JSON Schema document (deckrun-slide-format). For other categories, a small format descriptor.
          content:
            application/json:
              schema:
                $ref: 'https://agenticdecks.com/schema/v1/deckrun-slide-format.json'
        '500':
          $ref: '#/components/responses/InternalError'

  /slide-themes/schema:
    get:
      tags: [schema]
      summary: Get slide theme descriptor
      description: Returns the **public JSON Schema document** deckrun-theme-profile (canonical URL in x-public-schema-refs). Use for discovery and validation of theme create/update bodies (POST /slide-themes, PUT /slide-themes/{id}). Response shape is defined by the schema document.
      operationId: getSlideThemesSchema
      security: []
      responses:
        '200':
          description: The public JSON Schema document (deckrun-theme-profile) defining theme resource shape.
          content:
            application/json:
              schema:
                $ref: 'https://agenticdecks.com/schema/v1/deckrun-theme-profile.json'
        '500':
          $ref: '#/components/responses/InternalError'

  /slide-themes:
    get:
      tags: [slide-themes]
      summary: List slide themes
      description: List slide theme IDs and names (and optionally a short summary) for the account. Pagination via limit/offset.
      operationId: listSlideThemes
      parameters:
        - name: limit
          in: query
          description: Max items to return (default service-defined; max typically 100)
          schema: { type: integer, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          description: Number of items to skip (for pagination)
          schema: { type: integer, minimum: 0 }
      responses:
        '200':
          description: List of slide themes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlideThemeList'
              example:
                slide_themes:
                  - id: "theme_1"
                    name: "Brand A"
                    summary: "Default brand theme"
                  - id: "theme_2"
                    name: "Training"
                    summary: "Training deck theme"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [slide-themes]
      summary: Create slide theme
      description: |
        Create a new slide theme. Body as defined by SlideTheme in this spec (branding, contact, optional voice, category, diagram_formats). Server returns 201 with the created theme including id; use that id as slide_theme_id in POST /generate to apply this theme to slide/document markdown.
      operationId: createSlideTheme
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SlideTheme'
            example:
              category: "slide"
              branding:
                org_name: "Acme Inc"
                logo_path: "acme-logo.svg"
                favicon_path: "acme-mark.svg"
                logo_includes_wordmark: false
                colors:
                  primary: "#1a1a2e"
                  accent: "#e94560"
                  title_slide_bg: "#1a1a2e"
                  title_slide_text: "#ffffff"
                  heading_color: "#e94560"
                  body_text_color: "#1a1a2e"
                  footer_bg: "#f5f5f5"
                  footer_text: "#1a1a2e"
                typography:
                  title_font: "Inter"
                  body_font: "Inter"
                  slide_title_size: "36px"
                  body_text_size: "16px"
              contact:
                name: "Jane Doe"
                title: "Product Lead"
                email: "jane@acme.com"
                location: "San Francisco, CA"
      responses:
        '201':
          description: Slide theme created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlideTheme'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /slide-themes/{id}:
    parameters:
      - $ref: '#/components/parameters/SlideThemeId'
    get:
      tags: [slide-themes]
      summary: Get slide theme by ID
      description: Return slide theme details (read-only). Shape as defined by SlideTheme in this spec.
      operationId: getSlideTheme
      responses:
        '200':
          description: Slide theme
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlideTheme'
              example:
                branding:
                  org_name: "Acme Inc"
                  colors: { primary: "#1a1a2e", accent: "#16213e" }
                contact:
                  name: "Jane Doe"
                  title: "Product Lead"
                  email: "jane@acme.com"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags: [slide-themes]
      summary: Update slide theme
      description: Update an existing theme (user- or system-provided). Body as defined by SlideTheme in this spec. System-provided themes may be customized (e.g. colors, contact) but cannot be deleted.
      operationId: updateSlideTheme
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SlideTheme'
      responses:
        '200':
          description: Slide theme updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlideTheme'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [slide-themes]
      summary: Delete slide theme
      description: |
        Delete a **user-provided** theme. **System-provided themes cannot be deleted** (response 403 Forbidden);
        they may be customized via PUT (e.g. change colors, contact). Servers track system vs user themes
        (e.g. system_provided flag) so only user-created themes are removable.
      operationId: deleteSlideTheme
      responses:
        '204':
          description: Slide theme deleted (user-provided only)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /providers:
    get:
      tags: [providers]
      summary: List providers (voice and image capabilities)
      description: |
        List available options per capability (voice, image). For **voice**, returns **narration tiers** (system, premium)—not internal TTS backend names. Each entry includes id (tier or capability), display name, plan, and optional requires_credential. Aligns with x-narration-tiers and commercial model.
      operationId: getProviders
      responses:
        '200':
          description: Catalog by capability (voice = narration tiers; image = asset options)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvidersCatalog'
              example:
                voice:
                  - id: "system"
                    name: "System voice"
                    plan: "all"
                  - id: "premium"
                    name: "Premium voice"
                    plan: "paid"
                    requires_credential: true
                image:
                  - id: "url"
                    name: "URL / upload"
                    plan: "all"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /free/generate:
    post:
      operationId: freeGenerate
      summary: Generate a PDF from Markdown (free tier, no auth)
      description: >
        No-auth endpoint for free-tier PDF generation. Slide separator is `\n---\n`.
        Maximum 10 slides and 50 KB of Markdown. Returns a time-limited public URL
        to the generated PDF (expires after 90 days). Footer watermark is non-removable.
      tags: [Free]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FreeGenerateRequest' }
      responses:
        '200':
          description: PDF generated successfully.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FreeGenerateResponse' }
        '413':
          $ref: '#/components/responses/InputTooLarge'
        '500':
          description: Generation failed (engine error).

  /generate:
    post:
      tags: [generate]
      summary: Generate output from markdown
      description: |
        **Async by default.** Submit markdown; get 202 with job_id and status_url, then poll GET /jobs/{id} for result.
        **Optional sync mode:** Use ?sync=true&timeout_seconds=60 to block until completion (200 with artifact URLs) or timeout (then poll by job_id).
        **When output_destination is set:** Async (202) is recommended; sync may time out for heavy outputs (PDF+audio, video). Server may always return 202 for connector requests.
        Idempotency-Key header is **required**; bouncer enforces same key + same body => same job. Slide theme applied at render time.

        **Response shape:** Sync (200) and async polling (GET /jobs/{id} 200) return the same JobStatus schema; sync just skips the polling step.

        **Flow:** (1) POST /generate → 202 + job_id; (2) GET /jobs/{id} until status is success|failure|timeout|cancelled; (3) On success, use result.pdf_url etc.

        **Embedded video (MP4-first) policy:**
        - Slide markdown may include `<!-- <slide>...<video_src>...</video_src>...</slide> -->`.
        - `video_src` and `youtube_url` are mutually exclusive.
        - `youtube_url` is recognized for forward compatibility but returns `feature_not_supported` unless explicitly enabled; remote ingestion is not implemented in this deployment.
        - Hosted API caps for inline `assets`:
          - MP4 (`.mp4`) max **50 MB** per asset.
          - Image (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`) max **10 MB** per asset.
        - On cap/policy violations, API returns HTTP 400 with explicit `error.code` (for example `media_too_large`, `feature_not_supported`, `invalid_request`).

        **Example cURL (async):**
        ```
        curl -X POST "https://api.agenticdecks.com/generate" \\
          -H "Authorization: Bearer YOUR_API_KEY" \\
          -H "Idempotency-Key: $(uuidgen)" \\
          -H "Content-Type: application/json" \\
          -d '{"markdown": "# Hello\\n\\n---\\n\\n## Slide 1\\nContent.", "output_types": ["pdf"]}'
        ```
        **Example cURL (sync):**
        ```
        curl -X POST "https://api.agenticdecks.com/generate?sync=true&timeout_seconds=60" \\
          -H "Authorization: Bearer YOUR_API_KEY" \\
          -H "Idempotency-Key: $(uuidgen)" \\
          -H "Content-Type: application/json" \\
          -d '{"markdown": "# Hello\\n\\n---\\n\\n## Slide 1\\nContent.", "output_types": ["pdf"]}'
        ```
        **Example cURL (polling):**
        ```
        curl -H "Authorization: Bearer YOUR_API_KEY" "https://api.agenticdecks.com/jobs/JOB_ID"
        ```
      security:
        - FirebaseAuth: []
        - ApiKeyAuth: []
        - BearerAuth: []
      operationId: generate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
        - name: sync
          in: query
          required: false
          description: If true, wait for completion (up to timeout_seconds) and return 200 with artifact URLs; otherwise return 202 and poll GET /jobs/{id}.
          schema: { type: boolean, default: false }
        - name: timeout_seconds
          in: query
          required: false
          description: When sync=true, max seconds to wait for completion (e.g. 30–120). After timeout, client should poll GET /jobs/{id}.
          schema: { type: integer, minimum: 1, maximum: 300, default: 60 }
        - name: X-Request-Id
          in: header
          description: |
            Unique request identifier for tracing. Server generates one if omitted.
            Propagated in responses and errors; use for logs and debugging across bouncer → worker → usage.
          schema: { type: string }
        - name: X-Parent-Request-Id
          in: header
          description: Optional. Parent request's X-Request-Id for agent chains; server propagates and logs.
          schema: { type: string }
        - name: X-Request-Source
          in: header
          description: Optional source identifier (e.g. openclaw, mcp) for audit
          schema: { type: string }
        - name: X-Tenant-Id
          in: header
          description: Optional tenant or end-user id for platforms using one key; audit only
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateRequest'
            examples:
              basic_pdf:
                summary: Basic PDF generation
                value:
                  document_category: "slide"
                  markdown: "<!-- <title-slide /> -->\n\n# Hello\n\n---\n\n## Slide 1\n\nContent."
                  output_types: [pdf]
              video_with_embedded_clip_source_audio:
                summary: Video output with embedded MP4 clip (source audio)
                value:
                  document_category: "slide"
                  markdown: "<!-- <title-slide /> -->\n\n# Product Demo\n\n---\n\n<!-- <slide><layout>title-content</layout><video_src>videos/demo.mp4</video_src><video_audio_mode>source</video_audio_mode><video_start_secs>2.0</video_start_secs><video_end_secs>12.5</video_end_secs></slide> -->\n\n## Demo Clip\n\nWatch the embedded product segment."
                  output_types: [pdf, video]
                  assets:
                    "videos/demo.mp4": "<base64-mp4-bytes>"
              video_with_embedded_clip_muted:
                summary: Video output with embedded MP4 clip (muted)
                value:
                  document_category: "slide"
                  markdown: "<!-- <title-slide /> -->\n\n# Product Demo\n\n---\n\n<!-- <slide><layout>title-content</layout><video_src>videos/demo.mp4</video_src><video_audio_mode>mute</video_audio_mode></slide> -->\n\n## Demo Clip\n\nMuted clip segment."
                  output_types: [video]
                  assets:
                    "videos/demo.mp4": "<base64-mp4-bytes>"
      responses:
        '200':
          description: |
            **Synchronous completion (job succeeded).** Returned when ?sync=true and the job completes within timeout_seconds.
            Same JobStatus schema as GET /jobs/{id} when status=success; result contains artifact URLs.
          headers:
            Job-Id:
              description: Bouncer-assigned job ID. Present when server supports it.
              schema: { type: string }
            X-Job-Id:
              description: Optional. Same as Job-Id; useful for tracing across proxies and logs. Body job_id is sufficient.
              schema: { type: string }
            Idempotency-Status:
              description: new | replay | in-flight | completed. Present when Idempotency-Key was sent.
              schema: { type: string, enum: [new, replay, in-flight, completed] }
            Idempotency-Key:
              description: Optional echo of request Idempotency-Key for tracing; not required, body has idempotency_key when present.
              schema: { type: string }
            X-Request-Id:
              description: Echo of request X-Request-Id or server-generated.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
              example:
                job_id: "job_abc123"
                status: "success"
                result:
                  pdf_url: "https://api.agenticdecks.com/artifacts/abc123.pdf"
                  video_url: "https://api.agenticdecks.com/artifacts/abc123.mp4"
                  notes_text_url: "https://api.agenticdecks.com/artifacts/abc123.notes.txt"
                  render_units:
                    logical_units: 8
                    base_ru: 8.0
                    output_contributions:
                      - [pdf, 1.6]
                      - [video_embedded_mp4, 40.0]
                    cache_discount: 0.0
                    subtotal: 49.6
                    minimum_applied: false
                    total_ru: 50
        '202':
          description: |
            **Asynchronous acceptance (job queued or running).** No ?sync, or sync requested but job not yet complete.
            When ?sync=true and timeout_seconds is reached before completion, server returns 202 with job_id and status_url — client must poll GET /jobs/{job_id} for result.
            When queued, response may include queue_position and estimated_wait_seconds for backpressure.
          headers:
            Job-Id:
              description: Bouncer-assigned job ID (same as body job_id). Present when server supports it.
              schema: { type: string }
            X-Job-Id:
              description: Optional. Same as Job-Id; for tracing across proxies and logs. Body job_id is sufficient.
              schema: { type: string }
            Idempotency-Status:
              description: new | replay | in-flight | completed. Present when Idempotency-Key was sent.
              schema: { type: string, enum: [new, replay, in-flight, completed] }
            Idempotency-Key:
              description: Optional echo of request Idempotency-Key for tracing; not required.
              schema: { type: string }
            X-Request-Id:
              description: Echo of request X-Request-Id or server-generated; use for tracing.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateResponseAsync'
              examples:
                async_accepted:
                  summary: Async accepted
                  value:
                    job_id: "job_abc123"
                    status_url: "https://api.agenticdecks.com/jobs/job_abc123"
                queued:
                  summary: Job queued (queue visibility)
                  value:
                    job_id: "job_abc123"
                    status: "queued"
                    queue_position: 12
                    estimated_wait_seconds: 45
                    status_url: "https://api.agenticdecks.com/jobs/job_abc123"
                sync_timeout:
                  summary: Sync timeout — poll for result
                  description: When ?sync=true and the job does not complete within timeout_seconds, server returns 202. Client must poll GET /jobs/{job_id} (or status_url) until status is success, failure, timeout, or cancelled.
                  value:
                    job_id: "job_abc123"
                    status: "in-flight"
                    status_url: "https://api.agenticdecks.com/jobs/job_abc123"
                    message: "Sync timeout reached; poll GET /jobs/job_abc123 for result."
        '400':
          description: Bad request (validation/policy failure)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                media_too_large_video:
                  summary: MP4 asset exceeds hosted API cap
                  value:
                    code: media_too_large
                    message: "video asset 'videos/demo.mp4' exceeds API cap (73400320 bytes > 52428800 bytes)"
                media_too_large_image:
                  summary: Image asset exceeds hosted API cap
                  value:
                    code: media_too_large
                    message: "image asset 'images/hero.png' exceeds API cap (13631488 bytes > 10485760 bytes)"
                unsupported_youtube_url:
                  summary: youtube_url not supported in this deployment
                  value:
                    code: feature_not_supported
                    message: "youtube_url is not supported in this deployment"
                mutually_exclusive_video_fields:
                  summary: video_src and youtube_url cannot both be set
                  value:
                    code: invalid_request
                    message: "slide 2: video_src and youtube_url are mutually exclusive"
                invalid_video_trim:
                  summary: Invalid clip trim range
                  value:
                    code: invalid_request
                    message: "slide 3: invalid video trim: video_end_secs (5) must be greater than video_start_secs (10)"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '413':
          $ref: '#/components/responses/InputTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/InternalError'

  /jobs/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Job ID returned by POST /generate (async)
        schema:
          type: string
    get:
      tags: [jobs]
      summary: Get job status
      description: |
        Poll job status. Supports queue visibility (queued, queue_position, estimated_wait_seconds),
        in-flight, and terminal statuses (success, failure, timeout, cancelled).
        When status is success, result contains artifact URLs. Same JobStatus schema as POST /generate 200 (sync).
      security:
        - FirebaseAuth: []
        - ApiKeyAuth: []
        - BearerAuth: []
      operationId: getJob
      responses:
        '200':
          description: Job status (JobStatus in this spec)
          headers:
            Job-Id:
              description: Bouncer-assigned job ID (same as body job_id).
              schema: { type: string }
            X-Job-Id:
              description: Optional. Same as Job-Id; for tracing across proxies and logs. Body job_id is sufficient.
              schema: { type: string }
            Idempotency-Status:
              description: new | replay | in-flight | completed. Present when request was idempotent.
              schema: { type: string, enum: [new, replay, in-flight, completed] }
            Idempotency-Key:
              description: Optional echo of request Idempotency-Key for tracing; not required.
              schema: { type: string }
            X-Request-Id:
              description: Request id for tracing.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
              examples:
                completed:
                  summary: Job completed
                  value:
                    job_id: "job_abc123"
                    status: "success"
                    result:
                      pdf_url: "https://api.agenticdecks.com/artifacts/abc123.pdf"
                      video_url: "https://api.agenticdecks.com/artifacts/abc123.mp4"
                queued:
                  summary: Job queued (queue visibility)
                  value:
                    job_id: "job_abc123"
                    status: "queued"
                    queue_position: 12
                    estimated_wait_seconds: 45
                in_flight:
                  summary: Job in progress
                  value:
                    job_id: "job_abc123"
                    status: "in-flight"
                failed:
                  summary: Job failed
                  value:
                    job_id: "job_abc123"
                    status: "failure"
                    error: { code: "generation_failed", message: "Generation failed." }
                failed_feature_not_supported:
                  summary: Job failed due to unsupported feature
                  value:
                    job_id: "job_abc124"
                    status: "failure"
                    error:
                      code: "feature_not_supported"
                      message: "slide 2: embedded clip mode 'source' is not supported with narration output yet (use notes or mute)"
                failed_media_too_large:
                  summary: Job failed due to media cap
                  value:
                    job_id: "job_abc125"
                    status: "failure"
                    error:
                      code: "media_too_large"
                      message: "video asset 'videos/demo.mp4' exceeds API cap (73400320 bytes > 52428800 bytes)"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me:
    get:
      tags: [account]
      summary: Get current account
      description: |
        Returns the account associated with the API key (identity and plan).
        Use to show "logged in as" or to check plan/limits before calling generate or listing profiles.
      security:
        - FirebaseAuth: []
        - ApiKeyAuth: []
        - BearerAuth: []
      operationId: getCurrentAccount
      responses:
        '200':
          description: Current account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
              example:
                account_id: "acc_123"
                plan: "pro"
                credits:
                  period_allowance: 500
                  used_this_period: 42
                  period_start: "2025-01-01T00:00:00Z"
                  period_end: "2025-01-31T23:59:59Z"
                email: "user@example.com"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /validate:
    post:
      tags: [validate]
      summary: Validate markdown (lint)
      description: |
        Run linter on markdown without generating output. Returns list of issues (rule, severity, message, line, slide_index).
        Alternative to POST /generate with output_types ["lint"].
      operationId: validate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [markdown]
              properties:
                document_category:
                  $ref: '#/components/schemas/DocumentCategory'
                markdown:
                  type: string
                  description: Markdown to validate; must conform to format for document_category.
                output_types:
                  type: array
                  items: { type: string }
                  description: |
                    Output types to include in the RU estimate (e.g. ["pdf", "mp4"]).
                    Omit to skip the estimate — only lint issues are returned.
                    Same values as output_types in POST /generate.
            example:
              document_category: "slide"
              markdown: "<!-- <title-slide /> -->\n\n# Deck\n\n---\n\n## Slide 1\n\nBody."
              output_types: ["pdf"]
      responses:
        '200':
          description: Lint result with optional RU estimate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
              example:
                valid: true
                issues: []
                estimated_pages: 2
                estimated_ru:
                  estimated_ru: 4
                  basis: slide-count
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /connectors:
    get:
      tags: [connectors]
      summary: List connectors and supported types
      description: |
        List **configured** connectors for the current account and the **supported** connector types this server offers (read-only).
        Agents use this to discover what is configured (connectors) and what is supported but not yet configured (supported_types).
        Connector configuration is an **admin task** (config file, env, or admin tooling)—not available via this API. If a supported type has no configured connector, the agent should prompt the user to have an admin configure it, or use inline output_destination where the deployment allows. Credentials are never returned.
      operationId: listConnectors
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0 }
      responses:
        '200':
          description: Supported connector types and list of configured connectors (ids, types, config; no credentials)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectorList'
              example:
                supported_types: [dropbox, box, google_drive, onedrive, s3, slack]
                connectors:
                  - id: "conn_1"
                    type: "s3"
                    display_name: "My bucket"
                    config: { bucket: "my-deckrun-assets", prefix: "decks/" }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /connectors/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Connector ID
        schema: { type: string }
    get:
      tags: [connectors]
      summary: Get connector by ID
      description: Return connector metadata and config (no credentials). Used to pass connector_id in asset_source or output_destinations.
      operationId: getConnector
      responses:
        '200':
          description: Connector metadata and config (no credentials)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connector'
              example:
                id: "conn_1"
                type: "s3"
                display_name: "Deck outputs"
                config: { bucket: "my-bucket", prefix: "decks/" }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /voices/schema:
    get:
      tags: [schema, voices]
      summary: Get voice resource descriptor
      description: |
        Returns the **public JSON Schema document** deckrun-voice (canonical URL in x-public-schema-refs). Use for discovery of voice resource shape (voice_id, tier, display_name, status, etc.). See voice_onboarding.md.
      operationId: getVoicesSchema
      security: []
      responses:
        '200':
          description: The public JSON Schema document (deckrun-voice) defining voice resource shape.
          content:
            application/json:
              schema:
                $ref: 'https://agenticdecks.com/schema/v1/deckrun-voice.json'
        '500':
          $ref: '#/components/responses/InternalError'

  /voices:
    get:
      tags: [voices]
      summary: List voices
      description: List narration voices visible to the current account (onboarded voices). Use voice_id from the list in POST /generate or in a slide theme for audio/video output. Pagination via limit/offset.
      operationId: listVoices
      parameters:
        - name: limit
          in: query
          description: Max items to return (default service-defined; max typically 100)
          schema: { type: integer, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          description: Number of items to skip (for pagination)
          schema: { type: integer, minimum: 0 }
        - name: tier
          in: query
          description: Filter by narration tier (system, premium). See x-narration-tiers.
          schema: { $ref: '#/components/schemas/NarrationTier' }
        - name: status
          in: query
          description: Filter by status (active, inactive, archived)
          schema: { type: string, enum: [active, inactive, archived] }
      responses:
        '200':
          description: List of voices
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceList'
              example:
                voices:
                  - voice_id: "deckrun://voices/system-default-en"
                    tier: "system"
                    display_name: "System default (en-US)"
                    status: "active"
                  - voice_id: "deckrun://voices/shivaram-founder"
                    tier: "premium"
                    display_name: "Shivaram Founder Voice"
                    status: "active"
                next_offset: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [voices]
      summary: Create voice
      description: Onboard a new narration voice for the current account. Use tier (x-narration-tiers) and tier_params; plan gating applies by tier. See voice_onboarding.md.
      operationId: createVoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceCreate'
            example:
              tier: "premium"
              display_name: "Shivaram Founder Voice"
              tier_params:
                voice_identifier: "deckrun-founder-en"
                language: "en-US"
      responses:
        '201':
          description: Voice created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Voice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /voices/{voice_id}:
    parameters:
      - name: voice_id
        in: path
        required: true
        description: "Voice ID (e.g. deckrun://voices/shivaram-founder). URL-encode if needed."
        schema: { type: string }
    get:
      tags: [voices]
      summary: Get voice by ID
      description: Return a single voice resource (metadata only; no secrets). Shape as defined by Voice in this spec.
      operationId: getVoice
      responses:
        '200':
          description: Voice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Voice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags: [voices]
      summary: Update voice
      description: Update display_name or status (active, inactive, archived). Tier and tier-specific fields (voice_identifier, reference_audio_path) are typically immutable after creation.
      operationId: updateVoice
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoicePatch'
      responses:
        '200':
          description: Voice updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Voice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /assets:
    post:
      tags: [generate]
      summary: Upload asset (optional)
      description: |
        Optional. Upload a file and receive a temporary URL or asset ID for use in markdown (e.g. asset_urls or asset_inline).
        When not available, clients use asset_urls with external URLs or asset_inline with data URLs.
      operationId: uploadAsset
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                logical_name: { type: string, description: Key to use in markdown (e.g. diagram.png) }
      responses:
        '201':
          description: Asset uploaded; response includes url or asset_id for use in POST /generate
          content:
            application/json:
              schema:
                type: object
                properties:
                  logical_name: { type: string }
                  url: { type: string, format: uri }
                  asset_id: { type: string }
                  expires_at: { type: string, format: date-time }
              example:
                logical_name: "logo.png"
                url: "https://api.agenticdecks.com/assets/tmp_xyz123/logo.png"
                expires_at: "2025-01-02T12:00:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    FirebaseAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Firebase ID token (JWT). Validated at the edge; used for web and mobile clients.
        Obtain via Firebase Authentication; send as Authorization Bearer &lt;token&gt;.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for programmatic and agent access. **Secret**—do not log or expose. Preferred for server/agent calls.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Same as FirebaseAuth or API key as Bearer token. Authorization Bearer &lt;token_or_key&gt;. **Secret**—do not log or expose.
    OAuth2AuthorizationCode:
      type: oauth2
      description: OAuth 2.0 authorization code (e.g. social login, delegated access). Use for web and mobile clients.
      flows:
        authorizationCode:
          authorizationUrl: https://auth.agenticdecks.com/authorize
          tokenUrl: https://auth.agenticdecks.com/token
          scopes: {}
    OAuth2Device:
      type: oauth2
      description: |
        OAuth 2.0 device authorization (RFC 8628). Optional; for CLI and headless devices.
        Support when demand is validated. Device flow uses tokenUrl and a separate device
        authorization endpoint (see x-device-authorization-url).
      flows:
        clientCredentials:
          tokenUrl: https://auth.agenticdecks.com/token
          scopes: {}
      x-device-authorization-url: https://auth.agenticdecks.com/device

  parameters:
    SlideThemeId:
      name: id
      in: path
      required: true
      description: Slide theme ID
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Unique key for idempotent request handling; duplicate requests within TTL return original response or 409
      schema:
        type: string
        format: uuid
      example: "123e4567-e89b-12d3-a456-426614174000"
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      description: >
        **Required for POST /generate.** Idempotency key; bouncer enforces same key + same body => same job.
        Duplicate requests return the same job_id/result (200/202) or 409 Idempotency Conflict. Use UUIDs.
      schema:
        type: string
        format: uuid
      example: "123e4567-e89b-12d3-a456-426614174000"

  headers:
    RateLimit-Limit:
      description: Request limit per window (e.g. per hour)
      schema:
        type: integer
    RateLimit-Remaining:
      description: Requests remaining in current window
      schema:
        type: integer
    Retry-After:
      description: Seconds until rate limit resets (when 429)
      schema:
        type: integer

  schemas:
    SlideFormatResponse:
      type: object
      description: GET /schema?category=slide returns the public JSON Schema document (deckrun-slide-format); canonical URL in x-public-schema-refs. This component summarizes the shape for reference; the schema document is the single source of truth.
      properties:
        version:
          type: string
          example: "1.0.0"
        logical_schema:
          type: object
          description: Optional structured schema for the parsed format; format rules are described by surface_syntax and example_markdown.
        surface_syntax:
          type: object
          description: |
            Slide format rules. Typically includes slide_separator (---), layout_tags (title-slide, title-content-slide, section-header-slide, two-content-slide, title-only-slide, title-no-footer-slide, footer-only-slide, full-blank-slide, blank-slide, content-with-caption-slide, default); two_column (left_side, right_side, end_special_format); notes (slide-notes — multiple blocks per slide allowed, each with optional language attribute for multilingual narration); slide_timer (slide-timer N seconds, video only); slide_tag_attributes (layout, class, image, background_img, video_src, youtube_url, video_audio_mode, video_start_secs, video_end_secs, video_duck_db, etc. inside slide tag); inline_images (img positioning/sizing classes).
        example_markdown:
          type: string
        license:
          type: string
        api:
          type: object
          description: generate_request_options, providers_catalog

    SlideTheme:
      type: object
      description: Theme resource. GET /slide-themes/schema returns the public JSON Schema (deckrun-theme-profile; see x-public-schema-refs). Used for all document categories; PDF output always uses a theme. category = slide|resume|whitepaper|datasheet|patent|generic|book. diagram_formats = [ascii, mermaid] only. system_provided themes cannot be deleted (DELETE returns 403); users may customize them via PUT.
      properties:
        id:
          type: string
          description: Unique theme identifier (server-assigned).
        system_provided:
          type: boolean
          readOnly: true
          description: When true, theme is provided by the system. Only user-provided themes can be deleted; system-provided themes may be customized (PUT) but not deleted.
        category:
          type: string
          enum: [slide, resume, whitepaper, datasheet, patent, generic, book]
          description: Document category this theme applies to.
        diagram_formats:
          type: array
          items: { type: string, enum: [ascii, mermaid] }
          description: Diagram formats allowed in markdown; only ASCII and Mermaid supported.
        branding:
          type: object
          description: Organization branding. For resume and similar categories, logos are typically omitted; use colors and typography only.
          properties:
            org_name:
              type: string
              description: Organization name (footer and title slide). Suppressed on title slide when logo_includes_wordmark is true.
            assets_dir:
              type: string
              description: Directory for images, backgrounds, media; logo/favicon paths resolved relative to this.
            logo_path:
              type: string
              description: Logo with text for title slide. Resolved relative to assets_dir. Omit for resume.
            favicon_path:
              type: string
              description: Logo mark for footer. Resolved relative to assets_dir. Omit for resume.
            logo_includes_wordmark:
              type: boolean
              default: false
              description: When true, the logo_path image already contains the org name as part of the graphic; renderer suppresses the org_name text on the title slide to avoid duplication.
            colors:
              type: object
              description: Brand color palette. All values are CSS color strings (hex, rgb(), named).
              properties:
                primary: { type: string, description: "Primary brand color (e.g. footer background, primary text)." }
                accent: { type: string, description: "Accent color for decorative elements." }
                light_blue: { type: string, description: "Secondary accent color." }
                purple: { type: string, description: "Accent variation." }
                title_slide_bg: { type: string, description: "Title slide background color." }
                title_slide_bg_gradient: { type: string, description: "Full CSS background value for the title slide (overrides title_slide_bg when set). Accepts gradient, multi-layer, or repeating-pattern shorthand. See deckrun-theme-profile.schema.json for examples including dark diagonal gradient, radial glow, and dot-grid pattern on white." }
                title_slide_text: { type: string, description: "Title slide text color (e.g. #ffffff for dark backgrounds)." }
                title_line_gradient: { type: string, description: "CSS linear-gradient for the separator line under title slide h2 and content slide headings (::after pseudo-element). Typically 2-3 stops, left-to-right, accent to primary to soft neutral." }
                heading_color: { type: string, description: "Slide title and section header color." }
                body_text_color: { type: string, description: "Body text color. Defaults to primary when not set." }
                footer_bg: { type: string, description: "Footer bar background color." }
                footer_text: { type: string, description: "Footer bar text color." }
            typography:
              type: object
              description: Font families and sizes. Renderer falls back to defaults if a requested font is unavailable.
              properties:
                title_font: { type: string, description: "Font for slide/document titles (e.g. Inter, Lato)." }
                body_font: { type: string, description: "Font for body text." }
                footer_font: { type: string, description: "Font for footer text." }
                title_slide_title_size: { type: string, description: "Title slide main title size (e.g. 88px)." }
                title_slide_subtitle_size: { type: string, description: "Title slide subtitle size (e.g. 28px)." }
                slide_title_size: { type: string, description: "Regular slide title size (e.g. 36px)." }
                body_text_size: { type: string, description: "Body text size (e.g. 16px)." }
                footer_text_size: { type: string, description: "Footer text size (e.g. 14px)." }
            layout:
              type: object
              description: Page dimensions and logo sizing. For resume use page sizes (e.g. 8.27in x 11.69in A4); omit title_logo_* when no logo.
              properties:
                slide_width: { type: string, description: "Slide/page width (e.g. 13.333in for 16:9, 8.27in for A4)." }
                slide_height: { type: string, description: "Slide/page height (e.g. 7.5in for 16:9, 11.69in for A4)." }
                aspect_ratio: { type: string, description: "Aspect ratio (e.g. 16 / 9)." }
                title_logo_max_height: { type: string, description: "Title slide logo max height (e.g. 64px). Omit for resume." }
                title_logo_max_width: { type: string, description: "Title slide logo max width (e.g. 400px). Omit for resume." }
        contact:
          type: object
          properties:
            name: { type: string }
            title: { type: string }
            email: { type: string, format: email }
            location: { type: string }
        voice:
          type: object
          description: Default narration for this theme. When voice_id is set, it references an onboarded voice (GET /voices). Otherwise use tier (x-narration-tiers) and optional voice_identifier. Internal TTS backend names are not exposed.
          properties:
            voice_id: { type: string, description: Onboarded voice (deckrun://voices/...). Takes precedence when set. }
            tier: { $ref: '#/components/schemas/NarrationTier', description: Narration tier when voice_id is not set. }
            voice_identifier: { type: string, description: Opaque voice id for premium tier. }
            credential_id: { type: string, description: Optional credential for premium tier. }
        language:
          type: object
          properties:
            slide_language: { type: string }
            notes_language: { type: string, description: Default BCP-47 language for TTS narration. Applied to notes blocks without a per-block language attribute. Per-block <!-- <slide-notes language="..."> --> overrides this. Multiple blocks per slide with different languages enable multilingual narration from a single source. }
            audio_language: { type: string }
            translate: { type: boolean }
        image:
          type: object
          properties:
            provider_id: { type: string }
            credential_id: { type: string }
            options: { type: object }
        template:
          type: object
          properties:
            theme_id: { type: string }
            template_id: { type: string }
        paths:
          type: object
          properties:
            template_dir: { type: string }

    SlideThemeList:
      type: object
      description: List of slide themes for the account (paginated when limit/offset used)
      properties:
        slide_themes:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              summary: { type: string }
              system_provided: { type: boolean, description: When true, theme cannot be deleted; may be customized via PUT. }
        next_offset:
          type: integer
          description: If present, use offset=next_offset for next page; absent when no more pages

    Voice:
      type: object
      description: "Narration voice resource. Canonical schema: x-public-schema-refs voice (deckrun-voice.json). Used by GET /voices, GET /voices/{voice_id}, POST /voices response. Reference by voice_id in POST /generate or in a slide theme. Classified by tier (x-narration-tiers); internal TTS backend is not exposed."
      required: [voice_id, tier, display_name, status]
      properties:
        voice_id:
          type: string
          description: "Stable Deckrun voice identifier (e.g. deckrun://voices/shivaram-founder). Use in POST /generate."
        tier:
          $ref: '#/components/schemas/NarrationTier'
        voice_identifier:
          type: string
          description: Opaque backend voice id for premium tier. Omitted for system tier.
        display_name:
          type: string
          description: Human-readable name for this voice.
        status:
          type: string
          enum: [active, inactive, archived]
          description: active = usable for generation; inactive = temporarily disabled; archived = retired.
        owner_account_id:
          type: string
          description: Account that owns this voice (scoping, AuthZ).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        source:
          type: string
          enum: [system, user_upload, enterprise_import]
        capabilities:
          type: object
          description: Tier capabilities. Helps UIs avoid unsupported combinations.
          properties:
            supports_cloning: { type: boolean }
            supports_style_controls: { type: boolean }
        metadata:
          type: object
          additionalProperties: true
    VoiceCreate:
      type: object
      description: Request body for POST /voices. Tier determines plan gating; tier_params are tier-specific (e.g. voice_identifier for premium, system_voice_id for system). See voice_onboarding.md and x-narration-tiers.
      required: [tier, display_name]
      properties:
        tier:
          $ref: '#/components/schemas/NarrationTier'
        display_name:
          type: string
        tier_params:
          type: object
          description: Tier-specific options (e.g. voice_identifier, language for premium; system_voice_id for system). Opaque to API; see docs.
          additionalProperties: true
    VoicePatch:
      type: object
      description: Request body for PATCH /voices/{voice_id}. Only display_name and status are updatable.
      properties:
        display_name: { type: string }
        status: { type: string, enum: [active, inactive, archived] }
    VoiceList:
      type: object
      description: Response for GET /voices (paginated).
      properties:
        voices:
          type: array
          items: { $ref: '#/components/schemas/Voice' }
        next_offset:
          type: integer
          description: If present, use offset=next_offset for next page; absent when no more pages

    Connector:
      type: object
      description: "Storage or delivery connector (account-level). Used as output_destination (and optionally asset_source). Credentials never returned in API. Full output_destination/connection shape: x-public-schema-refs output_connector (deckrun-output-connector.json)."
      properties:
        id: { type: string, description: Unique connector id; use in output_destination.connector_id. }
        type:
          type: string
          enum: [dropbox, box, google_drive, onedrive, s3, slack]
          description: Connector type. Matches output_destination.connector_type.
        display_name: { type: string, description: Human-readable name (e.g. Team S3 bucket). }
        config:
          type: object
          description: Type-specific config (e.g. bucket/prefix for S3, channel for Slack). No credentials.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ConnectorList:
      type: object
      description: Response for GET /connectors. supported_types = connector types this server supports (for output_destination and optionally asset_source). connectors = configured connectors for this account (no credentials). Agents use this to see what is available and what is configured.
      properties:
        supported_types:
          type: array
          items: { type: string, enum: [dropbox, box, google_drive, onedrive, s3, slack] }
          description: Connector types supported by this server. If a type is missing from connectors, it is supported but not configured; agent may prompt user to add one or use inline connection.
        connectors:
          type: array
          items: { $ref: '#/components/schemas/Connector' }
          description: Configured connectors for the current account (metadata and config only; credentials never returned).
        next_offset: { type: integer, description: If present, use offset=next_offset for next page; absent when no more pages. }

    Account:
      type: object
      description: |
        Current account (identity, plan, credits). Canonical schema: x-public-schema-refs user_account (deckrun-user-account.json). GET /me returns this shape.
        Cursor-style: plan + credits for every account; seats (used/available) for team/enterprise. See https://agenticdecks.com/docs/ for plan and credits.
        **Sensitive**—response is scoped to the caller's API key; do not log or echo in agent UIs.
      required: [account_id, plan]
      properties:
        account_id:
          type: string
          description: Unique account identifier. GET /me returns identity, plan, and **usage summary** (credits.used_this_period, period_allowance, balance) for display and quota checks.
        plan:
          type: string
          enum: [basic, plus, pro, max, team, enterprise]
          description: Tier. basic/plus/pro/max = individual; team = multi-seat, per-user allowance; enterprise = org-level, pooled credits. organization typically set for team/enterprise.
        credits:
          type: object
          description: Always present. Period allowance, used this period, optional balance. For enterprise org = pooled; for team member = this user's slice.
          properties:
            balance: { type: integer, minimum: 0, description: Current available credits (e.g. top-up) }
            period_allowance: { type: integer, minimum: 0, description: Credits per billing period; zero for free tier }
            period_start: { type: string, format: date-time }
            period_end: { type: string, format: date-time }
            used_this_period: { type: integer, minimum: 0 }
            scope: { type: string, enum: [user, organization], description: user = this user's slice; organization = pooled/org-wide }
        seats:
          type: object
          description: Present for plan team or enterprise (GitHub-style). used = seats in use; available = total licensed.
          properties:
            used: { type: integer, minimum: 0 }
            available: { type: integer, minimum: 0 }
        connectors:
          type: object
          description: Storage connector usage. used = current count; limit = max by plan (-1 = unlimited). See x-connector-limits.
          properties:
            used: { type: integer, minimum: 0 }
            limit: { type: integer, minimum: -1, description: -1 = unlimited }
        email:
          type: string
          format: email
        name:
          type: string
          description: Display name for the account
        identity_sub:
          type: string
          description: Identity provider subject (stable, never changes); used for AuthN linking
        default_slide_theme_id:
          type: string
          description: ID of the account default slide theme, if set
        subscription_expires_at:
          type: string
          format: date-time
          description: When current subscription period ends; past implies expired
        billing_status:
          type: string
          enum: [active, past_due, canceled, trialing]
        billing_email:
          type: string
          format: email
        organization:
          type: object
          description: Optional; when account is org-scoped, primary organization
          properties:
            id: { type: string }
            name: { type: string }
            slug: { type: string }
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ProvidersCatalog:
      type: object
      description: Catalog by capability. Voice entries use narration tiers (system, premium); image entries use asset options. Internal TTS backend names are not exposed.
      additionalProperties:
        type: array
        items:
          $ref: '#/components/schemas/ProviderEntry'

    ProviderEntry:
      type: object
      description: For voice capability, id is a narration tier (system, premium). Internal TTS backend names are not exposed.
      properties:
        id: { type: string, description: Narration tier or capability id (e.g. system, premium for voice). }
        name: { type: string, description: Display name (e.g. System voice, Premium voice). }
        plan: { type: string, description: Minimum plan for this tier/capability (all, paid). }
        options: { type: array, items: { type: string }, description: Optional list of options or variants. }
        requires_credential: { type: boolean, description: When true, account must supply credential for this tier/capability. }

    DocumentCategory:
      type: string
      enum: [slide, resume, whitepaper, datasheet, patent, generic, book]
      description: Document category. slide = presentation deck (default); resume, whitepaper, datasheet = implemented document types with standardization specs and dedicated render pipelines; patent, generic, book = other markdown doc types. Determines which format schema and pipeline apply.
      default: slide

    NarrationTier:
      type: string
      enum: [system, premium]
      description: |
        Narration tier (product/plan level). Used in GET /providers (voice), Voice, VoiceCreate, and theme.voice. Internal TTS backends are not exposed; see x-narration-tiers.
        system = built-in voices (all plans); premium = high-fidelity expressive voices (paid plans).

    GenerateRequest:
      type: object
      description: Request body for POST /generate. Unknown top-level fields are ignored or reserved for future use.
      required: [markdown]
      additionalProperties: true
      properties:
        document_category:
          $ref: '#/components/schemas/DocumentCategory'
        markdown:
          type: string
          description: >-
            Slide/document content as markdown. Must be saved as markdown (this is the only accepted format)
            and should pass the markdown linter (POST /validate or POST /generate with output_types ["lint"]).
            Must conform to the format schema for document_category (GET /schema?category=...).
            For slide decks, `<slide>` metadata may include video tags (`video_src`, `youtube_url`,
            `video_audio_mode`, `video_start_secs`, `video_end_secs`, `video_duck_db`) for video output
            behavior; these tags do not alter PDF layout rendering.
            **Document CSS overrides (resume, whitepaper, datasheet):** Place a `<style>` block anywhere
            in the markdown to override CSS variables without creating a custom theme. Each document type
            uses its own variable prefix — use the correct prefix for the target type or the overrides
            will have no effect.
            **Resume (`--rs-*`):** `--rs-body-size` (default 10.5pt), `--rs-line-height` (1.4),
            `--rs-margin` (0.5in), `--rs-sidebar-width` (28%), `--rs-accent`, `--rs-text`,
            `--rs-text-secondary`, `--rs-rule`, `--rs-link`, `--rs-sidebar-bg`.
            **Whitepaper and datasheet (`--ds-*`):** `--ds-body-size` (whitepaper 10.5pt, datasheet 9pt),
            `--ds-line-height` (whitepaper 1.45, datasheet 1.35), `--ds-margin-left`, `--ds-margin-right`,
            `--ds-margin-top`, `--ds-margin-bottom`, `--ds-accent`, `--ds-text`, `--ds-text-secondary`,
            `--ds-rule`. Datasheet only: `--ds-cover-bg`, `--ds-cover-right-bg`, `--ds-footer-bg`.
            Example (resume): `<style>:root { --rs-body-size: 8.5pt; --rs-line-height: 1.1; --rs-margin: 0.4in; }</style>`
            Example (whitepaper): `<style>:root { --ds-body-size: 9pt; --ds-line-height: 1.2; --ds-margin-left: 0.4in; --ds-margin-right: 0.4in; }</style>`
        two_column:
          type: boolean
          default: false
          description: "For document_category=resume: render in two-column layout (Skills, Certifications, and Awards sections extracted to right sidebar). Ignored for other categories. CLI: --two-column flag."
        assets:
          type: object
          additionalProperties:
            type: string
          description: |
            Inline base64-encoded assets map. Key = relative path as used in the markdown (e.g. "images/foo.png" must match the path in background_img or <img src="...">, and "videos/demo.mp4" must match video_src). Value = base64-encoded file content (not a data URL). Used to resolve slide media references (images and embedded MP4 clips) in markdown. Path traversal ('..') and absolute paths are rejected (security). Hosted API caps: MP4 assets <= 50 MB each; image assets <= 10 MB each. Violations return HTTP 400 with code `media_too_large`.
        slide_theme_id:
          type: string
          description: Theme to apply to this generation (PDF and other outputs always use a theme). Use the id returned from POST /slide-themes. When document_category is slide, use a slide theme; for other categories, use a theme whose category matches. Omit for account default (GET /me.default_slide_theme_id).
        document_theme_id:
          type: string
          description: >-
            Document theme ID for resume, whitepaper, or datasheet. Resolved from
            {themes_dir}/document/<id>.json (deckrun-document-profile schema; see x-public-schema-refs).
            Overrides CSS variable defaults: branding.colors (accent, link_color, sidebar_bg,
            body_text_color, primary), branding.typography (body_font, body_text_size, line_height),
            branding.layout (margins, sidebar_width_percent). Absent fields leave stylesheet defaults
            in place. Omit to use system-provided defaults (doc-resume, doc-whitepaper, doc-datasheet).
            **System-provided themes:** `doc-resume` (default resume), `doc-whitepaper`, `doc-datasheet`,
            `no-branding` (neutral palette, suppresses all account-level branding — logo, org name,
            contact, and brand colors — suitable for open-source, generic, or personal documents where
            account identity should not appear).
        voice_id:
          type: string
          description: Onboarded voice identifier (from GET /voices). When output_types includes audio or video, use this voice for narration; overrides theme default voice when set. Omit to use theme voice or account default.
        output_types:
          type: array
          items:
            type: string
            enum: [pdf, video, notes, notes_html, notes_text, audio, manifest, lint, estimate]
          description: |
            Requested outputs. `lint` returns validation issues only (no artifact). `estimate` returns a pre-flight RU estimate in result.ru_estimate without generating any artifact — use before submitting a full job to check cost. All other values produce artifacts. Multiple values are allowed; use output_bundle zip for multi-artifact delivery.
        async:
          type: boolean
          default: false
          description: If true, returns 202 with job_id and status_url
        callback_url:
          type: string
          format: uri
          description: Webhook URL when job completes (Pro/Max). Payload shape see WebhookPayload schema.
        idempotency_key:
          type: string
          format: uuid
          description: Optional; header Idempotency-Key preferred
        asset_urls:
          type: object
          additionalProperties: { type: string, format: uri }
          description: "Map logical name to URL (e.g. diagram.png -> https://...)"
        asset_inline:
          type: object
          additionalProperties: { type: string }
          description: "Map logical name to data URL (e.g. data:image/png;base64,...)"
        asset_source:
          type: object
          description: Resolve asset paths from an account connector instead of asset_urls/asset_inline.
          properties:
            connector_id: { type: string, description: ID from GET /connectors }
            path_prefix: { type: string, description: Optional prefix for paths in markdown (e.g. images/) }
        output_bundle:
          type: string
          enum: [individual, zip]
          default: individual
          description: |
            When individual (default), each artifact is a separate URL or file.
            When zip, all requested artifacts are bundled into one zip file; exactly one object is delivered (response zip_url or connector output_url). Zip is a delivery format, not an artifact type.
            When output_types includes more than one artifact type (e.g. pdf + video, pdf + notes), use output_bundle zip; server may require zip for multi-artifact requests when output_destination is set.
            See output_destination and Connector in this spec.
        output_destination:
          type: object
          description: |
            Send generated output to a single connector (Dropbox, Box, Google Drive, OneDrive, S3, or Slack).
            Request-level path overrides connector default when present. Use either output_destination (inline/stored by connector_id) or response URLs, or both.
            connector_id is resolved server-side only; if inline connection is supplied for the same request, inline overrides the stored connector. Credentials are never merged between inline and stored.
            Schema shape matches deckrun-output-connector OutputDestination. When output_bundle is zip, connector receives one timestamped .zip file.
          required: [connector_type, artifacts]
          properties:
            connector_type: { type: string, enum: [dropbox, box, google_drive, onedrive, s3, slack], description: Which connector to use for output delivery. }
            connector_id: { type: string, description: Optional id when multiple connectors of the same type exist; resolved server-side. Use either connector_id (stored) or inline connection, not both; credentials never merged. }
            path: { type: string, description: Path or key for storage connectors; overrides connector default. Omitted for Slack. Server may append timestamp to filename. }
            artifacts: { type: array, items: { type: string, enum: [pdf, video, audio, notes] }, minItems: 1, description: Which artifacts to send; when output_bundle is zip, connector receives one zip. }
        output_destinations:
          type: array
          description: Deprecated in favor of output_destination (singular). Push generated artifacts to these connectors after generation.
          items:
            type: object
            required: [connector_id, path]
            properties:
              connector_id: { type: string }
              path: { type: string, description: Path in connector (e.g. decks/out.pdf) }
              artifacts: { type: array, items: { type: string, enum: [pdf, video, notes_html, notes_text, audio, manifest] }, description: Which artifacts to push; default all generated }
        voice_override:
          type: object
          description: Per-run override for narration. When set, overrides theme default and voice_id for this request. Use tier (or tier id as provider_id) per x-narration-tiers.
          properties:
            provider_id: { type: string, description: Narration tier id (system or premium) when overriding voice for this run. }
            credential_id: { type: string }
            options: { type: object }
        image_override:
          type: object
          description: Future; per-run image provider override
          properties:
            provider_id: { type: string }
            credential_id: { type: string }
            options: { type: object }
        slide_language:
          type: string
          description: Override primary deck language (e.g. en, es)
        notes_language:
          type: string
          description: Default BCP-47 language for TTS narration (e.g. "en-US", "ta-IN"). Applied to notes blocks without a per-block language attribute. Per-block override via <!-- <slide-notes language="ta-IN"> --> takes precedence. A single slide may have multiple notes blocks each with different languages for multilingual narration.
        audio_language:
          type: string
        translate:
          type: boolean
          description: When true, translate notes to audio_language before TTS (default false)
        category_options:
          type: object
          description: "Category-specific options (extensibility). resume: use top-level two_column field for layout. patent: uspto_compliant, ascii_to_svg. generic: template_id, css_id. See API documentation for document categories. Unknown keys are ignored."
          additionalProperties: true
        options:
          type: object
          properties:
            keep_html: { type: boolean }
            slide_transition_delay_secs: { type: number }
            custom_audio_url: { type: string, format: uri }

    GenerateResponseSync:
      type: object
      description: Deprecated. Use JobStatus for POST /generate 200 (sync returns same shape as GET /jobs/{id} when status=success).
      properties:
        pdf_url: { type: string, format: uri }
        notes_html_url: { type: string, format: uri }
        notes_text_url: { type: string, format: uri }
        audio_url: { type: string, format: uri }
        video_url: { type: string, format: uri }
        manifest_url: { type: string, format: uri }
        issues:
          type: array
          description: Present when output_types included lint
          items:
            $ref: '#/components/schemas/LintIssue'

    GenerateResponseAsync:
      type: object
      description: |
        202 response when async accepted or when job is queued.
        When queued, includes status, queue_position, estimated_wait_seconds (GenerateResponseAsync / JobStatus in this spec).
      properties:
        job_id: { type: string, description: Bouncer-assigned job ID }
        status_url: { type: string, format: uri, description: "URL for GET /jobs/{id}" }
        status:
          type: string
          enum: [queued, in-flight]
          description: Present when job is queued or in-flight; omitted when simply accepted without queue visibility
        queue_position:
          type: integer
          minimum: 0
          description: Present when status is queued; 1-based position in queue
        estimated_wait_seconds:
          type: number
          minimum: 0
          description: Present when status is queued; rough estimate in seconds
        message:
          type: string
          description: Optional hint (e.g. when ?sync=true and timeout_seconds exceeded — "Sync timeout reached; poll GET /jobs/{job_id} for result.")

    WebhookPayload:
      type: object
      description: Payload sent to callback_url when an async job completes (POST, application/json).
      properties:
        job_id: { type: string }
        status: { type: string, enum: [success, failure, timeout, cancelled], description: Terminal status; success = completed, failure = failed or error }
        result:
          type: object
          description: When status is success; same shape as GenerateResponseSync URLs (pdf_url, video_url, etc.)
          additionalProperties: true
        result_urls:
          type: object
          description: Deprecated. Use result. When status is success; artifact URLs
        error:
          $ref: '#/components/schemas/Error'
          description: Present when status is failure or timeout; same shape as JobStatus.error.
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    JobStatus:
      type: object
      description: |
        Job status (sync/async, POST /generate 200/202, GET /jobs/{id}). Canonical schema: x-public-schema-refs job_status (deckrun-job-status.json).
        queued = not yet assigned to a worker; in-flight = assigned; success|failure|timeout|cancelled = terminal.
      required: [job_id, status]
      properties:
        job_id:
          type: string
          description: Bouncer-assigned job identifier (preferred). id is deprecated alias.
        id:
          type: string
          description: Deprecated. Use job_id. Kept for backward compatibility.
        status:
          type: string
          enum: [queued, in-flight, success, failure, timeout, cancelled]
          description: queued = not assigned; in-flight = running; success|failure|timeout|cancelled = terminal
        queue_position:
          type: integer
          minimum: 0
          description: Present when status is queued
        estimated_wait_seconds:
          type: number
          minimum: 0
          description: Present when status is queued
        result:
          type: object
          description: |
            When status is success; artifact URLs and/or connector delivery URLs.
            When output_bundle was zip, exactly one of zip_url (response) or output_url (connector) is present.
            When output was sent to a remote storage connector, output_url is the URL in that store (object names are timestamped; see x-connector-timestamped-filenames).
            When output was sent to Slack, slack_message_url is the message permalink.
          properties:
            pdf_url: { type: string, format: uri, description: Legacy; GET /artifacts or transient URL; deleted after first download when no connector. }
            zip_url: { type: string, format: uri, description: When output_bundle was zip and response delivery; single zip containing requested artifacts. }
            output_url: { type: string, format: uri, description: When output_destination was a remote storage connector; URL of the uploaded file (or zip) in that store. }
            output_path: { type: string, description: Reserved for non-server runtimes; server connector responses use output_url (storage) or slack_message_url (Slack). }
            slack_message_url: { type: string, format: uri, description: When output_destination was slack; permalink to the posted message. }
            notes_html_url: { type: string, format: uri }
            notes_text_url: { type: string, format: uri }
            audio_url: { type: string, format: uri }
            video_url: { type: string, format: uri }
            manifest_url: { type: string, format: uri }
            render_units:
              $ref: '#/components/schemas/RuBreakdown'
              description: Render Unit breakdown for this job. Present when computed by the worker (v1+); omitted for legacy or connector-only jobs.
            ru_estimate:
              $ref: '#/components/schemas/RuEstimate'
              description: Pre-flight RU estimate. Present only when output_types included "estimate" without other artifact types.
          additionalProperties: true
        result_urls:
          type: object
          description: Deprecated. Use result. When status is success; same shape as result URLs
        error:
          $ref: '#/components/schemas/Error'
          description: Present when status is failure or timeout
        idempotency_key: { type: string, description: Echo of Idempotency-Key when provided }
        idempotency_status:
          type: string
          enum: [new, replay, in-flight, completed]
          description: Idempotency-Status for this response
        request_id: { type: string, description: Echo of X-Request-Id for tracing }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ValidateResponse:
      type: object
      properties:
        valid: { type: boolean }
        issues:
          type: array
          items:
            $ref: '#/components/schemas/LintIssue'
        estimated_pages:
          type: integer
          description: |
            Estimated page/slide count. For slides: exact parse count.
            For documents: word-count heuristic (ceil(words/600), min 1).
        estimated_ru:
          $ref: '#/components/schemas/RuEstimate'
          description: RU estimate. Only present when output_types was provided in the request.

    RuEstimate:
      type: object
      description: Pre-flight RU estimate. Accuracy is ±15% for document types.
      properties:
        estimated_ru:
          type: integer
          description: Estimated render units for the requested output types.
        basis:
          type: string
          enum: [slide-count, word-count]
          description: How logical units were determined.
        warning:
          type: string
          description: Present when estimate accuracy may be limited (document types).

    CapabilitiesResponse:
      type: object
      description: Static platform capability snapshot. Does not reflect personal plan limits — use GET /me for those.
      properties:
        outputs:
          type: array
          items: { type: string }
          description: All output types the platform can produce (union across all tiers).
        tiers:
          type: object
          description: Per-tier capability breakdown.
          properties:
            free:
              $ref: '#/components/schemas/TierCapabilities'
            paid:
              $ref: '#/components/schemas/TierCapabilities'
        document_categories:
          type: array
          items: { type: string }
          description: Supported document categories.
        job_model:
          type: array
          items: { type: string }
          description: Supported job execution models (sync, async).
        auth:
          type: array
          items: { type: string }
          description: Supported authentication methods for the paid tier.
        pricing_model:
          type: string
          description: Billing model identifier (e.g. render-units).
        schemas_version:
          type: string
          description: Schema version string for format and theme schemas.

    TierCapabilities:
      type: object
      properties:
        outputs:
          type: array
          items: { type: string }
          description: Output types available on this tier.
        auth:
          type: array
          items: { type: string }
          description: Authentication methods accepted on this tier.

    LintIssue:
      type: object
      properties:
        rule: { type: string }
        code: { type: string }
        severity: { type: string, enum: [error, warning] }
        message: { type: string }
        line: { type: integer }
        slide_index: { type: integer }
        range: { type: array, items: { type: integer } }

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Error code (e.g. INVALID_MARKDOWN, SCHEMA_VIOLATION, RATE_LIMITED, PROFILE_NOT_FOUND, JOB_FAILED, IDEMPOTENCY_CONFLICT, INSUFFICIENT_CREDITS, CAPACITY_EXCEEDED, PROVIDER_NOT_AVAILABLE_IN_PLAN, INPUT_TOO_LARGE)
        message:
          type: string
        details:
          type: object
          description: Optional additional context
    IdempotencyConflictError:
      type: object
      description: Returned when a duplicate request uses the same Idempotency-Key within TTL
      required: [code, message]
      properties:
        code:
          type: string
          enum: [IDEMPOTENCY_CONFLICT]
        message:
          type: string
        details:
          type: object
          properties:
            original_request_id:
              type: string
              description: Request or job id from the original response
            key_ttl_seconds:
              type: integer
              description: Remaining TTL for the idempotency key

    RuBreakdown:
      type: object
      description: |
        Render Unit (RU) breakdown for a completed job. The RU model is the billing denominator as of v1.
        total_ru is the authoritative billable amount. All other fields are for transparency and audit.
        Multipliers are loaded from live-infra-costs.json; see render-units-spec §12 for the formula.
      required: [logical_units, base_ru, subtotal, minimum_applied, total_ru]
      properties:
        logical_units:
          type: integer
          minimum: 0
          description: Logical pages or slides in the input. Exact for slides; word-count heuristic (ceil(words/600), min 1) for documents.
        base_ru:
          type: number
          minimum: 0
          description: Base RU before output-type multipliers — logical_units × base_ru_per_logical_unit.
        output_contributions:
          type: array
          description: Per-output-type RU contributions — each item is [output_type, ru_amount]. output_type keys are from live-infra-costs.json (e.g. pdf, tts_system, tts_premium, video_silent, video_embedded_mp4, notes_text, notes_cue, notes_audio_manifest, zip).
          items:
            type: array
            minItems: 2
            maxItems: 2
            prefixItems:
              - { type: string, description: Output type key }
              - { type: number, description: RU contribution }
        cache_discount:
          type: number
          description: RU discount applied when caching is enabled (live-infra-costs.json caching_discount).
        subtotal:
          type: number
          minimum: 0
          description: base_ru + sum(output_contributions) − cache_discount.
        minimum_applied:
          type: boolean
          description: True when total_ru was raised to the minimum_job_ru floor (e.g. 2 RU minimum per job).
        total_ru:
          type: integer
          minimum: 0
          description: Final RU charged for this job — ceil(subtotal), at least minimum_job_ru. This is the value recorded by the billing service.
      additionalProperties: false

    RuEstimate:
      type: object
      description: |
        Pre-flight RU estimate returned when output_types includes "estimate". No artifact is generated.
        Agents should call with ["estimate"] before a heavy generation job to check expected cost.
      required: [estimated_ru, basis]
      properties:
        estimated_ru:
          type: integer
          minimum: 0
          description: Estimated total RU for the requested outputs. Uses the same multipliers as post-render accounting.
        basis:
          type: string
          description: How logical units were counted (e.g. "slide_separators" or "word_count").
        warning:
          type: string
          description: Optional human-readable warning (e.g. "Large document — estimate may vary by ±20%").
      additionalProperties: false

    FreeGenerateRequest:
      type: object
      required: [markdown]
      additionalProperties: false
      properties:
        markdown:
          type: string
          maxLength: 51200
          description: >
            Markdown source for the slide deck. Slide separator is `\n---\n`.
            Maximum 10 slides and 50 KB total.
        schema_version:
          type: string
          description: >
            Optional. Declare which Deckrun format version the Markdown targets.
            Agents should send "deckrun.v1". Echoed back in the response.
            Currently informational only; future versions may enforce compatibility.
          example: "deckrun.v1"

    FreeGenerateResponse:
      type: object
      required: [url, slug]
      properties:
        url:
          type: string
          format: uri
          description: Public URL to the generated PDF. Expires after 90 days.
        slug:
          type: string
          description: 10-character hex identifier for this free artifact.
        slides:
          type: integer
          minimum: 1
          description: Number of slides in the generated deck. Useful for agents to verify the deck matches intent.
          example: 6
        warnings:
          type: array
          items:
            type: string
          description: >
            Non-fatal notices about the generation. Agents should inspect and self-correct
            on subsequent requests if possible.
          example:
            - "Title-slide layout tag missing from first slide; rendered with default layout."
        schema_version:
          type: string
          description: Echoed from the request, or "deckrun.v1" if not provided.
          example: "deckrun.v1"

  responses:
    BadRequest:
      description: Bad request (invalid markdown, schema violation, etc.)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: SCHEMA_VIOLATION
            message: "Unknown layout tag on slide 2"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found (e.g. slide theme, job)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: Payment required — over quota or insufficient credits (402)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: INSUFFICIENT_CREDITS
            message: "Insufficient credits to complete this request."
    ServiceUnavailable:
      description: Service overloaded or capacity exhausted (503). Retry with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: CAPACITY_EXCEEDED
            message: "Service at capacity; try again later."
    Forbidden:
      description: Operation not allowed (e.g. attempt to delete a system-provided theme)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: THEME_SYSTEM_PROVIDED
            message: "System-provided themes cannot be deleted; you may customize them via PUT."
    RateLimited:
      description: Rate limit exceeded (429). Headers may include RateLimit-Limit, RateLimit-Remaining, Retry-After.
      headers:
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimit-Limit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimit-Remaining'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InputTooLarge:
      description: |
        Input too large (413). The markdown or document exceeds the per-plan page/size limit.
        Agents should check GET /me for plan limits and use output_types ["estimate"] to pre-flight check
        RU cost before submitting large documents.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: INPUT_TOO_LARGE
            message: "Document exceeds the 10-page limit for the Free plan. Upgrade or reduce document length."
    IdempotencyConflict:
      description: Duplicate request; same Idempotency-Key was already used within TTL. Return original response or this error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/IdempotencyConflictError'
    InternalError:
      description: Server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
