> ## 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.

# Upload Media

> Get a presigned URL to upload a file to your media library

## Overview

Uploading a file from your machine takes three calls:

1. **This call**: send the file's name, MIME type and size. You get a `mediaId` (status `PENDING`) and a presigned `uploadUrl`.
2. **PUT the file** to `uploadUrl` with the `headers` returned, within 10 minutes (`expiresAt`). This request goes straight to storage, without your API key.
3. **[Complete Upload](/api-reference/media/complete)**: Hooked checks that the file arrived and the media becomes `COMPLETED`.

<Info>
  JPEG, PNG, WEBP, GIF, MP4, MOV or WEBM, up to **100 MB**. The extension of `fileName` must match `fileType`. Your plan's storage is checked now with `fileSize` and again on complete with the size actually uploaded; it is counted only on complete. Uploading is free.
</Info>

<RequestExample>
  ```bash cURL theme={null}
  # 1. Ask for an upload URL
  curl -X POST "https://api.hooked.so/v1/media/upload" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{ "fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": 8388608 }'

  # 2. PUT the file to data.uploadUrl
  curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
    -H "Content-Type: video/mp4" \
    --data-binary @demo.mp4

  # 3. Complete
  curl -X POST "https://api.hooked.so/v1/media/MEDIA_ID/complete" \
    -H "x-api-key: your_api_key_here"
  ```

  ```javascript JavaScript theme={null}
  import { readFile, stat } from "node:fs/promises";

  const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
  const { size } = await stat("demo.mp4");

  const start = await fetch("https://api.hooked.so/v1/media/upload", {
    method: "POST",
    headers,
    body: JSON.stringify({ fileName: "demo.mp4", fileType: "video/mp4", fileSize: size }),
  });
  const { data: upload } = await start.json();

  await fetch(upload.uploadUrl, {
    method: upload.method,
    headers: upload.headers,
    body: await readFile("demo.mp4"),
  });

  const done = await fetch(`https://api.hooked.so/v1/media/${upload.mediaId}/complete`, {
    method: "POST",
    headers: { "x-api-key": process.env.HOOKED_API_KEY },
  });
  const { data: media } = await done.json();
  console.log(media.id, media.status); // COMPLETED
  ```

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

  api_key = os.environ["HOOKED_API_KEY"]
  path = "demo.mp4"

  upload = requests.post(
      "https://api.hooked.so/v1/media/upload",
      headers={"x-api-key": api_key},
      json={"fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": os.path.getsize(path)},
  ).json()["data"]

  with open(path, "rb") as file:
      requests.put(upload["uploadUrl"], headers=upload["headers"], data=file).raise_for_status()

  media = requests.post(
      f"https://api.hooked.so/v1/media/{upload['mediaId']}/complete",
      headers={"x-api-key": api_key},
  ).json()["data"]
  print(media["id"], media["status"])
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "success": true,
    "message": "Upload URL created",
    "data": {
      "mediaId": "cm4x9s8u60009ab12kl56mn90",
      "uploadUrl": "https://files.hooked.so/team/public/demo--fid--9c1d.mp4?X-Amz-Signature=...",
      "method": "PUT",
      "headers": { "Content-Type": "video/mp4" },
      "expiresAt": "2026-10-01T09:40:00.000Z"
    }
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "message": "fileName: The extension must be one of mp4, mov, webm"
  }
  ```
</ResponseExample>

## Errors

| Status | Message | Cause |
| - | - | - |
| 400 | `fileName: Required`, `fileType: Required` | A field is missing. |
| 400 | `fileSize: Must be the file size in bytes, a positive integer` | `fileSize` is missing or not a whole number of bytes. |
| 400 | `fileType: Must be one of ...` | Not a supported image or video type. |
| 400 | `fileName: The extension must be one of ...` | The extension does not match `fileType`. |
| 400 | `fileSize: Max file size is 100 MB` | The file is too large. |
| 401 | `code: "not_authenticated"` | Missing or invalid `x-api-key`. |
| 403 | `Insufficient storage. ...` | The file does not fit your plan's storage. |

