openapi: 3.1.0
info:
  title: Memes.ai API
  version: "2026-08-16"
  summary: Generate scroll-stopping, on-brand ad creative programmatically.
  description: |
    The Memes.ai API lets developers generate scroll-stopping, on-brand ad creative
    programmatically: meme-style ad images, edits / inpaints / reframes, short-form video,
    multi-slide carousels, multi-panel storyboards, and brand kits — from a
    prompt, a website URL, and/or reference images, all grounded in a reusable brand profile.

    It is a key-authed REST API mounted at `/v1`, with a single async pattern (`generate -> poll`,
    optional webhooks) and one normalized job / asset model. An SMB tool, agency platform, or
    autonomous agent can spin up a complete branded ad campaign in a handful of calls without
    ever touching a model provider. Every billable creative operation returns a **Generation**
    job that you poll at `GET /generations/{id}` or receive via a signed webhook, and every
    output is a normalized **Asset**.

    > **Beta.** v1 endpoints are open for early integrations. Endpoints, schemas, and credit
    > costs may receive additive changes before general availability. Watch the changelog at
    > https://studio.memes.media/developers. The branded `model`
    > family (Forge) is the only model identifier the API ever emits; underlying engines may be
    > re-routed, versioned, or failed over without any change to public strings.
  termsOfService: https://studio.memes.media/developers/terms
  contact:
    name: Memes.ai Developer Platform
    email: members@memes.com
    url: https://studio.memes.media/developers
  license:
    name: Memes.ai API License
    url: https://studio.memes.media/developers/license

externalDocs:
  description: Memes.ai developer documentation
  url: https://studio.memes.media/developers

servers:
  - url: https://api.memes.media/v1
    description: Production base URL

security:
  - BearerAuth: []

x-request-controls:
  description: |
    Every request is charged weighted points across API-key, workspace, and client-IP
    fixed-window buckets. Expensive operations also acquire weighted concurrency leases.
    All responses include `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`;
    rejected requests also include `Retry-After`. Authentication or scope failures may return
    `401` or `403`, and any operation may return `429` when a rate, concurrency, simulator, or
    credit limit is exhausted. Every POST additionally requires `Idempotency-Key` and may return
    `409` for an in-flight or conflicting retry.

tags:
  - name: Generations
    description: |
      The core concept. Creative operations return a **Generation** resource. Most are
      asynchronous; reframe returns an already-completed generation. Poll asynchronous jobs —
      meme, image, edit, video, carousel, or storyboard — at `GET /generations/{id}` for one
      normalized envelope and `output[]` asset list, or subscribe to webhooks for push completion.
  - name: Images
    description: |
      Text-to-image and reference-conditioned ad images, instruction edits, mask inpaint, and
      aspect-ratio reframe (outpaint). Backed by the `forge-image`, `forge-image-fast`, and
      `forge-edit` models.
  - name: Memes
    description: |
      The flagship branded-ad generator with full lineage tracking (`parent_id` / `root_id`):
      brand-grounded ad images, edits, and reframes. Backed by `forge-image` and `forge-edit`.
  - name: Videos
    description: |
      Short-form ad video: text-to-video, reference-to-video (1–4 reference images), and
      image-to-video (animate an existing asset). Backed by `forge-video` and `forge-motion`.
  - name: Carousels
    description: |
      Two-step multi-slide ad sets. A zero-credit, synchronous strategy `plan` step (`forge-plan`)
      followed by a billable per-slide `render` fan-out (`forge-image`).
  - name: Storyboards
    description: |
      A single composite 3–4 panel narrative ad image (not a fan-out). Backed by `forge-image`.
  - name: Brand Kits
    description: |
      Persistent brand profiles — logo, colors, voice, audience, product references — that
      ground every generation. Persistence is zero-credit; live URL analysis and remote-media
      ingestion still require the public API's paid-workspace entitlement. A workspace may hold
      public API creation is rejected once the workspace has 50 total Brand Kits.
  - name: Prompt Enhancement
    description: |
      Turn a rough idea into a polished, production-ready creative prompt with `forge-prompt`.
  - name: Webhooks
    description: |
      Register and manage signed callback endpoints that receive job-completion events. See the
      top-level `webhooks` object for the outbound event payloads. A workspace may register at
      most 25 endpoints, including disabled endpoints.
  - name: Account
    description: |
      Read your workspace plan, key prefix, livemode flag, and current credit consumption.

