openapi: 3.1.0
info:
  title: DreamLayer Agent API
  version: "1"
  description: >-
    Generate images, sprite sheets, and quoted videos; follow executions and retrieve owned outputs.
    Start with https://docs.dreamlayer.io/agent-api for authentication, limits,
    runnable examples, event recovery, and billing. Sprite sheets require account
    capability support and an approved credit ceiling. Video requires a quote before
    acceptance. Video schemas document the customer integration fields; response
    objects may contain additional fields. See https://docs.dreamlayer.io/agent-api/video.
x-dreamlayer-sse-events: [started, thinking, progress, job, question, asset, done]
servers:
  - url: https://api.dreamlayer.io
security:
  - bearerAuth: []
paths:
  /v1/capabilities:
    get:
      operationId: getCapabilities
      x-mint:
        href: /api-reference/get-v1capabilities
      summary: Discover available operations
      description: Read the operations and pricing available to the authenticated key. Does not start generation.
      parameters:
        - name: pricing
          in: query
          required: false
          description: Use current for the current sprite formula. Legacy fields are historical compatibility estimates, not current charges.
          schema: { type: string, enum: [legacy, current], default: legacy }
      responses:
        "200":
          description: Closed public capability contract
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Capabilities" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/balance:
    get:
      operationId: getBalance
      x-mint:
        href: /api-reference/get-v1balance
      summary: Read available credits
      description: Returns only the balance owned by the authenticated API key.
      parameters:
        - name: pricing
          in: query
          required: false
          description: Use current for the account purchase rate and four-decimal balances. Legacy preserves the historical 0.17 rate field and one-decimal balances for older CLI/MCP clients; it is not today's purchase price.
          schema: { type: string, enum: [legacy, current], default: legacy }
      responses:
        "200":
          description: Transactionally consistent Agent API credit balance
          headers:
            Cache-Control:
              schema: { type: string, const: "private, no-store" }
            Pragma:
              schema: { type: string, const: no-cache }
            Vary:
              schema: { type: string, const: Authorization }
            DreamLayer-Credit-Price-USD:
              description: Current account price for newly purchased API credits.
              schema: { type: string, example: "0.15" }
            DreamLayer-Balance-Pricing:
              schema: { type: string, enum: [legacy, current] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Balance" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/TemporaryFailure" }
  /v1/sprite-pricing:
    get:
      operationId: getSpritePrice
      summary: Estimate sprite usage credits
      description: Free account-specific estimate, not a reservation or binding quote. Submission checks the current price and your max_credits ceiling again. No account selector is accepted.
      parameters:
        - name: frame_count
          in: query
          schema: { type: integer, minimum: 7, maximum: 100, default: 12 }
        - name: background
          in: query
          schema: { type: string, enum: [remove, keep], default: remove }
      responses:
        "200":
          description: Nonbinding account estimate
          headers:
            Cache-Control:
              schema: { type: string, const: "private, no-store" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SpriteEstimate" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/AccessDenied" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/input-assets:
    post:
      operationId: uploadInputAsset
      x-mint:
        href: /api-reference/post-v1input-assets
      summary: Upload a reference image
      description: Create an owned input asset. Keep its ID and expiry; reuse the same ID when replaying a request.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        "201":
          description: Normalized owner-scoped input
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InputAsset" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "413": { $ref: "#/components/responses/InvalidRequest" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/TemporaryFailure" }
  /v1/execute:
    post:
      operationId: execute
      x-mint:
        href: /api-reference/post-v1execute
      summary: Start or replay an execution
      description: For images and sprites, credit and quota admission failures appear later in execution state; retryable admission failures remain queued. Video instead accepts a ready quote using only quote_id and max_credits and returns 202 JSON, or synchronous errors such as 402 insufficient_credits and 429 too_many_active_jobs. Use a stable Idempotency-Key and save the acceptance body before submitting. Video does not return an initial SSE stream. See https://docs.dreamlayer.io/agent-api/text-to-video.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/ExecuteRequest"
                - $ref: "#/components/schemas/VideoAccept"
      responses:
        "200":
          description: Sanitized server-sent events
          content:
            text/event-stream:
              schema: { type: string }
        "202":
          description: Durable execution accepted
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ExecutionAccepted"
                  - $ref: "#/components/schemas/VideoExecution"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "402": { $ref: "#/components/responses/VideoFailure" }
        "403": { $ref: "#/components/responses/VideoOrSharedFailure" }
        "409": { $ref: "#/components/responses/VideoOrSharedFailure" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/VideoOrSharedFailure" }
        "500": { $ref: "#/components/responses/TemporaryFailure" }
        "503": { $ref: "#/components/responses/VideoOrSharedFailure" }
  /v1/executions/{execution_id}:
    get:
      operationId: getExecution
      x-mint:
        href: /api-reference/get-v1executions
      summary: Read execution state
      description: Read canonical status after a timeout or dropped stream. A successful HTTP response does not mean the execution itself succeeded.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ExecutionId"
      responses:
        "200":
          description: Canonical execution state
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ExecutionStatus"
                  - $ref: "#/components/schemas/VideoExecution"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/executions/{execution_id}/events:
    get:
      operationId: resumeExecutionEvents
      x-mint:
        href: /api-reference/get-v1executions-events
      summary: Resume execution events
      description: Reconnect using the last processed Last-Event-ID. A stream boundary does not end or cancel the execution. Video streams use progress, asset, and done, without an initial started event; terminal events may repeat without IDs. Video permits three open streams per key. Read canonical state for expiry and billing. See https://docs.dreamlayer.io/agent-api/video-jobs.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ExecutionId"
        - name: Last-Event-ID
          in: header
          schema: { type: string }
      responses:
        "200":
          description: Persisted server-sent events after the supplied cursor
          content:
            text/event-stream:
              schema: { type: string }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/executions/{execution_id}/cancel:
    post:
      operationId: cancelExecution
      x-mint:
        href: /api-reference/post-v1executions-cancel
      summary: Cancel an eligible execution
      description: Cancel an eligible image execution or a Video execution that is still queued. Video returns 409 once submission has started. Sprite executions do not support customer cancellation.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ExecutionId"
      responses:
        "202":
          description: Eligible image or queued Video execution cancelled and any reservation restored once. Sprite requests cannot be cancelled.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ExecutionAccepted"
                  - $ref: "#/components/schemas/VideoExecution"
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "409": { $ref: "#/components/responses/VideoOrSharedFailure" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/video/capabilities:
    get:
      operationId: getVideoCapabilities
      x-mint:
        href: /api-reference/get-v1video-capabilities
      summary: Discover Video availability and defaults
      description: Does not start generation. Check enabled before quoting. Separate from image and sprite capabilities.
      parameters:
        - $ref: "#/components/parameters/Version"
      responses:
        "200":
          description: Customer-facing capability fields; additional response fields may be present.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VideoCapabilities" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/video/quotes:
    post:
      operationId: createVideoQuote
      x-mint:
        href: /api-reference/post-v1video-quotes
      summary: Quote a video before generation
      description: Returns resolved settings, expiry, and customer_price without starting generation or reserving credits. Sound defaults on unless silence is requested. An image-to-video quote requires one owned input asset. Review a ready quote and its API credit price before accepting it at /v1/execute. Quotes normally expire after ten minutes for new acceptance.
      parameters:
        - $ref: "#/components/parameters/Version"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VideoQuoteRequest" }
            examples:
              text:
                value:
                  operation: text_to_video
                  prompt: A slow camera push toward a forest waterfall with rushing water and birdsong.
                  options: { duration_seconds: 10, resolution: 720p, aspect_ratio: "16:9", generate_audio: true }
              image:
                value:
                  operation: image_to_video
                  input_asset_id: d34db33f-0000-4000-8000-000000000001
                  input_mode: first_frame
                  prompt: Animate the scene with gentle movement and soft outdoor ambience.
                  options: { duration_seconds: 10, generate_audio: true }
      responses:
        "200":
          description: A ready or pricing_pending quote. Only ready quotes can be accepted. Documented integration fields are a subset of the response.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VideoQuote" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/VideoFailure" }
        "404": { $ref: "#/components/responses/VideoFailure" }
        "422": { $ref: "#/components/responses/VideoOrSharedFailure" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/VideoFailure" }
  /v1/video/executions/{execution_id}/asset:
    get:
      operationId: downloadVideoAsset
      x-mint:
        href: /api-reference/get-v1video-executions-asset
      summary: Download an owned completed video
      description: Checks ownership and completion, then redirects to a signed MP4 URL valid for fifteen minutes. Output retention is thirty days after completion. Never forward the API Authorization header to the redirect destination. A renewed download link does not generate or charge again.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ExecutionId"
      responses:
        "302":
          description: Signed HTTPS download location for the MP4.
          headers:
            Location:
              required: true
              schema: { type: string, format: uri }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "404": { $ref: "#/components/responses/VideoFailure" }
        "410": { $ref: "#/components/responses/VideoFailure" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/VideoFailure" }
  /v1/conversations:
    get:
      operationId: listConversations
      x-mint:
        href: /api-reference/get-v1conversations
      summary: List recent conversations
      description: Read recent conversations belonging to the authenticated account. This endpoint is not a batch execution API.
      parameters:
        - $ref: "#/components/parameters/Version"
      responses:
        "200":
          description: Recent conversations owned by the authenticated account
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ConversationSummary" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/conversations/{conversation_id}:
    delete:
      operationId: deleteConversation
      x-mint:
        href: /api-reference/delete-v1conversations
      summary: Delete a conversation
      description: Delete an owned conversation. This is a destructive action; confirm the intended conversation before calling.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ConversationId"
      responses:
        "204":
          description: Terminal conversation deleted
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/image-jobs/{job_id}/asset:
    get:
      operationId: downloadImageAsset
      x-mint:
        href: /api-reference/get-v1image-jobsjob_idasset
      summary: Download a completed output
      description: Checks ownership and completion, then redirects to a signed download URL valid for 15 minutes. Use the download_url from the execution. Never forward the API Authorization header to the redirect destination. Returns PNG for images or ZIP for sprites.
      parameters:
        - name: job_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "302":
          description: Signed download location
          headers:
            Location:
              required: true
              schema: { type: string, format: uri }
        "200":
          description: Completed output bytes when served directly
          content:
            image/png:
              schema: { type: string, format: binary }
            application/zip:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/TemporaryFailure" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    Version:
      name: DreamLayer-Version
      in: header
      required: true
      schema: { type: string, const: "1" }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 1, maxLength: 200 }
    ExecutionId:
      name: execution_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ConversationId:
      name: conversation_id
      in: path
      required: true
      schema: { type: string, format: uuid }
  responses:
    VideoFailure:
      description: Video request rejected. Inspect error.reason and retryable. Credit or capacity rejection on acceptance does not create an execution.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/VideoErrorEnvelope" }
    VideoOrSharedFailure:
      description: Video business error or shared validation, authentication, or request-limit error. See the operation-specific recovery guide.
      content:
        application/json:
          schema:
            anyOf:
              - $ref: "#/components/schemas/VideoErrorEnvelope"
              - $ref: "#/components/schemas/ErrorEnvelope"
    InvalidRequest:
      description: Invalid request, including a missing required Authorization header
      headers: &errorHeaders
        Cache-Control:
          schema: { type: string, const: no-store }
        Pragma:
          schema: { type: string, const: no-cache }
        X-Request-ID:
          schema: { type: string, format: uuid }
      content: &errorContent
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Unauthenticated:
      description: Invalid or revoked API key. A missing Authorization header returns 422 invalid_request.
      headers: *errorHeaders
      content: *errorContent
    PaymentRequired:
      description: Insufficient credits or an account quota was reached
      headers: *errorHeaders
      content: *errorContent
    AccessDenied:
      description: The authenticated key cannot access this resource
      headers: *errorHeaders
      content: *errorContent
    NotFound:
      description: Owned resource not found
      headers: *errorHeaders
      content: *errorContent
    Conflict:
      description: Idempotency or state conflict
      headers: *errorHeaders
      content: *errorContent
    RateLimited:
      description: Synchronous request or open-stream limit reached. See https://docs.dreamlayer.io/agent-api/limits for scopes and backoff. Retry-After and remaining-request headers are not guaranteed.
      headers: *errorHeaders
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          example:
            error:
              code: RATE_LIMITED
              reason: rate_limited
              message: Too many requests. Please try again shortly.
              category: RATE_LIMITED
              retryable: true
              request_id: d34db33f-0000-4000-8000-000000000001
    TemporaryFailure:
      description: Retryable service failure
      headers: *errorHeaders
      content: *errorContent
  schemas:
    VideoCapabilities:
      type: object
      additionalProperties: true
      description: Integration-focused response fields, not an exhaustive wire schema. Do not log full responses.
      required: [enabled, operations, max_images, default_duration_seconds, resolution, audio_default, quote_required]
      properties:
        enabled: { type: boolean }
        operations:
          type: array
          items: { type: string, enum: [text_to_video, image_to_video] }
        max_images: { type: integer, const: 1 }
        default_duration_seconds: { type: integer, const: 10 }
        resolution: { type: string, const: 720p }
        audio_default: { type: boolean, const: true }
        quote_required: { type: boolean, const: true }
    VideoOptions:
      type: object
      additionalProperties: false
      description: Settings for the documented automatic Video workflow. Defaults are resolved with the prompt; conflicting instructions are rejected. This request schema describes the supported integration subset, not every server field.
      properties:
        duration_seconds:
          type: [integer, "null"]
          minimum: 4
          maximum: 30
          description: Whole seconds. Omitted or null uses a supported prompt duration, otherwise ten seconds. Quote validation can reject unsupported combinations.
        resolution: { type: string, enum: [720p], default: 720p }
        aspect_ratio:
          type: [string, "null"]
          enum: ["16:9", "9:16", "1:1", null]
          description: Text video only; defaults to 16:9 unless the prompt supplies a ratio. Omit for image-to-video, which uses a source-driven profile.
        generate_audio:
          type: [boolean, "null"]
          description: Omitted or null defaults to sound on unless the prompt requests silence. Explicit false requests silence. Conflicting sound instructions are rejected.
    VideoContext:
      type: object
      additionalProperties: false
      properties:
        use_case: { type: [string, "null"], enum: [product_showcase, general, null] }
        requested_background: { type: string, enum: [clean_white, non_white, unknown], default: unknown }
    VideoQuoteRequest:
      type: object
      additionalProperties: false
      required: [prompt]
      description: Describe a video. Exactly one source is required for image_to_video and none for text_to_video. The quote validates prompt/options consistency and source ownership, expiry, and normalized dimensions. It does not start generation.
      properties:
        operation: { type: [string, "null"], enum: [text_to_video, image_to_video, null] }
        prompt: { type: string, minLength: 1, maxLength: 4000, pattern: '\S' }
        input_asset_id: { type: [string, "null"], format: uuid }
        input_mode: { type: [string, "null"], enum: [text, first_frame, null] }
        options: { $ref: "#/components/schemas/VideoOptions" }
        context: { $ref: "#/components/schemas/VideoContext" }
    VideoSettings:
      type: object
      additionalProperties: false
      required: [duration_seconds, resolution, aspect_ratio, generate_audio]
      properties:
        duration_seconds: { type: integer }
        resolution: { type: string, const: 720p }
        aspect_ratio:
          type: string
          description: Resolved text aspect or the source-driven profile marker for image video. Read delivered metadata for actual width and height.
        generate_audio: { type: boolean }
    VideoQuote:
      type: object
      additionalProperties: true
      description: Customer integration fields. Additional response fields are allowed; this is not an exhaustive wire schema. Review resolved settings and customer_price, not a hardcoded price.
      required: [status, quote_id, expires_at, resolved_settings, customer_price, pricing_version, retention_days]
      properties:
        status: { type: string, enum: [ready, pricing_pending] }
        quote_id: { type: string, format: uuid }
        expires_at: { type: string, format: date-time }
        resolved_settings:
          type: object
          additionalProperties: false
          required: [operation, duration_seconds, resolution, aspect_ratio, generate_audio]
          properties:
            operation: { type: string, enum: [text_to_video, image_to_video] }
            duration_seconds: { type: integer }
            resolution: { type: string, const: 720p }
            aspect_ratio: { type: string }
            generate_audio: { type: boolean }
        customer_price:
          type: object
          additionalProperties: false
          required: [currency, amount]
          properties:
            currency: { type: string, const: api_credits }
            amount: { type: [string, "null"], description: "Decimal API credit amount, or null while pricing is pending." }
        pricing_version: { type: [string, "null"] }
        retention_days: { type: integer, const: 30 }
    VideoAccept:
      type: object
      additionalProperties: false
      required: [quote_id, max_credits]
      properties:
        quote_id: { type: string, format: uuid }
        max_credits:
          description: Positive API credit ceiling, at most 100, covering the quoted amount. Decimal strings preserve exact amounts. Never increase without approval.
          oneOf:
            - { type: number, exclusiveMinimum: 0, maximum: 100 }
            - { type: string, pattern: '^(?:0\.(?=[0-9]*[1-9])[0-9]+|[1-9][0-9]?(?:\.[0-9]+)?|100(?:\.0+)?)$' }
      examples:
        - { quote_id: d34db33f-0000-4000-8000-000000000002, max_credits: "33" }
    VideoState:
      type: string
      enum: [queued, submitting, uncertain, generating, delivering, completed, failed, cancelled, expired]
    VideoExecution:
      type: object
      additionalProperties: false
      required: [execution_id, conversation_id, status, image_job, video_job]
      properties:
        execution_id: { type: string, format: uuid }
        conversation_id: { type: string, format: uuid }
        status: { $ref: "#/components/schemas/VideoState" }
        image_job: { type: "null" }
        video_job:
          type: object
          additionalProperties: true
          description: Documented integration fields. Additional response fields can include original request data; do not log full responses.
          required: [stage, requested_settings, metadata, credits, credits_restored, settled_credits, reserved_credits, billing_note, error_code, retention_expires_at, download_url]
          properties:
            stage: { $ref: "#/components/schemas/VideoState" }
            requested_settings: { $ref: "#/components/schemas/VideoSettings" }
            metadata:
              type: [object, "null"]
              description: Actual delivered metadata, available after validation. Dimensions and duration can differ slightly from nominal settings.
              properties:
                width: { type: integer }
                height: { type: integer }
                duration_seconds: { type: number }
                fps: { type: number }
                audio_streams: { type: integer }
                codec: { type: string }
                content_type: { type: string, const: video/mp4 }
                bytes: { type: integer, maximum: 268435456 }
                sha256: { type: string }
            credits: { type: string }
            credits_restored: { type: boolean }
            settled_credits: { type: string }
            reserved_credits: { type: string }
            billing_note: { type: string }
            error_code: { type: [string, "null"] }
            retention_expires_at: { type: [string, "null"], format: date-time }
            download_url: { type: [string, "null"], description: Owned relative asset path on completion; authenticate only at the API origin. }
            created_at: { type: [string, "null"], format: date-time }
            completed_at: { type: [string, "null"], format: date-time }
    VideoErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, reason, message, retryable]
          properties:
            code: { type: string }
            reason: { type: string }
            message: { type: string }
            retryable: { type: boolean }
    PublicErrorReason:
      type: string
      enum:
        - invalid_request
        - authentication_failed
        - access_denied
        - resource_not_found
        - insufficient_credits
        - conflict
        - too_many_active_jobs
        - rate_limited
        - quota_exceeded
        - content_refused
        - temporarily_unavailable
        - generation_failed
        - insufficient_frames
        - prompt_policy_blocked
        - feature_unavailable
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, reason, message, category, retryable, request_id]
          properties:
            code:
              type: string
              description: Legacy uppercase code retained for existing clients.
            reason: { $ref: "#/components/schemas/PublicErrorReason" }
            message: { type: string }
            category: { type: string }
            retryable: { type: boolean }
            request_id: { type: string, format: uuid }
            details:
              type: object
              description: Optional sanitized validation detail. Submitted values are never echoed.
              additionalProperties: true
    Balance:
      type: object
      additionalProperties: false
      required: [promotional, purchased, available, credit_usd]
      properties:
        promotional: { type: number, minimum: 0, multipleOf: 0.0001 }
        purchased: { type: number, minimum: 0, multipleOf: 0.0001 }
        available: { type: number, minimum: 0, multipleOf: 0.0001 }
        credit_usd:
          type: string
          pattern: '^\d+\.\d{2}$'
          example: "0.15"
          description: Account purchase price with pricing=current; legacy balance reads retain 0.17 for compatibility. Not a wallet valuation.
    Capabilities:
      type: object
      additionalProperties: false
      required:
        - api_version
        - key_mode
        - operations
        - managed_execute
        - account_profile
        - managed_chat
        - managed_chat_versions
        - included_chat
        - image_batches
        - image_batch_versions
        - maximum_batch_images
        - image_workflows
        - image_workflow_versions
        - maximum_workflow_steps
        - server_sent_events
        - interactive_questions
        - maximum_input_bytes
        - maximum_input_dimension
        - staged_input_uploads
        - maximum_source_input_bytes
        - supported_input_extensions
        - sprite_sheet_credits
        - sprite_pricing
        - live_charge_credits_per_image
        - test_mode_uses_credits
      properties:
        api_version: { type: string, const: "1" }
        key_mode: { type: string, enum: [test, live] }
        operations:
          type: array
          items: { type: string, enum: [text_to_image, image_to_image, background_remove, upscale, sprite_sheet] }
        managed_execute: { type: boolean, const: true }
        account_profile: { type: boolean }
        managed_chat: { type: boolean }
        managed_chat_versions: { type: array, items: { type: integer } }
        included_chat: { type: boolean }
        image_batches: { type: boolean }
        image_batch_versions: { type: array, items: { type: integer } }
        maximum_batch_images: { type: integer }
        image_workflows: { type: boolean }
        image_workflow_versions: { type: array, items: { type: integer } }
        maximum_workflow_steps: { type: integer, const: 2 }
        server_sent_events: { type: boolean, const: true }
        interactive_questions: { type: boolean, const: true }
        maximum_input_bytes: { type: integer, const: 20971520 }
        maximum_input_dimension: { type: integer, const: 4096 }
        staged_input_uploads: { type: boolean, const: true }
        maximum_source_input_bytes: { type: integer, const: 209715200 }
        supported_input_extensions:
          type: array
          items: { type: string }
        live_charge_credits_per_image: { type: integer, const: 1 }
        sprite_sheet_credits: { type: [number, "null"], minimum: 0.1, maximum: 100, description: "Historical default-frame estimate unless pricing=current is requested. Deprecated for spending decisions; use the authenticated /v1/sprite-pricing account estimate." }
        sprite_pricing:
          oneOf:
            - $ref: "#/components/schemas/SpritePricing"
            - $ref: "#/components/schemas/CurrentSpritePricing"
        test_mode_uses_credits: { type: boolean, const: false }
    ExecuteRequest:
      type: object
      additionalProperties: false
      properties:
        prompt: { type: string, minLength: 1, maxLength: 4000 }
        respond: { type: string, minLength: 1, maxLength: 4000 }
        conversation_id: { type: string, format: uuid }
        input_asset_id: { type: string, format: uuid }
        aspect_ratio: { type: string, default: "1:1" }
        max_credits: { type: number, minimum: 0.1, maximum: 100, default: 1 }
        options: { $ref: "#/components/schemas/SpriteOptions" }
        operation:
          type: string
          enum: [text_to_image, image_to_image, background_remove, upscale, sprite_sheet]
          description: >-
            Optional. Name the operation to execute it deterministically: DreamLayer
            skips interpreting the prompt, so the request cannot be reclassified and
            cannot return a question event. No word in the prompt changes a named
            operation, and a named operation ignores an image remembered from an
            earlier turn in the same conversation. Omit it to let DreamLayer decide
            from the prompt. text_to_image must not carry input_asset_id;
            image_to_image, background_remove, upscale and sprite_sheet require it.
            sprite_sheet requires exactly one of options.action or
            options.animation_prompt. Read the authenticated /v1/sprite-pricing
            estimate for the requested frame_count and background, then set an
            explicitly authorized max_credits ceiling. Current amounts can have
            four decimal places. Estimates are non-binding; admission refuses a
            price above the ceiling before debiting. Default capabilities pricing
            is historical compatibility data, not a current account estimate.
            Put custom animation instructions in options.animation_prompt, not prompt.
            Sprite requests have no customer cancellation. Other operations require
            max_credits of 1 and do not accept options.


            upscale doubles each side, so the input's longest side must be 2048 or
            smaller: 4096 is the delivery ceiling and an oversize request is refused at
            execution admission before paid processing starts. That also means an
            upscaled image is AT maximum_input_dimension and cannot be upscaled again.
      oneOf:
        - required: [operation, input_asset_id, options]
          properties:
            operation: { const: sprite_sheet }
          not: { required: [respond] }
        - required: [prompt]
          not:
            anyOf:
              - required: [respond]
              - required: [operation]
                properties:
                  operation: { const: sprite_sheet }
        - required: [respond, conversation_id]
          not: { required: [prompt] }
    SpriteOptions:
      type: object
      additionalProperties: false
      description: >-
        Supply exactly one non-null action or animation_prompt. Presets default to
        loop playback; custom instructions default to once. Frame size affects the
        square export canvas, not price or guaranteed source detail. Insufficient
        distinct frames fail explicitly; frames are not duplicated or interpolated
        to meet the count.
      oneOf:
        - required: [action]
          properties:
            action: { type: string, enum: [walk, run, idle] }
            animation_prompt: { type: "null" }
        - required: [animation_prompt]
          properties:
            animation_prompt: { type: string, minLength: 1, maxLength: 4000, pattern: '\S' }
            action: { type: "null" }
      properties:
        action:
          type: [string, "null"]
          enum: [walk, run, idle, null]
          description: Preset alternative to animation_prompt. Walk and run use a right-facing side view; idle keeps the reference viewpoint.
        animation_prompt:
          type: [string, "null"]
          minLength: 1
          maxLength: 4000
          pattern: '\S'
          description: Describe the subject's animation, viewpoint, movement and style. Characters, creatures, objects, effects and turntables can be requested; quality is not guaranteed.
        animation_mode:
          type: [string, "null"]
          enum: [loop, once, null]
          description: Defaults to loop for presets and once for custom instructions. Once preserves the beginning and ending; a loop is not guaranteed seamless. Use atlas frame_durations_ms for playback timing.
        frame_count: { type: integer, minimum: 7, maximum: 100, default: 12 }
        frame_size:
          type: integer
          enum: [32, 64, 128, 256, 512, 720, 1080]
          default: 512
          description: Square PNG frame and sheet-cell size in pixels, with shared alignment and transparent padding. Does not change pricing.
        background:
          type: [string, "null"]
          enum: [remove, keep, null]
          description: Omit or send remove for transparent frames. Send keep to skip background removal and take the frames as generated, priced at $0.32 plus $0.01 per frame converted to credits at the account's undiscounted purchase rate.
      examples:
        - action: walk
          frame_count: 12
        - animation_prompt: Rotate this character through a full 360-degree turn with a fixed pose and stationary camera.
          animation_mode: loop
          frame_count: 7
          frame_size: 512
        - animation_prompt: Open the chest lid and hold it open.
          animation_mode: once
          frame_count: 7
          frame_size: 256
    SpritePricing:
      type: [object, "null"]
      description: Deprecated historical fields for older clients. Not the current charge. Use pricing=current and the account-specific sprite estimate endpoint.
      additionalProperties: false
      properties:
        minimum_frames: { type: integer, const: 7 }
        maximum_frames: { type: integer, const: 100 }
        default_frames: { type: integer, const: 12 }
        tier_boundary: { type: integer, const: 14 }
        first_tier_cents: { type: integer, const: 14 }
        additional_frame_cents: { type: integer, const: 7 }
        credit_cents: { type: integer, const: 17 }
        charge_rounding: { type: string, const: up_to_tenth }
        plain_frame_cents: { type: integer, const: 7 }
    CurrentSpritePricing:
      type: object
      additionalProperties: false
      required: [price_version, minimum_frames, maximum_frames, default_frames, transparent_base_credits, transparent_frame_credits, kept_base_reference_usd, kept_frame_reference_usd, charge_rounding, estimate_path]
      properties:
        price_version: { type: string, const: sprite-base-frame-20261017-v1 }
        minimum_frames: { type: integer, const: 7 }
        maximum_frames: { type: integer, const: 100 }
        default_frames: { type: integer, const: 12 }
        transparent_base_credits: { type: string, const: "4.2" }
        transparent_frame_credits: { type: string, const: "0.2" }
        kept_base_reference_usd: { type: string, const: "0.32" }
        kept_frame_reference_usd: { type: string, const: "0.01" }
        charge_rounding: { type: string, const: up_to_four_decimals }
        estimate_path: { type: string, const: /v1/sprite-pricing }
    SpriteEstimate:
      type: object
      additionalProperties: false
      required: [price_version, frame_count, background, credits, reference_usd, reference_credit_cents, binding]
      properties:
        price_version: { type: string, const: sprite-base-frame-20261017-v1 }
        frame_count: { type: integer, minimum: 7, maximum: 100 }
        background: { type: string, enum: [remove, keep] }
        credits: { type: string, pattern: '^\d+(\.\d{1,4})?$' }
        reference_usd: { type: string, pattern: '^\d+\.\d{2}$' }
        reference_credit_cents:
          type: integer
          minimum: 1
          description: The authenticated account's undiscounted API credit purchase rate in US cents.
        binding: { type: boolean, const: false }
    InputAsset:
      type: object
      additionalProperties: false
      required: [input_asset_id, content_type, width, height, expires_at]
      properties:
        input_asset_id: { type: string, format: uuid }
        content_type: { type: string, const: image/png }
        width: { type: integer, minimum: 1, maximum: 4096 }
        height: { type: integer, minimum: 1, maximum: 4096 }
        expires_at: { type: string, format: date-time }
    ExecutionAccepted:
      type: object
      additionalProperties: false
      required: [execution_id, conversation_id, status]
      properties:
        execution_id: { type: string, format: uuid }
        conversation_id: { type: string, format: uuid }
        status:
          type: string
          enum: [needs_input, queued, running, completed, failed, cancelled]
    ExecutionStatus:
      type: object
      additionalProperties: false
      required: [execution_id, conversation_id, status, image_job, question, error]
      properties:
        execution_id: { type: string, format: uuid }
        conversation_id: { type: string, format: uuid }
        status:
          type: string
          enum: [needs_input, queued, running, completed, failed, cancelled]
        image_job:
          oneOf:
            - $ref: "#/components/schemas/ImageJob"
            - type: "null"
        question:
          description: >-
            The outstanding clarification, so a client that reconnected or never held the
            stream can still answer it. Null whenever nothing is outstanding, which
            includes an execution whose question has already been answered: answering
            creates a new execution and the one that asked stays at status needs_input
            with a null question. A non-null question is the only reliable signal that a
            turn is still waiting. Additive: treat a missing field as null.
          oneOf:
            - $ref: "#/components/schemas/ExecutionQuestion"
            - type: "null"
        error:
          description: Public admission failure when status is failed; otherwise null.
          oneOf:
            - $ref: "#/components/schemas/ExecutionError"
            - type: "null"
    ExecutionError:
      type: object
      additionalProperties: false
      required: [reason, message, retryable]
      properties:
        reason: { $ref: "#/components/schemas/PublicErrorReason" }
        message: { type: string }
        retryable: { type: boolean }
    ExecutionQuestion:
      type: object
      additionalProperties: false
      required: [question_id, text, requires_asset]
      properties:
        question_id: { type: string, format: uuid }
        text: { type: string }
        requires_asset:
          type: boolean
          description: >-
            When true the answer must carry input_asset_id as well as respond. Answering
            without one is refused with a 422 whose details name input_asset_id.
    ImageJob:
      type: object
      additionalProperties: false
      required: [public_job_id, status, finished_assets, sanitized_error]
      properties:
        public_job_id: { type: string, format: uuid }
        status: { type: string, enum: [queued, running, completed, failed] }
        finished_assets:
          type: array
          maxItems: 1
          items:
            oneOf:
              - { $ref: "#/components/schemas/FinishedAsset" }
              - { $ref: "#/components/schemas/FinishedBundle" }
        sanitized_error:
          oneOf:
            - $ref: "#/components/schemas/SanitizedError"
            - type: "null"
        frame_review:
          description: >-
            Optional completed-sprite metadata. Null or absent means unknown, not
            a clean inspection result. Older jobs need not contain this field.
            Ordinary images and unfinished jobs return null. All frame numbers
            are one-based output slots, not source-video frame indices.
          oneOf:
            - $ref: "#/components/schemas/SpriteFrameReview"
            - type: "null"
    SpriteFrameReview:
      type: object
      additionalProperties: false
      required: [frame_count, background, kept_background_frames, inspection_frames, source_shifted_frames]
      description: >-
        Each frame list is ordered, unique, and bounded by frame_count. Intentional
        keep mode lists every slot in kept_background_frames and leaves inspection
        and source-shift lists empty; keeping a requested background is not a defect.
      properties:
        frame_count: { type: integer, minimum: 7, maximum: 100 }
        background: { type: string, enum: [remove, keep] }
        kept_background_frames:
          type: array
          maxItems: 100
          uniqueItems: true
          description: Output slots delivered with their backgrounds still present.
          items: { type: integer, minimum: 1, maximum: 100 }
        inspection_frames:
          type: array
          maxItems: 100
          uniqueItems: true
          description: Slots worth inspecting, not a claim that the artwork is defective.
          items: { type: integer, minimum: 1, maximum: 100 }
        source_shifted_frames:
          type: array
          maxItems: 100
          uniqueItems: true
          description: Slots successfully rescued using a nearby source-video moment.
          items: { type: integer, minimum: 1, maximum: 100 }
    FinishedAsset:
      type: object
      additionalProperties: false
      required: [asset_id, download_url, content_type, width, height]
      properties:
        asset_id: { type: string, format: uuid }
        download_url: { type: string }
        content_type: { type: string, const: image/png }
        width: { type: integer, minimum: 1, maximum: 4096 }
        height: { type: integer, minimum: 1, maximum: 4096 }
    FinishedBundle:
      type: object
      additionalProperties: false
      description: >-
        Complete sprite sheet dimensions, not individual frame dimensions. The
        bounds below cover all supported frame counts and sizes. The correction
        is deployed; frame_size 512 is the default, not a required workaround.
        Existing completed outputs do not require regeneration or another charge;
        retry status and download with the original execution ID.
      required: [asset_id, download_url, content_type, width, height, size_bytes]
      properties:
        asset_id: { type: string, format: uuid }
        download_url: { type: string }
        content_type: { type: string, const: application/zip }
        width: { type: integer, minimum: 32, maximum: 10800 }
        height: { type: integer, minimum: 32, maximum: 10800 }
        size_bytes: { type: integer, minimum: 1, maximum: 134217728 }
    SanitizedError:
      type: object
      additionalProperties: false
      required: [code, reason, message, retryable, request_id]
      properties:
        code:
          type: string
          const: generation_failed
          description: Legacy value retained for existing clients. New clients use reason.
        reason:
          type: string
          enum: [invalid_request, rate_limited, content_refused, temporarily_unavailable, generation_failed, insufficient_frames]
        message: { type: string }
        retryable: { type: boolean }
        request_id: { type: string, format: uuid }
    ConversationSummary:
      type: object
      additionalProperties: false
      required: [conversation_id, created_at, updated_at]
      properties:
        conversation_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
