{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/creativefabrica/json-schema/creativefabrica-generation-schema.json",
  "title": "Generation",
  "description": "A generation resource. Returned by create (202), get and list.",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/creativefabrica-idea-ai-genai-openapi.yml#/components/schemas/Generation",
  "type": "object",
  "required": [
    "id",
    "status",
    "modality",
    "outputType",
    "model",
    "createdAt"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "Generation id (KSUID)."
    },
    "status": {
      "$ref": "#/$defs/GenerationStatus"
    },
    "modality": {
      "$ref": "#/$defs/Modality"
    },
    "outputType": {
      "$ref": "#/$defs/OutputType"
    },
    "model": {
      "type": "string"
    },
    "provider": {
      "type": "string",
      "description": "Upstream provider serving the model."
    },
    "prompt": {
      "type": "string",
      "description": "The prompt this generation was created with. Absent on preset runs,\nwhose prompt is the preset's own.\n"
    },
    "options": {
      "description": "The validated options this generation was created with — image or\nvideo shape per the sibling `modality` field. Absent on preset runs.\n",
      "anyOf": [
        {
          "$ref": "#/$defs/ImageGenerationOptions"
        },
        {
          "$ref": "#/$defs/VideoGenerationOptions"
        },
        {
          "$ref": "#/$defs/GenericGenerationOptions"
        }
      ]
    },
    "coinAmount": {
      "type": "integer",
      "format": "int64",
      "description": "Coins reserved for this generation — the total across every requested\noutput. Present on the create (202) response. Get and list omit it:\nthe amount is not stored on the generation, so read the coin ledger\nfor the charge on a past generation.\n\nFor most models this matches `POST /v1/models/{model}/cost`. On\nreserve-and-settle models it can differ from the quote and is not the\nfinal charge:\n\n- `seed_audio_1_0`: the maximum reserved; the generation settles down\n  to the real cost on completion.\n- `fal_veed_clean_audio_v1` (Clean Audio): an uploaded audio file is\n  held at the quoted declaration-based amount; a **URL-sourced**\n  audio file holds the role maximum — **6,440 coins** (30 minutes) —\n  even when the quote is lower, and settles down onto fal's reported\n  input duration.\n"
    },
    "promptVariables": {
      "type": "object",
      "description": "The prompt variable values a preset run used: the submitted values\nwith the preset's defaults applied. Absent on direct generations.\n",
      "additionalProperties": true
    },
    "presetId": {
      "type": [
        "string",
        "null"
      ],
      "description": "The preset this generation ran, absent on direct generations."
    },
    "presetVersion": {
      "type": [
        "integer",
        "null"
      ],
      "format": "int32",
      "description": "The preset version this generation ran, absent on direct generations."
    },
    "metadata": {
      "$ref": "#/$defs/Metadata"
    },
    "error": {
      "$ref": "#/$defs/GenerationError"
    },
    "inputs": {
      "type": "array",
      "description": "Input media (present on reads).",
      "items": {
        "$ref": "#/$defs/InputMediaInfo"
      }
    },
    "outputs": {
      "type": "array",
      "description": "Output media (present on reads). One entry per requested output.",
      "items": {
        "$ref": "#/$defs/Output"
      }
    },
    "uploads": {
      "type": "array",
      "description": "Presigned upload targets — one per file declared in upload mode,\npresent only on create responses with `status: upload_pending`.\nFiles given by `url` need no upload and get no target, so a request\nmixing the two modes gets fewer targets than it declared files;\npair them by `fileName`. PUT each file's raw\nbytes to its `uploadUrl` with `Content-Type` set to the file's real\ntype, before `expiresAt`. The PUTs go straight to S3, may run in any\norder, and need no confirmation call: the generation dispatches once\nthe last one lands. See **Uploading input files**.\n",
      "items": {
        "$ref": "#/$defs/UploadTarget"
      }
    },
    "createdAt": {
      "type": "string",
      "format": "date-time"
    },
    "completedAt": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "failedAt": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    }
  },
  "$defs": {
    "AudioOutput": {
      "type": "object",
      "required": [
        "url"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Generated audio file URL."
        },
        "fileName": {
          "type": "string"
        },
        "mimeType": {
          "type": "string",
          "description": "Container type of the audio file, e.g. `audio/mpeg`."
        },
        "durationSeconds": {
          "type": "number",
          "format": "double",
          "description": "Playback length of the generated audio in seconds, reported exactly\nrather than rounded.\n\nDo not assume this is the duration a duration-priced model billed on.\nSeed Audio bills on a speed-independent duration, so a nonzero\n`speech_rate` shortens playback without reducing the charge; at the\ndefault rate the two agree.\n"
        },
        "fileSizeBytes": {
          "type": "integer",
          "format": "int64"
        }
      }
    },
    "BoundingBox": {
      "type": "object",
      "description": "Placement on the base plate, base-image pixel coordinates, origin top-left.",
      "required": [
        "left",
        "top",
        "right",
        "bottom"
      ],
      "properties": {
        "left": {
          "type": "integer"
        },
        "top": {
          "type": "integer"
        },
        "right": {
          "type": "integer"
        },
        "bottom": {
          "type": "integer"
        }
      }
    },
    "GenerationError": {
      "type": [
        "object",
        "null"
      ],
      "description": "Terminal failure detail, set when `status` is `failed`.",
      "required": [
        "code"
      ],
      "properties": {
        "code": {
          "type": "string",
          "description": "Stable machine-readable failure code (e.g. `NSFW`, `PROVIDER_ERROR`, `EXPIRED`)."
        },
        "message": {
          "type": "string"
        }
      }
    },
    "GenerationStatus": {
      "type": "string",
      "description": "Generation lifecycle:\n`upload_pending` → awaiting client file uploads;\n`pending` → queued;\n`processing` → running at the provider;\n`completed` / `failed` / `cancelled` → terminal.\n",
      "enum": [
        "upload_pending",
        "pending",
        "processing",
        "completed",
        "failed",
        "cancelled"
      ]
    },
    "GenericGenerationOptions": {
      "type": "object",
      "description": "Model-specific generation options for audio, text/analysis and 3D\nmodalities. The authoritative fields, types, allowed values and defaults\nare published by `GET /v1/models/{model}` under\n`capabilities.<modality>.inputSchema`; undeclared fields are rejected.\n",
      "additionalProperties": true
    },
    "ImageGenerationOptions": {
      "type": "object",
      "description": "Options for image generations (`textToImage`, `imageToImage`). This is\nthe cross-model vocabulary — every field is optional and only valid for\nmodels that declare it. The authoritative per-model field list, allowed\nvalues and defaults live in the catalog:\n`GET /v1/models/{model}` → `capabilities.<modality>.inputSchema`.\nOptions a model does not declare are rejected with `validation_error`.\n",
      "properties": {
        "outputCount": {
          "type": "integer",
          "minimum": 1,
          "description": "How many outputs to generate in one request. Only valid for models\nwhose `inputSchema` declares it — no current model does, so sending\nit is rejected with `validation_error` until a model adds support.\n"
        },
        "aspectRatio": {
          "type": "string",
          "description": "Image aspect ratio, e.g. `1:1`, `16:9`. Allowed values vary per\nmodel — check the model's `inputSchema`.\n",
          "x-go-name": "ImageAspectRatio"
        },
        "imageSize": {
          "type": "string",
          "description": "Image size tier, e.g. `1k`, `2k`, `4k`. Only for models whose\n`inputSchema` declares it.\n"
        }
      },
      "additionalProperties": true
    },
    "ImageOutput": {
      "type": "object",
      "required": [
        "url"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Full-resolution image URL."
        },
        "previewUrl": {
          "type": "string",
          "format": "uri"
        },
        "thumbnails": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Thumbnail"
          }
        },
        "width": {
          "type": "integer"
        },
        "height": {
          "type": "integer"
        },
        "mimeType": {
          "type": "string"
        },
        "fileSizeBytes": {
          "type": "integer",
          "format": "int64"
        }
      }
    },
    "InputMediaInfo": {
      "type": "object",
      "required": [
        "id",
        "status"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "role": {
          "type": "string"
        },
        "alias": {
          "type": "string"
        },
        "fileName": {
          "type": "string"
        },
        "mimeType": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Resolved URL of the input once available."
        },
        "status": {
          "type": "string",
          "description": "Upload state of this input (`pending`, `uploaded`, `failed`). On a\ngeneration stuck in `upload_pending` this is what identifies the file\nwhose bytes never arrived.\n"
        }
      }
    },
    "Layer": {
      "type": "object",
      "required": [
        "url",
        "stackingOrder",
        "boundingBox"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Transparent PNG of this layer."
        },
        "mimeType": {
          "type": "string"
        },
        "fileSizeBytes": {
          "type": "integer",
          "format": "int64"
        },
        "width": {
          "type": "integer",
          "description": "Layer bitmap width in px — may exceed the bounding box (layers are\nupscaled crops); scale into `boundingBox` when compositing.\n"
        },
        "height": {
          "type": "integer"
        },
        "stackingOrder": {
          "type": "integer",
          "description": "1..N above the base (the base is 0); draw ascending."
        },
        "boundingBox": {
          "$ref": "#/$defs/BoundingBox"
        },
        "name": {
          "type": "string",
          "description": "Provider-supplied label; language not guaranteed."
        },
        "description": {
          "type": "string",
          "description": "Provider-supplied description; language not guaranteed."
        }
      }
    },
    "LayeredImageOutput": {
      "type": "object",
      "description": "A layer-decomposed image: one base plate plus an ordered stack of\ntransparent layers positioned on the base canvas. Recompose by drawing the\nbase, then each layer ascending by `stackingOrder`, scaled into its\n`boundingBox`.\n",
      "required": [
        "baseImage",
        "layers"
      ],
      "properties": {
        "baseImage": {
          "allOf": [
            {
              "$ref": "#/$defs/ImageOutput"
            }
          ],
          "description": "Base plate (stacking order 0). Carries the preview and thumbnails for\nthe whole layered output; its dimensions define the bounding-box\ncoordinate space.\n"
        },
        "layers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Layer"
          }
        }
      }
    },
    "Metadata": {
      "type": "object",
      "description": "Opaque client key/value metadata, echoed back on reads.",
      "additionalProperties": {
        "type": "string"
      }
    },
    "Modality": {
      "type": "string",
      "description": "What the generation transforms. Valid combinations per model are listed in the model catalog.",
      "enum": [
        "textToImage",
        "imageToImage",
        "textToAudio",
        "textToVideo",
        "mediaToVideo",
        "mediaToAudio",
        "mediaToImage",
        "imageToText",
        "mediaToText",
        "imageTo3D",
        "textTo3D",
        "textToText"
      ]
    },
    "Model3DOutput": {
      "type": "object",
      "required": [
        "url"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri"
        },
        "fileName": {
          "type": "string"
        },
        "mimeType": {
          "type": "string"
        },
        "fileSizeBytes": {
          "type": "integer",
          "format": "int64"
        }
      }
    },
    "Output": {
      "type": "object",
      "description": "One generated output. Exactly one type-specific payload is set (matching\nthe generation's `outputType`) once the output completes.\n\nThe file behind `url` is deleted 3 days after the generation was\ncreated — see **Output retention** in the API overview.\n",
      "required": [
        "id",
        "type",
        "status"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "type": {
          "$ref": "#/$defs/OutputType"
        },
        "status": {
          "$ref": "#/$defs/OutputStatus"
        },
        "errorCode": {
          "type": "string",
          "description": "Stable failure code when `status` is `failed`."
        },
        "failureReason": {
          "type": "string",
          "description": "Human-readable failure detail when `status` is `failed`."
        },
        "image": {
          "$ref": "#/$defs/ImageOutput"
        },
        "video": {
          "$ref": "#/$defs/VideoOutput"
        },
        "audio": {
          "$ref": "#/$defs/AudioOutput"
        },
        "text": {
          "$ref": "#/$defs/TextOutput"
        },
        "model3d": {
          "$ref": "#/$defs/Model3DOutput"
        },
        "layeredImage": {
          "$ref": "#/$defs/LayeredImageOutput"
        }
      }
    },
    "OutputStatus": {
      "type": "string",
      "description": "Per-output lifecycle. Terminal states are `completed` and `failed`.",
      "enum": [
        "pending",
        "processing",
        "completed",
        "failed"
      ]
    },
    "OutputType": {
      "type": "string",
      "description": "The media type this generation produces. `layered_image` is one base image\nplus positioned transparent layers, consolidated into a single output.\n",
      "enum": [
        "image",
        "audio",
        "video",
        "text",
        "3d_model",
        "layered_image"
      ]
    },
    "TextOutput": {
      "type": "object",
      "description": "Plain text or structured JSON output.",
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Download URL when the response is stored externally."
        },
        "mimeType": {
          "type": "string"
        },
        "data": {
          "type": "object",
          "description": "Inline text/JSON data. Plain text is returned as `{ \"text\": \"...\" }`.\n",
          "additionalProperties": true
        }
      }
    },
    "Thumbnail": {
      "type": "object",
      "required": [
        "url",
        "width"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri"
        },
        "width": {
          "type": "integer",
          "description": "Rendered width in pixels."
        }
      }
    },
    "UploadTarget": {
      "type": "object",
      "description": "Where to PUT one declared file. Returned only on `upload_pending`\ncreates — see **Uploading input files** for the full flow.\n",
      "required": [
        "fileName",
        "uploadUrl",
        "expiresAt"
      ],
      "properties": {
        "role": {
          "type": "string",
          "description": "Echoes the request file's `role`."
        },
        "fileName": {
          "type": "string",
          "description": "Echoes the request file's `fileName` — this is what pairs an upload\ntarget with the file you declared, so name two files in one request\ndifferently.\n"
        },
        "uploadId": {
          "type": "string",
          "description": "Upload id assigned by the uploads service. Informational: nothing has\nto be sent back with it, but quoting it helps support trace a\nspecific upload.\n"
        },
        "uploadUrl": {
          "type": "string",
          "format": "uri",
          "description": "Presigned S3 URL. Send the file's raw bytes as the body of a `PUT`\nto it — not multipart, and with no `Authorization` header of your\nown, since the URL carries its own signature and S3 rejects a\nrequest that presents two. Set `Content-Type` to the file's real\ntype: nothing pins it, so S3 stores what you send, and a stored\ntype that is not an image, video or audio type — or that its own\nleading bytes contradict — has the object deleted on arrival and\nfails the generation. The model's `acceptedFormats` are checked\nseparately, against the extension of `fileName`.\n"
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the URL stops working — 15 minutes after it was issued. It\ncannot be renewed: there is no re-issue endpoint, so a generation\nwhose URLs expired has to be created again. Note this is not the\ngeneration's own deadline, which runs from `createdAt` and covers\nupload plus generation.\n"
        }
      }
    },
    "VideoGenerationOptions": {
      "type": "object",
      "description": "Options for video generations (`textToVideo`, `mediaToVideo`). This is\nthe cross-model vocabulary — every field is optional and only valid for\nmodels that declare it. The authoritative per-model field list, allowed\nvalues and defaults live in the catalog:\n`GET /v1/models/{model}` → `capabilities.<modality>.inputSchema`.\nOptions a model does not declare are rejected with `validation_error`\n(e.g. `resolution` is not accepted by `kling_v3`, and\n`happy_horse_v1_video_editing` accepts neither `duration_seconds` nor\n`aspect_ratio`). Model-specific options outside this vocabulary (e.g.\n`audio_setting` on `happy_horse_v1_video_editing`) are passed as\nadditional properties, typed in the model's `inputSchema`.\n",
      "properties": {
        "duration_seconds": {
          "type": "integer",
          "description": "Requested video length in seconds. Range varies per model (e.g.\n4–15 on `seedance_v2`) and some models derive length from the\nsource media instead — check the model's `inputSchema`.\n"
        },
        "aspect_ratio": {
          "type": "string",
          "description": "Video aspect ratio, e.g. `16:9`, `9:16`. Allowed values vary per\nmodel and some models do not accept it — check the model's\n`inputSchema`.\n"
        },
        "resolution": {
          "type": "string",
          "description": "Output resolution, e.g. `720p`, `1080p`. Only for models whose\n`inputSchema` declares it.\n"
        },
        "mode": {
          "type": "string",
          "description": "Quality mode for models that tier their output (e.g. `kling_v3`:\n`std` / `pro` / `4k`). Only for models whose `inputSchema`\ndeclares it.\n"
        }
      },
      "additionalProperties": true
    },
    "VideoOutput": {
      "type": "object",
      "required": [
        "url"
      ],
      "description": "Every URL on this object is retained for 72 hours after the generation\ncompletes, then deleted. Download anything you need to keep within that\nwindow; afterwards the URLs stop resolving and cannot be reissued.\n",
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "The provider's original video file, served unchanged. Container and\nencoder details depend on the provider and the requested output options.\nThis is the only rendition: no preview, thumbnails or streaming manifests\nare produced.\n"
        },
        "durationSeconds": {
          "type": "integer"
        },
        "aspectRatio": {
          "type": "string"
        },
        "width": {
          "type": "integer"
        },
        "height": {
          "type": "integer"
        }
      }
    }
  }
}