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

# POST /api/external/v1/media/uploads — Start a media upload session

> Opens an upload session for one image, video or audio file. Video gets tus credentials for a direct upload; image and audio get 3 MB part URLs to PUT.

- Source: https://www.dropfans.io/developers/reference/create-media-upload
- Section: API reference
- OpenAPI: https://www.dropfans.io/developers/openapi.json

Also served at `/api/external/media/uploads` (the unversioned alias).

Opens an upload session for one image, video or audio file. Video gets tus credentials for a direct upload; image and audio get 3 MB part URLs to PUT.

Then call [complete](https://www.dropfans.io/developers/reference/complete-media-upload.md) and poll [the session](https://www.dropfans.io/developers/reference/get-media-upload.md) for the vault item and its moderation. The session is valid 8 hours; abandoned sessions are cleaned up daily.

**Video** (up to 500 MB): upload the bytes with any tus client to `tus.endpoint`, sending `tus.headers` as headers — the same direct-to-CDN upload as [start-video-upload](https://www.dropfans.io/developers/reference/start-video-upload.md). **Image** (up to 30 MB) and **audio** (up to 20 MB): PUT each part's bytes to its `url` with your API key — parts go through us because our storage has no presigned writes. Parts can go in any order and in parallel.

The stored file has its metadata (EXIF, GPS, XMP, ID3 tags …) removed, as with every vault upload.

## Authentication

`Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`.

**For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/media/uploads` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: <account id>` on the plain path still works but is deprecated.

## Request body (application/json)

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. |
| `filename` | string | yes | File name, stored on the vault item. |
| `sizeBytes` | integer | yes | Exact file size in bytes. Image ≤ 30 MB, video ≤ 500 MB, audio ≤ 20 MB. |
| `contentType` | string | yes | MIME type; must match `kind` (image/*, video/*, audio/*). |
| `folderId` | string \\| null |  | Optional vault folder of this account. |
| `durationSeconds` | number |  | Audio only: the voice note's length (capped at 3600s). |

Example — An 8 MB photo:

```json
{
  "kind": "image",
  "filename": "beach.jpg",
  "sizeBytes": 7904267,
  "contentType": "image/jpeg"
}
```

Example — A video:

```json
{
  "kind": "video",
  "filename": "teaser.mp4",
  "sizeBytes": 52428800,
  "contentType": "video/mp4"
}
```

## Responses

### 201

Session opened: the session fields plus `parts` (image / audio) or `tus` (video).

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Upload id. |
| `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. |
| `status` | `uploading` \\| `processing` \\| `completed` \\| `failed` \\| `expired` | yes | `uploading` until you complete it; `processing` while a complete runs; `completed` once the vault item exists. |
| `fileName` | string | yes | The file name, stored on the vault item. |
| `contentType` | string | yes | The stored content type. |
| `sizeBytes` | integer | yes | The declared size. |
| `folderId` | string \\| null |  | The vault folder the item goes in. |
| `partSize` | integer \\| null |  | Image / audio: bytes per part (the last part is the remainder). Null for video. |
| `totalParts` | integer \\| null |  | Image / audio: number of parts. Null for video. |
| `receivedParts` | integer[] \\| null |  | Part numbers received so far. |
| `missingParts` | integer[] \\| null |  | Part numbers still to upload. |
| `vaultItemId` | string \\| null |  | The vault item, once completed. |
| `moderationStatus` | `PENDING` \\| `APPROVED` \\| `FLAGGED` \\| `REJECTED` \\| null |  | The vault item’s moderation status, once completed. |
| `bunnyStreamId` | string \\| null |  | Video: the Stream video id — poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with it. |
| `error` | object \\| null |  | `{ code, message }` when the upload failed or expired. |
| `error.code` | string | yes | Machine-readable reason. |
| `error.message` | string \\| null |  | Human-readable reason. |
| `expiresAt` | string | yes | Upload the parts (or the tus bytes) and complete before this. |
| `createdAt` | string | yes | When the session was opened. |
| `completedAt` | string \\| null |  | When it completed. |
| `parts` | object[] |  | Image / audio: PUT each part to its `url`. |
| `tus` | object |  | Video: tus upload credentials (the CDN API key itself is never exposed). |
| `tus.endpoint` | string | yes | tus endpoint. |
| `tus.videoId` | string | yes | The Stream video id. |
| `tus.libraryId` | string | yes | The Stream library id. |
| `tus.signature` | string | yes | sha256 upload signature. |
| `tus.expires` | integer | yes | Signature expiry (unix seconds). |
| `tus.headers` | object | yes | Send these as tus headers: AuthorizationSignature, AuthorizationExpire, VideoId, LibraryId. |

```json
{
  "id": "mu_3f9c1a7be0d24c55a1e8b6f2",
  "kind": "image",
  "status": "uploading",
  "fileName": "beach.jpg",
  "contentType": "image/jpeg",
  "sizeBytes": 7904267,
  "folderId": null,
  "partSize": 3145728,
  "totalParts": 3,
  "receivedParts": [],
  "missingParts": [
    1,
    2,
    3
  ],
  "vaultItemId": null,
  "moderationStatus": null,
  "bunnyStreamId": null,
  "error": null,
  "expiresAt": "2026-10-31T18:00:00.000Z",
  "createdAt": "2026-10-31T10:00:00.000Z",
  "completedAt": null,
  "parts": [
    {
      "partNumber": 1,
      "sizeBytes": 3145728,
      "method": "PUT",
      "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/1"
    },
    {
      "partNumber": 2,
      "sizeBytes": 3145728,
      "method": "PUT",
      "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/2"
    },
    {
      "partNumber": 3,
      "sizeBytes": 1612811,
      "method": "PUT",
      "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/3"
    }
  ]
}
```

## Errors

| Status | Body | When |
|---|---|---|
| 400 | `{"error":"kind must be image, video or audio","code":"invalid_kind"}` | Unknown `kind`. |
| 400 | `{"error":"filename is required (at most 255 characters)","code":"invalid_filename"}` | `filename` missing or too long. |
| 400 | `{"error":"sizeBytes must be a positive integer","code":"invalid_size"}` | `sizeBytes` missing or not a positive integer. |
| 400 | `{"error":"contentType must be a image/* type","code":"invalid_content_type"}` | `contentType` does not match `kind`. |
| 400 | `{"error":"folderId is not one of this account’s vault folders","code":"folder_not_found"}` | Unknown folder, or another account’s. |
| 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. |
| 413 | `{"error":"File too large. Max 30MB for image.","code":"file_too_large","maxBytes":31457280}` | `sizeBytes` is over the limit for `kind`. |
| 502 | `{"error":"Could not start the video upload — retry later","code":"video_create_failed"}` | The CDN did not create the video. |
| 503 | `{"error":"Video uploads are not available right now","code":"video_uploads_unavailable"}` | Video (or, with media_uploads_unavailable, image / audio) uploads are switched off. |
| 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. |
| 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. |
| 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. |
| 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. |
| 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. |

## Rate limiting

Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md).

## Code samples

### curl

```bash
curl -X POST "https://www.dropfans.io/api/external/v1/media/uploads" \
  -H "Authorization: Bearer $DROPFANS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "image",
  "filename": "beach.jpg",
  "sizeBytes": 7904267,
  "contentType": "image/jpeg"
}'
```

### Node

```javascript
const res = await fetch(`https://www.dropfans.io/api/external/v1/media/uploads`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "kind": "image",
    "filename": "beach.jpg",
    "sizeBytes": 7904267,
    "contentType": "image/jpeg"
  }),
});
console.log(await res.json());
```

### Python

```python
import os
import requests

res = requests.post(
    "https://www.dropfans.io/api/external/v1/media/uploads",
    headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"},
    json={
        "kind": "image",
        "filename": "beach.jpg",
        "sizeBytes": 7904267,
        "contentType": "image/jpeg",
    },
)
print(res.json())
```

## OpenAPI

```json
{
  "method": "POST",
  "path": "/api/external/v1/media/uploads",
  "operationId": "createMediaUpload",
  "tags": [
    "vault"
  ],
  "summary": "Start a media upload session",
  "description": "Opens an upload session for one image, video or audio file. Video gets tus credentials for a direct upload; image and audio get 3 MB part URLs to PUT.\n\nThen call [complete](https://www.dropfans.io/developers/reference/complete-media-upload.md) and poll [the session](https://www.dropfans.io/developers/reference/get-media-upload.md) for the vault item and its moderation. The session is valid 8 hours; abandoned sessions are cleaned up daily.\n\n**Video** (up to 500 MB): upload the bytes with any tus client to `tus.endpoint`, sending `tus.headers` as headers — the same direct-to-CDN upload as [start-video-upload](https://www.dropfans.io/developers/reference/start-video-upload.md). **Image** (up to 30 MB) and **audio** (up to 20 MB): PUT each part's bytes to its `url` with your API key — parts go through us because our storage has no presigned writes. Parts can go in any order and in parallel.\n\nThe stored file has its metadata (EXIF, GPS, XMP, ID3 tags …) removed, as with every vault upload.",
  "parameters": [
    {
      "name": "X-Dropfans-Account",
      "in": "header",
      "required": false,
      "deprecated": true,
      "description": "Deprecated: call the …ForCreator operation (/api/external/v1/creators/{creatorId}/…) instead; the header keeps working and such responses carry Deprecation and Link: rel=\"successor-version\". With a verified agency’s own key, the id of a creator or AI persona the agency manages (from GET /api/external/v1/agency/creators): the request then acts as that account, exactly as if it were sent with that account’s own key. Account ids only — usernames are refused (400 invalid_account_header). Checked on every request: the management must be ACCEPTED, nobody may be suspended, banned or deleted (else 403 account_not_managed), and the agency needs the endpoint’s permission on that account (else 403 permission_denied). Omit it to act as the key’s own account.",
      "schema": {
        "type": "string",
        "pattern": "^c[a-z0-9]{20,32}$"
      },
      "example": "cmh4v2q8k0003xy9z7w6v5u4t"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "kind": {
              "type": "string",
              "description": "What you are uploading.",
              "enum": [
                "image",
                "video",
                "audio"
              ]
            },
            "filename": {
              "type": "string",
              "description": "File name, stored on the vault item.",
              "example": "beach.jpg",
              "maxLength": 255
            },
            "sizeBytes": {
              "type": "integer",
              "description": "Exact file size in bytes. Image ≤ 30 MB, video ≤ 500 MB, audio ≤ 20 MB.",
              "example": 7904267
            },
            "contentType": {
              "type": "string",
              "description": "MIME type; must match `kind` (image/*, video/*, audio/*).",
              "example": "image/jpeg"
            },
            "folderId": {
              "type": [
                "string",
                "null"
              ],
              "description": "Optional vault folder of this account."
            },
            "durationSeconds": {
              "type": "number",
              "description": "Audio only: the voice note's length (capped at 3600s)."
            }
          },
          "required": [
            "kind",
            "filename",
            "sizeBytes",
            "contentType"
          ]
        },
        "examples": {
          "image": {
            "summary": "An 8 MB photo",
            "value": {
              "kind": "image",
              "filename": "beach.jpg",
              "sizeBytes": 7904267,
              "contentType": "image/jpeg"
            }
          },
          "video": {
            "summary": "A video",
            "value": {
              "kind": "video",
              "filename": "teaser.mp4",
              "sizeBytes": 52428800,
              "contentType": "video/mp4"
            }
          }
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Session opened: the session fields plus `parts` (image / audio) or `tus` (video).",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-Dropfans-Account": {
          "description": "The id of the account this request acted on — the key’s own account, or the managed account named in the X-Dropfans-Account request header. Assert it when acting for several accounts.",
          "schema": {
            "type": "string"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Upload id."
              },
              "kind": {
                "type": "string",
                "description": "What you are uploading.",
                "enum": [
                  "image",
                  "video",
                  "audio"
                ]
              },
              "status": {
                "type": "string",
                "description": "`uploading` until you complete it; `processing` while a complete runs; `completed` once the vault item exists.",
                "enum": [
                  "uploading",
                  "processing",
                  "completed",
                  "failed",
                  "expired"
                ]
              },
              "fileName": {
                "type": "string",
                "description": "The file name, stored on the vault item."
              },
              "contentType": {
                "type": "string",
                "description": "The stored content type."
              },
              "sizeBytes": {
                "type": "integer",
                "description": "The declared size."
              },
              "folderId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The vault folder the item goes in."
              },
              "partSize": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Image / audio: bytes per part (the last part is the remainder). Null for video."
              },
              "totalParts": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Image / audio: number of parts. Null for video."
              },
              "receivedParts": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "integer",
                  "description": "Part number."
                },
                "description": "Part numbers received so far."
              },
              "missingParts": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "integer",
                  "description": "Part number."
                },
                "description": "Part numbers still to upload."
              },
              "vaultItemId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The vault item, once completed."
              },
              "moderationStatus": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The vault item’s moderation status, once completed.",
                "enum": [
                  "PENDING",
                  "APPROVED",
                  "FLAGGED",
                  "REJECTED"
                ]
              },
              "bunnyStreamId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Video: the Stream video id — poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with it."
              },
              "error": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "Machine-readable reason."
                  },
                  "message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Human-readable reason."
                  }
                },
                "required": [
                  "code"
                ],
                "description": "`{ code, message }` when the upload failed or expired."
              },
              "expiresAt": {
                "type": "string",
                "description": "Upload the parts (or the tus bytes) and complete before this.",
                "format": "date-time"
              },
              "createdAt": {
                "type": "string",
                "description": "When the session was opened.",
                "format": "date-time"
              },
              "completedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When it completed.",
                "format": "date-time"
              },
              "parts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "partNumber": {
                      "type": "integer",
                      "description": "1-based."
                    },
                    "sizeBytes": {
                      "type": "integer",
                      "description": "Exact bytes this part must have."
                    },
                    "method": {
                      "type": "string",
                      "description": "Always PUT."
                    },
                    "url": {
                      "type": "string",
                      "description": "Where to PUT it (with your API key).",
                      "format": "uri"
                    }
                  },
                  "required": [
                    "partNumber",
                    "sizeBytes",
                    "method",
                    "url"
                  ],
                  "description": "One part."
                },
                "description": "Image / audio: PUT each part to its `url`."
              },
              "tus": {
                "type": "object",
                "properties": {
                  "endpoint": {
                    "type": "string",
                    "description": "tus endpoint.",
                    "format": "uri"
                  },
                  "videoId": {
                    "type": "string",
                    "description": "The Stream video id."
                  },
                  "libraryId": {
                    "type": "string",
                    "description": "The Stream library id."
                  },
                  "signature": {
                    "type": "string",
                    "description": "sha256 upload signature."
                  },
                  "expires": {
                    "type": "integer",
                    "description": "Signature expiry (unix seconds)."
                  },
                  "headers": {
                    "type": "object",
                    "description": "Send these as tus headers: AuthorizationSignature, AuthorizationExpire, VideoId, LibraryId."
                  }
                },
                "required": [
                  "endpoint",
                  "videoId",
                  "libraryId",
                  "signature",
                  "expires",
                  "headers"
                ],
                "description": "Video: tus upload credentials (the CDN API key itself is never exposed)."
              }
            },
            "required": [
              "id",
              "kind",
              "status",
              "fileName",
              "contentType",
              "sizeBytes",
              "expiresAt",
              "createdAt"
            ]
          },
          "example": {
            "id": "mu_3f9c1a7be0d24c55a1e8b6f2",
            "kind": "image",
            "status": "uploading",
            "fileName": "beach.jpg",
            "contentType": "image/jpeg",
            "sizeBytes": 7904267,
            "folderId": null,
            "partSize": 3145728,
            "totalParts": 3,
            "receivedParts": [],
            "missingParts": [
              1,
              2,
              3
            ],
            "vaultItemId": null,
            "moderationStatus": null,
            "bunnyStreamId": null,
            "error": null,
            "expiresAt": "2026-10-31T18:00:00.000Z",
            "createdAt": "2026-10-31T10:00:00.000Z",
            "completedAt": null,
            "parts": [
              {
                "partNumber": 1,
                "sizeBytes": 3145728,
                "method": "PUT",
                "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/1"
              },
              {
                "partNumber": 2,
                "sizeBytes": 3145728,
                "method": "PUT",
                "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/2"
              },
              {
                "partNumber": 3,
                "sizeBytes": 1612811,
                "method": "PUT",
                "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/3"
              }
            ]
          }
        }
      }
    },
    "400": {
      "description": "Unknown `kind`. Also: `filename` missing or too long. Also: `sizeBytes` missing or not a positive integer. Also: `contentType` does not match `kind`. Also: Unknown folder, or another account’s. Also: `X-Dropfans-Account` is not an account id. Also: Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "kind must be image, video or audio",
            "code": "invalid_kind"
          }
        }
      }
    },
    "401": {
      "description": "Missing or invalid API key.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "Unauthorized",
            "code": "unauthorized"
          }
        }
      }
    },
    "403": {
      "description": "The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. Also: For a managed creator: the agency lacks `canManageVault` on that account. Also: Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "This key cannot act for that account.",
            "code": "account_not_managed"
          }
        }
      }
    },
    "413": {
      "description": "`sizeBytes` is over the limit for `kind`.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "File too large. Max 30MB for image.",
            "code": "file_too_large",
            "maxBytes": 31457280
          }
        }
      }
    },
    "429": {
      "description": "Rate limit exceeded for the current minute or day window. Wait Retry-After seconds and retry.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "Retry-After": {
          "description": "Seconds to wait before retrying (sent with 429s).",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "Rate limit exceeded",
            "code": "rate_limited"
          }
        }
      }
    },
    "502": {
      "description": "The CDN did not create the video.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "Could not start the video upload — retry later",
            "code": "video_create_failed"
          }
        }
      }
    },
    "503": {
      "description": "Video (or, with media_uploads_unavailable, image / audio) uploads are switched off.",
      "headers": {
        "X-RateLimit-Tier": {
          "description": "Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).",
          "schema": {
            "type": "string",
            "enum": [
              "personal",
              "app",
              "first_party"
            ]
          }
        },
        "X-RateLimit-Limit": {
          "description": "Requests allowed per minute for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining": {
          "description": "Requests left in the current minute window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset": {
          "description": "Epoch seconds when the minute window resets.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Limit-Day": {
          "description": "Requests allowed per UTC day for this key (absent on first_party).",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Remaining-Day": {
          "description": "Requests left in the current UTC day window.",
          "schema": {
            "type": "integer"
          }
        },
        "X-RateLimit-Reset-Day": {
          "description": "Epoch seconds when the day window resets.",
          "schema": {
            "type": "integer"
          }
        }
      },
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Human-readable message.",
                "example": "Rate limit exceeded"
              },
              "code": {
                "type": "string",
                "description": "Machine-readable code: unauthorized, app_suspended, first_party_only, api_category_disabled, rate_limited, username_required, invalid_account_header, account_not_managed, permission_denied, account_header_not_allowed, agency_key_required — plus endpoint-specific codes listed on each reference page.",
                "example": "rate_limited"
              }
            },
            "required": [
              "error",
              "code"
            ]
          },
          "example": {
            "error": "Video uploads are not available right now",
            "code": "video_uploads_unavailable"
          }
        }
      }
    }
  },
  "x-codeSamples": [
    {
      "lang": "cURL",
      "label": "curl",
      "source": "curl -X POST \"https://www.dropfans.io/api/external/v1/media/uploads\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"kind\": \"image\",\n  \"filename\": \"beach.jpg\",\n  \"sizeBytes\": 7904267,\n  \"contentType\": \"image/jpeg\"\n}'"
    },
    {
      "lang": "JavaScript",
      "label": "Node",
      "source": "const res = await fetch(`https://www.dropfans.io/api/external/v1/media/uploads`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"kind\": \"image\",\n    \"filename\": \"beach.jpg\",\n    \"sizeBytes\": 7904267,\n    \"contentType\": \"image/jpeg\"\n  }),\n});\nconsole.log(await res.json());"
    },
    {
      "lang": "Python",
      "source": "import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/v1/media/uploads\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"kind\": \"image\",\n        \"filename\": \"beach.jpg\",\n        \"sizeBytes\": 7904267,\n        \"contentType\": \"image/jpeg\",\n    },\n)\nprint(res.json())"
    }
  ],
  "x-dropfans-docs": "https://www.dropfans.io/developers/reference/create-media-upload",
  "x-dropfans-category": "vault",
  "x-dropfans-agency-permission": "canManageVault"
}
```

---

Previous: [Check video transcoding status (batch)](https://www.dropfans.io/developers/reference/video-status.md) · Next: [Upload one part (image / audio)](https://www.dropfans.io/developers/reference/upload-media-part.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt)
