Start a media upload session
API reference

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.

View as MarkdownUpdated Oct 31, 2026
POST/api/external/v1/media/uploadsalso served at /api/external/media/uploadsNew

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

NameTypeRequiredDescription
kindstringRequiredWhat you are uploading.
One of: image, video, audio
filenamestringRequiredFile name, stored on the vault item.
Max length: 255
sizeBytesintegerRequiredExact file size in bytes. Image ≤ 30 MB, video ≤ 500 MB, audio ≤ 20 MB.
contentTypestringRequiredMIME type; must match kind (image/*, video/*, audio/*).
folderIdstring | nullOptionalOptional vault folder of this account.
durationSecondsnumberOptionalAudio only: the voice note's length (capped at 3600s).
An 8 MB photo
{
  "kind": "image",
  "filename": "beach.jpg",
  "sizeBytes": 7904267,
  "contentType": "image/jpeg"
}
A video
{
  "kind": "video",
  "filename": "teaser.mp4",
  "sizeBytes": 52428800,
  "contentType": "video/mp4"
}

Responses

201Session opened: the session fields plus `parts` (image / audio) or `tus` (video).
NameTypeDescription
idstringUpload id.
kindstringWhat you are uploading.
One of: image, video, audio
statusstringuploading until you complete it; processing while a complete runs; completed once the vault item exists.
One of: uploading, processing, completed, failed, expired
fileNamestringThe file name, stored on the vault item.
contentTypestringThe stored content type.
sizeBytesintegerThe declared size.
folderIdstring | nullThe vault folder the item goes in.
partSizeinteger | nullImage / audio: bytes per part (the last part is the remainder). Null for video.
totalPartsinteger | nullImage / audio: number of parts. Null for video.
receivedPartsinteger[] | nullPart numbers received so far.
missingPartsinteger[] | nullPart numbers still to upload.
vaultItemIdstring | nullThe vault item, once completed.
moderationStatusstring | nullThe vault item’s moderation status, once completed.
One of: PENDING, APPROVED, FLAGGED, REJECTED
bunnyStreamIdstring | nullVideo: the Stream video id — poll [video-status](/developers/reference/video-status) with it.
errorobject | null{ code, message } when the upload failed or expired.
Show child attributes
codestringMachine-readable reason.
messagestring | nullHuman-readable reason.
expiresAtstringUpload the parts (or the tus bytes) and complete before this.
Format: date-time
createdAtstringWhen the session was opened.
Format: date-time
completedAtstring | nullWhen it completed.
Format: date-time
partsobject[]Image / audio: PUT each part to its url.
Show child attributes
partNumberinteger1-based.
sizeBytesintegerExact bytes this part must have.
methodstringAlways PUT.
urlstringWhere to PUT it (with your API key).
Format: uri
tusobjectVideo: tus upload credentials (the CDN API key itself is never exposed).
Show child attributes
endpointstringtus endpoint.
Format: uri
videoIdstringThe Stream video id.
libraryIdstringThe Stream library id.
signaturestringsha256 upload signature.
expiresintegerSignature expiry (unix seconds).
headersobjectSend these as tus headers: AuthorizationSignature, AuthorizationExpire, VideoId, LibraryId.
Example response
{
  "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

StatusBodyWhen
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"
}'

Questions? [email protected]