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

# Upload Project

> Create a new video project by uploading a video file directly for AI-powered caption generation

# Upload Project

Create a new video project by uploading a video file directly to Submagic. This endpoint accepts multipart/form-data uploads and is ideal for applications where you have video files stored locally or want to upload directly from user devices.

<Note>
  This endpoint requires authentication and has a rate limit of 500 requests per
  hour due to the resource-intensive nature of file uploads.
</Note>

## Authentication

<ParamField header="x-api-key" type="string" required>
  Your Submagic API key starting with `sk-`
</ParamField>

## Request Body (multipart/form-data)

<ParamField body="title" type="string" required>
  A descriptive title for your video project (1-100 characters)
</ParamField>

<ParamField body="language" type="string" required>
  Language code for transcription (e.g., "en", "es", "fr"). Use the [languages
  endpoint](/api-reference/languages) to get available options.
</ParamField>

<ParamField body="file" type="file" required>
  Video file to upload. Must be in a supported format and under 2GB.
</ParamField>

<ParamField body="aiEditTemplate" type="string">
  Name of an AI edit template to apply. AI edit templates automatically apply
  AI-powered scene splitting, B-roll, music, and styling to your video. Available templates: `"kelly"` (minimal, design), `"karl"`
  (effective, modern), `"ella"` (dynamic, bold).

  When `aiEditTemplate` is provided, the only other fields you can pass
  alongside it are `title`, `language`, `file`, `webhookUrl`, and `dictionary`.
  All other fields will be ignored or rejected.
</ParamField>

<ParamField body="presetId" type="string">
  ID of a saved preset to apply to the project. A preset is a snapshot of your
  project settings — including captions style, hook title, music, effects, and
  more. When provided, all preset settings are automatically applied after
  transcription completes. To get your preset ID, go to the Presets page in the
  app, open the dropdown menu on any preset card, and click **"Copy ID"**.

  Cannot be combined with `templateName`, `userThemeId`, `hookTitle`, `music`,
  `items`, `magicZooms`, `magicBrolls`, `magicBrollsPercentage`,
  `removeSilencePace`, or `removeBadTakes` — the preset already controls these
  settings.
</ParamField>

<ParamField body="templateName" type="string">
  Template to apply for styling. Use the [templates
  endpoint](/api-reference/templates) to get available options. Defaults to
  "Sara" if not specified. Cannot be used together with `userThemeId`.
</ParamField>

<ParamField body="userThemeId" type="string">
  ID of a custom user theme to apply for styling. Must be a valid UUID of a
  theme that belongs to you or your team. Cannot be used together with
  `templateName`. You can find the id of your custom theme by opening a project,
  selecting the theme, pressing the pen icon to edit it. You'll see the id of
  the theme under its name.
</ParamField>

<ParamField body="hookTitle" type="string">
  Adds an animated hook caption. Pass `"true"` to enable the default hook, or
  pass a JSON string such as
  `{"text":"Stop scrolling—watch this in 30 seconds","template":"tiktok","top":45,"size":32}`.

  * `text`: Optional custom copy (1-100 characters)
  * `template`: Optional template name (defaults to `"tiktok"`). Use the [hook
    title templates endpoint](/api-reference/hook-title-templates) to discover
    valid names.
  * `top`: Optional vertical position between 0-80 (default `50`)
  * `size`: Optional font size between 0-80 (default `30`)

  Template names are validated before the upload begins; invalid names return a
  `VALIDATION_ERROR`.
</ParamField>

<ParamField body="webhookUrl" type="string">
  URL to receive webhook notifications when processing is complete. Must be a
  valid HTTPS URL.
</ParamField>

<ParamField body="dictionary" type="string">
  JSON array string of custom words or phrases to improve transcription accuracy
  (max 100 items, 50 characters each).
</ParamField>

