Upload an image, small video or voice message
Uploads one file into the creator's vault as `multipart/form-data`.
/api/external/v1/vaultalso served at /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/v1/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.
For a creator you manage or onboarded
POST /api/external/v1/creators/{creatorId}/vault — 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: 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
displayFile=@photo.jpg
thumbnailFile=@photo-thumb.jpgfileType=audio
originalName=voice-note.ogg
durationSeconds=42
file=@voice-note.oggResponses
| 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. |
| 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/vault" \
-H "Authorization: Bearer $DROPFANS_API_KEY" \
-F "fileType=image" \
-F "originalName=beach-set-01.jpg" \
-F "displayFile=@photo.jpg" \
-F "thumbnailFile=@photo-thumb.jpg"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/v1/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/v1/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? support@dropfans.io
