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

# Upload File

> Upload images and videos to Genviral CDN for use in TikTok slideshows, Instagram carousels, YouTube Shorts, Pinterest pins, LinkedIn posts, and Facebook content. Supports programmatic uploads from OpenClaw agents and automation pipelines.

Upload media files directly to Genviral's storage, then finalize the stored bytes into a
CDN-backed Media Library record. A preparation never creates a visible file by itself.

One upload URL carries up to 100 MB (images up to 50 MB). For larger videos, up to 500 MB, use
[Start Multipart Upload](/api-reference/start-multipart-upload).

## How It Works

1. Call this endpoint with the file's content type and a stable `Idempotency-Key`
2. Receive a file `id` and presigned `uploadUrl`
3. Upload your file directly to the `uploadUrl` using a PUT request
4. Call `POST /api/partner/v1/files/{fileId}/finalize` with the same media metadata and a stable `Idempotency-Key`
5. Use the finalized `file.url` in post creation or pack-image attachment requests

## Body Parameters

<ParamField body="contentType" type="string" required>
  MIME type of the file. Supported types:

  * **Images**: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/heif`
  * **Videos**: `video/mp4`, `video/quicktime`, `video/x-msvideo`, `video/webm`, `video/x-m4v`
</ParamField>

<ParamField body="filename" type="string">
  Original filename for reference (optional). Used for display purposes only.
</ParamField>

<ParamField body="duration_sec" type="number">
  Optional video duration in seconds. Also accepts `duration_seconds`, `duration`, `durationSec`,
  or `video_duration_sec`. Stored with the CDN file record so `/posts` can hydrate validation
  metadata when you use the returned `url`.
</ParamField>

<ParamField body="bytes" type="number">
  Optional file size in bytes. Also accepts `size`. Images: 1 through 52,428,800 (50 MB). Videos:
  1 through 104,857,600 (100 MB) on this endpoint; larger videos use
  [Start Multipart Upload](/api-reference/start-multipart-upload).
</ParamField>

## Response

Successful requests return `201` with:

* `uploadUrl` - Presigned URL to upload your file (expires in 10 minutes)
* `id` - Stable upload identity used by the finalize endpoint
* `contentType` - The content type you specified
* `expiresIn` - Seconds until the upload URL expires (600)

The finalize response contains `file`, including its verified byte size and public
`https://cdn.vireel.io/...` URL. Invalid, absent, empty, oversized, or MIME-mismatched bytes are
rejected without creating a Media Library row.

## Using With Packs

If your goal is to add a local file to a pack:

1. Call this endpoint and capture `data.id` + `data.uploadUrl`.
2. Upload your bytes to `data.uploadUrl` with `PUT`.
3. Finalize the upload and capture `data.file.url`.
4. Call [Add Pack Image](/api-reference/add-pack-image) with `image_url = data.file.url`.

## Examples

### Upload a video

<RequestExample>
  ```javascript theme={null}
  import fetch from "node-fetch";

  const fileBuffer = fs.readFileSync("./my-video.mp4");

  // Step 1: Get upload URL
  const response = await fetch("https://www.genviral.io/api/partner/v1/files", {
    method: "POST",
    headers: {
      Authorization: "Bearer <token>",
      "Content-Type": "application/json",
      "Idempotency-Key": "prepare-my-video-1",
    },
    body: JSON.stringify({
      contentType: "video/mp4",
      filename: "my-video.mp4",
      duration_sec: 42,
      bytes: fileBuffer.length,
    }),
  });

  const { data } = await response.json();
  // data.uploadUrl = presigned S3 URL
  // data.id = upload identity used for finalization

  // Step 2: Upload file to presigned URL
  await fetch(data.uploadUrl, {
    method: "PUT",
    headers: {
      "Content-Type": "video/mp4",
    },
    body: fileBuffer,
  });

  // Step 3: Verify the stored bytes and create the Media Library record
  const finalizeResponse = await fetch(
    `https://www.genviral.io/api/partner/v1/files/${data.id}/finalize`,
    {
      method: "POST",
      headers: {
        Authorization: "Bearer <token>",
        "Content-Type": "application/json",
        "Idempotency-Key": `finalize-${data.id}`,
      },
      body: JSON.stringify({
        contentType: "video/mp4",
        filename: "my-video.mp4",
        duration_sec: 42,
        bytes: fileBuffer.length,
      }),
    },
  );
  const { data: finalized } = await finalizeResponse.json();

  // Step 4: Use the CDN URL in your post
  await fetch("https://www.genviral.io/api/partner/v1/posts", {
    method: "POST",
    headers: {
      Authorization: "Bearer <token>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      caption: "Check this out!",
      media: {
        type: "video",
        url: finalized.file.url,
      },
      accounts: [{ id: "account-id" }],
    }),
  });
  ```
</RequestExample>

```bash theme={null}
# Step 1: Get upload URL
curl --request POST \
  --url https://www.genviral.io/api/partner/v1/files \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: prepare-my-video-1' \
  --data '{
    "contentType": "video/mp4",
    "filename": "my-video.mp4",
    "duration_sec": 42,
    "bytes": 8000000
  }'

# Step 2: Upload to presigned URL (use uploadUrl from response)
curl --request PUT \
  --url "<uploadUrl from step 1>" \
  --header 'Content-Type: video/mp4' \
  --data-binary @my-video.mp4

# Step 3: Finalize with POST /files/{fileId}/finalize and the exact step-1 metadata
```

<ResponseExample>
  ```json Response theme={null}
  {
    "ok": true,
    "code": 201,
    "message": "Upload URL generated",
    "data": {
      "id": "11111111-1111-4111-8111-111111111111",
      "uploadUrl": "https://storage.example.com/presigned-url...",
      "contentType": "video/mp4",
      "expiresIn": 600
    }
  }
  ```
</ResponseExample>

## Error Responses

* `400 invalid_request` - `Idempotency-Key` is missing or invalid
* `400 invalid_json` - Request body is not valid JSON
* `422 invalid_payload` - Invalid content type, missing required fields, or a declared size above
  the single-upload limit
* `401` - authentication failed (missing/invalid/revoked token)
* `402 subscription_required` - active Creator/Professional/Business plan required
* `403 tier_not_allowed` - Scheduler tier cannot use Partner API
* `500 create_failed` - Failed to initialize upload (retry)

<Note>
  The presigned upload URL expires after 10 minutes. If it expires before you upload,
  simply request a new one.
</Note>

<Tip>
  `POST /files` only prepares an expiring upload destination. The file is ready for use only after
  the PUT succeeds and `POST /files/{fileId}/finalize` returns the canonical CDN-backed record.
</Tip>


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