<ParamField body="items" type="string">
  JSON string describing optional items to insert into the video. Pass an
  array of objects. Each item must include a `type` field to specify whether it's
  user media from your library or AI-generated content.

  <Expandable title="User Media Item JSON" defaultOpen>
    <ParamField body="type" type="string" required>
      Must equal `"user-media"`
    </ParamField>

    <ParamField body="startTime" type="number" required>
      Start time in seconds where the media should begin (≥ 0)
    </ParamField>

    <ParamField body="endTime" type="number" required>
      End time in seconds where the media should end (must be greater than
      `startTime`)
    </ParamField>

    <ParamField body="userMediaId" type="string" required>
      UUID of the user media from your library. You can find this ID in the
      editor's 'B-roll' tab → 'My videos' section under each video.
    </ParamField>

    <ParamField body="layout" type="string">
      Layout mode for the inserted media. Available values depend on the media type:

      **Video media:** `"cover"`, `"contain"`, `"rounded"`, `"square"`, `"split-50-50"`, `"split-35-65"`, `"split-50-50-bordered"`, `"split-35-65-bordered"`, `"pip-top-right"`, `"pip-bottom-right"`

      **Image media:** `"cover"`, `"contain"`, `"rounded"`, `"square"`
    </ParamField>
  </Expandable>

  <Expandable title="AI B-roll Item JSON" defaultOpen>
    <ParamField body="type" type="string" required>
      Must equal `"ai-broll"`
    </ParamField>

    <ParamField body="startTime" type="number" required>
      Start timestamp in seconds (≥ 0)
    </ParamField>

    <ParamField body="endTime" type="number" required>
      End timestamp in seconds; must be greater than `startTime` and within a
      12-second window
    </ParamField>

    <ParamField body="prompt" type="string" required>
      1-2500 character description of the B-roll to generate
    </ParamField>

    <ParamField body="layout" type="string">
      Layout mode for the AI B-roll. Allowed values: `"cover"`, `"contain"`, `"rounded"`, `"square"`, `"split-50-50"`, `"split-35-65"`, `"split-50-50-bordered"`, `"split-35-65-bordered"`, `"pip-top-right"`, `"pip-bottom-right"`
    </ParamField>
  </Expandable>

  **Important:** Each item must have a `type` field. Items entries cannot overlap
  in time. When present, this metadata is parsed after the upload completes and
  queued for rendering.
</ParamField>

<ParamField body="magicZooms" type="string">
  Enable automatic zoom effects on the video to enhance visual engagement. Pass
  "true" or "false" as string. Optional, defaults to "false".
</ParamField>

<ParamField body="magicBrolls" type="string">
  Enable automatic B-roll insertion to enhance video content with relevant
  supplementary footage. Pass "true" or "false" as string. Optional, defaults to
  "false".
</ParamField>

<ParamField body="magicBrollsPercentage" type="string">
  Percentage of automatic B-rolls to include in the video (0-100). Pass as
  string. Only effective when magicBrolls is enabled. Optional, defaults to
  "50".
</ParamField>

<ParamField body="removeSilencePace" type="string">
  Automatically remove silence from the video at the specified pace. Pass as
  string. Optional. Allowed values: "natural", "fast", "extra-fast". -
  "extra-fast": 0.1-0.2 seconds of silence removal - "fast": 0.2-0.6 seconds of
  silence removal - "natural": 0.6+ seconds of silence removal
</ParamField>

<ParamField body="removeBadTakes" type="string">
  Automatically detect and remove bad takes and silence from the video using AI
  analysis. Pass "true" or "false" as string. Optional, defaults to "false".
</ParamField>

<ParamField body="cleanAudio" type="string">
  Enable AI-powered audio cleanup that removes background noises from the video.
  Pass "true" or "false" as string. Optional, defaults to "false".
</ParamField>

<ParamField body="disableCaptions" type="string">
  Hide captions from the exported video. Pass `"true"` or `"false"` as
  string. Optional, defaults to `"false"`.
</ParamField>

