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

# Complete Upload

> Confirm an upload once the file is PUT, and make the media ready

## Overview

The last step of [Upload Media](/api-reference/media/upload). Call it after the PUT to `uploadUrl` succeeded; it takes no body. Hooked checks that storage really holds the file and reads its real size, counts it against your plan's storage, reads a video's length and starts its thumbnail. The media is then `COMPLETED` and its id works in any create endpoint.

* The file is not there yet: `400 The file was not uploaded`. The media stays `PENDING`; PUT the file and call again.
* The file is over 100 MB, or its real size does not fit your storage: the file is deleted and the media becomes `FAILED`. Start a new upload.
* Calling it again on a `COMPLETED` media answers `200` with the same media; nothing is counted twice.

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/media/cm4x9s8u60009ab12kl56mn90/complete" \
    -H "x-api-key: your_api_key_here"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api.hooked.so/v1/media/cm4x9s8u60009ab12kl56mn90/complete",
    { method: "POST", headers: { "x-api-key": process.env.HOOKED_API_KEY } }
  );
  const { data } = await response.json();
  console.log(data.status, data.durationSeconds);
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.post(
      "https://api.hooked.so/v1/media/cm4x9s8u60009ab12kl56mn90/complete",
      headers={"x-api-key": os.environ["HOOKED_API_KEY"]},
  )
  print(response.json()["data"]["status"])
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "success": true,
    "message": "Upload completed",
    "data": {
      "id": "cm4x9s8u60009ab12kl56mn90",
      "name": "demo.mp4",
      "type": "video",
      "status": "COMPLETED",
      "error": null,
      "url": "https://files.hooked.so/team/public/demo--fid--9c1d.mp4",
      "thumbnail": null,
      "durationSeconds": 12.4,
      "size": 8388608,
      "aspectRatio": null,
      "isAIGenerated": false,
      "createdAt": "2026-10-01T09:30:00.000Z"
    }
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "message": "The file was not uploaded"
  }
  ```

  ```json 409 theme={null}
  {
    "success": false,
    "message": "The upload failed; start a new one"
  }
  ```
</ResponseExample>

## Errors

| Status | Message | Cause |
| - | - | - |
| 400 | `The file was not uploaded` | Nothing at the upload URL yet. PUT the file, then call again. |
| 400 | `The file is larger than 100 MB` | The uploaded file is too large. It was deleted; the media is `FAILED`. |
| 401 | `code: "not_authenticated"` | Missing or invalid `x-api-key`. |
| 403 | `Insufficient storage. ...` | The real size does not fit your plan's storage. The file was deleted; the media is `FAILED`. |
| 404 | `Media not found` | No such media in your library. |
| 409 | `The upload failed; start a new one` | The media is `FAILED`. |
| 409 | `Media is not a pending upload (status ...)` | The media did not come from Upload Media (an import, a generated file). |

The thumbnail of a video is made a few seconds later: read it with [Get Media](/api-reference/media/details).


## OpenAPI

````yaml POST /v1/media/{mediaId}/complete
openapi: 3.0.0
info:
  title: Hooked API
  version: 1.0.0
  description: AI Video Generation API
servers:
  - url: https://api.hooked.so
security:
  - ApiKeyAuth: []
tags:
  - name: Videos
    description: Create projects, follow them and render videos
  - name: Account Analysis
    description: Analyze TikTok and YouTube accounts
  - name: Channels
    description: Connected social accounts you publish to
  - name: Publish
    description: Schedule videos to your connected accounts
  - name: Images
    description: 'Image editing: background removal and expansion'
  - name: Media
    description: 'Your media library: list, import from a URL, upload a file'
  - name: Avatars
    description: Your team's own avatars (Actors) and their Hook + Demo reactions
paths:
  /v1/media/{mediaId}/complete:
    post:
      tags:
        - Media
      summary: Complete Upload
      description: >-
        Step two of an upload, after the PUT: checks that the file arrived (and
        its real size), counts it against your storage, and makes the media
        `COMPLETED`. For a video it also reads the length and starts the
        thumbnail. No body. Calling it again on a completed media answers the
        same media.
      operationId: completeMediaUpload
      parameters:
        - name: mediaId
          in: path
          required: true
          schema:
            type: string
          description: The `mediaId` from Upload Media.
      responses:
        '200':
          description: Upload completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/LibraryMedia'
              example:
                success: true
                message: Upload completed
                data:
                  id: cm4x9s8u60009ab12kl56mn90
                  name: Demo clip
                  type: video
                  status: COMPLETED
                  error: null
                  url: https://files.hooked.so/team/public/demo--fid--9c1d.mp4
                  thumbnail: https://files.hooked.so/team/public/demo--fid--9c1d.jpg
                  durationSeconds: 12.4
                  size: 8388608
                  aspectRatio: ratio_9_16
                  isAIGenerated: false
                  createdAt: '2026-10-01T09:30:00.000Z'
        '400':
          description: >-
            The file is not in storage yet (PUT it, then call again), or it is
            over 100 MB (it is deleted and the media is `FAILED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError400'
              example:
                success: false
                message: The file was not uploaded
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            The real size does not fit your plan's storage (the file is deleted
            and the media is `FAILED`), or the team does not have the Hooked app
            product
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: false
                message: >-
                  Insufficient storage. The pro plan has a limit of 50GB and you
                  have already used 50GB
        '404':
          description: No such media in your library
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFound404'
              example:
                success: false
                message: Media not found
        '409':
          description: >-
            The media is `FAILED` (start a new upload), is not an upload, or is
            being completed by another call
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
              example:
                success: false
                message: The upload failed; start a new one
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error500'
              example:
                success: false
                message: Failed to complete the upload
      security:
        - ApiKeyAuth: []
