openapi: 3.1.0
info:
  title: DreamLayer Managed Image API
  version: "1"
  description: >-
    Generate or edit images, follow durable 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.
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.
      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.
      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 }
          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/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: Credit and quota admission failures appear later as a failed execution with error, not a submit-time 402. Retryable admission failures remain queued for service retries. Submit one logical request with a stable Idempotency-Key. Accept application/json for a 202 acknowledgement or text/event-stream for SSE. Save the execution ID before following events. See https://docs.dreamlayer.io/agent-api/examples.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExecuteRequest" }
      responses:
        "200":
          description: Sanitized server-sent events
          content:
            text/event-stream:
              schema: { type: string }
        "202":
          description: Durable execution accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExecutionAccepted" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/AccessDenied" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/TemporaryFailure" }
        "503": { $ref: "#/components/responses/TemporaryFailure" }
  /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: { $ref: "#/components/schemas/ExecutionStatus" }
        "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 to existing work using the last processed Last-Event-ID. A stream boundary does not end or cancel the execution. See https://docs.dreamlayer.io/agent-api/jobs-and-events.
      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. Sprite executions do not support customer cancellation.
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/ExecutionId"
      responses:
        "202":
          description: Eligible image execution cancelled and any reservation restored once. Sprite requests cannot be cancelled.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExecutionAccepted" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "422": { $ref: "#/components/responses/InvalidRequest" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /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:
    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:
    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.1 }
        purchased: { type: number, minimum: 0, multipleOf: 0.1 }
        available: { type: number, minimum: 0, multipleOf: 0.1 }
        credit_usd: { type: string, const: "0.17" }
    Capabilities:
      type: object
      additionalProperties: false
      required:
        - api_version
        - key_mode
        - operations
        - managed_execute
        - 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 }
        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: Rounded charge for the default twelve frames. }
        sprite_pricing: { $ref: "#/components/schemas/SpritePricing" }
        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. Set max_credits to at least the rounded quote
            for the requested frame_count using sprite_pricing from capabilities.
            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.
      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"]
      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 }
    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"
    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 }