<ParamField body="music" type="string">
  JSON string describing an optional background music track that spans the full
  project duration. The referenced media must be an `AUDIO` type file in your
  user media library.

  <Expandable title="Music Object JSON" defaultOpen>
    <ParamField body="userMediaId" type="string" required>
      UUID of an audio file from your media library. Must reference a user media
      item with type `AUDIO`. Use the [List User Media](/api-reference/list-user-media)
      endpoint to find available audio files.
    </ParamField>

    <ParamField body="volume" type="number" required>
      Playback volume (1-100)
    </ParamField>

    <ParamField body="startFromTime" type="number">
      Start offset within the audio file in seconds. Defaults to `0`.
    </ParamField>

    <ParamField body="fade" type="boolean">
      Apply a fade-in/fade-out effect to the music track. Defaults to `true`.
    </ParamField>
  </Expandable>
</ParamField>

## Supported Formats & Limits

<CardGroup cols={2}>
  <Card title="Supported Formats" icon="video">
    * **MP4** (.mp4) - **MOV** (.mov)
  </Card>

  <Card title="File Limits" icon="upload">
    * **Max size:** 2GB - **Max duration:** 2 hours
  </Card>
</CardGroup>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.submagic.co/v1/projects/upload" \
    -H "x-api-key: sk-your-api-key-here" \
    -F "title=My Uploaded Video" \
    -F "language=en" \
    -F "file=@./video.mp4" \
    -F "templateName=Hormozi 2" \
    -F 'items=[{"type":"user-media","startTime":5,"endTime":10,"userMediaId":"123e4567-e89b-12d3-a456-426614174000","layout":"pip-top-right"},{"type":"ai-broll","startTime":15,"endTime":21,"prompt":"drone shot of a modern city skyline at dusk","layout":"split-50-50"}]' \
    -F "webhookUrl=https://yoursite.com/webhook/submagic" \
    -F 'dictionary=["Submagic", "AI-powered", "captions"]' \
    -F "magicZooms=true" \
    -F "magicBrolls=true" \
    -F "magicBrollsPercentage=75" \
    -F "removeSilencePace=fast" \
    -F "removeBadTakes=true" \
    -F "cleanAudio=true" \
    -F 'hookTitle={"text":"Stop scrolling—watch this in 30 seconds","template":"tiktok","top":45,"size":32}' \
    -F 'music={"userMediaId":"88a08eec-712a-45d0-8d0b-3b631700cb3a","volume":30,"startFromTime":0,"fade":true}'
  ```

  ```javascript JavaScript theme={null}
  const uploadProject = async (file) => {
    const formData = new FormData();
    formData.append("title", "My Uploaded Video");
    formData.append("language", "en");
    formData.append("file", file);
    formData.append("templateName", "Hormozi 2");
    formData.append(
      "items",
      JSON.stringify([
        {
          type: "user-media",
          startTime: 5,
          endTime: 10,
          userMediaId: "123e4567-e89b-12d3-a456-426614174000",
          layout: "pip-top-right",
        },
        {
          type: "ai-broll",
          startTime: 15,
          endTime: 21,
          prompt: "drone shot of a modern city skyline at dusk",
          layout: "split-50-50",
        },
      ])
    );
    formData.append("webhookUrl", "https://yoursite.com/webhook/submagic");
    formData.append(
      "dictionary",
      JSON.stringify(["Submagic", "AI-powered", "captions"])
    );
    formData.append("magicZooms", "true");
    formData.append("magicBrolls", "true");
    formData.append("magicBrollsPercentage", "75");
    formData.append("removeSilencePace", "fast");
    formData.append("removeBadTakes", "true");
    formData.append("cleanAudio", "true");
    formData.append(
      "hookTitle",
      JSON.stringify({
        text: "Stop scrolling—watch this in 30 seconds",
        template: "tiktok",
        top: 45,
        size: 32,
      })
    );
    formData.append(
      "music",
      JSON.stringify({
        userMediaId: "88a08eec-712a-45d0-8d0b-3b631700cb3a",
        volume: 30,
        startFromTime: 0,
        fade: true,
      })
    );

    const response = await fetch("https://api.submagic.co/v1/projects/upload", {
      method: "POST",
      headers: {
        "x-api-key": "sk-your-api-key-here",
      },
      body: formData,
    });

    const project = await response.json();
    console.log("Project uploaded:", project.id);
    return project;
  };

  // Usage with file input
  const fileInput = document.getElementById("video-file");
  fileInput.addEventListener("change", async (event) => {
    const file = event.target.files[0];
    if (file) {
      await uploadProject(file);
    }
  });
  ```

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

  def upload_project(file_path):
      url = 'https://api.submagic.co/v1/projects/upload'
      headers = {
          'x-api-key': 'sk-your-api-key-here'
      }

      with open(file_path, 'rb') as video_file:
          files = {
              'file': video_file
          }
          data = {
              'title': 'My Uploaded Video',
              'language': 'en',
              'templateName': 'Hormozi 2',
              'items': json.dumps([
                  {
                      'type': 'user-media',
                      'startTime': 5,
                      'endTime': 10,
                      'userMediaId': '123e4567-e89b-12d3-a456-426614174000',
                      'layout': 'pip-top-right'
                  },
                  {
                      'type': 'ai-broll',
                      'startTime': 15,
                      'endTime': 21,
                      'prompt': 'drone shot of a modern city skyline at dusk',
                      'layout': 'split-50-50'
                  }
              ]),
              'webhookUrl': 'https://yoursite.com/webhook/submagic',
              'dictionary': '["Submagic", "AI-powered", "captions"]',
              'magicZooms': 'true',
              'magicBrolls': 'true',
              'magicBrollsPercentage': '75',
              'removeSilencePace': 'fast',
              'removeBadTakes': 'true',
              'cleanAudio': 'true',
              'hookTitle': json.dumps({
                  'text': 'Stop scrolling—watch this in 30 seconds',
                  'template': 'tiktok',
                  'top': 45,
                  'size': 32
              }),
              'music': json.dumps({
                  'userMediaId': '88a08eec-712a-45d0-8d0b-3b631700cb3a',
                  'volume': 30,
                  'startFromTime': 0,
                  'fade': True
              })
          }

          response = requests.post(url, headers=headers, files=files, data=data)
          project = response.json()

          print(f"Project uploaded: {project['id']}")
          return project

  # Usage
  project = upload_project('./video.mp4')
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "My Uploaded Video",
    "language": "en",
    "status": "processing",
    "webhookUrl": "https://yoursite.com/webhook/submagic",
    "templateName": "Hormozi 2",
    "magicZooms": true,
    "magicBrolls": true,
    "magicBrollsPercentage": 75,
    "removeSilencePace": "fast",
    "removeBadTakes": true,
    "cleanAudio": true,
    "createdAt": "2024-01-15T10:30:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
  ```