paths:
  ############################################################################
  # GENERATIONS
  ############################################################################
  /generations:
    get:
      operationId: listGenerations
      summary: List generations
      description: |
        List jobs for the workspace, newest first by default. Filter by `type` and `status`.
        Returns a cursor-paginated list of full generation envelopes.
      tags: [Generations]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/Order"
        - name: type
          in: query
          description: Filter by generation type.
          required: false
          schema:
            $ref: "#/components/schemas/GenerationType"
        - name: status
          in: query
          description: Filter by job status.
          required: false
          schema:
            $ref: "#/components/schemas/GenerationStatus"
      responses:
        "200":
          description: A paginated list of generations.
          headers:
            X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Generation" }
              example:
                data:
                  - id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
                    object: generation
                    type: meme
                    status: succeeded
                    model: forge-image
                    requested_count: 4
                    completed_count: 4
                    failed_count: 0
                    output:
                      - id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
                        type: image
                        url: https://cdn.memes.media/assets/5Tk8wQ.png
                        thumbnail_url: https://cdn.memes.media/assets/5Tk8wQ_thumb.png
                        aspect_ratio: "1:1"
                        slide_index: null
                        parent_id: null
                        model: forge-image
                        created_at: "2026-06-18T17:42:10Z"
                    error: null
                    created_at: "2026-06-18T17:41:55Z"
                    completed_at: "2026-06-18T17:42:11Z"
                has_more: true
                next_cursor: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /generations/{id}:
    get:
      operationId: getGeneration
      summary: Retrieve a generation
      description: |
        Poll any async job, regardless of type. Returns the standard generation envelope (§5.0)
        with normalized `output[]`. The serializer whitelists fields and never returns raw
        internal job metadata or verbatim upstream error strings.
      tags: [Generations]
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: The generation.
          headers:
            X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationEnvelope" }
              example:
                generation:
                  id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
                  object: generation
                  type: video
                  status: processing
                  model: forge-video
                  requested_count: 1
                  completed_count: 0
                  failed_count: 0
                  output: []
                  error: null
                  created_at: "2026-06-18T17:41:55Z"
                  completed_at: null
                usage:
                  credits_used: 0
                  credits_remaining: 470
                  interval: rolling_30_days
                  reset_at: "2026-07-18T00:00:00Z"
                meta:
                  request_id: req_8fQ2Lm
                  livemode: true
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /generations/{id}/cancel:
    post:
      operationId: cancelGeneration
      summary: Cancel a generation
      description: |
        Cancel a `queued` or `processing` job. Reserved credits are released. A job that already
        reached a terminal state is returned unchanged. Some in-flight jobs that can no longer be
        canceled return `409 conflict`.
      tags: [Generations]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: The canceled generation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationEnvelope" }
        "202":
          description: Video cancellation was accepted and finalization is still pending.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  ############################################################################
  # IMAGES
  ############################################################################
  /images:
    post:
      operationId: createImage
      summary: Generate an image
      description: |
        Text-to-image or reference-conditioned ad image (1–4 variants). At least one of
        `prompt`, `brand_kit_id` / `brand_context`, `template_prompt`, or `reference_images` is
        required. Returns a `202` with a generation envelope of `type: image`.
      tags: [Images]
      x-credits: "1 credit per delivered image"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateImageRequest" }
            example:
              prompt: "Bold product hero shot of a cold brew can on a sun-drenched kitchen counter, playful meme caption space at top"
              brand_kit_id: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
              aspect_ratio: "4:5"
              count: 4
              quality: standard
              model: forge-image
              controls:
                platform: instagram
                copy_density: minimal
                brand_strictness: high
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      operationId: listImages
      summary: List images
      description: List image assets generated by the workspace, cursor-paginated.
      tags: [Images]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/Query"
        - $ref: "#/components/parameters/Order"
      responses:
        "200": { $ref: "#/components/responses/AssetList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /images/{id}:
    get:
      operationId: getImage
      summary: Retrieve an image
      description: Fetch a single generated image asset by id.
      tags: [Images]
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200": { $ref: "#/components/responses/AssetSingle" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /images/edits:
    post:
      operationId: editImage
      summary: Edit an image
      description: |
        Apply a natural-language instruction to an existing image, optionally inside a drawn
        mask (inpaint), optionally folding in reference images. Produces a child asset with
        lineage. Returns a `202` with a generation of `type: edit`.
      tags: [Images]
      x-credits: "1 credit per edit"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EditImageRequest" }
            example:
              image_id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
              instruction: "Make the background a deep navy and add a soft rim light on the can"
              aspect_ratio: "1:1"
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /images/reframe:
    post:
      operationId: reframeImage
      summary: Reframe an image
      description: |
        Outpaint an existing image into a new aspect ratio, extending the scene. Produces a
        child asset with lineage. Reframe is fast, so this is **synchronous**: it returns `200`
        with the completed generation (`type: reframe`, `status: succeeded`) — no polling needed.
        The deterministic test simulator returns `202` and progresses its fixture asynchronously.
      tags: [Images]
      x-credits: "1 credit per reframe"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReframeImageRequest" }
            example:
              image_id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
              aspect_ratio: "9:16"
      responses:
        "200":
          description: The completed reframe (synchronous).
          content:
            application/json:
              schema:
                type: object
                properties:
                  generation: { $ref: "#/components/schemas/Generation" }
                  meta: { $ref: "#/components/schemas/Meta" }
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/GenerationError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  ############################################################################
  # MEMES
  ############################################################################
  /memes:
    post:
      operationId: createMeme
      summary: Generate a meme
      description: |
        Primary brand-grounded meme ad generation (1–4 variants). At least one of `prompt`,
        `brand_kit_id` / `brand_context`, `template_prompt`, or `reference_images` is required.
        Set `enhance: true` to run Forge Prompt first. Returns a `202` with a generation of
        `type: meme`.
      tags: [Memes]
      x-credits: "1 credit per delivered meme variant"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateMemeRequest" }
            example:
              prompt: "Relatable Monday-morning struggle, our app saves the day"
              brand_kit_id: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
              aspect_ratio: "1:1"
              count: 4
              enhance: true
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      operationId: listMemes
      summary: List memes
      description: List memes generated by the workspace, cursor-paginated with optional search.
      tags: [Memes]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/Query"
        - $ref: "#/components/parameters/Order"
        - name: exclude_carousel
          in: query
          description: Exclude memes that were derived as carousel slides.
          required: false
          schema: { type: boolean, default: false }
      responses:
        "200": { $ref: "#/components/responses/AssetList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /memes/{id}:
    get:
      operationId: getMeme
      summary: Retrieve a meme
      description: Fetch a single meme asset by id.
      tags: [Memes]
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200": { $ref: "#/components/responses/AssetSingle" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      operationId: deleteMeme
      summary: Delete a meme
      description: Soft-delete a meme. No credits are refunded.
      tags: [Memes]
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: The meme was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
              example:
                deleted: true
                id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /memes/{id}/lineage:
    get:
      operationId: getMemeLineage
      summary: Retrieve meme lineage
      description: |
        Return the full edit / reframe family tree for a meme — the root plus every derived
        asset in chronological order.
      tags: [Memes]
      parameters:
        - $ref: "#/components/parameters/AssetId"
      responses:
        "200":
          description: The lineage of the meme.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
                  meta: { $ref: "#/components/schemas/Meta" }
              example:
                data:
                  - id: 1d2c3b4a-5e6f-4789-a012-b3c4d5e6f701
                    type: image
                    url: https://cdn.memes.media/assets/root01.png
                    aspect_ratio: "1:1"
                    parent_id: null
                    model: forge-image
                    created_at: "2026-06-18T16:00:00Z"
                  - id: 2e3d4c5b-6f70-489a-b123-c4d5e6f70812
                    type: image
                    url: https://cdn.memes.media/assets/edit02.png
                    aspect_ratio: "1:1"
                    parent_id: 1d2c3b4a-5e6f-4789-a012-b3c4d5e6f701
                    model: forge-edit
                    created_at: "2026-06-18T16:05:00Z"
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  ############################################################################
  # VIDEOS
  ############################################################################
  /videos:
    post:
      operationId: createVideo
      summary: Generate a video
      description: |
        Text-to-video or reference-to-video (presence of `reference_images` routes
        reference-to-video). At least one of `prompt`, `business_url`, or `brand_kit_id` /
        `brand_context` is required. The model resolves internally by reference count — the
        caller never sets it. Returns a `202` with a generation of `type: video`.
        `Idempotency-Key` is required. Protocol-v2
        admission fingerprints the body and atomically owns job creation plus
        credit reservation: the same key and body return the existing generation;
        changed-body reuse returns `409 idempotency_key_conflict`.
      tags: [Videos]
      x-credits: "10 credits per completed video"
      parameters:
        - $ref: "#/components/parameters/VideoIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateVideoRequest" }
            example:
              prompt: "A cinematic slow push-in on our sneaker as city lights blur behind it"
              brand_kit_id: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
              aspect_ratio: "16:9"
              duration: 6
              resolution: 720p
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      operationId: listVideos
      summary: List videos
      description: List video jobs for the workspace, cursor-paginated.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/Order"
      responses:
        "200": { $ref: "#/components/responses/AssetList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /videos/animate:
    post:
      operationId: animateImage
      summary: Animate an image into a video
      description: |
        One-click image-to-video from an existing workspace-owned image asset. Duration is fixed
        at 5 seconds, resolution `720p`, model `forge-motion`. Returns a `202` with a generation
        of `type: video`.
        `Idempotency-Key` is required. Protocol-v2
        admission fingerprints the body and atomically owns job creation plus
        credit reservation: the same key and body return the existing generation;
        changed-body reuse returns `409 idempotency_key_conflict`.
      tags: [Videos]
      x-credits: "10 credits per completed video"
      parameters:
        - $ref: "#/components/parameters/VideoIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AnimateImageRequest" }
            example:
              image_id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /videos/{id}:
    get:
      operationId: getVideo
      summary: Retrieve a video
      description: Fetch a single video job and its result by id.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200": { $ref: "#/components/responses/AssetSingle" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      operationId: deleteVideo
      summary: Delete a video
      description: |
        Delete a terminal video job and its stored file. Completed work is not
        refunded. For in-flight work, cancellation can return `202` while the
        provider confirms the terminal state; the video remains visible and
        credits stay reserved until confirmation.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: The video was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
              example:
                deleted: true
                id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "202":
          description: Cancellation or terminal notification delivery is still pending.
          content:
            application/json:
              schema:
                type: object
                required: [deleted, id, meta]
                properties:
                  deleted: { type: boolean, enum: [false] }
                  cancellation_pending: { type: boolean }
                  finalization_pending: { type: boolean }
                  id: { type: string }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  ############################################################################
  # CAROUSELS
  ############################################################################
  /carousels/plan:
    post:
      operationId: planCarousel
      summary: Plan a carousel
      description: |
        Generate a multi-slide carousel strategy (concept, viral mechanic, audience, design
        system, CTA, per-slide briefs) without rendering any images. Synchronous and zero-credit;
        live use still requires the paid public-API entitlement.
        Returns `200` with the plan and `model: forge-plan`.
      tags: [Carousels]
      x-credits: "0 credits; paid live API entitlement required"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PlanCarouselRequest" }
            example:
              brief: "5 reasons busy parents love our meal-prep kit"
              business_website: https://example.com
              goal: leads
              platform: ig-portrait
              slide_count: 5
              brand_kit_id: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
      responses:
        "200":
          description: The carousel strategy plan.
          headers:
            X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarouselPlanResponse" }
              example:
                plan:
                  id: 9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20
                  concept_title: "5 Dinners, Zero Stress"
                  viral_mechanic: "Numbered listicle with a relatable pain hook"
                  audience: "Time-strapped parents 28–45"
                  design_system:
                    palette: { background: "#0E1B2A", primary_text: "#FFFFFF", accent: "#FF7A45" }
                    headline_typeface: "Inter Tight"
                    body_typeface: "Inter"
                  cta: "Tap to start your first box"
                  slides:
                    - slide_index: 0
                      role: hook
                      on_image_text: "Dinner solved in 15 minutes"
                      visual_direction: "Steaming plate on a bright counter"
                      image_directive: "Hero food shot, warm light, top text space"
                plan_id: 9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20
                model: forge-plan
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /carousels:
    post:
      operationId: renderCarousel
      summary: Render a carousel
      description: |
        Render a carousel: persist the set and fan out one image job per slide. Pass a `plan_id`
        from `/carousels/plan`, a bounded inline `plan`, or bounded planning inputs. Bills
        `1 × slide_count` credits. Returns a `202` carousel submission; poll its set id at
        `GET /carousels/{id}` as per-slide assets complete.
      tags: [Carousels]
      x-credits: "1 credit per slide (1 × slide_count)"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RenderCarouselRequest" }
            example:
              plan_id: 9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20
              quality: standard
              controls:
                platform: instagram
                brand_strictness: high
      responses:
        "202":
          description: The carousel set and per-slide jobs were accepted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarouselSubmissionResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      operationId: listCarousels
      summary: List carousels
      description: List carousel sets with hydrated slides, cursor-paginated.
      tags: [Carousels]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/Order"
      responses:
        "200":
          description: A paginated list of carousel sets.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/CarouselSet" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /carousels/{id}:
    get:
      operationId: getCarousel
      summary: Retrieve a carousel
      description: Fetch a carousel set with per-slide status and urls.
      tags: [Carousels]
      parameters:
        - name: id
          in: path
          required: true
          description: The carousel set id.
          schema: { type: string, example: 7c4a2e91-6b8d-4f35-a1c7-9e2d5b8f3a60 }
      responses:
        "200":
          description: The carousel set.
          content:
            application/json:
              schema:
                type: object
                required: [carousel, meta]
                properties:
                  carousel: { $ref: "#/components/schemas/CarouselSet" }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      operationId: deleteCarousel
      summary: Delete a carousel
      description: Soft-delete a carousel set.
      tags: [Carousels]
      parameters:
        - name: id
          in: path
          required: true
          description: The carousel set id.
          schema: { type: string, example: 7c4a2e91-6b8d-4f35-a1c7-9e2d5b8f3a60 }
      responses:
        "200":
          description: The carousel set was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  ############################################################################
  # STORYBOARDS
  ############################################################################
  /storyboards:
    post:
      operationId: createStoryboard
      summary: Generate a storyboard
      description: |
        Generate a single composite 3–4 panel narrative ad image. At least one of `goal` or
        `brand_kit_id` / `brand_context` is required. Returns a `202` with a generation of
        `type: storyboard`. Bills 4 credits.
      tags: [Storyboards]
      x-credits: "4 credits per storyboard"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateStoryboardRequest" }
            example:
              goal: "Show how our app turns a chaotic morning into a calm one"
              brand_kit_id: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
              aspect_ratio: "9:16"
              preset: problem-solution
              panel_count: 3
              tone: confident
              visual_style: clean-storyboard
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /storyboards/{id}:
    get:
      operationId: getStoryboard
      summary: Retrieve a storyboard
      description: |
        Poll a storyboard generation by id. Returns the standard generation envelope; the single
        composite image appears in `output[]` when terminal.
      tags: [Storyboards]
      parameters:
        - $ref: "#/components/parameters/GenerationId"
      responses:
        "200":
          description: The storyboard generation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  ############################################################################
  # BRAND KITS
  ############################################################################
  /brand-kits:
    get:
      operationId: listBrandKits
      summary: List brand kits
      description: List brand kits owned by the workspace, default-first then newest.
      tags: [Brand Kits]
      responses:
        "200":
          description: The brand kits.
          content:
            application/json:
              schema:
                type: object
                required: [brand_kits, meta]
                properties:
                  brand_kits:
                    type: array
                    items: { $ref: "#/components/schemas/BrandKit" }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      operationId: createBrandKit
      summary: Create a brand kit
      description: |
        Create a zero-credit brand kit. The first kit a workspace creates is marked default.
        Public API creation is rejected once the workspace has 50 total Brand Kits.
      tags: [Brand Kits]
      x-credits: "0 credits"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateBrandKitRequest" }
            example:
              name: "Cold Brew Co"
              company_name: "Cold Brew Co"
              website_url: https://coldbrew.example.com
              brand_colors: ["#0E1B2A", "#FF7A45"]
              tone_of_voice: "Playful, punchy, a little irreverent"
              target_audience: "Urban professionals 25–40"
              industry: "Food & Beverage"
              auto_attach_product_references: true
      responses:
        "201":
          description: The brand kit was created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "501": { $ref: "#/components/responses/TestModeNotSupported" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /brand-kits/{id}:
    get:
      operationId: getBrandKit
      summary: Retrieve a brand kit
      description: Fetch a single brand kit with its product reference images.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/BrandKitId"
      responses:
        "200":
          description: The brand kit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      operationId: updateBrandKit
      summary: Update a brand kit
      description: Partially update a brand kit. Only fields present in the body are changed.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/BrandKitId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateBrandKitRequest" }
            example:
              tone_of_voice: "Confident and clean"
              brand_colors: ["#0E1B2A", "#2BD9A6"]
      responses:
        "200":
          description: The updated brand kit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }
        "501": { $ref: "#/components/responses/TestModeNotSupported" }
    delete:
      operationId: deleteBrandKit
      summary: Delete a brand kit
      description: Delete a brand kit and its logo / product reference images.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/BrandKitId"
      responses:
        "200":
          description: The brand kit was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /brand-kits/{id}/set-default:
    post:
      operationId: setDefaultBrandKit
      summary: Set a brand kit as default
      description: Mark this brand kit as the workspace default, unsetting any other default.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/BrandKitId"
      responses:
        "200":
          description: The updated brand kit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "501": { $ref: "#/components/responses/TestModeNotSupported" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /brand-kits/{id}/logo:
    post:
      operationId: uploadBrandKitLogo
      summary: Upload a brand kit logo
      description: Upload or replace the brand kit logo. Provide the image as a `logo_url` (https URL, workspace asset id, or `data:` URL) — fetched and stored server-side.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/BrandKitId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [logo_url]
              properties:
                logo_url:
                  type: string
                  description: An https URL, a workspace asset id, or a `data:` URL for the logo image.
                  example: https://yourcdn.example/logo.png
      responses:
        "200":
          description: The brand kit with the new logo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "501": { $ref: "#/components/responses/TestModeNotSupported" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    delete:
      operationId: deleteBrandKitLogo
      summary: Remove a brand kit logo
      description: Remove the logo from a brand kit.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/BrandKitId"
      responses:
        "200":
          description: The brand kit without a logo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKitResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /brand-kits/{id}/product-references:
    post:
      operationId: addBrandKitProductReference
      summary: Add a product reference
      description: Add a product reference image to a brand kit (max 5 per kit) for product-faithful generation. Provide the image as a `url` (https URL, workspace asset id, or `data:` URL).
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/BrandKitId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: An https URL, a workspace asset id, or a `data:` URL for the product image.
                  example: https://yourcdn.example/product.png
                name:
                  type: string
                  maxLength: 160
                  description: Optional label for the reference.
                  example: "Signature can — front"
      responses:
        "201":
          description: The created product reference.
          content:
            application/json:
              schema:
                type: object
                required: [product_reference, meta]
                properties:
                  product_reference: { $ref: "#/components/schemas/ProductReference" }
                  meta: { $ref: "#/components/schemas/Meta" }
              example:
                product_reference:
                  id: 5f2e8a31-7c4d-4b90-a6e1-3d9f7b2c5a80
                  url: https://cdn.memes.media/refs/5f2e8a31.png
                  name: "Signature can — front"
                  sort_order: 0
                  created_at: "2026-06-18T16:00:00Z"
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "501": { $ref: "#/components/responses/TestModeNotSupported" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /brand-kits/{id}/product-references/{ref_id}:
    delete:
      operationId: removeBrandKitProductReference
      summary: Remove a product reference
      description: Remove one product reference image from a brand kit.
      tags: [Brand Kits]
      parameters:
        - $ref: "#/components/parameters/BrandKitId"
        - name: ref_id
          in: path
          required: true
          description: The product reference image id.
          schema: { type: string, example: 5f2e8a31-7c4d-4b90-a6e1-3d9f7b2c5a80 }
      responses:
        "200":
          description: The product reference was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /brand-kits/analyze-url:
    post:
      operationId: analyzeBrandUrl
      summary: Extract a brand profile from a URL
      description: |
        Extract a structured brand profile from a website URL. Does not persist anything — edit
        the result, then `POST /brand-kits` to save. Synchronous and zero-credit, with the paid
        live public-API entitlement required; `model: forge-copy-fast`.
      tags: [Brand Kits]
      x-credits: "0 credits; paid live API entitlement required"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: The website URL to analyze.
                  example: https://coldbrew.example.com
      responses:
        "200":
          description: The extracted brand profile.
          content:
            application/json:
              schema:
                type: object
                required: [extraction, model, meta]
                properties:
                  extraction: { $ref: "#/components/schemas/BrandContext" }
                  model: { $ref: "#/components/schemas/Model" }
                  meta: { $ref: "#/components/schemas/Meta" }
              example:
                extraction:
                  company_name: "Cold Brew Co"
                  industry: "Food & Beverage"
                  tone_of_voice: "Playful, punchy"
                  brand_colors: ["#0E1B2A", "#FF7A45"]
                  target_audience: "Urban professionals 25–40"
                  website_context: "DTC ready-to-drink cold brew, subscription-first"
                model: forge-copy-fast
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  ############################################################################
  # PROMPT ENHANCEMENT
  ############################################################################
  /prompts/enhance:
    post:
      operationId: enhancePrompt
      summary: Enhance a prompt
      description: |
        Turn a rough idea into a polished, production-ready image prompt. Synchronous and
        zero-credit, with the paid live public-API entitlement required; `model: forge-prompt`.
      tags: [Prompt Enhancement]
      x-credits: "0 credits; paid live API entitlement required"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnhancePromptRequest" }
            example:
              prompt: "make a funny meme about our coffee being strong"
              website_url: https://coldbrew.example.com
              has_logo: true
              aspect_ratio: "1:1"
      responses:
        "200":
          description: The enhanced prompt.
          content:
            application/json:
              schema:
                type: object
                required: [enhanced_prompt, original_prompt, model, meta]
                properties:
                  enhanced_prompt: { type: string }
                  original_prompt: { type: string }
                  model: { $ref: "#/components/schemas/Model" }
                  meta: { $ref: "#/components/schemas/Meta" }
              example:
                enhanced_prompt: "A bold meme-style ad: a wide-eyed office worker clutching a Cold Brew can like a lifeline, exaggerated 'this is the only thing keeping me alive' caption space at top, punchy high-contrast lighting, brand navy and orange palette, 1:1"
                original_prompt: "make a funny meme about our coffee being strong"
                model: forge-prompt
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  ############################################################################
  # WEBHOOK ENDPOINTS
  ############################################################################
  /webhooks:
    get:
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      description: |
        List the workspace's registered webhook endpoints. This read is side-effect free: it does
        not provision webhook-provider state for a workspace that has never registered an endpoint.
      tags: [Webhooks]
      responses:
        "200":
          description: The webhook endpoints.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      operationId: createWebhookEndpoint
      summary: Register a webhook endpoint
      description: |
        Register an HTTPS endpoint to receive events, up to 25 per workspace including disabled
        endpoints. The signing `secret` (`whsec_…`) is returned on the original successful
        idempotent creation lineage, including a same-key/same-body recovery retry. Store it to
        verify the `svix-signature` header; ordinary endpoint reads never expose it.
      tags: [Webhooks]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWebhookRequest" }
            example:
              url: https://hooks.example.com/memes
              events: ["generation.completed", "generation.failed"]
              enabled: true
      responses:
        "201":
          description: The webhook endpoint with the signing secret for this idempotent creation lineage.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpointCreatedResponse" }
              example:
                id: ep_01J1H8P7Y6Q5W4E3R2T1
                object: webhook_endpoint
                url: https://hooks.example.com/memes
                events: ["generation.completed", "generation.failed"]
                enabled: true
                secret: whsec_4Kp2LmQ9aZ8xW
                created_at: "2026-06-18T17:00:00Z"
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "409": { $ref: "#/components/responses/Conflict" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "500": { $ref: "#/components/responses/InternalError" }

  /webhooks/{id}:
    get:
      operationId: getWebhookEndpoint
      summary: Retrieve a webhook endpoint
      description: Fetch a single webhook endpoint.
      tags: [Webhooks]
      parameters:
        - $ref: "#/components/parameters/WebhookId"
      responses:
        "200":
          description: The webhook endpoint.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpointResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "500": { $ref: "#/components/responses/InternalError" }
    patch:
      operationId: updateWebhookEndpoint
      summary: Update a webhook endpoint
      description: Update the url, subscribed events, or enabled state of a webhook endpoint.
      tags: [Webhooks]
      parameters:
        - $ref: "#/components/parameters/WebhookId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  pattern: '^https://'
                  maxLength: 2048
                  description: A public HTTPS destination without embedded credentials.
                  example: https://hooks.example.com/memes-v2
                events:
                  type: array
                  maxItems: 4
                  uniqueItems: true
                  items: { $ref: "#/components/schemas/WebhookEventType" }
                enabled: { type: boolean }
      responses:
        "200":
          description: The updated webhook endpoint.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpointResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "500": { $ref: "#/components/responses/InternalError" }
    delete:
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      description: Remove a webhook endpoint.
      tags: [Webhooks]
      parameters:
        - $ref: "#/components/parameters/WebhookId"
      responses:
        "200":
          description: The webhook endpoint was deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeletedResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "500": { $ref: "#/components/responses/InternalError" }

  /webhooks/{id}/test:
    post:
      operationId: testWebhookEndpoint
      summary: Send a test event
      description: Send a sample event to a webhook endpoint to verify connectivity and signing.
      tags: [Webhooks]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/WebhookId"
      responses:
        "202":
          description: The test event was queued for delivery.
          content:
            application/json:
              schema:
                type: object
                required: [delivered, meta]
                properties:
                  delivered: { type: boolean, example: true }
                  event_id: { type: string, example: evt_test_9Lk2Mn }
                  meta: { $ref: "#/components/schemas/Meta" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "502": { $ref: "#/components/responses/WebhookProviderError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "500": { $ref: "#/components/responses/InternalError" }

  ############################################################################
  # ACCOUNT / USAGE
  ############################################################################
  /account:
    get:
      operationId: getAccount
      summary: Retrieve account
      description: Read workspace info — plan, key prefix, and livemode.
      tags: [Account]
      responses:
        "200":
          description: The account.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Account" }
              example:
                id: 3a6f1d28-9c5b-4e70-a2d8-7b3f6c1e5a90
                object: account
                plan: pro
                key_prefix: sk_live_8f3k
                livemode: true
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

  /account/usage:
    get:
      operationId: getAccountUsage
      summary: Retrieve usage
      description: Read credit consumption and quota by interval for the workspace.
      tags: [Account]
      responses:
        "200":
          description: The usage by interval.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountUsage" }
              example:
                plan: pro
                credits_remaining: 470
                intervals:
                  images:
                    limit: 1000
                    used: 530
                    remaining: 470
                    interval: rolling_30_days
                    reset_at: "2026-07-18T00:00:00Z"
                  video:
                    limit: 50
                    used: 4
                    remaining: 46
                    interval: rolling_30_days
                    reset_at: "2026-07-18T00:00:00Z"
                meta: { request_id: req_8fQ2Lm, livemode: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/InternalError" }

############################################################################
# OUTBOUND WEBHOOKS (OpenAPI 3.1 webhooks object)
############################################################################
webhooks:
  generation.completed:
    post:
      operationId: onGenerationCompleted
      summary: Generation completed
      description: |
        Sent when a generation reaches `succeeded` — all requested outputs were produced. The
        payload `data.generation` is the full standard generation envelope (§5.0).
      tags: [Webhooks]
      requestBody:
        description: The event payload.
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: evt_9Lk2MnQ8aZ
              type: generation.completed
              created_at: "2026-06-18T17:42:12Z"
              livemode: true
              data:
                generation:
                  id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
                  object: generation
                  type: meme
                  status: succeeded
                  model: forge-image
                  requested_count: 4
                  completed_count: 4
                  failed_count: 0
                  output:
                    - id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
                      type: image
                      url: https://cdn.memes.media/assets/5Tk8wQ.png
                      aspect_ratio: "1:1"
                      model: forge-image
                      created_at: "2026-06-18T17:42:10Z"
                  error: null
                  created_at: "2026-06-18T17:41:55Z"
                  completed_at: "2026-06-18T17:42:11Z"
      responses:
        "200":
          description: Return any 2xx to acknowledge receipt. Non-2xx triggers retry with backoff over ~24h.

  generation.partial:
    post:
      operationId: onGenerationPartial
      summary: Generation partially succeeded
      description: |
        Sent when a generation reaches `partial` — some but not all of N requested outputs
        succeeded (e.g. 3 of 4 meme variants).
      tags: [Webhooks]
      requestBody:
        description: The event payload.
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200":
          description: Return any 2xx to acknowledge receipt.

  generation.failed:
    post:
      operationId: onGenerationFailed
      summary: Generation failed
      description: |
        Sent when a generation reaches `failed` (terminal failure or timeout). Reserved credits
        are refunded. `data.generation.error` carries a stable, sanitized error object.
      tags: [Webhooks]
      requestBody:
        description: The event payload.
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: evt_4Kp2LmQ9aZ
              type: generation.failed
              created_at: "2026-06-18T17:45:00Z"
              livemode: true
              data:
                generation:
                  id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
                  object: generation
                  type: video
                  status: failed
                  model: forge-video
                  requested_count: 1
                  completed_count: 0
                  failed_count: 1
                  output: []
                  error:
                    type: generation_error
                    code: generation_failed
                    message: "Generation failed."
                  created_at: "2026-06-18T17:41:55Z"
                  completed_at: "2026-06-18T17:45:00Z"
      responses:
        "200":
          description: Return any 2xx to acknowledge receipt.

  generation.canceled:
    post:
      operationId: onGenerationCanceled
      summary: Generation canceled
      description: Sent when a generation is canceled before reaching a terminal state.
      tags: [Webhooks]
      requestBody:
        description: The event payload.
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200":
          description: Return any 2xx to acknowledge receipt.

############################################################################
# COMPONENTS
############################################################################
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_<48 lowercase hex> or sk_test_<48 lowercase hex>
      x-api-key-pattern: '^sk_(?:live|test)_[0-9a-f]{48}$'
      description: |
        Bearer secret API keys. Send `Authorization: Bearer <key>`.

        Keys are shaped `sk_live_<48 lowercase hexadecimal characters>` (production /
        `livemode=true`) and `sk_test_<48 lowercase hexadecimal characters>` (simulator /
        `livemode=false`). A key is returned in plaintext exactly
        once at creation — store it securely. Test keys are intercepted before every live handler:
        they use deterministic, one-hour state isolated to the exact key, return fixture assets,
        never call generation or webhook-delivery providers, never spend credits or quota, and never
        read or mutate canonical workspace resources. Unsupported simulator operations return
        `501 test_mode_not_supported` instead of falling through to live behavior.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        A caller-generated key required for every POST. Live-key POST requests use a durable,
        body-fingerprinted 24-hour ledger scoped to API key, method, and path. Same-key/same-body
        retries replay the stored outcome; changed-body reuse returns `409 idempotency_conflict`.
        Webhook creation uses a stable external id and can recover its one-time signing secret on
        same-key retries without storing that secret in the response ledger.
      schema:
        type: string
        minLength: 1
        maxLength: 200
        example: 6f9619ff-8b86-d011-b42d-00c04fc964ff
    VideoIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        A caller-generated opaque string retained across transport failures,
        `5xx`, or an unreadable `202` body. Live video protocol-v2 admission
        fingerprints the body and atomically owns job creation plus credit reservation;
        same-key/same-body retries return the existing generation and changed-body
        reuse returns `409 idempotency_key_conflict`.
      schema:
        type: string
        minLength: 1
        maxLength: 200
        example: catalog:sku-42:hero-render-v1
    Limit:
      name: limit
      in: query
      required: false
      description: Page size. Default 40, max 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 40
    StartingAfter:
      name: starting_after
      in: query
      required: false
      description: A cursor — the id of the last item from the previous page.
      schema:
        type: string
        description: A live UUID or the last simulator resource id returned by a test-key list.
        example: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
    Query:
      name: q
      in: query
      required: false
      description: Free-text search (max 200 characters).
      schema:
        type: string
        maxLength: 200
    Order:
      name: order
      in: query
      required: false
      description: Sort order.
      schema:
        type: string
        enum: [newest, oldest]
        default: newest
    GenerationId:
      name: id
      in: path
      required: true
      description: The generation id.
      schema:
        $ref: "#/components/schemas/GenerationIdentifier"
    AssetId:
      name: id
      in: path
      required: true
      description: The asset id.
      schema:
        $ref: "#/components/schemas/AssetIdentifier"
    BrandKitId:
      name: id
      in: path
      required: true
      description: The brand kit id.
      schema:
        type: string
        format: uuid
        example: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70
    WebhookId:
      name: id
      in: path
      required: true
      description: The webhook endpoint id.
      schema:
        type: string
        description: A live endpoint id, or an `ep_test_<32 hex>` id returned by the simulator.
        examples: [ep_01J1H8P7Y6Q5W4E3R2T1, ep_test_0123456789abcdef0123456789abcdef]

  headers:
    X-Request-Id:
      description: Stable server-generated identifier for this HTTP request.
      schema: { type: string, example: req_8fQ2Lm }
    Idempotency-Key:
      description: Effective key for a live request-ledger or video-admission response.
      schema: { type: string, example: catalog:sku-42:hero-render-v1 }
    Idempotent-Replayed:
      description: Present with value `true` when a stored live-key outcome was replayed.
      schema: { type: boolean, example: true }
    RateLimit-Limit:
      description: Weighted request-point capacity for the most constrained active bucket.
      schema: { type: integer, example: 120 }
    RateLimit-Remaining:
      description: Weighted request points remaining in the current fixed window.
      schema: { type: integer, example: 117 }
    RateLimit-Reset:
      description: Seconds until the current fixed window resets.
      schema: { type: integer, example: 42 }
    Retry-After:
      description: Seconds to wait before retrying a rate- or concurrency-limited request.
      schema: { type: integer, example: 2 }

  responses:
    GenerationAccepted:
      description: |
        Accepted. The operation is async — poll `GET /generations/{id}` or subscribe to webhooks
        for completion.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
        Idempotency-Key: { $ref: "#/components/headers/Idempotency-Key" }
        Idempotent-Replayed: { $ref: "#/components/headers/Idempotent-Replayed" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GenerationEnvelope" }
          example:
            generation:
              id: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
              object: generation
              type: meme
              status: queued
              model: forge-image
              requested_count: 4
              completed_count: 0
              failed_count: 0
              output: []
              error: null
              created_at: "2026-06-18T17:41:55Z"
              completed_at: null
            usage:
              credits_used: 0
              credits_remaining: 480
              interval: rolling_30_days
              reset_at: "2026-07-18T00:00:00Z"
            meta:
              request_id: req_8fQ2Lm
              livemode: true
    AssetSingle:
      description: A single asset.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema:
            type: object
            required: [data, meta]
            properties:
              data: { $ref: "#/components/schemas/Asset" }
              meta: { $ref: "#/components/schemas/Meta" }
          example:
            data:
              id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
              type: image
              url: https://cdn.memes.media/assets/5Tk8wQ.png
              thumbnail_url: https://cdn.memes.media/assets/5Tk8wQ_thumb.png
              aspect_ratio: "1:1"
              slide_index: null
              parent_id: null
              model: forge-image
              created_at: "2026-06-18T17:42:10Z"
            meta: { request_id: req_8fQ2Lm, livemode: true }
    AssetList:
      description: A paginated list of assets.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/ListResponse"
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
          example:
            data:
              - id: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
                type: image
                url: https://cdn.memes.media/assets/5Tk8wQ.png
                aspect_ratio: "1:1"
                model: forge-image
                created_at: "2026-06-18T17:42:10Z"
            has_more: true
            next_cursor: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
    BadRequest:
      description: The request was malformed or missing required input.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: invalid_request_error
              code: invalid_request
              message: "Either prompt, brand_context, or reference_images is required."
              param: prompt
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    Unauthorized:
      description: The API key is missing, invalid, expired, or revoked.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: authentication_error
              code: invalid_api_key
              message: "Invalid API key provided."
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    Forbidden:
      description: The API key lacks the required scope or the live workspace lacks API entitlement.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            scope:
              value:
                error:
                  type: permission_error
                  code: insufficient_scope
                  message: "This API key does not have the required scope."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
            entitlement:
              value:
                error:
                  type: permission_error
                  code: plan_feature_unavailable
                  message: "This public API operation requires an eligible paid workspace."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
    NotFound:
      description: The resource does not exist or is not owned by this workspace.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: not_found_error
              code: resource_not_found
              message: "No such resource."
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    Conflict:
      description: |
        An idempotency conflict or in-flight retry, a job that can no longer be canceled, or a
        workspace resource cap such as 50 total Brand Kits or 25 webhook endpoints.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            idempotency:
              value:
                error:
                  type: conflict_error
                  code: idempotency_conflict
                  message: "This Idempotency-Key was already used with a different request."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
            brandKitCap:
              value:
                error:
                  type: conflict_error
                  code: brand_kit_limit_reached
                  message: "Public API creation is limited to workspaces with fewer than 50 total Brand Kits."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
            webhookCap:
              value:
                error:
                  type: conflict_error
                  code: webhook_endpoint_limit_reached
                  message: "A workspace can have up to 25 webhook endpoints."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
    GenerationError:
      description: A dependent generation request failed. The message is sanitized.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: generation_error
              code: generation_failed
              message: "Generation failed. Please try again."
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    WebhookProviderError:
      description: Webhook management or test delivery failed. The message is sanitized.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: api_error
              code: webhook_provider_error
              message: "Webhook provider request failed."
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    TooManyRequests:
      description: |
        The workspace lacks credits, a weighted key/workspace/IP rate bucket is exhausted, or
        the expensive-operation concurrency or test-simulator capacity limit is full. Inspect
        `error.code` to distinguish `quota_exceeded`, `rate_limited`,
        `authentication_rate_limited`, `concurrency_limited`, and `test_mode_capacity_limited`.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
        RateLimit-Limit: { $ref: "#/components/headers/RateLimit-Limit" }
        RateLimit-Remaining: { $ref: "#/components/headers/RateLimit-Remaining" }
        RateLimit-Reset: { $ref: "#/components/headers/RateLimit-Reset" }
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            quota:
              value:
                error:
                  type: quota_error
                  code: quota_exceeded
                  message: "Generation quota reached for this workspace."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
            rate:
              value:
                error:
                  type: rate_limit_error
                  code: rate_limited
                  message: "Too many public API requests. Retry after 42 seconds."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
    InternalError:
      description: An unexpected error. Never leaks upstream text.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: api_error
              code: internal_error
              message: "An unexpected error occurred."
              request_id: req_8fQ2Lm
            meta: { request_id: req_8fQ2Lm, livemode: true }
    TestModeNotSupported:
      description: The operation cannot be represented in the isolated test simulator.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: test_mode_error
              code: test_mode_not_supported
              message: "This operation is not available in the provider-free test simulator."
              request_id: req_test_8fQ2Lm
            meta: { request_id: req_test_8fQ2Lm, livemode: false }
    ServiceUnavailable:
      description: Temporarily unavailable due to admission, persistence, configuration, or capacity.
      headers:
        X-Request-Id: { $ref: "#/components/headers/X-Request-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            transient:
              value:
                error:
                  type: service_unavailable
                  code: temporarily_unavailable
                  message: "The service is temporarily unavailable. Please retry shortly."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }
            webhookConfiguration:
              value:
                error:
                  type: service_unavailable
                  code: webhook_provider_unavailable
                  message: "Webhooks are not configured."
                  request_id: req_8fQ2Lm
                meta: { request_id: req_8fQ2Lm, livemode: true }

  schemas:
    ############################ Shared primitives ############################
    GenerationIdentifier:
      description: A live generation UUID or deterministic test-simulator generation id.
      oneOf:
        - type: string
          format: uuid
        - type: string
          pattern: '^gen_test_[0-9a-f]{32}$'
      examples:
        - 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40
        - gen_test_0123456789abcdef0123456789abcdef

    AssetIdentifier:
      description: A live asset UUID or deterministic test-simulator asset id.
      oneOf:
        - type: string
          format: uuid
        - type: string
          pattern: '^asset_test_[0-9a-f]{32}_[0-9]+$'
      examples:
        - 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
        - asset_test_0123456789abcdef0123456789abcdef_0

    BrandKitIdentifier:
      type: string
      format: uuid
      description: |
        A workspace-owned Brand Kit UUID. Supplying an unknown or non-owned UUID returns `404`
        before credit reservation or provider work.
      example: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70

    CarouselPlanIdentifier:
      description: A live carousel-plan UUID or deterministic test-simulator plan id.
      oneOf:
        - type: string
          format: uuid
        - type: string
          pattern: '^plan_test_[0-9a-f]{32}$'
      examples:
        - 9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20
        - plan_test_0123456789abcdef0123456789abcdef

    Model:
      type: string
      description: |
        A branded Forge model id — the only model identifier the API ever emits. Underlying
        engines are re-routed, versioned, or failed over without changing these public strings.
      enum:
        - forge-image
        - forge-image-fast
        - forge-edit
        - forge-video
        - forge-motion
        - forge-copy-fast
        - forge-prompt
        - forge-plan
        - forge-test-fixture
      example: forge-image

    AspectRatio:
      type: string
      description: Output aspect ratio.
      enum:
        - auto
        - "1:1"
        - "2:1"
        - "3:1"
        - "2:3"
        - "3:2"
        - "3:4"
        - "4:5"
        - "4:3"
        - "16:9"
        - "9:16"
        - "21:9"
      example: "1:1"

    Quality:
      type: string
      description: Render quality tier.
      enum: [fast, standard, premium]
      default: standard

    GenerationType:
      type: string
      description: The kind of creative operation a generation represents.
      enum:
        - meme
        - image
        - edit
        - reframe
        - video
        - carousel
        - storyboard
        - remix

    GenerationStatus:
      type: string
      description: |
        Normalized job status. Terminal states are `succeeded`, `partial`, `failed`, `canceled`.
      enum:
        - queued
        - processing
        - succeeded
        - partial
        - failed
        - canceled

    Meta:
      type: object
      description: Metadata present on every response.
      required: [request_id, livemode]
      properties:
        request_id:
          type: string
          description: A unique id for this request. Also returned in the `X-Request-Id` header.
          example: req_8fQ2Lm
        livemode:
          type: [boolean, "null"]
          description: Whether a live key was used; null when authentication did not resolve a key.
          example: true
        simulated:
          type: boolean
          description: Present with value `true` for provider-free test-mode responses.
          example: true
    Usage:
      type: object
      description: Credit consumption summary carried on every generation response.
      required: [credits_used, credits_remaining, interval, reset_at]
      properties:
        credits_used:
          type: integer
          description: Credits consumed by this operation.
          example: 4
        credits_remaining:
          type: [integer, "null"]
          description: Credits remaining in the current interval, or null when no finite live balance applies.
          example: 476
        interval:
          type: string
          description: The metering interval.
          enum: [daily, rolling_30_days, unlimited]
          example: rolling_30_days
        reset_at:
          type: [string, "null"]
          format: date-time
          description: When the current interval resets.
          example: "2026-07-18T00:00:00Z"

    ListResponse:
      type: object
      description: Shared cursor-paginated list envelope.
      required: [data, has_more]
      properties:
        data:
          type: array
          items: {}
        has_more:
          type: boolean
          description: Whether more items exist beyond this page.
          example: true
        next_cursor:
          type: [string, "null"]
          description: Pass as `starting_after` to fetch the next page.
          example: 8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40

    DeletedResponse:
      type: object
      required: [deleted, id, meta]
      properties:
        deleted:
          type: boolean
          example: true
        id:
          type: string
          example: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80
        meta: { $ref: "#/components/schemas/Meta" }

    Error:
      type: object
      description: |
        The single error envelope. No provider or model strings ever appear in any field.
      required: [error, meta]
      properties:
        error:
          type: object
          required: [type, code, message, request_id]
          properties:
            type:
              type: string
              description: The error category.
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - conflict_error
                - generation_error
                - quota_error
                - rate_limit_error
                - api_error
                - service_unavailable
                - test_mode_error
              example: invalid_request_error
            code:
              type: string
              description: A stable, machine-readable error code.
              example: invalid_request
            message:
              type: string
              description: A human-readable, sanitized explanation.
              example: "Either prompt, brand_context, or reference_images is required."
            param:
              type: [string, "null"]
              description: The offending parameter, when applicable.
              example: prompt
            request_id:
              type: string
              description: Duplicates `meta.request_id` for backward-compatible error handling.
              example: req_8fQ2Lm
        meta: { $ref: "#/components/schemas/Meta" }

    ############################ Brand & references ############################
    BrandContext:
      type: object
      description: |
        An inline brand profile used to ground a generation when no brand kit is passed. It uses
        the same bounded text and color contract as a persisted Brand Kit. If `brand_context` and
        `brand_kit_id` are both supplied, inline text wins, but the saved kit must still exist and
        belong to the workspace because its media may be attached automatically.
      properties:
        company_name: { type: string, maxLength: 200, example: "Cold Brew Co" }
        website_url: { type: string, format: uri, maxLength: 2048, example: https://coldbrew.example.com }
        website_context:
          type: string
          maxLength: 8000
          example: "DTC ready-to-drink cold brew, subscription-first"
        industry: { type: string, maxLength: 160, example: "Food & Beverage" }
        tone_of_voice: { type: string, maxLength: 2000, example: "Playful, punchy" }
        target_audience: { type: string, maxLength: 2000, example: "Urban professionals 25–40" }
        brand_colors:
          type: array
          maxItems: 16
          items:
            type: string
            pattern: '^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$'
          example: ["#0E1B2A", "#FF7A45"]

    ReferenceImage:
      type: object
      description: A typed reference image input. `source` is an https URL, data URI, or workspace asset id.
      required: [source]
      properties:
        type:
          type: string
          enum: [ref, template, logo]
          default: ref
          description: How the reference is used.
        source:
          type: string
          minLength: 1
          description: An https URL, data URI, or workspace asset id from a prior generation output. The API validates and normalizes it server-side.
          example: https://yourcdn.example/logo.png

    VideoReferenceImage:
      type: object
      description: |
        A video reference. Video accepts only `ref` or `logo`; `source` must be a public HTTPS URL
        or an owned workspace asset id. Data URIs are not accepted for video.
      required: [source]
      properties:
        type:
          type: string
          enum: [ref, logo]
          default: ref
        source:
          type: string
          minLength: 1
          description: A public HTTPS URL or owned workspace asset id.
          example: https://yourcdn.example/product.png

    Controls:
      type: object
      description: Optional structured creative controls. Each value is a short string.
      properties:
        platform: { type: string, example: instagram }
        visual_taste: { type: string, example: "clean, premium" }
        product_role: { type: string, example: hero }
        brand_strictness: { type: string, example: high }
        copy_density: { type: string, example: minimal }
        reference_strength: { type: string, example: medium }
        risk_level: { type: string, example: balanced }

    ############################ Generation & assets ############################
    Asset:
      type: object
      description: >-
        A normalized output asset — one shape for every tool. `id` is a UUID
        (example ids below are illustrative). `aspect_ratio` is the canonical
        size hint; pixel dimensions and thumbnails are not yet returned.
      required: [id, type, url, model, created_at]
      properties:
        id: { $ref: "#/components/schemas/AssetIdentifier" }
        type:
          type: string
          enum: [image, video, carousel_slide, analysis]
          example: image
        url: { type: [string, "null"], format: uri, example: https://cdn.memes.media/assets/5Tk8wQ.png }
        aspect_ratio: { type: [string, "null"], example: "1:1" }
        slide_index: { type: [integer, "null"], example: null }
        parent_id: { type: [string, "null"], example: null }
        model: { $ref: "#/components/schemas/Model" }
        created_at: { type: string, format: date-time, example: "2026-06-18T17:42:10Z" }

    Generation:
      type: object
      description: An async creative job.
      required:
        - id
        - object
        - type
        - status
        - requested_count
        - completed_count
        - failed_count
        - output
        - created_at
      properties:
        id: { $ref: "#/components/schemas/GenerationIdentifier" }
        object: { type: string, const: generation, example: generation }
        type: { $ref: "#/components/schemas/GenerationType" }
        status: { $ref: "#/components/schemas/GenerationStatus" }
        model:
          oneOf:
            - $ref: "#/components/schemas/Model"
            - type: "null"
          description: The branded model that produced the outputs, when applicable.
        requested_count: { type: integer, example: 4 }
        completed_count: { type: integer, example: 4 }
        failed_count: { type: integer, example: 0 }
        output:
          type: array
          description: Populated when terminal; a normalized asset list.
          items: { $ref: "#/components/schemas/Asset" }
        error:
          oneOf:
            - type: "null"
            - type: object
              properties:
                type: { type: string, example: generation_error }
                code: { type: string, example: generation_failed }
                message: { type: string, example: "The generation could not be completed." }
        created_at: { type: string, format: date-time, example: "2026-06-18T17:41:55Z" }
        completed_at:
          type: [string, "null"]
          format: date-time
          example: "2026-06-18T17:42:11Z"

    GenerationEnvelope:
      type: object
      description: The standard async response envelope returned by every submit and poll.
      required: [generation, meta]
      properties:
        generation: { $ref: "#/components/schemas/Generation" }
        usage: { $ref: "#/components/schemas/Usage" }
        meta: { $ref: "#/components/schemas/Meta" }

    ############################ Request bodies ############################
    CreateImageRequest:
      type: object
      description: At least one of `prompt`, `brand_kit_id` / `brand_context`, `template_prompt`, or `reference_images`.
      properties:
        prompt: { type: string, maxLength: 4000, description: Creative direction. }
        business_name: { type: string, maxLength: 200 }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }
        template_prompt: { type: string, maxLength: 4000, description: An optional template / format prompt. }
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        count:
          type: integer
          minimum: 1
          maximum: 4
          default: 1
          description: Number of variants, subject to available credits and endpoint limits.
        quality: { $ref: "#/components/schemas/Quality" }
        model:
          type: string
          enum: [forge-image, forge-image-fast]
          default: forge-image
        controls: { $ref: "#/components/schemas/Controls" }

    EditImageRequest:
      type: object
      required: [image_id, instruction]
      properties:
        image_id: { type: string, example: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80 }
        instruction:
          type: string
          maxLength: 2000
          description: The natural-language edit instruction.
          example: "Make the background a deep navy"
        mask:
          type: string
          description: A data URI for a mask image — enables inpaint.
          example: data:image/png;base64,iVBORw0KGgo...
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }

    ReframeImageRequest:
      type: object
      required: [image_id, aspect_ratio]
      properties:
        image_id: { type: string, example: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80 }
        aspect_ratio:
          type: string
          description: Target aspect ratio.
          enum: ["9:16", "16:9", "1:1", "4:5", "2:3", "3:2"]
          example: "9:16"

    CreateMemeRequest:
      type: object
      description: At least one of `prompt`, `brand_kit_id` / `brand_context`, `template_prompt`, or `reference_images`.
      properties:
        prompt: { type: string, maxLength: 4000 }
        business_name: { type: string, maxLength: 200 }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }
        template_prompt: { type: string, maxLength: 4000, description: An optional template / format prompt. }
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        count: { type: integer, minimum: 1, maximum: 4, default: 1 }
        enhance:
          type: boolean
          default: false
          description: Run Forge Prompt enhancement before generating.

    CreateVideoRequest:
      type: object
      description: At least one of `prompt`, `business_url`, or `brand_kit_id` / `brand_context`.
      properties:
        prompt: { type: string, maxLength: 4000 }
        business_name: { type: string, maxLength: 200 }
        business_url: { type: string, format: uri, maxLength: 2048, example: https://example.com }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }
        reference_images:
          type: array
          maxItems: 4
          description: Presence routes reference-to-video. Data URIs and `template` references are rejected.
          items: { $ref: "#/components/schemas/VideoReferenceImage" }
        aspect_ratio:
          type: string
          enum: ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"]
          default: "16:9"
        duration:
          type: integer
          minimum: 1
          maximum: 15
          default: 5
          description: Duration in seconds.
        resolution:
          type: string
          enum: [480p, 720p]
          default: 720p

    AnimateImageRequest:
      type: object
      required: [image_id]
      properties:
        image_id:
          type: string
          description: A workspace-owned image asset to animate.
          example: 5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80

    PlanCarouselRequest:
      type: object
      required: [brief]
      properties:
        brief: { type: string, maxLength: 4000 }
        business_website: { type: string, format: uri, maxLength: 2048, example: https://example.com }
        goal:
          type: string
          enum: [awareness, leads, sales, followers, engagement]
          default: leads
        platform:
          type: string
          enum: [ig-portrait, ig-square, linkedin, tiktok]
          default: ig-portrait
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        slide_count: { type: integer, minimum: 3, maximum: 10, default: 5 }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }

    RenderCarouselRequest:
      type: object
      description: |
        Provide a `plan_id` from `/carousels/plan`, an inline `plan` with 3–10 slides, or enough
        planning inputs to create a plan. An inline plan's canonical JSON may not exceed 128 KiB.
      properties:
        plan_id: { $ref: "#/components/schemas/CarouselPlanIdentifier" }
        plan:
          allOf:
            - $ref: "#/components/schemas/CarouselPlan"
          description: A bounded inline plan containing 3–10 slides.
          x-max-bytes: 131072
        brief: { type: string, maxLength: 4000 }
        business_website: { type: string, format: uri, maxLength: 2048 }
        goal:
          type: string
          enum: [awareness, leads, sales, followers, engagement]
          default: leads
        platform:
          type: string
          enum: [ig-portrait, ig-square, linkedin, tiktok]
          default: ig-portrait
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        slide_count: { type: integer, minimum: 3, maximum: 10, default: 5 }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }
        quality: { $ref: "#/components/schemas/Quality" }
        controls: { $ref: "#/components/schemas/Controls" }
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }

    CreateStoryboardRequest:
      type: object
      description: At least one of `goal` or `brand_kit_id` / `brand_context`.
      properties:
        goal: { type: string, maxLength: 4000, description: The storyboard brief. }
        business_name: { type: string, maxLength: 200 }
        brand_kit_id: { $ref: "#/components/schemas/BrandKitIdentifier" }
        brand_context: { $ref: "#/components/schemas/BrandContext" }
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }
        aspect_ratio:
          type: string
          enum: ["16:9", "9:16", "1:1"]
          default: "1:1"
        preset:
          type: string
          enum:
            - auto
            - comic-strip
            - problem-solution
            - before-after
            - how-it-works
            - customer-journey
            - testimonial-sequence
            - founder-story
          default: auto
        panel_count: { type: integer, enum: [3, 4], default: 3 }
        tone:
          type: string
          enum: [funny, confident, educational, dramatic, chaotic]
          default: funny
        text_density:
          type: string
          enum: [minimal, balanced, caption-heavy]
          default: balanced
        product_role:
          type: string
          enum: [story-first, hero-every-panel, reveal-panel, supporting-prop]
          default: story-first
        visual_style:
          type: string
          enum: [comic-strip, clean-storyboard]
          default: comic-strip

    EnhancePromptRequest:
      type: object
      required: [prompt]
      properties:
        prompt: { type: string, minLength: 1, maxLength: 4000, example: "make a funny meme about our coffee being strong" }
        website_url: { type: string, format: uri }
        has_logo: { type: boolean }
        has_reference_image: { type: boolean }
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        reference_images:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ReferenceImage" }

    CreateBrandKitRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 160, example: "Cold Brew Co" }
        company_name: { type: string, maxLength: 200 }
        website_url: { type: string, format: uri, maxLength: 2048 }
        website_context: { type: string, maxLength: 8000 }
        brand_colors:
          type: array
          maxItems: 16
          items:
            type: string
            pattern: '^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$'
          example: ["#0E1B2A", "#FF7A45"]
        tone_of_voice: { type: string, maxLength: 2000 }
        target_audience: { type: string, maxLength: 2000 }
        industry: { type: string, maxLength: 160 }
        auto_attach_product_references: { type: boolean, default: true }

    UpdateBrandKitRequest:
      type: object
      description: Any subset of the create fields. `name`, if present, must be non-empty.
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        company_name: { type: string, maxLength: 200 }
        website_url: { type: string, format: uri, maxLength: 2048 }
        website_context: { type: string, maxLength: 8000 }
        brand_colors:
          type: array
          maxItems: 16
          items:
            type: string
            pattern: '^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$'
        tone_of_voice: { type: string, maxLength: 2000 }
        target_audience: { type: string, maxLength: 2000 }
        industry: { type: string, maxLength: 160 }
        auto_attach_product_references: { type: boolean }

    CreateWebhookRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          pattern: '^https://'
          maxLength: 2048
          description: A public HTTPS destination without embedded credentials.
          example: https://hooks.example.com/memes
        events:
          type: array
          description: Event types to subscribe to. Omit to subscribe to all four explicit generation events.
          maxItems: 4
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEventType" }
          example: ["generation.completed", "generation.failed"]
        enabled: { type: boolean, default: true }

    ############################ Resource objects ############################
    CarouselPlan:
      type: object
      description: A carousel strategy plan.
      properties:
        id: { $ref: "#/components/schemas/CarouselPlanIdentifier" }
        concept_title: { type: string, example: "5 Dinners, Zero Stress" }
        viral_mechanic: { type: string, example: "Numbered listicle with a relatable pain hook" }
        audience: { type: string, example: "Time-strapped parents 28–45" }
        design_system:
          type: object
          properties:
            palette:
              type: object
              properties:
                background: { type: string, example: "#0E1B2A" }
                primary_text: { type: string, example: "#FFFFFF" }
                accent: { type: string, example: "#FF7A45" }
            headline_typeface: { type: string, example: "Inter Tight" }
            body_typeface: { type: string, example: "Inter" }
        cta: { type: string, example: "Tap to start your first box" }
        slides:
          type: array
          minItems: 3
          maxItems: 10
          items:
            type: object
            properties:
              slide_index: { type: integer, example: 0 }
              role: { type: string, example: hook }
              on_image_text: { type: string, example: "Dinner solved in 15 minutes" }
              visual_direction: { type: string, example: "Steaming plate on a bright counter" }
              image_directive: { type: string, example: "Hero food shot, warm light" }

    CarouselPlanResponse:
      type: object
      required: [plan, plan_id, model, meta]
      properties:
        plan: { $ref: "#/components/schemas/CarouselPlan" }
        plan_id: { $ref: "#/components/schemas/CarouselPlanIdentifier" }
        model: { $ref: "#/components/schemas/Model" }
        meta: { $ref: "#/components/schemas/Meta" }

    CarouselSubmissionResponse:
      type: object
      required: [carousel, meta]
      properties:
        carousel:
          type: object
          required: [id, object, status, slide_count, slides]
          properties:
            id: { $ref: "#/components/schemas/GenerationIdentifier" }
            object: { type: string, const: carousel_set, example: carousel_set }
            status: { $ref: "#/components/schemas/GenerationStatus" }
            slide_count: { type: integer, minimum: 3, maximum: 10, example: 5 }
            slides:
              type: array
              minItems: 3
              maxItems: 10
              items:
                type: object
                required: [id, slide_index, status]
                properties:
                  id: { $ref: "#/components/schemas/AssetIdentifier" }
                  slide_index: { type: integer, minimum: 0, example: 0 }
                  status: { $ref: "#/components/schemas/GenerationStatus" }
        meta: { $ref: "#/components/schemas/Meta" }

    CarouselSet:
      type: object
      description: A rendered carousel set with hydrated slides.
      properties:
        id: { type: string, example: 7c4a2e91-6b8d-4f35-a1c7-9e2d5b8f3a60 }
        object: { type: string, const: carousel_set, example: carousel_set }
        name: { type: string, example: "5 Dinners, Zero Stress" }
        status: { $ref: "#/components/schemas/GenerationStatus" }
        slide_count: { type: integer, example: 5 }
        plan:
          oneOf:
            - $ref: "#/components/schemas/CarouselPlan"
            - type: "null"
          description: Null only for legacy carousel sets that predate persisted plans.
        slides:
          type: array
          items:
            type: object
            properties:
              slide_index: { type: [integer, "null"], example: 0 }
              status: { $ref: "#/components/schemas/GenerationStatus" }
              asset:
                oneOf:
                  - $ref: "#/components/schemas/Asset"
                  - type: "null"
        created_at: { type: string, format: date-time, example: "2026-06-18T17:00:00Z" }
        updated_at: { type: string, format: date-time, example: "2026-06-18T17:05:00Z" }

    BrandKit:
      type: object
      description: A persistent brand profile.
      properties:
        id: { type: string, example: 3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70 }
        object: { type: string, const: brand_kit, example: brand_kit }
        name: { type: string, example: "Cold Brew Co" }
        is_default: { type: boolean, example: true }
        company_name: { type: [string, "null"], example: "Cold Brew Co" }
        website_url: { type: [string, "null"], format: uri, example: https://coldbrew.example.com }
        website_context: { type: [string, "null"] }
        logo_url: { type: [string, "null"], format: uri, example: https://cdn.memes.media/logos/3aZ9xK.png }
        brand_colors:
          type: array
          items: { type: string }
          example: ["#0E1B2A", "#FF7A45"]
        tone_of_voice: { type: [string, "null"], example: "Playful, punchy" }
        target_audience: { type: [string, "null"], example: "Urban professionals 25–40" }
        industry: { type: [string, "null"], example: "Food & Beverage" }
        auto_attach_product_references: { type: boolean, example: true }
        product_references:
          type: array
          maxItems: 5
          items: { $ref: "#/components/schemas/ProductReference" }
        created_at: { type: string, format: date-time, example: "2026-06-18T15:00:00Z" }
        updated_at: { type: string, format: date-time, example: "2026-06-18T16:00:00Z" }

    ProductReference:
      type: object
      required: [id, url, name, sort_order, created_at]
      properties:
        id: { type: string, format: uuid, example: 5f2e8a31-7c4d-4b90-a6e1-3d9f7b2c5a80 }
        url: { type: string, format: uri, example: https://cdn.memes.media/refs/5f2e8a31.png }
        name: { type: [string, "null"], maxLength: 160, example: "Signature can — front" }
        sort_order: { type: integer, minimum: 0, example: 0 }
        created_at: { type: string, format: date-time, example: "2026-06-18T16:00:00Z" }

    BrandKitResponse:
      type: object
      required: [brand_kit, meta]
      properties:
        brand_kit: { $ref: "#/components/schemas/BrandKit" }
        meta: { $ref: "#/components/schemas/Meta" }

    WebhookEventType:
      type: string
      description: A supported webhook event type.
      enum:
        - generation.completed
        - generation.partial
        - generation.failed
        - generation.canceled
      example: generation.completed

    WebhookEndpoint:
      type: object
      description: A registered webhook endpoint.
      required: [id, object, url, events, enabled, created_at]
      properties:
        id: { type: string, example: ep_01J1H8P7Y6Q5W4E3R2T1 }
        object: { type: string, const: webhook_endpoint, example: webhook_endpoint }
        url: { type: string, format: uri, example: https://hooks.example.com/memes }
        events:
          type: array
          items: { $ref: "#/components/schemas/WebhookEventType" }
        enabled: { type: boolean, example: true }
        created_at: { type: string, format: date-time, example: "2026-06-18T17:00:00Z" }

    WebhookEndpointResponse:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpoint"
        - type: object
          required: [meta]
          properties:
            meta: { $ref: "#/components/schemas/Meta" }

    WebhookEndpointCreatedResponse:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpointResponse"
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: Returned only on the idempotent endpoint-creation lineage. Store it securely.
              example: whsec_4Kp2LmQ9aZ8xW

    WebhookEvent:
      type: object
      description: |
        The outbound webhook envelope. Delivery is at-least-once; dedupe on the `svix-id`
        header (or this `id`). Each delivery carries `svix-id`, `svix-timestamp`, and
        `svix-signature` headers — verify with the official `svix` libraries
        (`Webhook(secret).verify(rawBody, headers)`), or manually: `svix-signature` is a
        space-separated list of `v1,<base64>` HMAC-SHA256 signatures over
        `"{svix-id}.{svix-timestamp}.{raw_body}"`; reject if `|now - svix-timestamp| > 5min`.
      required: [id, type, created_at, livemode, data]
      properties:
        id: { type: string, example: evt_9Lk2MnQ8aZ }
        type: { $ref: "#/components/schemas/WebhookEventType" }
        created_at: { type: string, format: date-time, example: "2026-06-18T17:42:12Z" }
        livemode: { type: boolean, example: true }
        data:
          type: object
          required: [generation]
          properties:
            generation: { $ref: "#/components/schemas/Generation" }

    Account:
      type: object
      description: Workspace account info.
      required: [id, object, plan, livemode, meta]
      properties:
        id: { type: string, example: 3a6f1d28-9c5b-4e70-a2d8-7b3f6c1e5a90 }
        object: { type: string, const: account, example: account }
        plan: { type: string, example: pro }
        key_prefix:
          type: string
          description: The 8-char prefix of the key that made the request.
          example: sk_live_8f3k
        livemode: { type: boolean, example: true }
        meta: { $ref: "#/components/schemas/Meta" }

    AccountUsage:
      type: object
      description: Credit consumption and quota by interval.
      required: [plan, credits_remaining, intervals, meta]
      properties:
        plan: { type: string, example: pro }
        credits_remaining:
          type: [integer, "null"]
          description: Null for test keys because simulator calls do not consume credits.
          example: 470
        intervals:
          type: object
          additionalProperties:
            type: object
            properties:
              limit: { type: [integer, "null"], example: 1000 }
              used: { type: integer, example: 530 }
              remaining: { type: [integer, "null"], example: 470 }
              interval:
                type: string
                enum: [daily, rolling_30_days, test, unlimited]
                example: rolling_30_days
              reset_at: { type: [string, "null"], format: date-time, example: "2026-07-18T00:00:00Z" }
        meta: { $ref: "#/components/schemas/Meta" }