<Tip>
  The file is already online? [Import Media from URL](/api-reference/media/import) takes one call.
</Tip>


## OpenAPI

````yaml POST /v1/media/upload
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/upload:
    post:
      tags:
        - Media
      summary: Upload Media
      description: >-
        Step one of uploading a file from your machine: creates a `PENDING`
        media and returns a presigned URL. PUT the file's bytes to `uploadUrl`
        within 10 minutes, then call Complete Upload. JPEG, PNG, WEBP, GIF, MP4,
        MOV or WEBM, up to 100 MB. Free.
      operationId: uploadMedia
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - fileName
                - fileType
                - fileSize
              properties:
                fileName:
                  type: string
                  description: >-
                    The file's name with its extension: jpg, jpeg, png, webp,
                    gif, mp4, mov or webm.
                fileType:
                  type: string
                  enum:
                    - image/jpeg
                    - image/png
                    - image/webp
                    - image/gif
                    - video/mp4
                    - video/quicktime
                    - video/webm
                  description: >-
                    The file's MIME type. Its extension must match (a
                    `video/mp4` named `.png` is refused).
                fileSize:
                  type: integer
                  minimum: 1
                  maximum: 104857600
                  description: >-
                    Size in bytes, at most 100 MB. Checked against your storage
                    now and again, with the real size, on complete.
                name:
                  type: string
                  description: Name in your library. Defaults to `fileName`.
            example:
              fileName: demo.mp4
              fileType: video/mp4
              fileSize: 8388608
              name: Demo clip
      responses:
        '200':
          description: Upload URL created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      mediaId:
                        type: string
                        description: The new `PENDING` media
                      uploadUrl:
                        type: string
                        description: Presigned URL to PUT the file's bytes to
                      method:
                        type: string
                        enum:
                          - PUT
                      headers:
                        type: object
                        additionalProperties:
                          type: string
                        description: Headers to send with the PUT
                      expiresAt:
                        type: string
                        format: date-time
                        description: When `uploadUrl` stops working (10 minutes)
              example:
                success: true
                message: Upload URL created
                data:
                  mediaId: cm4x9s8u60009ab12kl56mn90
                  uploadUrl: >-
                    https://files.hooked.so/team/public/demo--fid--9c1d.mp4?X-Amz-Signature=...
                  method: PUT
                  headers:
                    Content-Type: video/mp4
                  expiresAt: '2026-10-01T09:40:00.000Z'
        '400':
          description: Invalid body, type, extension or size
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError400'
              example:
                success: false
                message: 'fileName: The extension must be one of mp4, mov, webm'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Not enough storage left on your plan, 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
        '409':
          $ref: '#/components/responses/IdempotencyInProgress'
        '422':
          $ref: '#/components/responses/IdempotencyMismatch'
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error500'
              example:
                success: false
                message: Failed to create the upload URL
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Makes the request safe to retry. 1-255 printable ASCII characters, one
        per operation (your job id, or a UUID you store), reused on every retry.
        Within 24 hours the same key with the same body answers with the first
        response and the header `Idempotent-Replayed: true`, without running or
        charging again. Keys are scoped to your team and the endpoint. 5xx
        answers and refusals before anything ran (401, 402, 403, 409, 429) are
        not kept. See [Idempotency](/guides/idempotency).
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[\x20-\x7E]+$
      example: order-1042-video
  schemas:
    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'
    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'
    IdempotencyInProgress:
      description: >-
        A request with the same `Idempotency-Key` is still running. Retry in a
        few seconds.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_conflict
            message: A request with this Idempotency-Key is still in progress
            details: {}
    IdempotencyMismatch:
      description: >-
        The `Idempotency-Key` was already used with a different body. Use a new
        key for a new request.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_mismatch
            message: Idempotency-Key was used with a different request
            details: {}
    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.