</ResponseExample>

## Error Responses

<ResponseField name="400 Validation Error" type="object">
  ```json theme={null}
  {
    "error": "VALIDATION_ERROR",
    "message": "File validation failed",
    "details": [
      {
        "field": "file",
        "message": "File size exceeds 10GB limit",
        "value": null
      }
    ]
  }
  ```
</ResponseField>

<ResponseField name="413 Payload Too Large" type="object">
  ```json theme={null}
  {
    "error": "PAYLOAD_TOO_LARGE",
    "message": "File size exceeds maximum allowed size"
  }
  ```
</ResponseField>

<ResponseField name="415 Unsupported Media Type" type="object">
  ```json theme={null}
  {
    "error": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Video format not supported"
  }
  ```
</ResponseField>

<ResponseField name="400 Preset Conflict" type="object">
  ```json theme={null}
  {
    "error": "VALIDATION_ERROR",
    "message": "presetId cannot be combined with templateName, userThemeId, magicZooms, magicBrolls, magicBrollsPercentage, removeBadTakes, removeSilencePace, items, hookTitle, or music. The preset controls these settings."
  }
  ```
</ResponseField>

<ResponseField name="400 Invalid Hook Title Template" type="object">
  ```json theme={null}
  {
    "error": "VALIDATION_ERROR",
    "message": "Invalid template name"
  }
  ```
</ResponseField>