components:
  schemas:
    LibraryMedia:
      type: object
      description: >-
        An image or video of your team's media library. Pass its `id` wherever a
        create endpoint takes a media id, once `status` is `COMPLETED`.
      required:
        - id
        - name
        - type
        - status
        - error
        - url
        - thumbnail
        - durationSeconds
        - size
        - aspectRatio
        - isAIGenerated
        - createdAt
      properties:
        id:
          type: string
          description: The media id
        name:
          type: string
          description: The name shown in your library
        type:
          type: string
          enum:
            - image
            - video
        status:
          type: string
          enum:
            - PENDING
            - PROCESSING
            - COMPLETED
            - FAILED
          description: >-
            `PENDING`: an upload waiting for its file and the complete call.
            `PROCESSING`: an import downloading or a generation running.
            `COMPLETED`: ready to use. `FAILED`: the import, upload or
            generation did not work (a failed generation's credits are given
            back); an import still running after 15 minutes, or a generation
            after 60, also reads as `FAILED`.
        error:
          type: string
          nullable: true
          description: >-
            Why it failed, when `status` is `FAILED` (a provider's refusal, a
            URL that did not answer, a file of the wrong type, a job that timed
            out). `null` otherwise, and for a failure from before the reason was
            kept.
        url:
          type: string
          description: URL of the file; an empty string until `status` is `COMPLETED`
        thumbnail:
          type: string
          nullable: true
          description: >-
            URL of the video's thumbnail, once made; `null` for images and until
            then
        durationSeconds:
          type: number
          nullable: true
          description: Length of a video in seconds; `null` for images and while unknown
        size:
          type: integer
          description: File size in bytes (0 until the file is in your library)
        aspectRatio:
          type: string
          nullable: true
          description: >-
            `ratio_9_16`, `ratio_1_1` or `ratio_16_9` when known, otherwise
            `null`
        isAIGenerated:
          type: boolean
          description: Whether a model made it in Hooked
        createdAt:
          type: string
          format: date-time
    ValidationError400:
      type: object
      description: >-
        The request is invalid. `message` is `<field>: <reason>` when a field is
        to blame.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: 'webhook: Must be a valid HTTPS URL'
    NotFound404:
      type: object
      description: >-
        Not found. Another team's resource answers the same way as a missing
        one.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: Project not found
    Error500:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
        error:
          type: string
          description: Present on some endpoints
      example:
        success: false
        message: Internal server error
    Error401:
      type: object
      description: Missing or invalid `x-api-key`.
      properties:
        code:
          type: string
          enum:
            - not_authenticated
        message:
          type: string
          example: Not authenticated
        errorCode:
          type: string
          enum:
            - NOT_AUTHENTICATED
        details:
          type: object
          additionalProperties:
            type: string
      example:
        code: not_authenticated
        message: Not authenticated
        errorCode: NOT_AUTHENTICATED
        details:
          x-api-key: Header not provided or API Key invalid
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error401'
    RateLimited429:
      description: >-
        Your team is over a rate limit: 30 POST requests per minute, 120 other
        requests per minute, or 3 downloads of your URLs running at once.
        Nothing ran and nothing was charged: wait `Retry-After` seconds and send
        the request again. See [Rate
        limits](/guides/error-handling#rate-limits-429).
      headers:
        Retry-After:
          description: Seconds to wait before sending again.
          schema:
            type: integer
        X-RateLimit-Limit:
          description: The limit of the bucket you hit (not sent for the download cap).
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: 'Requests left in this window: 0 (not sent for the download cap).'
          schema:
            type: integer
        X-RateLimit-Reset:
          description: >-
            When the window ends, in Unix seconds (not sent for the download
            cap).
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              errorCode:
                type: string
                enum:
                  - RATE_LIMITED
              message:
                type: string
              retryAfterSeconds:
                type: integer
            required:
              - success
              - errorCode
              - message
              - retryAfterSeconds
          example:
            success: false
            errorCode: RATE_LIMITED
            message: >-
              Rate limit exceeded: 30 POST requests per minute per team. Retry
              after 12 s.
            retryAfterSeconds: 12
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.