Finish a video upload (step 3 of 3)
API reference

Finish a video upload (step 3 of 3)

Registers a finished TUS upload as a vault item and kicks moderation.

View as MarkdownUpdated Aug 19, 2026
POST/api/external/v1/vault/video-upload/completealso served at /api/external/vault/video-upload/complete

Registers a finished TUS upload as a vault item and kicks moderation.

Idempotent: a retried completion for an already-registered video returns the existing item instead of creating a duplicate — retry freely. 409 means "not finished yet, retry": the CDN can lag a few seconds after the TUS client reports done, so on a 409 back off (2s, 5s, 10s, 30s) and call again with the same body.

The item starts PENDING and finalizes asynchronously (the NSFW pipeline plus transcoding). Poll video-status with the bunnyStreamId to know when the video is playable, and the vault list for moderation.

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/video-upload/complete — 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
videoIdstringRequiredThe videoId from step 1.
originalNamestringRequiredSame file name you sent in step 1.
completionTokenstringRequiredThe token from step 1 (valid 8h). Proves this key started the upload.
folderIdstring | nullOptionalOptional folder — silently ignored if you don’t own it.
Register the uploaded video
{
  "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9",
  "originalName": "teaser.mp4",
  "completionToken": "eyJ…"
}

Responses

200Registered (or already registered — idempotent).
NameTypeDescription
successbooleanAlways true.
itemVideoVaultItemThe new video vault item, starting PENDING.
Show child attributes
idstringThe new vault item id.
fileNamestringThe originalName you sent.
filePathstringSigned playback URL, valid ~1 hour. For a long-lived URL, re-list the vault later.
thumbnailPathstring | nullSigned Stream thumbnail (600px wide).
fileTypestringAlways "video" here.
One of: video
fileSizeinteger | nullByte count Bunny reported for the finished upload.
bunnyStreamIdstring | nullThe Stream GUID — poll POST /api/external/v1/vault/video-status with it while the video transcodes.
moderationStatusstringStarts PENDING — the NSFW pipeline finalizes it asynchronously. The item only appears in the default vault list once APPROVED.
One of: PENDING, APPROVED, REJECTED, FLAGGED
moderationTagsstring[]Always empty at creation (moderation has not run yet).
createdAtstringCreation time.
Format: date-time
aiEnhancedbooleanTrue when the account is AI-labelled — every upload from such an account is force-marked AI.
Example response
{
  "success": true,
  "item": {
    "id": "clxv9z8y70002item",
    "fileName": "teaser.mp4",
    "filePath": "https://vz-example.b-cdn.net/c2f7f9e2-…/playlist.m3u8?token=…",
    "thumbnailPath": "https://vz-example.b-cdn.net/c2f7f9e2-…/thumbnail.jpg?token=…",
    "fileType": "video",
    "fileSize": 52428800,
    "bunnyStreamId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9",
    "moderationStatus": "PENDING",
    "moderationTags": [],
    "createdAt": "2026-08-01T10:20:00.000Z",
    "aiEnhanced": false
  }
}

Errors

StatusBodyWhen
400{"error":"Missing required fields: videoId and originalName"}Either field absent or blank.
401{"error":"Unauthorized","code":"unauthorized"}Missing or invalid API key.
403{"error":"Invalid or expired upload token — please retry the upload"}The completionToken is wrong, for another key, or older than 8h — start over from step 1.
409{"error":"The video upload has not finished — please retry."}The CDN has not finished receiving the bytes — back off and retry the same call.
413{"error":"Video too large. Max 500MB."}The stored byte count exceeds the cap — the upload is discarded.
500{"error":"Failed to register the uploaded video"}Registration 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/video-upload/complete" \
  -H "Authorization: Bearer $DROPFANS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9",
  "originalName": "teaser.mp4",
  "completionToken": "eyJ…"
}'

Notes

This item shape adds moderationStatus, moderationTags and aiEnhanced (and omits durationSeconds) compared to the plain upload endpoint. filePath here is signed for ~1 hour only.

Questions? support@dropfans.io