> ## Documentation Index
> Fetch the complete documentation index at: https://docs.viggle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate Video (from First-Last Frames and/or Text)

> Generate a MiniMax H3 video anchored on both a starting and an ending frame.

Send `multipart/form-data` with both a first frame and a last frame, each independently as an uploaded file or a URL. Supplying both is what selects this mode of `POST /v1/videos` — a last frame is only valid alongside a first frame. Generates a Viggle-optimized MiniMax H3 video with native audio.

This uses the same `POST /v1/videos` endpoint as [Generate Video (from Text)](/v1/api-reference/videos/create-from-text) and [Generate Video (from First Frame and/or Text)](/v1/api-reference/videos/create-from-first-frame) — which mode runs is selected by which frame fields you send, not by a separate parameter.

<Note>
  Every generated video includes **native audio** — there's no separate audio flag or field. **Pricing** (1 credit = $0.01): ⌈`duration_s`⌉ × the per-second rate for `quality` — **$0.01/sec (1 credit/second)\*\* for both `low` and `high`. Image-conditioned generation costs the same as text-only for the same quality and duration; `resolution` and `aspect_ratio` don't affect price. See [Pricing and retention](/v1/pricing).
</Note>

## Request parameters

Send `multipart/form-data`.

| Parameter               | Type         | Required | Default | Description                                                                                                                                        |
| ----------------------- | ------------ | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`                | string       |    Yes   | —       | Non-empty text description of the desired video. Still required even with frame images supplied.                                                   |
| `quality`               | string       |    Yes   | —       | `low` for faster generation and quicker iteration, or `high` for higher-fidelity output. Both tiers are billed at the same **\$0.01/sec** rate.    |
| `first_frame_image`     | file         |  One of  | —       | First-frame image uploaded directly. Supply exactly one of `first_frame_image` or `first_frame_image_url`.                                         |
| `first_frame_image_url` | string (URI) |  One of  | —       | Publicly reachable URL of the first-frame image; the service fetches and re-hosts it. Supply instead of `first_frame_image`.                       |
| `last_frame_image`      | file         |  One of  | —       | Last-frame image uploaded directly. Supply exactly one of `last_frame_image` or `last_frame_image_url`. Requires a first frame.                    |
| `last_frame_image_url`  | string (URI) |  One of  | —       | Publicly reachable URL of the last-frame image; the service fetches and re-hosts it. Supply instead of `last_frame_image`. Requires a first frame. |
| `duration_s`            | number       |    No    | `5`     | Target duration in seconds, 3–15. Rounded up to determine the credit charge.                                                                       |
| `resolution`            | string       |    No    | `768p`  | `480p`, `768p`, or `1080p`. Does not affect price.                                                                                                 |
| `aspect_ratio`          | string       |    No    | `16:9`  | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, or `21:9`. Does not affect price.                                                                             |
| `seed`                  | integer      |    No    | —       | ≥ 0. Omit for a random seed.                                                                                                                       |
| `watermark`             | boolean      |    No    | `false` | Whether to burn in a Viggle watermark.                                                                                                             |

Provide exactly one of `first_frame_image`/`first_frame_image_url`, and exactly one of `last_frame_image`/`last_frame_image_url`. Supplying both forms of the same slot answers `400 INVALID_REQUEST` — "supply only one of first\_frame\_image or first\_frame\_image\_url" (or the `last_frame_*` equivalent). Supplying a last frame with no first frame answers `400 INVALID_REQUEST` — "a last frame requires a first frame".

## Response parameters

Returns `200 OK` — an acceptance acknowledgment, not the final result.

| Field        | Type            | Always present | Description                                    |
| ------------ | --------------- | :------------: | ---------------------------------------------- |
| `id`         | string          |       Yes      | Public video ID, `vid_`-prefixed.              |
| `status`     | string          |       Yes      | Always `queued` on acceptance.                 |
| `progress`   | integer or null |       Yes      | Always `null` on acceptance.                   |
| `created_at` | string          |       Yes      | ISO 8601 creation timestamp, second precision. |

```json theme={null}
{
  "id": "vid_3f2a9c",
  "status": "queued",
  "progress": null,
  "created_at": "2026-08-24T09:12:03Z"
}
```

This response deliberately carries none of the fields `GET /v1/videos/{video_id}` returns (`stage`, `video_url`, `alpha_url`, `completed_at`, `error`). See [Get Video](/v1/api-reference/videos/get) for the full shape once the video is processing or done.

## Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl https://apis.viggle.ai/v1/videos \
    -H "Authorization: Bearer $VIGGLE_API_KEY" \
    -F prompt="camera pans across the room" \
    -F quality=low \
    -F first_frame_image_url=https://example.test/first.png \
    -F last_frame_image_url=https://example.test/last.png
  ```

  ```python Python theme={null}
  import os, requests
  response = requests.post(
      "https://apis.viggle.ai/v1/videos",
      headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
      data={
          "prompt": "camera pans across the room",
          "quality": "low",
          "first_frame_image_url": "https://example.test/first.png",
          "last_frame_image_url": "https://example.test/last.png",
      },
  )
  response.raise_for_status()
  video = response.json()
  ```

  ```javascript JavaScript theme={null}
  const form = new FormData();
  form.append("prompt", "camera pans across the room");
  form.append("quality", "low");
  form.append("first_frame_image_url", "https://example.test/first.png");
  form.append("last_frame_image_url", "https://example.test/last.png");
  const response = await fetch("https://apis.viggle.ai/v1/videos", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.VIGGLE_API_KEY}` },
    body: form,
  });
  const video = await response.json();
  ```
</CodeGroup>

## Common errors

| Error                                                                                        | Cause                                                                                                                |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `400 INVALID_REQUEST` — "a last frame requires a first frame"                                | `last_frame_image`/`last_frame_image_url` supplied without a first frame.                                            |
| `400 INVALID_REQUEST` — "supply only one of first\_frame\_image or first\_frame\_image\_url" | Both the file and URL form of the same frame slot supplied (the `last_frame_*` pair answers the equivalent message). |

## Next step

Use [Get Video](/v1/api-reference/videos/get) with the returned `id` to poll for the result.

<CardGroup cols={3}>
  <Card title="Quick Start: First-Last Frames to Video" icon="rocket" href="/v1/guides/quickstart-first-last-frame-to-video">
    Follow the complete create-and-poll flow.
  </Card>

  <Card title="Generate Video (from Text)" icon="text" href="/v1/api-reference/videos/create-from-text">
    Generate from a prompt alone, with no frame image.
  </Card>

  <Card title="Generate Video (from First Frame and/or Text)" icon="image" href="/v1/api-reference/videos/create-from-first-frame">
    Anchor only the start of the video, with no last frame.
  </Card>
</CardGroup>


## OpenAPI

````yaml openapi.yaml POST /v1/videos
openapi: 3.0.3
info:
  title: Viggle API
  description: Generate AI-powered character animation videos
  version: 2.0.0
  contact:
    name: Viggle Support
    url: https://viggle.ai
servers:
  - url: https://apis.viggle.ai
    description: Production server
security:
  - bearerAuth: []
tags:
  - name: Renders
    description: Prepare inputs, create renders, observe progress, and retrieve results.
  - name: Credits
    description: Read the credit balance available to the calling principal.
  - name: Characters
    description: |
      Create, list, inspect, and delete reusable character assets. `type`
      selects whether creation also extracts a 3D vsplat; export its
      download URL separately once ready.
  - name: Motions
    description: |
      Create, list, inspect, and delete reusable motion assets, and import one
      from an official motion template. `type` selects whether creation also
      extracts or generates a 3D animation; export its download URL
      separately once ready.
  - name: Videos
    description: |
      Generate a MiniMax H3 text-to-video (`vid_` prefix), and read the
      unified view over every video the caller owns — H3 generations and
      character+motion Renders (`render_` prefix) — merged into one
      resource.
paths:
  /v1/videos:
    post:
      tags:
        - Videos
      summary: Generate a video from text, or from a first and optional last frame
      description: >
        Generates a Viggle-optimized MiniMax H3 video with native audio.

        `multipart/form-data` only. The frame fields supplied select one of

        three generation modes:


        - Neither frame field supplied: text-to-video from `prompt` alone.

        - `first_frame_image`/`first_frame_image_url` supplied, no last
          frame: first-frame-to-video.
        - Both a first frame and a last frame supplied:
        first+last-frame-to-video.


        `quality=low` generates faster for quicker iteration; `quality=high`

        produces higher-fidelity output. Both tiers are billed at the same

        $0.01/sec rate — see `CreateVideoForm` below.


        All three modes answer `200` with the same minimal acceptance shape;

        poll `GET /v1/videos/{video_id}` for the final result.
      operationId: createVideo
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/SourceChannel'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateVideoForm'
      responses:
        '200':
          description: Video generation accepted and queued.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoCreateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/V1Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  parameters:
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: >-
        Optional caller-supplied correlation ID, up to 128 characters. The
        service returns the effective value in the response header for tracing
        and support.
      schema:
        type: string
        minLength: 1
        maxLength: 128
    SourceChannel:
      name: X-Viggle-Source
      in: header
      required: false
      description: >-
        Optional source-channel label, up to 128 characters, used to attribute
        traffic to an SDK, integration, product surface, or internal workflow.
      schema:
        type: string
        minLength: 1
        maxLength: 128
  schemas:
    CreateVideoForm:
      description: |
        Body of `POST /v1/videos`, `multipart/form-data` only. Which frame
        fields are supplied selects the generation mode:

        - Neither `first_frame_image`/`first_frame_image_url` nor
          `last_frame_image`/`last_frame_image_url` supplied: text-to-video.
        - A first frame supplied, no last frame: first-frame-to-video.
        - Both a first frame and a last frame supplied:
          first+last-frame-to-video.

        Within a frame slot, supply at most one of the file and the URL
        form. Supplying both `first_frame_image` and `first_frame_image_url`
        (or both `last_frame_image` and `last_frame_image_url`) answers
        `400 INVALID_REQUEST` with "supply only one of first_frame_image or
        first_frame_image_url" (or the `last_frame_*` equivalent). A last
        frame requires a first frame; supplying a last frame with no first
        frame answers `400 INVALID_REQUEST` with "a last frame requires a
        first frame".

        Billed at ⌈`duration_s`⌉ × the per-second credit rate for `quality`
        (currently 1 credit/second — $0.01/sec — for both `low` and `high`).
        `resolution` and `aspect_ratio` do not affect price, and
        image-conditioned generation (first-frame or first-last-frame) costs
        the same as text-only for the same quality and duration.

        Every generated video includes native audio; there is no separate
        audio flag or field.
      type: object
      required:
        - prompt
        - quality
      properties:
        prompt:
          type: string
          minLength: 1
          description: >-
            Text description of the desired video. Required and non-empty in
            every mode, including first-frame and first+last-frame modes.
          example: A paper airplane gliding through a sunlit office
        quality:
          type: string
          enum:
            - low
            - high
          description: >-
            Generation quality tier — also selects the per-second credit rate
            charged for this request. `low` generates faster, for quicker
            iteration; `high` generates slower but produces higher-fidelity
            output. Both tiers are billed at the same $0.01/sec rate.
          example: low
        first_frame_image:
          type: string
          format: binary
          description: >-
            First-frame image uploaded directly. Mutually exclusive with
            `first_frame_image_url`. Supplying either switches on
            first-frame-to-video mode.
        first_frame_image_url:
          type: string
          format: uri
          description: >-
            Publicly reachable URL of the first-frame image; the service fetches
            and re-hosts it. Mutually exclusive with `first_frame_image`.
        last_frame_image:
          type: string
          format: binary
          description: >-
            Last-frame image uploaded directly. Mutually exclusive with
            `last_frame_image_url`. Valid only alongside a first frame.
        last_frame_image_url:
          type: string
          format: uri
          description: >-
            Publicly reachable URL of the last-frame image; the service fetches
            and re-hosts it. Mutually exclusive with `last_frame_image`. Valid
            only alongside a first frame.
        duration_s:
          type: number
          minimum: 3
          maximum: 15
          default: 5
          description: Target video duration in seconds.
        resolution:
          type: string
          enum:
            - 480p
            - 768p
            - 1080p
          default: 768p
          description: Output resolution tier.
        aspect_ratio:
          type: string
          enum:
            - '16:9'
            - '9:16'
            - '1:1'
            - '4:3'
            - '3:4'
            - '21:9'
          default: '16:9'
          description: Output aspect ratio.
        seed:
          type: integer
          minimum: 0
          description: Deterministic generation seed. Omit for a random seed.
        watermark:
          type: boolean
          default: false
          description: Whether to burn in a Viggle watermark.
    VideoCreateResponse:
      description: |
        Acceptance acknowledgment for `POST /v1/videos`, shared by all three
        generation modes. This is a queued confirmation, not the final
        result — poll `GET /v1/videos/{video_id}` (or watch it via
        `GET /v1/videos`) until `status` reaches `ready`, then read
        `video_url` from that response.
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - progress
        - created_at
      properties:
        id:
          type: string
          description: Public video ID, `vid_`-prefixed.
          minLength: 1
        status:
          description: Lifecycle state at acceptance time; always `queued`.
          allOf:
            - $ref: '#/components/schemas/ResourceStatus'
        progress:
          type: integer
          description: Always null on acceptance.
          nullable: true
          minimum: 0
          maximum: 100
        created_at:
          type: string
          description: ISO 8601 creation timestamp, second precision.
          nullable: true
          format: date-time
    ResourceStatus:
      description: |
        The public lifecycle shared by every asynchronous resource. It carries
        the same values as `RenderStatus`.
      type: string
      enum:
        - queued
        - processing
        - ready
        - failed
        - cancelled
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          description: >-
            Top-level draft-flow error payload containing the stable code,
            message, retry guidance, correlation ID, and error-specific details.
          allOf:
            - $ref: '#/components/schemas/ErrorBody'
    ErrorBody:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - retryable
        - request_id
        - details
        - remediation
      properties:
        code:
          description: Stable error category from the draft-based render error vocabulary.
          allOf:
            - $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          description: >-
            Human-readable explanation suitable for logs or presentation to a
            developer.
        retryable:
          type: boolean
          description: >-
            Whether retrying the operation without changing its semantic input
            may succeed.
        request_id:
          type: string
          description: >-
            Correlation identifier for tracing and support. Include it when
            reporting the failure.
        details:
          type: object
          description: Error-specific structured context; available keys depend on `code`.
          additionalProperties: true
        remediation:
          description: Suggested recovery action and optional retry delay.
          allOf:
            - $ref: '#/components/schemas/ErrorRemediation'
    ErrorCode:
      description: |
        The failure vocabulary of the draft-based render flow. The resource
        operations project the same vocabulary onto the lower_snake_case codes
        described by `ResourceError`.
      type: string
      enum:
        - UNAUTHENTICATED
        - INVALID_CREDENTIAL
        - FORBIDDEN
        - INVALID_REQUEST
        - INVALID_CHARACTER
        - INVALID_MOTION
        - UNSUPPORTED_MEDIA
        - CONTENT_POLICY_VIOLATION
        - UPLOAD_REQUIRED
        - UPLOAD_MISMATCH
        - UPLOAD_EXPIRED
        - DRAFT_NOT_FOUND
        - DRAFT_ALREADY_CONSUMED
        - RENDER_NOT_FOUND
        - RENDER_NOT_CANCELLABLE
        - CHARACTER_NOT_FOUND
        - MOTION_NOT_FOUND
        - MOTION_NOT_READY
        - MOTION_TEMPLATE_NOT_FOUND
        - AVATAR_NOT_FOUND
        - ANIMATION_NOT_FOUND
        - EXTRACTION_UNAVAILABLE
        - EXTRACTION_FAILED
        - IDEMPOTENCY_KEY_REUSED
        - INSUFFICIENT_CREDITS
        - RATE_LIMITED
        - SERVICE_BUSY
        - INTERNAL_ERROR
    ErrorRemediation:
      type: object
      additionalProperties: false
      required:
        - action
        - retry_after_ms
      properties:
        action:
          type: string
          description: >-
            Suggested machine-readable or human-readable next action, such as
            retrying later, uploading media again, or correcting input.
        retry_after_ms:
          type: integer
          description: >-
            Recommended delay in milliseconds before retrying; null when no
            timed retry is advised.
          nullable: true
          format: int64
          minimum: 0
  headers:
    RequestId:
      description: Stable request identifier for support and tracing.
      schema:
        type: string
  responses:
    BadRequest:
      description: Invalid request.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    V1Unauthorized:
      description: Missing, invalid, or expired credential.
      headers:
        WWW-Authenticate:
          schema:
            type: string
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PaymentRequired:
      description: Insufficient credits.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Credential does not grant access to this resource.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema:
            type: integer
            minimum: 0
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalServerError:
      description: Unexpected server error.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServiceUnavailable:
      description: Service temporarily unavailable.
      headers:
        Retry-After:
          schema:
            type: integer
            minimum: 0
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key or OAuth access token
      description: |
        Server-side SDK clients use a project API key. Remote MCP clients use
        an OAuth access token. Never expose a project API key in browser code.

````