Upload Sessions
Preferred resumable upload API for modern Fangate clients.
Use upload sessions when you need:
- direct-to-storage uploads
- resumable multipart uploads
- large video uploads
- remote-import staging
- an explicit backend-ready state before creating a product
After an upload session reaches ready, attach it to a product with POST /api/products using upload_session_id.
Session lifecycle
Device multipart flow
POST /api/upload-sessions- inspect
upload_strategy - if
multipart, callPOST /api/upload-sessions/{id}/partsfor part instructions - upload parts directly to storage
POST /api/upload-sessions/{id}/complete- poll
GET /api/upload-sessions/{id} - when status is
ready, callPOST /api/productswithupload_session_id
Remote import flow
POST /api/upload-sessionswithsession_type=remote_importPOST /api/upload-sessions/{id}/remotewith incoming file payload- poll or finalize according to session state
- attach ready session to product
Uploading from your server or from a browser
The upload instructions are presigned storage URLs: whoever holds one can write exactly that file for 15 minutes, nothing else.
- From your server (recommended). Your backend calls the API with the key, receives the instructions and sends the bytes. This works from any host without further setup.
- From a browser on your own domain. The API calls work, but the browser only sends the bytes to storage if your site's origin is on the storage allow-list (CORS). An origin that is not listed fails with a generic network error although key and URL are valid. If a browser upload is the only option for your product, write to support (info@fangate.app) with the exact origin (for example
https://app.example.com) and we add it. Alternatively hand the presigned URL to your own backend and let it send the bytes.
Keep the API key on your server in either case - a key placed in browser code can be read by every user of your application. Only the presigned URLs belong in the browser.
Session fields
Current session resources expose fields such as:
| Field | Type | Notes |
|---|---|---|
id | string | Upload session id. |
session_type | string | device_multipart or remote_import. |
status | string | created, uploading, uploaded, finalizing, ready, failed, aborted, expired. |
upload_strategy | string | single_part or multipart. |
asset_kind | string or null | Usually image or video. |
file_name | string or null | Original filename. |
file_size_bytes | integer or null | File size in bytes. |
mime_type | string or null | Uploaded mime type. |
staging_disk | string | Current staging disk. |
uploaded_parts | array | Multipart parts already accepted. |
uploaded_asset | object or null | Finalized asset reference when ready. |
initial_upload | object or null | Single-part upload instruction when applicable. |
POST /api/upload-sessions
Create a new upload session.
- Auth required: Yes
- Roles: Authenticated creator
- Content type:
application/json
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
session_type | string | Yes | device_multipart or remote_import. |
product_id | integer | No | Existing product the upload will later attach to. |
asset_kind | string | No | Current config supports image/video kinds. |
file_name | string | Yes for device_multipart | Original filename. |
file_size_bytes | integer | Yes for device_multipart | Max upload size is environment-configured, currently 7 GB. |
mime_type | string | Yes for device_multipart | Must be an allowed mime type. |
remote_provider | string | Yes for remote_import | Must be enabled in upload config. |
remote_provider_file_id | string | No | Provider-specific identifier. |
remote_provider_metadata | object | No | Provider-specific metadata blob. |
ai_origin | string | No | The AI classification of the file this session will carry: none, synthetic, real_person_self, real_person_other. Sent here, it covers the later POST /api/media / POST /api/products call (a value on that call wins). See AI content. |
ai_attested | boolean | With AI values | Must be true for every value other than none. |
remote_attestation | boolean | Yes for remote_import | Rights and consent attestation for imported third-party material. |
Allowed remote providers in current config
urlonedriveunsplashgoogle_drive_pickergoogle_photos_pickerdropboxboxgoogle_drive
Success response example
{
"success": true,
"errors_message": null,
"data": {
"id": "01knyn6vkpnvh2rzgg9c0n6xz5",
"session_type": "device_multipart",
"status": "created",
"upload_strategy": "multipart",
"asset_kind": "video",
"file_name": "sample.mp4",
"file_size_bytes": 52428800,
"mime_type": "video/mp4",
"staging_disk": "uppy_staging_s3",
"uploaded_parts": [],
"uploaded_asset": null,
"initial_upload": null
}
}Validation and side effects
- if
product_idis sent, ownership is verified - upload strategy is derived from configured multipart policy and file size
- current default presign TTL is 15 minutes
- current multipart threshold is 100 MB with an 8 MB minimum part size
GET /api/upload-sessions/
Return the current state of one upload session.
- Auth required: Yes
- Roles: Session owner
Use this endpoint for polling after complete.
POST /api/upload-sessions/{uploadSession}/remote
Upload a remote-import payload into an existing remote session.
- Auth required: Yes
- Roles: Session owner
- Content type:
multipart/form-data
Request format
Send the file in either:
files[]file
If the client sends X-Fangate-Upload-Session, it must match the route session id.
POST /api/upload-sessions/{uploadSession}/parts
Request a presigned instruction for one multipart part.
- Auth required: Yes
- Roles: Session owner
- Content type:
application/json
Request body
{
"part_number": 1
}Success response example
{
"success": true,
"errors_message": null,
"data": {
"session": {
"id": "01knyn6vkpnvh2rzgg9c0n6xz5",
"status": "uploading"
},
"part": {
"part_number": 1,
"method": "PUT",
"url": "https://example-presigned-url",
"headers": {
"Content-Type": "video/mp4"
},
"expires_at": "2026-04-20T10:15:00Z"
}
}
}POST /api/upload-sessions/{uploadSession}/complete
Finalize a direct upload.
- Auth required: Yes
- Roles: Session owner
- Content type:
application/json
Multipart request body
{
"parts": [
{
"part_number": 1,
"etag": "etag-value",
"size_bytes": 8388608
}
]
}For single_part or remote_import flows, the body can be omitted.
Responses
200 OK: session is already ready202 Accepted: finalization continues asynchronously
Side effects
- may dispatch
FinalizeUploadSessionJob - finalizer processes on
filesystems.media_process_disk - if processing disk and final disk differ, backend mirrors the finalized asset to
uploads.final_disk
DELETE /api/upload-sessions/
Abort an unfinished upload session.
- Auth required: Yes
- Roles: Session owner
Returns the final session representation with status=aborted.
Accepted file types
What an upload session accepts - checked by the mime_type you declare when creating the session (422 mime_type_not_allowed_for_asset_kind otherwise), or for an imported file by its content type, falling back to the extension listed here. This is what you may send; Fangate re-encodes some files for playback on its own, which does not change what you can upload.
Images - up to 50 MB per file
mime_type | Usual extensions |
|---|---|
image/jpeg | .jpg, .jpeg |
image/png | .png |
image/webp | .webp |
image/gif | .gif |
image/heic | .heic |
image/heif | .heif |
Video - up to 7 GB per file
mime_type | Usual extensions |
|---|---|
video/mp4 | .mp4 |
video/quicktime | .mov |
video/x-msvideo | .avi |
video/x-matroska | .mkv |
video/webm | .webm |
Codecs are not checked up front. Any video with a readable video or audio track in one of these containers is taken (H.264, HEVC/H.265, VP8/VP9 and AV1 are the common ones). A file with no readable track at all - an interrupted recording or export, a split file, something that is not a media file - fails once during finalization: the session goes to status: failed with last_error_code: media_unreadable. Export the file again and start a new session; a retry of the same file gives the same answer.
The size limits are checked against the size actually uploaded at complete (422 upload_size_limit_exceeded).
Example flow: upload then create product
POST /api/upload-sessionsPOST /api/upload-sessions/{id}/partsuntil all parts are uploadedPOST /api/upload-sessions/{id}/completeGET /api/upload-sessions/{id}untilstatus=readyPOST /api/productswithupload_session_id