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.
/api/external/v1/media/uploadsalso served at /api/external/media/uploadsNewOpens 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 and poll the session 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. 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
Send the creator's API key as a bearer token: Authorization: Bearer dpfn_…. See Authentication & API keys.
For a creator you manage or onboarded
POST /api/external/v1/creators/{creatorId}/media/uploads — an agency's own key for a creator or AI persona it manages (needs canManageVault), or your app owner's own key for an account your app onboarded. The X-Dropfans-Account header still works but is deprecated. See Acting for a creator.
Request body
Content type: application/json
| Name | Type | Required | Description |
|---|---|---|---|
kind | string | Required | What you are uploading. One of: image, video, audio |
filename | string | Required | File name, stored on the vault item. Max length: 255 |
sizeBytes | integer | Required | Exact file size in bytes. Image ≤ 30 MB, video ≤ 500 MB, audio ≤ 20 MB. |
contentType | string | Required | MIME type; must match kind (image/*, video/*, audio/*). |
folderId | string | null | Optional | Optional vault folder of this account. |
durationSeconds | number | Optional | Audio only: the voice note's length (capped at 3600s). |
{
"kind": "image",
"filename": "beach.jpg",
"sizeBytes": 7904267,
"contentType": "image/jpeg"
}{
"kind": "video",
"filename": "teaser.mp4",
"sizeBytes": 52428800,
"contentType": "video/mp4"
}Responses
| Name | Type | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | string | Upload id. | ||||||||||||||||||
kind | string | What you are uploading. One of: image, video, audio | ||||||||||||||||||
status | string | uploading until you complete it; processing while a complete runs; completed once the vault item exists.One of: uploading, processing, completed, failed, expired | ||||||||||||||||||
fileName | string | The file name, stored on the vault item. | ||||||||||||||||||
contentType | string | The stored content type. | ||||||||||||||||||
sizeBytes | integer | 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 | string | null | The vault item’s moderation status, once completed. One of: PENDING, APPROVED, FLAGGED, REJECTED | ||||||||||||||||||
bunnyStreamId | string | null | Video: the Stream video id — poll [video-status](/developers/reference/video-status) with it. | ||||||||||||||||||
error | object | null | { code, message } when the upload failed or expired.Show child attributes
| ||||||||||||||||||
expiresAt | string | Upload the parts (or the tus bytes) and complete before this. Format: date-time | ||||||||||||||||||
createdAt | string | When the session was opened. Format: date-time | ||||||||||||||||||
completedAt | string | null | When it completed. Format: date-time | ||||||||||||||||||
parts | object[] | Image / audio: PUT each part to its url.Show child attributes
| ||||||||||||||||||
tus | object | Video: tus upload credentials (the CDN API key itself is never exposed).Show child attributes
|
{
"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](/developers/concepts/api-categories) switched off. |
Rate limiting
Every response carries the X-RateLimit-Tier header and, on limited tiers, the per-minute and per-day trios — read X-RateLimit-Remaining and X-RateLimit-Reset instead of hardcoding limits. Details in Rate limits.
Code samples
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"
}'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());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())Questions? [email protected]
