Upload an image, small video or voice message
Uploads one file into the creator's vault as `multipart/form-data`.
/api/external/vaultUploads one file into the creator's vault as multipart/form-data.
Three kinds, three shapes: images need two parts — a pre-compressed displayFile and a thumbnailFile (both JPEG); audio (voice messages) sends one file part (≤20MB, ≤60 min); video sends one file part — but the whole request must stay under ~4MB (a platform body cap rejects bigger requests before the app runs), so for real videos use the three-step TUS flow starting at POST /api/external/vault/video-upload instead.
Every upload enters the same moderation pipeline as a dashboard upload: images are scored immediately, videos asynchronously, audio goes to human review (or auto-approves for trusted creators). The item only shows up in the default vault list once APPROVED.
Authentication
Send the creator's API key as a bearer token: Authorization: Bearer dpfn_…. See Authentication & API keys.
Request body
Content type: multipart/form-data
| Name | Type | Required | Description |
|---|---|---|---|
fileType | string | Required | What you are uploading. One of: image, video, audio |
originalName | string | Required | The file’s name, stored as fileName. |
folderId | string | Optional | Optional folder to file the item into. |
durationSeconds | integer | Optional | Audio only — the voice message length in seconds (capped at 3600). Max: 3600 |
displayFile | file | Optional | Images only (required): the pre-compressed JPEG display copy. |
thumbnailFile | file | Optional | Images only (required): the JPEG thumbnail. |
file | file | Optional | Video/audio only (required): the media file. Audio ≤20MB; video effectively ≤~4MB here — use the TUS flow for anything bigger. |
fileType=image
originalName=beach-set-01.jpg
[email protected]
[email protected]fileType=audio
originalName=voice-note.ogg
durationSeconds=42
[email protected]Responses
| Name | Type | Description | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
success | boolean | Always true. | |||||||||||||||||||||||||||
item | VaultItemUploaded | The new vault item.Show child attributes
|
{
"success": true,
"item": {
"id": "clxv1a2b30001item",
"fileName": "beach-set-01.jpg",
"filePath": "https://cdn.dropfans.io/valeria/vault/1721990000-ab12cd.jpg",
"thumbnailPath": "https://cdn.dropfans.io/valeria/thumbnails/1721990000-ab12cd.jpg",
"fileType": "image",
"fileSize": 482113,
"durationSeconds": null,
"bunnyStreamId": null,
"createdAt": "2026-08-01T10:15:00.000Z"
}
}Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error":"Missing required fields: fileType and originalName"} | Either required form field is absent. |
| 400 | {"error":"Only images, videos and audio are allowed"} | `fileType` is anything other than image, video or audio. |
| 400 | {"error":"Missing displayFile or thumbnailFile for image upload"} | Image upload without both file parts. |
| 400 | {"error":"Missing video file"} | fileType=video without a `file` part. |
| 400 | {"error":"Missing audio file"} | fileType=audio without a `file` part. |
| 401 | {"error":"Unauthorized","code":"unauthorized"} | Missing or invalid API key. |
| 413 | {"error":"Audio too large"} | Audio file over 20MB. |
| 500 | {"error":"Failed to upload file"} | Storage or moderation kickoff failed — retry later. |
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/vault" \
-H "Authorization: Bearer $DROPFANS_API_KEY" \
-F "fileType=image" \
-F "originalName=beach-set-01.jpg" \
-F "[email protected]" \
-F "[email protected]"import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('fileType', "image");
form.append('originalName', "beach-set-01.jpg");
form.append('displayFile', new Blob([await readFile('photo.jpg')]), 'photo.jpg');
form.append('thumbnailFile', new Blob([await readFile('photo-thumb.jpg')]), 'photo-thumb.jpg');
const res = await fetch(`https://www.dropfans.io/api/external/vault`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,
},
body: form,
});
console.log(await res.json());import os
import requests
res = requests.post(
"https://www.dropfans.io/api/external/vault",
headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"},
files={
"displayFile": open("photo.jpg", "rb"),
"thumbnailFile": open("photo-thumb.jpg", "rb"),
},
data={
"fileType": "image",
"originalName": "beach-set-01.jpg",
},
)
print(res.json())Notes
The success shape here differs from the video-complete endpoint's item (that one adds moderationStatus, moderationTags and aiEnhanced and omits durationSeconds). OPTIONS on this path is an unauthenticated CORS preflight for browser uploads.
Questions? [email protected]
